@ethlete/agent-rules 0.1.0-next.26 → 0.1.0-next.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/AGENTS.md +28 -0
  2. package/CHANGELOG.md +114 -0
  3. package/CLAUDE.md +1 -0
  4. package/THIRD-PARTY-LICENSES.md +1 -1
  5. package/content/hooks/context-warning.py +46 -7
  6. package/content/mods/.claude-plugin/marketplace.json +11 -0
  7. package/content/mods/plugins/e2e-progress/.claude-plugin/plugin.json +6 -0
  8. package/content/mods/plugins/e2e-progress/hooks/hooks.json +1 -0
  9. package/content/mods/plugins/e2e-progress/hooks/register.tsx +442 -0
  10. package/content/mods/plugins/e2e-progress/tsconfig.json +3 -0
  11. package/content/mods/plugins/e2e-progress/types/index.d.ts +23 -0
  12. package/content/rules/agent-sessions-wake-up.md +11 -0
  13. package/content/rules/lint-and-format.md +2 -1
  14. package/content/rules/subagent-models.md +4 -0
  15. package/content/skills/agent-sessions/SKILL.md +81 -0
  16. package/content/skills/agent-sessions/agent-sessions.mjs +436 -0
  17. package/content/skills/design-exploration/SKILL.md +429 -135
  18. package/content/skills/e2e-triage/SKILL.md +80 -0
  19. package/content/skills/git-commit/SKILL.md +5 -0
  20. package/content/skills/grilling/SKILL.md +17 -0
  21. package/content/skills/handoff/SKILL.md +51 -1
  22. package/content/skills/sdk-update/SKILL.md +7 -0
  23. package/package.json +1 -1
  24. package/src/index.js +7 -0
  25. package/src/index.js.map +1 -1
  26. package/src/lib/config.d.ts +2 -0
  27. package/src/lib/config.js +5 -3
  28. package/src/lib/config.js.map +1 -1
  29. package/src/lib/plan.js +4 -0
  30. package/src/lib/plan.js.map +1 -1
  31. package/src/lib/sessions-command.d.ts +4 -0
  32. package/src/lib/sessions-command.js +16 -0
  33. package/src/lib/sessions-command.js.map +1 -0
  34. package/src/lib/targets/claude-hooks.d.ts +2 -0
  35. package/src/lib/targets/claude-hooks.js +4 -1
  36. package/src/lib/targets/claude-hooks.js.map +1 -1
  37. package/src/lib/targets/claude-mods.d.ts +20 -0
  38. package/src/lib/targets/claude-mods.js +47 -0
  39. package/src/lib/targets/claude-mods.js.map +1 -0
  40. package/src/lib/targets/hooks-shared.d.ts +1 -0
  41. package/src/lib/targets/hooks-shared.js +3 -2
  42. package/src/lib/targets/hooks-shared.js.map +1 -1
@@ -11,15 +11,19 @@ vars: [storybookUrl]
11
11
  A design exploration is a dialog. **The user is the designer. You find and frame. The user
12
12
  chooses.** A finding is not a licence to pick the fix.
13
13
 
14
- The unit of work is a **call**: one open question, with every answer drawn side by side at
15
- the same geometry. A variant wins or loses only against the other variants of its call.
14
+ The unit of work is one open question, with every answer drawn side by side at the same
15
+ geometry: a **sketch** on a product, or a **call** in a repository with the old setup. A variant wins
16
+ or loses only against the other variants of its question.
16
17
 
17
18
  ## The rules
18
19
 
19
20
  1. **One open call at a time.** Take one question. Draw its variants. Stop.
20
21
  2. **Put the variants in the call, then name your pick.** Two to four variants, drawn for
21
- real, side by side, labelled, with what each one costs. Your pick is a proposal.
22
- 3. **Commit only when the user says commit.**
22
+ real, side by side, labelled, with what each one costs. Your pick is a proposal. Never ask
23
+ a yes/no question about content, and never ask the user what they want: deliver strong
24
+ variants, and the user picks.
25
+ 3. **Commit only when the user says commit.** A merge the user makes in Ethlete Studio counts as
26
+ that word for the merged part: commit the part, not the open sketches.
23
27
  4. **A clear rejection starts the next call.** Settle and record the rejected call, then draw
24
28
  the next focused question automatically, within the user's original scope. Stop only when the
25
29
  user asks to pause or end the exploration, or when their feedback leaves no grounded next
@@ -32,13 +36,17 @@ finished while the user is still talking about it.
32
36
  ## What one stop looks like
33
37
 
34
38
  - The question, in one sentence.
35
- - The call slug to look at.
39
+ - The sketch (or call slug) to look at.
36
40
  - The variants, labelled A, B, C, one line each.
37
41
  - Your pick, one line on why.
38
42
  - Nothing else. No next step, no second finding.
39
43
 
40
44
  **Do not send a screenshot.** The user keeps the call open. Screenshot only for your own
41
- check that a variant renders.
45
+ check that a variant renders. In a repository with `et design`, take it with
46
+ `et design shot <layer>/<name> --states [--variants]`: one contact sheet of every state, and
47
+ every variant of the open sketch, with a label per cell; below each variant row, a diff row marks
48
+ the pixels that differ from the merged state in red, or says "no pixel change" (with ", but the markup differs" when only the markup changed, e.g. both sides miss the same content). It prints the PNG path last. `--scale 2` enlarges a small part; `--crop x,y,w,h` cuts each cell to that CSS-pixel box, to judge hairlines. `--interaction hover` forces an interaction on every cell. `--state a,b` draws only those states, to compare them side by side. With `--variants`, `--round r2` or `--variant i,j` draws only those variants next to the merged row, so the current round stays readable; it filters the picture, never the checks. With `--variants`, `--diff-props` compares each variant with the merged part drawn with the variant's state props, so changed props do not paint the diff red. It draws
49
+ undimmed, whatever Studio shows. Do not write a scratch script for batch shots.
42
50
 
43
51
  If a second defect appears, add it to the open list and give it one line. Do not draw it.
44
52
 
@@ -46,108 +54,375 @@ If a second defect appears, add it to the open list and give it one line. Do not
46
54
 
47
55
  The exploration's plan file names the tool. Two shapes exist.
48
56
 
49
- **A repository with a `.ethlete/design` folder** draws each variant of a call in its own
50
- frame, and the user reads the call in Ethlete Studio. The tool is `et design`, from `@ethlete/cli`, so the repository itself needs no design
51
- tooling. A call argues in the repository it is about:
57
+ **A repository with a `.ethlete/design` folder** keeps one design system per **product**, and the
58
+ user reads and decides in Ethlete Studio. The tool is `et design`, from `@ethlete/cli`; run
59
+ `et design --help` for every command, `et design <command> [<subcommand>] --help` for one.
60
+ In the SDK checkout, `yarn design` builds the CLI before each call; in a check-and-shot loop,
61
+ build once and call `node dist/libs/cli/src/index.js design` instead.
52
62
 
53
63
  ```
54
64
  .ethlete/design/
55
- config.json the port, the default call, and one entry per project
56
- calls/<project>/<group>/<slug>/
57
- call.ts the eyebrow, the headline, the intro, frameWidth, the variants and the round prose
58
- fixture.ts the data every variant shares
59
- variant-a.ts one default-exported drawing per variant
60
- variant-b.ts
65
+ config.json the port and one `products` entry each
66
+ <product>/<layer>/<part>/ layer: foundations, components, elements, pages
67
+ <product>/flows/<name>.json a flow: pages and their states joined by lines
68
+ <product>/sketches/<layer>-<name>/ an open sketch: sketch.json and one folder per variant
69
+ <product>/comments/<id>.json a comment the user wrote in Studio
70
+ calls/ old calls: Legacy, read-only
61
71
  ```
62
72
 
63
- The first segment of a slug is the **project**. `config.json` gives each project its own
64
- `styles` and its own `head`, so one project's type scale and fonts never reach another's
65
- drawings. A project the config names not draws bare.
73
+ Studio no longer takes new calls. **New work goes to a product, never to a new call**; do not run
74
+ `et design new`. Old calls stay in Studio's Legacy tab as reference for moving them into a
75
+ product. They take no picks and no new variants.
66
76
 
67
- A project can also name `themes`: `{ "surface": ["light", "dark"], "color": ["brand", …] }`; the
68
- first name of each list is the default. A frame asked for a theme gets the
69
- `et-surface--<name>` and `et-color--<name>` classes on its root, so a drawing in such a project
70
- must not set a surface or colour class on its own root, and must take its colours from the theme
71
- tokens. An inner panel may set `et-surface--<name>`, to put a dark and a light panel side by side
72
- or to draw an elevated card. To try another value of a colour theme, a variant calls `themeCss()`
73
- in its own `styles`; it never changes the project's providers.
77
+ ### Create a product
74
78
 
75
- ### Make and track a call
79
+ ```bash
80
+ et design product new <name> [--base @ethlete/components|<path>] [--from-project <name>]
81
+ ```
82
+
83
+ It writes the `config.json` entry and the empty layer folders. A product on `@ethlete/components`
84
+ takes the SDK as its base, so its parts start from the real components rendered with `ng()`.
85
+ Without a base it gets the presets phone (390x844) and desktop (1440x900). A product's own `presets`
86
+ replace its base's whole: a desktop-only app lists `desktop` alone and draws no phone state. `--from-project` copies
87
+ `styles`, `head`, `providers` and `themes` from an old project entry. Studio never writes
88
+ `config.json`; the command does.
89
+
90
+ Every frame loads the product's tokens stylesheets once, before the part's own `styles`, so a part
91
+ never imports the tokens and never adds them to its `styles`. The product's config `tokens` lists
92
+ those parts in load order (`["foundations/tokens"]` when left out); a listed part that does not
93
+ exist yet is skipped. A product's own copy of a listed part, or a sketch of it, replaces the base's
94
+ in every frame. Never list a `tokens.css` in config `styles`.
95
+
96
+ A part file default-exports its `definePart(...)` and may also export named helpers, such as a piece of its
97
+ markup: `import { studioMark } from '@system/foundations/logo'`. Import a piece from the part that draws it;
98
+ never copy its markup into another part. A piece that uses the part's classes needs its stylesheet too
99
+ (`@system/foundations/logo.css?inline`), or from the part's default export when the part sets
100
+ `definePart({ styles })`: every drawing of that part gets it, so `render` returns only extra styles, or `drawing({ body })` with none. The layer
101
+ rules hold for these imports as well.
102
+
103
+ A component a host embeds exports its markup as such a helper, and a wrapper only its own frame needs, such as
104
+ padding that makes room for a focus ring, goes in `render` alone, never in that helper: the host places the
105
+ component itself and would inherit the wrapper's offset.
76
106
 
77
- `et design --help` lists every command. Start a call with the tool, never by hand:
107
+ A product whose app uses Tailwind lists one stylesheet in config `styles` that imports the app's styles and
108
+ registers the drawing folders with `@source './'`. Sketches then use the app's utilities; plain CSS stays for what
109
+ utilities cannot express, such as camera geometry.
110
+
111
+ ### Propose a change as a sketch
112
+
113
+ A change to a part, or a new part, is a **sketch**: the question is the part, the variants are the
114
+ answers. Variants are drawn at the same geometry, and one variant wins only against the others.
78
115
 
79
116
  ```bash
80
- et design new <project>/<group>/<name> --stage component --variants a,b --feature <name>
81
- et design status <slug> drawing # while you draw; check sets checking, then ready or failed
117
+ et design sketch new <layer>/<name> --variants a,b --by <model id> [--like <layer>/<name>] [--variant-note a=<text>]… [--product <p>] # each variant starts as a copy of the part
118
+ et design sketch report <layer>/<name> --variant b --by <model id> "phone" # tell Studio what you build now
119
+ et design sketch report <layer>/<name> --variant b --by <model id> --done # you stopped drawing b; Studio shows its frames
120
+ et design sketch check [<layer>/<name>] [--fix] # each variant laid over the product
121
+ et design sketch ready <layer>/<name> --by <model id> # now the user can pick
122
+ et design sketch reject <layer>/<name> --variant b [--reason <text>] --by <model id> # only when the user rejected b; no pick or merge until reopened
123
+ et design sketch reopen <layer>/<name> --variant b [--reason <text>] --by <model id> # only when the user takes b back; the rejection stays in the history
82
124
  ```
83
125
 
84
- `new` writes `call.ts`, `fixture.ts` and one variant file per key, and the call renders at
85
- once. `stage` in `call.ts` names the stage of the design (see "The order of a design"). Every
86
- call needs one: `new` refuses to start without `--stage`, and `et design check --call` fails on a
87
- call without it. Ethlete Studio groups the calls by it, and the check warns when an element, page
88
- or prototype call imports nothing from the design system. The status shows on the call's row in Studio.
89
-
90
- `feature: '<name>'` in `call.ts` groups a call under a feature in Studio. Set it at creation with
91
- `new --feature <name>`. A call without one shows under "No feature".
92
-
93
- `et design [checkout]` serves the frames on the port `config.json` names. The
94
- checkout defaults to the working directory, and `DE_PORT` overrules the config, so two
95
- checkouts can be drawn at once. A repository that builds the tool rather than installing it
96
- wraps this in a script - in the ethlete SDK, `yarn design:check`. There, the user reads the
97
- calls in Ethlete Studio, started with `yarn studio`, and Studio serves the frames itself. Do not
98
- start `yarn design` next to it.
99
-
100
- Ethlete Studio carries its own copy of the tool, so it draws a checkout that installs nothing.
101
- The machine still needs Node, and the render stage still needs `playwright` where the copy in
102
- use can reach it.
103
-
104
- `call.ts` calls `defineCall` from `@design-explore`. Each variant carries a `key`, a `name`,
105
- a `claim`, a `cost`, an optional `verdict` of `chosen` or `rejected`, and a `load` that
106
- imports its own module.
107
-
108
- In a call that runs past about a dozen variants, every variant names the pass that drew it with
109
- `round: 'r3'`. **That tag is the only thing that makes a round exist**, in the order the variants introduce
110
- them. A round whose variants all carry a verdict is **settled**.
111
-
112
- `rounds` is prose only. Each entry gives a `key`, a `title` and a `note` saying what came out
113
- of that pass, which is how the intro stays short and each pass reads as a reply to the one
114
- before. An untagged round still draws - it says the bare key until somebody writes the entry. A
115
- `rounds` entry that no variant names is stale prose, and the check reports it. Write the note in the same pass that sets the verdicts, never
116
- before the user rules.
117
-
118
- A call with one variant and no `claim` is a **view**: one reference picture, drawn full
119
- width with no verdict tag. Use it for a picture that answers no question.
120
-
121
- Two call-level fields take a call out of the open list:
122
-
123
- - `closed: 'shipped'` once the app implements the call, `closed: 'superseded'` once another
124
- call replaced it. A closed call counts as settled, even with unruled variants.
125
- - `waitsFor: '<slug>'`, the full slug of another call, when this call is parked on it. Studio lists
126
- it under "On hold" and leaves it out of the open counts until that call settles. Then it is
127
- open again by itself, so remove the field only when the reason changes.
128
-
129
- Three constraints the tool puts on a variant file:
130
-
131
- - **Never import a package barrel.** The libraries resolve to source, so one barrel makes
132
- the browser request every module in the library, and the requests fail with
133
- `ERR_INSUFFICIENT_RESOURCES`. Import the one file you need.
134
- - **The fixture is shared, and a variant may not change it.** Variants drawn at three
135
- geometries cannot be compared.
136
- - **One file per variant**, so a broken variant breaks its own frame only.
126
+ Variants are alternatives. A variant never stands for a state of another variant: a hidden or
127
+ collapsed mode of variant d is a further state of d, not a variant e. Before you draw a new control,
128
+ look for a control that already does the job (an active rail button that toggles its panel), and
129
+ draw that first.
130
+
131
+ A section of an app is a page under `pages/`, never a state of the shell. An element holds its
132
+ own chrome only. A sketch on an element adds no state whose difference is page content in a slot.
133
+
134
+ A variant mirrors the product layout and holds only the parts it changes. Draw two to four
135
+ variants, each with what it costs, and run `sketch check` before `ready`. A variant with problems
136
+ cannot be picked. A part in a variant keeps every state the part has in the product: `sketch check`
137
+ and `sketch ready` fail on a dropped state, and warn when two stylesheets of one part define the same class. A problem the product already had prints as `NOTE (was there before)` and fails
138
+ neither. Use `sketch list` to see the open ones.
139
+ `sketch check` also runs tsc over each variant with its `@shared/` and `@system/` imports, so never write a
140
+ tsconfig of your own. It warns of a variant stylesheet that restyles a class of `shared/`, of a
141
+ variant file nothing imports, and of a `shared/` export the variant does not import; `design check` warns of
142
+ an export in a merged part's `shared/` that nothing imports, and (`STALE-REPLACE`) of a `.replace('<…')` or
143
+ `.replace(/<…/)` on markup that neither the part nor a part it uses draws. To delete a file of a part, delete it in the variant: a merge deletes the
144
+ product file the picked variant no longer holds.
145
+
146
+ Each variant's note, what it is and what it costs, goes in with `sketch new --variant-note a=<text>`, once
147
+ per variant: `sketch.json` keeps it apart from the part's `definePart({ note })`, and Studio shows it in the
148
+ variant's head. A merge keeps the part's note, takes its `--note`, or gives a new part the picked variant's note.
149
+ `sketch new` and `sketch variant` write their files in the checkout's Prettier format; after an edit,
150
+ `sketch check --fix` formats the variants before it checks them. A sketch command
151
+ without `--product` takes the product that holds the sketch.
152
+
153
+ Start a new part from a close one with `--like`: `sketch new pages/inbox --like pages/system`
154
+ copies the product's `pages/system` into each variant, renamed to the target and without its
155
+ `madeBy`; `--like pages/system:b` copies variant b of the open sketch on it, with that sketch's
156
+ `shared/` folder.
157
+
158
+ Code the variants of one sketch have in common (sample data, a switch, a helper) goes in the
159
+ sketch's `shared/` folder, next to the variant folders, never into copies per variant. A part in a
160
+ variant imports it as `@shared/<path>`, and a shared file may import `@system/...` and
161
+ `@shared/...`. A part outside a sketch may not import `@shared/`. A variant that needs a shared file
162
+ changed holds its own copy at `<variant>/shared/<path>`; `@shared/<path>` finds that copy first, and a merge
163
+ of that variant takes it. On a merge, `shared/` moves into
164
+ the target's folder as `shared/` with its imports made relative; when no part imports it, it is
165
+ dropped. Only the target may import it: a merge refuses while another part of the picked variant
166
+ does, so move that code into the part. `sketch new` prints each file it puts in `shared/`, for example
167
+ the `shared/` folder of a merged page: edit it there, as part of the sketch, not in a variant copy.
168
+
169
+ ```ts
170
+ // sketches/pages-flows/shared/flow-data.ts
171
+ export const flows = [{ name: 'Checkout', steps: 4 }];
172
+
173
+ // sketches/pages-flows/a/pages/flows/flows.ts
174
+ import { flows } from '@shared/flow-data';
175
+ ```
176
+
177
+ An asset (a mark, logo, icon, picture or font) is one file in `.ethlete/design/<product>/assets/`,
178
+ never a copy inlined into parts. A part imports it as `@assets/<file>`: the default import is a URL
179
+ for `<img src>`; an SVG that must follow tokens (`currentColor`, `var(--accent)`) imports with
180
+ `?raw` and goes inline, for example `@assets/icons/<name>.svg?raw`. A stylesheet, the product's
181
+ `styles.css` or a part's, uses `url('@assets/<file>')`, for example in an `@font-face`. A foundation
182
+ part may wrap an asset in a named export. A product finds its base product's assets too. A sketch
183
+ variant adds or changes an asset in its own `<variant>/assets/` folder (`sketch new` and `sketch variant`
184
+ print it next to the part file), and a merge copies it into the product's. Size an inline SVG with
185
+ `svgAsset(mark, { size: 32 })` from `@design-system`, never by replacing its `width`/`height` as a
186
+ string; `class` and `attrs` (`{ 'aria-hidden': 'true' }`) set the rest of the root's attributes. `design check` fails an SVG asset with no root `<svg>` or without
187
+ `xmlns="http://www.w3.org/2000/svg"` (an `<img src>` and an image viewer do not draw it). The part hash behind `design check` and "changed outside a sketch" counts the bytes of
188
+ the assets a part uses.
189
+
190
+ Every change to a merged part goes through a sketch, also a small one: new copy, sample data, an
191
+ added state. When there is nothing to choose, the sketch has one variant (`--variants a`), and the
192
+ user merges it or comments on it. Never edit a merged part directly, or the user cannot compare or
193
+ comment on the change in Studio. `design check` warns, and Studio marks the part "changed", when a
194
+ merged part changed outside a sketch; only a merged sketch on that part clears it.
195
+
196
+ A sketch on a foundations part such as `foundations/tokens` keeps the part's overview drawing: the
197
+ variant adds its token group as a state, or extends that group's state. It never replaces the
198
+ drawing with a demo scene, since the merge would drop every other group. A demo may be an extra state.
199
+
200
+ Name the variant you work on with `--variant`. Studio marks it as drawing, and hides its frames,
201
+ while the latest report names it. `sketch new` starts no report, so `report --done` follows only a report
202
+ of your own; it prints the next steps. `sketch new`, `sketch variant` and each report record your
203
+ model id as an author of the variants they write, and `sketch ready` records it on every variant
204
+ that has no author yet. A merge adds the picked variant's authors to the `madeBy` the part file
205
+ already holds, and never removes it.
206
+
207
+ ### Combine variants in a new round
208
+
209
+ Studio never combines variants. When the user asks for one in a comment ("solid like a, outline
210
+ like b"), draw it as a new variant in a new round:
137
211
 
138
- A `<script>` in a drawing's `body` never runs. For behavior - a play button that restarts a
139
- CSS animation, a click that toggles a class - give the drawing a `setup(root)` function. The
140
- frame calls it once, after the markup is in `root`. `setup` has no cleanup hook, because each
141
- frame load is fresh. Wire behavior with one delegated listener on `root`. `setup` can also be an array of functions, run in order. If one throws, the
142
- markup stays and the error shows on top of it.
212
+ ```bash
213
+ et design sketch variant <layer>/<name> c --from a --by <model id> --round "Solid a, outline b" --note "r1 showed pill and square; the user wants a's solid with b's outline"
214
+ ```
215
+
216
+ `--from` copies a variant into the new one, sets the sketch back to in progress and clears the
217
+ pick; it refuses while the decision is a discard (the user picks a variant first). Without `--round` the new variant joins the running round; with it, `--note` is required and
218
+ goes on the running round: what it showed and why its variants lost. Older variants stay pickable.
219
+ The command prints the notes of the previous round. Fix a round's title or note later with `sketch round <layer>/<name> <round> --title … --note … --by <model id>`; `sketch list` shows each sketch's rounds.
220
+ Letters continue across rounds (r1: a, b; r2: c, d). `sketch variant <layer>/<name> c,d --from a` adds
221
+ several variants at once; with `--round` they all start the new round. A new round answers the comments on the
222
+ `--from` variant and marks them "addressed in round" with the new round's key. A variant without `--round` fails while the sketch has open comments and lists them: a variant that answers a comment starts a new round. Draw the combined
223
+ variant and mark the sketch `ready` again. `sketch new --round <title> --note <text>` names
224
+ round 1 and sets its note; `--round` without `--note` fails. `sketch ready` refuses while the running round has no note; set it with `sketch round`.
225
+ `sketch ready` also shoots every state of every variant and of the product's part: when not one pixel differs
226
+ in any of them, it merges the sketch itself and logs it as an `auto` entry in the product's `accepted.json`
227
+ ("No visible change in any state: merged automatically."). Any difference or failed shot leaves it `ready`.
228
+ A problem the product already had before the merge does not block it (the check prints it as a note); only a problem the merge adds does.
229
+ Run `sketch ready` again on a `ready` sketch to repeat the check and the auto merge.
230
+
231
+ ### Your model id: `--by`
232
+
233
+ Every command that writes takes `--by <model id>` and fails without it: `sketch new`, `variant`, `report`,
234
+ `ready`, `rebase`, `merge`, `discard`, `inbox done` and `comment reply`. The id is the exact model
235
+ id as free text, for example `claude-opus-5-5`, `gpt-5.6-sol` or `gemini-3-argon`. Studio shows it
236
+ on the variant, the reply and the part, so the user sees which model made what.
237
+
238
+ **Never guess your id.** Use the one your prompt names. If your prompt names none and you do not
239
+ know your own exact id, ask the coordinator or the user before you write.
240
+
241
+ ### The user decides, Studio merges
242
+
243
+ The user picks a variant, or discards the sketch, in Studio. **Studio merges on the user's click.
244
+ Never run `sketch merge` or `sketch discard` yourself, and never pick a variant.** Both mark
245
+ the open comments on the sketch's variants addressed: the user's click answers them. Comments on the
246
+ product's parts stay. If a part the variant replaces changed
247
+ in the product since the sketch was created, the merge refuses: bring each variant up to date, run
248
+ `et design sketch rebase <layer>/<name>`, and mark it `ready` again. `sketch preview <layer>/<name>
249
+ <variant>` lists the elements and pages a merge changes. The note on a part records why it has its
250
+ current shape. The user's reason is optional: with one, the merge sets the note from it; without
251
+ one, the part keeps the `note` the picked variant file holds, so write that note while you draw.
252
+
253
+ After the user approves a direct edit of a merged part (no sketch), run `et design accept <layer>/<name> --product <p> --by <model id> --note <why>` so `design check` stops warning `changed outside a sketch`. Never accept an edit the user did not approve.
254
+
255
+ To drop a merged part the user no longer wants (folded back into its host, say), run `et design part remove <layer>/<name> --product <p> --by <model id>`: it deletes the folder, its `merged.json` entry and its tsconfig path, and resolves its comments. It refuses while another part imports it or an open sketch targets it. Never `git rm` a part by hand.
256
+
257
+ ### Read the inbox
258
+
259
+ The user comments on a state of a part in Studio, at a point, or on a sketch variant. The sketch of
260
+ an element also shows each page that uses it, with each variant swapped in: a comment there names
261
+ the page as its part and the element's sketch variant, so change the variant, not the page.
262
+
263
+ ```bash
264
+ et design inbox [--json] [--product <p>] # open comments and rebases, then what waits for the user
265
+ et design inbox done <id> --note <text> --by <model id> # set the part note, mark the thread addressed
266
+ et design comment reply <id> --by <model id> <text> [--question] # answer a comment thread (takes --product)
267
+ ```
268
+
269
+ Read the inbox at the start of a session and after each sketch you mark `ready`. Answer a comment
270
+ by changing the sketch or the part, then `inbox done <id> --note "…"`. The note replaces the `note` of the part the comment was made on (for a sketch variant, the product's part), so write it to explain the whole shape of the part, and pass the old note unchanged when it still holds. `done` fails without `--note`; only the user withdraws a comment without one, in Studio. `done` also stores your `--by` where the comment sat: in the authors of the sketch variant, or in the `madeBy` of the part. A pick or discard decision is listed as waiting
271
+ for the user: Studio merges or discards on the user's click, so never run `sketch merge` after a pick.
272
+
273
+ A comment is a thread: the user and you both reply. When a comment is unclear, ask with
274
+ `comment reply --question` instead of guessing. The thread then moves to the "waiting for the user" group
275
+ (`waitingForUser` in `--json`) and returns to the inbox when the user answers. A thread there is
276
+ not work for you.
277
+
278
+ A thread has three states. **Open** waits for an answer. **Addressed** (`addressed`: who, when, why,
279
+ the round) means `inbox done`, a new round, a merge or a discard answered it: it leaves the inbox and
280
+ the check, Studio shows it with an "addressed" badge, and you may still `comment reply` on it to say
281
+ what you changed. A reply by the user opens it again. **Resolved** means the user closed it in Studio -
282
+ only the user resolves. The file stays so the user can look back at it; a reply or `done` on it fails.
283
+
284
+ Pass `--question` only when you cannot go on without the user's answer. Studio lists only those
285
+ threads under "Questions for you". A report of what you did is no question: reply without the
286
+ flag, and the thread shows as "answered" until the user withdraws it or replies.
287
+
288
+ ### Draw a flow
289
+
290
+ A flow joins states of pages into a path the user reads left to right in Studio's Flows view.
291
+ It is `<product>/flows/<name>.json`; `position` and `label` are optional:
292
+
293
+ ```json
294
+ {
295
+ "frames": [{ "part": "pages/library", "state": "empty", "position": { "x": 0, "y": 0 } }],
296
+ "lines": [
297
+ {
298
+ "from": { "part": "pages/library", "state": "empty" },
299
+ "to": { "part": "pages/film-detail", "state": "default" },
300
+ "label": "user adds a film"
301
+ }
302
+ ]
303
+ }
304
+ ```
305
+
306
+ Every flow line is labelled, and the element the label names is visible in the frame before it: a line
307
+ labelled "user clicks 'Log in'" starts from a frame that shows a "Log in" button. A line that names
308
+ nothing the reader can see, or a label for a control the page lacks, is a defect.
309
+
310
+ A line may join two states of one page. A frame may name a page that exists only as an open sketch;
311
+ the flow then shows its first variant. Lines hold for every preset. Without `position`, Studio
312
+ lays the frames out along the lines.
313
+
314
+ ### Draw a wireframe
315
+
316
+ A wireframe exists to iterate fast, in minutes per round, rough enough to redraw with a client at
317
+ the table. It answers one thing: which regions a screen has and what each region holds. These
318
+ rules are strict:
319
+
320
+ - **Keep what exists.** Before a sketch, list what the running app already does on that screen.
321
+ Keep it as a labelled box, and draw only what is still open.
322
+ - **One sketch per screen or flow**, never one per region or per detail. The variants differ in
323
+ layout: where the regions sit and how big they are.
324
+ - **Regions and names only.** A region is a box with a label; a list shows item names. Draw no
325
+ counts, badges, status markers, inspectors, size labels or field lists. If a detail matters,
326
+ write it as a word in the region label.
327
+ - **Never polish.** Do not fix alignment, spacing or a kit gap. When the kit lacks a shape, draw a
328
+ labelled box. Look at each shot only for a broken frame: overflow, a scroll bar, clipped text.
329
+ - **Draw it yourself.** A wireframe round is small work: no subagent, no friction report.
330
+ - **Stop early.** When the regions of the main screens are settled, the stage ends. Comments on
331
+ the look of a wireframe belong to the next stage, not to a new wireframe round.
332
+
333
+ A page may not import another page. To show a page inside a page ("in context"), embed it:
334
+ `embedPage('pages/system', { state, width, height, scale })` from `@design-system` answers a
335
+ drawing whose `body` is a frame of that page, drawn in the same product, sketch variant and theme
336
+ as the embedding frame. `width` and `height` are the viewport the page is drawn at (default
337
+ 1280 x 800; `height: 'auto'` fits its content), `scale` shrinks the frame. Never rebuild a page
338
+ from its elements to show it.
339
+
340
+ A page that has no parts yet draws grey stand-ins from the wireframe kit; pages and elements may
341
+ import it, components and foundations may not:
342
+
343
+ ```ts
344
+ import { box, heading, textLines, image, button, input, icon, select } from '@design-system/kit';
345
+ ```
143
346
 
144
- For a prototype with states, render each state with `ngStates({ closed: …, open: … }, { imports })`
145
- and put its `body` in the drawing. `setup: flow.on([{ click: '.trigger', go: 'open' }])` swaps the
146
- states on a click, in a View Transition. To draw an open overlay, open it in
147
- the template (`<et-select [open]="true">`, `<div etMenu [open]="true">`): `ng()` returns the real
148
- panel, and the frame places it below its trigger. See `components/ssr/02-open-overlays`. `ngStates` wraps the states in a
149
- `<div data-de-states style="display: contents">` host. So `flow.body` must be the drawing's body,
150
- or contain it. A selector rule must not depend on that wrapper. See `components/ssr/01-states`.
347
+ Signatures are in `libs/cli/design/wireframe-kit.ts`: `box(children, { direction, gap, padding, width, height, grow, plain, stackBelow, fill })`,
348
+ `heading(text, { size })`, `textLines({ lines, width, size })`, `image({ width, height, ratio })`,
349
+ `button(label, { primary })`, `input({ label, placeholder, width })`, `icon({ size, glyph })`,
350
+ `select({ label, value, width })`, `tabs(labels, { active })`, `segmented(options, { active })`,
351
+ `checkbox(label, { checked })`, `badge(text)` and `dot({ hollow, size })`. Draw a control as its
352
+ type: a picker is a `select`, a section switch `tabs`, a size switch `segmented`, a count a `badge`,
353
+ a status a `dot`, never a row of buttons or an empty `icon`. Give an icon a `glyph` (`'plus'`,
354
+ `'close'`, `'menu'`, `'more'`, …; another name draws its first letter). Give the root box of a page or a
355
+ shell `fill: true` so it is as tall as the frame, instead of a hand-written `min-height`. Draw for
356
+ every preset, not one width: give a picture a `ratio` (`'2/3'`) instead of a fixed height, and give
357
+ a row `stackBelow: <px>`, the width below which it stacks its children, instead of fixed widths
358
+ that only fit the desktop. Never use `vh`, `dvh` or `svh` in a part: a part has no viewport of its own, so the unit measures Studio's frame, not the page. A full page is a frame of a fixed size. Put titles in `heading`, never in hand-written markup. Studio lists such a page under
359
+ "Wireframes" with its kit-use count. A shell is a normal element named by purpose (`shell-main`); draw a rough one with the kit, so each
360
+ page imports the shell instead of repeating it, and a shell pick lands in one file.
361
+ A dialog without its own URL is an element that a page state shows ("login open"). Examples in
362
+ `.ethlete/design/studio-lab`: `pages/film-detail`, `elements/shell-main`, `elements/login-dialog`,
363
+ `flows/sign-in.json`.
364
+
365
+ ### Check a product
366
+
367
+ ```bash
368
+ et design check --product <name> # layer dependencies, hard-coded colours, undefined tokens, copied components, scripted motion, tsc (open sketch variants too), render
369
+ et design system --json # each part's origin, note, states, presets and problems
370
+ ```
371
+
372
+ Without `--product`, `check` covers every product. A part may only use its own layer and the
373
+ layers below it (see "The order of a design"). Rule `FLOW` reports a misshaped flow file, a frame
374
+ naming a part or state that exists neither in the product nor in an open sketch of that part, a duplicate state, and a line to a state that is not a frame of
375
+ the flow. Rule `WIREFRAME-KIT` reports a component or foundation importing the kit. Pass the changed files to the check below as before.
376
+
377
+ Once the other rules pass, the check draws every state in a headless Chromium on a server of its own
378
+ (pages at each preset, a part with `widths` at each width, any other part fitted), and
379
+ `sketch check` does the same for the open variants and the pages they show in. Each line names
380
+ product, part, state, size and a selector path:
381
+
382
+ - `OVERFLOW` - the frame scrolls: content wider than the frame, or taller than a preset height.
383
+ - `CLIPPED` - an element with `overflow: hidden|clip` cuts its content, unless it sets
384
+ `text-overflow: ellipsis` or a line clamp.
385
+ - `WRAP` - text in a button, tab, badge, chip or `white-space: nowrap` box runs onto a second line.
386
+ - `ZERO-SIZE` - the part root has no width or no height.
387
+ - `UNKNOWN-THEME` - a `et-color--<name>` class names a colour theme no stylesheet of the frame defines, so it draws in the fallback colour.
388
+ - `RENDER` - the frame refused to draw, or the part did not load.
389
+
390
+ A `WARNING` line does not fail the check:
391
+
392
+ - `UNDEFINED-CLASS` - a class carries a prefix the product's part CSS uses (`st-`, not the SDK's
393
+ `et-`), but no stylesheet of the frame defines it. Named once, at the first part that draws it.
394
+ - `GRID-COVERAGE` - some `grid` rows fill every interaction column (`rest`, `hover`, `active`,
395
+ `focus-visible`, `disabled`) and others leave some out, so a state can break there unseen.
396
+
397
+ A part that breaks a rule on purpose names it: `definePart({ checks: { skip: ['CLIPPED'] }, … })`;
398
+ the skip covers the part wherever it is drawn. Without Playwright Chromium the check prints
399
+ `RENDER CHECK SKIPPED` and passes on the other rules.
400
+
401
+ A project's `config.json` entry can name `themes`, `{ "surface": [...], "color": [...] }`; a
402
+ frame asked for a theme gets the `et-surface--<name>` and `et-color--<name>` classes on its root,
403
+ so a drawing must take its colours from the theme tokens and never set a surface or colour class
404
+ on its own root. An inner panel may set `et-surface--<name>`.
405
+
406
+ `et design [checkout]` serves the frames; the checkout defaults to the working directory and
407
+ `DE_PORT` overrules the port. In the ethlete SDK the user reads Studio, started with `yarn studio`,
408
+ which serves the frames itself: do not start `yarn design` next to it. Studio carries its own copy
409
+ of the tool, so a checkout that installs nothing still draws.
410
+
411
+ Three constraints on a drawing file:
412
+
413
+ - **Never import a package barrel.** The libraries resolve to source, so one barrel makes the
414
+ browser request every module and the requests fail with `ERR_INSUFFICIENT_RESOURCES`. Import
415
+ the one file you need.
416
+ - **The fixture is shared, and a variant may not change it.**
417
+ - **One file per variant**, so a broken variant breaks its own frame only.
418
+
419
+ A `<script>` in a drawing never runs. For behavior give the drawing a `setup(root)` function
420
+ (called once after the markup is in `root`, no cleanup, wire one delegated listener). Render each
421
+ state of a prototype with `ngStates({ closed: …, open: … }, { imports })` and swap them with
422
+ `setup: flow.on([{ click: '.trigger', go: 'open' }])`. To draw an open overlay, open it in the
423
+ template (`<et-select [open]="true">`); `ng()` returns the real panel and the frame places it.
424
+ `ngStates` wraps the states in `<div data-de-states style="display: contents">`, so a selector
425
+ rule must not depend on that wrapper.
151
426
 
152
427
  **A repository with a sketch Storybook** draws every variant in the **same** story, side by
153
428
  side, under the real geometry the thing ships in. The default Storybook is
@@ -160,15 +435,31 @@ Either way:
160
435
  settled.
161
436
  - Keep the rejected variants drawn, marked as rejected.
162
437
 
438
+ ### Notes
439
+
440
+ A note explains a state: why it looks this way, what it shows. A comment is feedback that needs
441
+ an answer. Write a note when nothing is asked of the reader.
442
+
443
+ A note is `<product>/notes/<timestamp>-<id>.json`:
444
+
445
+ ```json
446
+ { "part": "button", "state": "disabled", "point": { "x": 24, "y": 8 }, "size": "mobile", "text": "Muted, not hidden." }
447
+ ```
448
+
449
+ - `part`, `state` and `text` are required. `point` is optional, in CSS pixels.
450
+ - `size` is required when `point` is set: the preset, the CSS pixel width, or `"fitted"` the note was written at. A
451
+ pointed note shows only at that size; a note without a point shows at every size.
452
+ - A free note on a flow is an entry in the `notes` array of `flows/<name>.json`:
453
+ `{ "id": "…", "text": "…", "at": { "x": 0, "y": -40 } }`. `at` is optional, the note's top-left
454
+ corner on the flow canvas, and may be negative.
455
+
163
456
  ## The order of a design
164
457
 
165
458
  A product design runs in this order. Each stage settles in calls before the next starts, and
166
- each stage draws with what the stages before it settled:
459
+ each stage draws with what the stages before it settled (the wireframes draw with the kit only):
167
460
 
168
- 1. **Foundations**: colours, colour themes and surface themes (the project's `themes` in
169
- `config.json`), plus the type scale, spacing and radius.
170
- 2. **Logo and CI**, if the project needs them.
171
- 3. **Wireframes**: early, rough concepts of the main flows (`mode: 'wireframe'`). They show
461
+ 1. **Wireframes**: early, rough concepts of the main flows (a legacy call with `mode: 'wireframe'`;
462
+ in a product, draw them with the wireframe kit, see "Draw a wireframe"). They show
172
463
  which components the product needs, not how they look. The stage ends with an inventory in
173
464
  the wireframe call: every part the wireframes need, each mapped to the SDK component it
174
465
  starts from, or marked new with the component call that draws it. The inventory is the
@@ -181,7 +472,10 @@ each stage draws with what the stages before it settled:
181
472
  ],
182
473
  ```
183
474
 
184
- 4. **Components**, from the SDK ones rendered with `ng()`, themed and set up for the project.
475
+ 2. **Foundations**: colours, colour themes and surface themes (the project's `themes` in
476
+ `config.json`), plus the type scale, spacing and radius.
477
+ 3. **Logo and CI**, if the project needs them.
478
+ 4. **Components**, the inventory is their queue, from the SDK ones rendered with `ng()`, themed and set up for the project.
185
479
  Each accepted one becomes a design-system entry.
186
480
  5. **Larger elements**: the shell, the navigation, headers, lists, built from the entries.
187
481
  6. **Pages**, built from the elements and the entries.
@@ -191,27 +485,36 @@ Do not start a stage before the one above it is settled, and do not draw a page
191
485
  are not in the design system. If the user asks for a later stage first, say which earlier stage
192
486
  is missing, and draw that first.
193
487
 
194
- A quick exploration of an idea does not run this order: start it with `--stage sketch`. A
195
- sketch needs no settled stage before it, and Studio lists it apart from the seven stages. When
196
- the idea holds, draw it again at its real stage.
197
-
198
- A call made before the stage was required carries `legacy: true`, which
199
- `et design legacy <project>` writes once. It also writes the stage its group folder implies:
200
- `foundations` is `themes`, `components` is `component`, `pages` is `page`; another folder gets
201
- none. Run it again to add the stage to an old legacy call; a stage already there stays. A
202
- settled legacy call with a stage counts for that stage in this order. Never add `legacy` to a
203
- new call.
488
+ The stages map onto the layers of a product: wireframes, foundations, components, elements, pages. A settled
489
+ Legacy call with a stage counts for that stage in this order.
204
490
 
205
491
  A later stage can show that an earlier one is incomplete. If a page needs a part that the system
206
- does not have, add it to the inventory and open a component call for it. Never draw the part
207
- inline in the page.
492
+ does not have, add it to the inventory. A part becomes a component only when at least two hosts
493
+ draw the same case. Until then it stays in the one element or page that draws it. When a second
494
+ host draws it, open a component sketch, and list in its brief every host and every case each one
495
+ draws, so the component covers them all.
496
+
497
+ A component documents its states per axis, not every combination: for example kind, end mark and
498
+ interaction (rest, hover, focus-visible, selected, inert), plus only the combinations that look
499
+ different. A smaller piece with no states of its own, such as an icon set, is a foundation asset,
500
+ not a component.
501
+
502
+ When each of several cases is drawn at rest and in the same interactions, build the states and their grid with
503
+ `interactionRows(rows, { interactions: ['hover', 'active'], force: (props, interaction) => ({ ...props, interaction }) })`
504
+ from `@design-system` and spread the result into `definePart`; never write those states and the grid by hand.
505
+ `sketch check` prints `grid <rows> x <columns> ok`, or the states the grid leaves out; a state in two cells
506
+ fails it as `GRID`, in `design check` too.
507
+ `definePart({ interactions })` forces an interaction on the part's root only. To draw one element of the part
508
+ hovered, focused, pressed or disabled, such as one row of a list, wrap that element's markup in
509
+ `forceInteraction(markup, 'hover')` from `@design-system` inside `render`, driven by a prop; never write
510
+ `data-et-force` by hand.
208
511
 
209
512
  ## Draw from the design system
210
513
 
211
514
  A call that invents its own button, field or row does not match the calls beside it. **Before
212
515
  you draw a part, look for it in this order, and draw new only when none of them has it:**
213
516
 
214
- 1. The project's design system, `.ethlete/design/system/<project>/`.
517
+ 1. The product's design system, `.ethlete/design/<product>/` (and its base).
215
518
  2. The real SDK components, rendered on the design server with `ng()` from `@design-explore`:
216
519
 
217
520
  ```ts
@@ -244,22 +547,10 @@ you draw a part, look for it in this order, and draw new only when none of them
244
547
  3. Only then a new drawing. Build it from the two above, and say in the variant's `claim`
245
548
  that the part is new.
246
549
 
247
- One entry is one folder:
248
-
249
- ```
250
- .ethlete/design/system/<project>/<entry>/
251
- <entry>.ts exports the drawing function, with typed options; built from the SDK mocks
252
- entry.md what it is, when to use it, which SDK parts and options, do and do not
253
- ```
254
-
255
- Its preview is a view call at `.ethlete/design/calls/<project>/system/<entry>/`, because the
256
- tool reads calls from `calls/` only. A variant imports the entry with a relative path. Read
257
- every `entry.md` of the project before the first drawing of a session.
258
-
259
- **An entry exists only because the user accepted it.** A `chosen` verdict is not enough: a
260
- chosen variant is often a layout or a one-off. When the user says to accept a part into the
261
- system, extract it in one commit: the entry folder, its preview call, and the source call's
262
- variant changed to import it. Never change an entry's look without a call the user settled.
550
+ A part is one folder, `<product>/<layer>/<part>/`, with its drawing and states. Read the parts of
551
+ the product before the first drawing of a session. **A part exists only because the user picked a
552
+ sketch.** A `chosen` verdict in an old call is not enough. Never change a part's look without a
553
+ sketch the user decided.
263
554
 
264
555
  ## Check before you look
265
556
 
@@ -273,7 +564,8 @@ With `et design`, run the checker from the checkout it draws:
273
564
  et design check --lint <changed files> --tsconfig
274
565
  ```
275
566
 
276
- `--tsconfig` with no path types every call of the checkout. `--checkout <path>` names another
567
+ `--tsconfig` with no path types every call of the checkout. `--tsconfig` also types each variant of
568
+ an open sketch and prints the tsc output of a failing one. `--checkout <path>` names another
277
569
  one.
278
570
 
279
571
  With a Storybook, copy {%resource:check-story.mjs%} to the **repository root** first:
@@ -293,11 +585,13 @@ et design check --call <slug> --variant b # one of them
293
585
  node check-story.mjs --tsconfig <path> --story <story-id>
294
586
  ```
295
587
 
296
- ## Delegating a call
588
+ ## Delegating a sketch
297
589
 
298
- Every brief names `model: opus`.
590
+ Every brief names `model: opus`, and every prompt names the exact model id the sub-agent runs on,
591
+ for example "Your model id is `claude-opus-5-5`; pass `--by claude-opus-5-5` to every write
592
+ command." You know the id because you chose the model; the sub-agent does not, and must not guess.
299
593
 
300
- 1. **One agent per call.** It draws all the variants of the call. Give it the call folder, the
594
+ 1. **One agent per sketch.** It draws all the variants of the sketch. Give it the sketch folder, the
301
595
  fixture it must not change, and the claim of each variant. Do not spawn one agent per
302
596
  variant: each would re-read the skill, the call, the fixture and the example, about 45-57k
303
597
  tokens each, so the cost is several times higher. Calls are independent, so several calls
@@ -305,9 +599,9 @@ Every brief names `model: opus`.
305
599
  2. **One check-and-fix agent, after the drawing agents return.** Give it the changed files
306
600
  and the call slug. It runs the check above, fixes what it reports, and repeats until the
307
601
  check says `ok`. It may not change what a variant draws, only what stops it rendering.
308
- 3. **One write-up agent, once the user settles the call.** Give it the verdict in the
602
+ 3. **One write-up agent, once the user settles the sketch in Studio.** Give it the verdict in the
309
603
  user's own words and the plan file. It records what won, what lost and why, it sets each
310
- variant's `verdict` in `call.ts`, and it writes that round's `note`. It runs while you open
604
+ variant's `verdict` in an old call's `call.ts` (a product sketch needs none; the merge sets the note). It runs while you open
311
605
  the next call.
312
606
 
313
607
  ## Screenshots