ucode-agent 1.0.0 → 1.1.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucode-agent",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "ucode - a terminal coding agent that reads, edits and runs your code. NVIDIA and Cohere models over OpenRouter.",
5
5
  "type": "module",
6
6
  "main": "ucode.js",
@@ -1,81 +1,160 @@
1
1
  ---
2
2
  name: build-app
3
- description: Take something from nothing to running choosing the stack, laying out the files, installing, wiring it up, and proving it works before saying it does.
4
- auto: scaffold, new project, from scratch, build an app, make an app, create an app, build a website, make a website, build a site, build me a, next.js app, nextjs app, next app, react app, vite app, shadcn, create-next-app
3
+ description: Take an app from nothing to running and finished stack choice, non-interactive scaffolding, project structure, secrets, AI and API integration, error handling, and proving it works before saying it does.
4
+ auto: scaffold, new project, from scratch, build an app, make an app, create an app, build a website, make a website, build a site, build me a, next.js app, nextjs app, next app, react app, vite app, shadcn, create-next-app, full stack, fullstack, saas, mvp
5
5
  ---
6
6
 
7
7
  # Building something from nothing
8
8
 
9
- The failure mode here is not writing bad code. It is delivering a folder of
10
- files that has never been run, described as if it works.
9
+ The failure mode is not bad code. It is a folder of files that has never been
10
+ run, handed over as if it works. Everything here is ordered to prevent that.
11
11
 
12
- ## Decide the shape before writing anything
12
+ ## 1. Decide the shape, out loud, before any file exists
13
13
 
14
- State these in one line each, out loud, then build to them:
14
+ One line each:
15
15
 
16
- - **What it does** — the single sentence a user would say.
17
- - **The stack** — and why. Default to the smallest thing that does the job: a
18
- single HTML file with no build step is a legitimate answer and often the
19
- right one. Reach for a framework when routing, state or a component tree
20
- genuinely earns it, not because the project sounds serious.
21
- - **The files** — the whole list, before you create any of them.
16
+ - **What it does** — the sentence a user would say.
17
+ - **The core loop** — the one path through it that must work perfectly
18
+ (e.g. upload a photo analysed see a score and the problems).
19
+ - **The stack, and why** the smallest thing that does the job:
22
20
 
23
- If the request has a user interface in it, the `ui-ux` skill is already loaded.
24
- Follow it. Do not design as you go and tidy up afterwards.
21
+ | Need | Choose |
22
+ | --- | --- |
23
+ | One page, no secrets, no server | a single `index.html`, no build step |
24
+ | Interactive client app, no secrets | Vite + React + TypeScript |
25
+ | Pages plus a server, secrets, API routes, SEO | Next.js App Router + TypeScript |
26
+ | An API on its own | Node (Hono/Express) or Python (FastAPI) |
25
27
 
26
- ## Scaffold without being asked questions
28
+ - **The file list** the whole tree, before creating any of it.
27
29
 
28
- Nothing you run has a keyboard. A scaffolder that stops to ask "Would you like
29
- to use TypeScript?" gets no answer and fails, so give it every answer up front:
30
+ If there is a user interface, the `ui-ux` skill is already loaded. Decide the
31
+ design direction now, not after the logic works.
32
+
33
+ ## 2. Scaffold without being asked questions
34
+
35
+ Nothing you run has a keyboard. A scaffolder that asks "Would you like to use
36
+ TypeScript?" gets no answer and fails, so give it every answer up front:
30
37
 
31
38
  ```bash
32
- # Next.js creates ./my-app; use . instead of a name to fill the current folder
39
+ # Next.js into ./my-app (use . to fill the current folder — it must be empty)
33
40
  npx create-next-app@latest my-app --ts --tailwind --eslint --app --src-dir --import-alias "@/*" --use-npm --yes
34
41
 
35
- # shadcn/ui, from inside the Next.js project
42
+ # shadcn/ui, from inside the project — every component you need, in one add
36
43
  npx shadcn@latest init -d -y
37
- npx shadcn@latest add button card input label badge progress separator skeleton sonner -y
44
+ npx shadcn@latest add button card input label badge progress separator skeleton sonner tooltip -y
38
45
  ```
39
46
 
40
- For any other scaffolder, find its flags for every question it would ask
41
- (`--help` lists them) before running it for real.
42
-
43
- `create-next-app` refuses a folder that already has files in it. If the current
44
- folder is not empty, scaffold into a named subfolder and pass that as `cwd` to
45
- every command after it.
46
-
47
- Add every shadcn component you will need in one `add` call, not one per call.
48
-
49
- ## Lay it out in one pass
50
-
51
- Use `batch_write` for the whole skeleton rather than `write_file` twenty times.
52
- One call, every file, in dependency order. Then `run_command` the install, and
53
- `run_commands` for anything independent that can happen at the same time.
47
+ - `create-next-app` refuses a folder that already has files. If the current
48
+ folder is not empty, scaffold into a named subfolder and pass it as `cwd` to
49
+ every later command.
50
+ - For any other scaffolder, find the flag for every question (`--help`) first.
51
+ - Install dependencies once, all together: `npm i zod lucide-react` not one
52
+ `npm i` per package.
54
53
 
55
- Real content from the first pass. Placeholder copy, `TODO`, and a commented-out
56
- function are all the same thing: a promise you did not keep, in a file the user
57
- now has to find.
54
+ ## 3. Structure it like a real project
58
55
 
59
- ## Wire everything
56
+ For Next.js App Router:
60
57
 
61
- Every button does its thing. Every form submits, validates and says what went
62
- wrong. Every list has an empty state. Every request has a loading state and a
63
- failure state. A control that does nothing is worse than no control, because
64
- the user has to try it to find out.
65
-
66
- If it stores anything, decide where, and make it survive a reload.
67
-
68
- ## Run it, then look at it
69
-
70
- - Start it with `run_command`. A dev server needs `background: true`, which
71
- returns immediately with a PID — a foreground server just burns the turn and
72
- gets killed.
73
- - Then actually exercise it: `curl` the routes, run the tests, open the page.
74
- A clean build is not evidence that it works, only that it compiles.
75
- - Fix what you find and run it again.
58
+ ```
59
+ src/
60
+ app/
61
+ layout.tsx fonts, metadata, <body> shell, Toaster
62
+ page.tsx the screen — composes components, holds little logic
63
+ globals.css design tokens and the shadcn theme variables
64
+ api/<name>/route.ts server-only endpoints; the only place secrets live
65
+ components/
66
+ <feature>/ one folder per feature: its pieces, split by job
67
+ ui/ shadcn components (generated edit via the theme)
68
+ lib/
69
+ <service>.ts calls to outside services, typed in and out
70
+ schemas.ts zod schemas shared by client and server
71
+ utils.ts
72
+ types/ shared TypeScript types, if lib/ does not own them
73
+ ```
76
74
 
77
- ## Report what happened
75
+ - **One component per file**, named for what it is (`ScoreDial.tsx`,
76
+ `NutrientFindings.tsx`, `LabelUpload.tsx`), not a 600-line `page.tsx`.
77
+ - Server components by default; `"use client"` only on the interactive parts.
78
+ - Types at every boundary. Parse external data with zod rather than trusting
79
+ its shape.
80
+
81
+ ## 4. Secrets and outside services
82
+
83
+ - **A key never reaches the browser.** It lives in a server route or server
84
+ action. Anything imported by a `"use client"` file ships to every visitor —
85
+ including a "hardcoded for now" key. If the user asks to hardcode one, put it
86
+ in a server-only module (`lib/server/*.ts`, or `import 'server-only'`) and
87
+ say where it is so they can move it to `.env.local` later.
88
+ - Every outbound call gets: a timeout (`AbortSignal.timeout(60_000)`), a check
89
+ of the response status, and an error that says what failed — surfaced to the
90
+ UI as a real message, never a silent `catch {}`.
91
+
92
+ ### Calling an AI model (OpenRouter or any OpenAI-compatible API)
93
+
94
+ ```ts
95
+ // src/app/api/analyze/route.ts — runs on the server only
96
+ export const runtime = 'nodejs';
97
+ export const maxDuration = 60;
98
+
99
+ const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
100
+ method: 'POST',
101
+ headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
102
+ body: JSON.stringify({
103
+ model: 'provider/model-id',
104
+ messages: [
105
+ { role: 'system', content: 'Reply with JSON only, matching this shape: {...}' },
106
+ { role: 'user', content: [
107
+ { type: 'text', text: 'Analyse this nutrition label.' },
108
+ { type: 'image_url', image_url: { url: dataUrl } }, // data:image/jpeg;base64,...
109
+ ] },
110
+ ],
111
+ }),
112
+ signal: AbortSignal.timeout(60_000),
113
+ });
114
+ ```
78
115
 
79
- Say what you built, how to start it, and what you checked. If something is
80
- untested or unfinished, say which part and why that sentence costs you
81
- nothing and saves the user an hour of finding out on their own.
116
+ - **Ask for JSON and parse it defensively.** Models wrap JSON in prose or code
117
+ fences: extract the first `{...}` block, `JSON.parse` it, validate with zod,
118
+ and on failure return a clear "could not read the result" error rather than
119
+ crashing. Clamp numbers to their range.
120
+ - **Put the judgement rules in the prompt, explicitly** — thresholds, what
121
+ counts as "too much", what to omit. A vague prompt gives a different answer
122
+ every time; a specific one gives the product its consistency.
123
+ - **Images:** check type and size on the client (e.g. ≤ 5 MB, jpeg/png/webp),
124
+ downscale large photos in a canvas before upload, send as a base64 data URL.
125
+ - Reasoning models may take 10–60s. Show progress, and make the route's
126
+ timeout longer than the model's.
127
+
128
+ ## 5. Build order
129
+
130
+ 1. Skeleton and design tokens, so every later piece is styled correctly first time.
131
+ 2. The server route with the real integration, tested with `curl` before any UI.
132
+ 3. The core loop UI, wired to the real route.
133
+ 4. Every state: empty, loading, success, error, and invalid input.
134
+ 5. Polish: motion, responsive, copy, favicon, page title and metadata.
135
+
136
+ Use `batch_write` for the skeleton — one call, every file.
137
+
138
+ ## 6. Prove it works
139
+
140
+ - `npm run build` — it type-checks and lints; a build that fails is not done.
141
+ - Start it: `npm run dev` goes to the background on its own and comes back with
142
+ the URL once ready. Do not start it twice.
143
+ - Exercise it: `curl` the API route with real input, load the page, check the
144
+ core loop end to end. A clean build proves it compiles, not that it works.
145
+ - Fix what you find and check again.
146
+
147
+ ## 7. Definition of done
148
+
149
+ - The core loop works end to end against the real service.
150
+ - No `TODO`, no placeholder copy, no dead buttons, no console errors.
151
+ - Every async action has loading, success and error states.
152
+ - Invalid input is caught with a useful message before it reaches the server.
153
+ - Secrets only on the server.
154
+ - `npm run build` passes.
155
+
156
+ ## 8. Report
157
+
158
+ What you built, the URL, how to start it again, and what you checked. If
159
+ anything is untested or unfinished, name it — one sentence of honesty saves the
160
+ user an hour of finding out on their own.
@@ -1,36 +1,81 @@
1
1
  ---
2
2
  name: code-review
3
- description: Review a change the way a careful colleague would — correctness first, then the things that will hurt later, with the reasoning attached.
3
+ description: Review code the way a senior engineer would — correctness first, then security, failure handling, contracts, performance and tests, with every finding ranked, located and backed by a concrete failing scenario.
4
+ auto: review, code review, review my, review this, audit this, look over, check my code, pr review, pull request
4
5
  ---
5
6
 
6
7
  # Reviewing code
7
8
 
8
- A review that lists everything is not a review, it is a linter with opinions.
9
- Rank what you find, and be honest about which parts you actually verified.
9
+ A review that lists everything is a linter with opinions. The value is in
10
+ finding what will actually break, ranking it, and being honest about what you
11
+ verified versus what you suspect.
10
12
 
11
- ## Read it in this order
13
+ ## 1. Understand before judging
12
14
 
13
- 1. **Correctness.** Does it do what it claims? Walk the edge cases: empty,
14
- one, many, null, the boundary value, the concurrent call, the second run.
15
- 2. **Failure.** What happens when the thing it depends on fails? A swallowed
16
- error and a bare `catch {}` are bugs waiting for the worst possible moment.
17
- 3. **Security.** Untrusted input reaching a query, a shell, a path, or the DOM.
18
- Secrets in the source. Credentials in a log line.
19
- 4. **Contracts.** Did a signature, a return shape or a stored format change
20
- without every caller and every existing row being accounted for?
21
- 5. **Clarity.** Names that say what the thing is. Comments that explain why,
22
- never what. A function that fits in your head.
23
- 6. **Tests.** Does the test actually fail when the code is wrong? A test that
24
- asserts a mock was called proves nothing about behaviour.
15
+ - What is the change trying to do? Read the description, the diff, and enough of
16
+ the surrounding code to know the callers and the data it touches.
17
+ - Get the diff: `git diff`, `git diff main...HEAD`, or the files named. Read
18
+ every changed file in full, not only the hunks context is where bugs hide.
19
+ - If there are tests, run them. If it runs, run it.
25
20
 
26
- ## Say it usefully
21
+ ## 2. Passes, in order of what hurts most
27
22
 
28
- For each finding: the file and line, what breaks, and the input that breaks it.
29
- "This could be cleaner" is not actionable. "`parse()` throws on an empty body,
30
- which the retry path hits on a 204" is.
23
+ 1. **Correctness** does it do what it claims for every input? Walk: empty,
24
+ one, many, null/undefined, the boundary value, duplicates, the second call,
25
+ concurrent calls, a slow or failing dependency.
26
+ 2. **Security** — untrusted input reaching SQL, a shell, a file path, a URL
27
+ fetch (SSRF), `innerHTML`/`dangerouslySetInnerHTML`, `eval`, a redirect.
28
+ Secrets in source, in client bundles, or in logs. Missing auth or
29
+ authorization checks on a route. IDs a user can change to see someone
30
+ else's data.
31
+ 3. **Failure handling** — errors swallowed (`catch {}`), a fallback that hides
32
+ a real failure, no timeout on an outbound call, a partial write left behind,
33
+ an error message that leaks internals.
34
+ 4. **Contracts** — a changed signature, return shape, API response, or stored
35
+ format without every caller and every existing record accounted for.
36
+ Migrations that are not reversible or not safe on live data.
37
+ 5. **Performance** — N+1 queries, unbounded loops over user data, missing
38
+ pagination, work in a render loop, a huge dependency for a small job,
39
+ blocking I/O on a hot path.
40
+ 6. **Concurrency and state** — shared mutable state, race conditions,
41
+ stale caches, React effects with missing or wrong dependencies.
42
+ 7. **Tests** — do they fail when the code is wrong? A test asserting a mock was
43
+ called proves nothing about behaviour. Are the risky paths covered?
44
+ 8. **Clarity** — names that say what things are, functions that fit in your
45
+ head, comments that explain *why*. Only flag this when it will cause a real
46
+ misunderstanding.
31
47
 
32
- Separate what you know from what you suspect, and say which is which. If you
33
- did not run it, do not describe the behaviour as if you watched it happen.
48
+ ## 3. Verify before you report
34
49
 
35
- Say what is good, briefly, and only where it is genuinely worth copying.
36
- Reviews that never approve of anything stop being read.
50
+ For each suspected issue, check it: read the caller, trace the value, run the
51
+ case if you can. Drop anything you cannot substantiate, or label it clearly as
52
+ a question rather than a finding.
53
+
54
+ ## 4. Write findings that can be acted on
55
+
56
+ Rank by severity:
57
+
58
+ - **Blocker** — wrong results, data loss, a security hole, a crash on a normal path.
59
+ - **Major** — breaks on a realistic edge case, or a failure that will be hard to diagnose.
60
+ - **Minor** — a real but small risk, or a clear maintainability cost.
61
+ - **Nit** — style and preference. Keep these few, or leave them out.
62
+
63
+ Each finding: **where** (`path:line`), **what breaks**, **the input or sequence
64
+ that breaks it**, and **the fix**. For example:
65
+
66
+ > **Major** — `src/app/api/analyze/route.ts:42` — `JSON.parse(text)` throws when
67
+ > the model wraps its reply in a code fence, which it does intermittently. The
68
+ > route then returns a 500 with no message. Extract the first `{…}` block and
69
+ > validate it with the zod schema; return a 422 with "could not read the label"
70
+ > on failure.
71
+
72
+ "This could be cleaner" is not a finding.
73
+
74
+ ## 5. Close out
75
+
76
+ Start with a one-line verdict (ship / ship after fixes / needs rework), then
77
+ the findings, most severe first. Mention what is genuinely good only where it
78
+ is worth copying. State what you did not review or could not run.
79
+
80
+ If asked to fix the findings, fix blockers and majors first, re-run the tests,
81
+ and report what changed.
@@ -1,47 +1,89 @@
1
1
  ---
2
2
  name: debug
3
- description: Find the actual cause of a bug instead of the first plausible one — reproduce it, narrow it, prove the fix, and leave a test behind.
3
+ description: Find and fix the real cause of a bug — reproduce it, narrow it down with evidence, prove the fix, guard it with a test, and check for the same bug elsewhere.
4
+ auto: bug, crash, crashes, crashing, broken, not working, doesn't work, does not work, stack trace, exception, traceback, throws, failing, fails, regression, undefined is not, cannot read properties, 500 error, blank page, hangs, freezes
4
5
  ---
5
6
 
6
7
  # Debugging
7
8
 
8
- The temptation is to read the code, form a theory, change something, and
9
- declare victory when the symptom disappears. That is how a bug gets moved
10
- rather than fixed.
9
+ The temptation is to read the code, form a theory, change something, and call
10
+ it fixed when the symptom goes away. That moves bugs rather than fixing them.
11
+ Work from evidence, in this order.
11
12
 
12
- ## Reproduce it first
13
+ ## 1. Reproduce it yourself
13
14
 
14
- Do not start from the description. Run the thing and see the failure with your
15
- own eyes: the command, the input, the exact error and where it comes from. If
16
- you cannot reproduce it, say so and ask for what you need the input, the
17
- version, the full stack. Guessing from a paraphrase wastes everyone's turn.
15
+ Do not debug from the description. Run it and see it fail: the exact command,
16
+ the input, the full error, the line it comes from. Write down the reproduction
17
+ as a single command or a few steps you will run it again at the end.
18
18
 
19
- ## Narrow before you theorise
19
+ If you cannot reproduce it, say so and ask for exactly what is missing: the
20
+ input, the environment, the version, the full output. Guessing from a
21
+ paraphrase wastes everyone's turn.
20
22
 
21
- - Read the whole stack trace, including the frames you assume are irrelevant.
22
- The top frame is where it surfaced, not necessarily where it went wrong.
23
- - `grep` for the message text to find where it is produced.
24
- - Cut the search space in half at a time: does the smaller input fail? Does it
25
- fail on the previous commit? Does the layer below get the right value?
26
- - Print or log the values at the boundary rather than reasoning about what they
27
- should be. What you believe is in that variable is the thing under suspicion.
23
+ ## 2. Read the whole error
28
24
 
29
- ## Fix the cause
25
+ - The whole stack trace, including frames you assume are irrelevant. The top
26
+ frame is where it surfaced, not necessarily where it went wrong.
27
+ - The first error, not the last. Later errors are often consequences.
28
+ - `grep` for the exact message text to find where it is produced.
29
+ - Check the obvious before the clever: is the file saved, the server restarted,
30
+ the right branch checked out, the env var set, the dependency installed, the
31
+ cache cleared (`.next`, `node_modules/.vite`, `__pycache__`)?
30
32
 
31
- State the cause in one sentence before you change anything: *this value is
32
- undefined here because the caller only sets it on the success path*. If you
33
- cannot write that sentence, you have not found it yet.
33
+ ## 3. Narrow it down
34
34
 
35
- Then fix that, not the symptom. A guard that hides the undefined value leaves
36
- the real defect in place, with one more layer over it.
35
+ Cut the search space in half each step:
37
36
 
38
- ## Prove it
37
+ - **Input:** does a smaller or simpler input still fail? Find the smallest one
38
+ that does.
39
+ - **Code:** comment out or bypass half the path. Does it still fail?
40
+ - **Time:** did it work before? `git log` / `git diff` since then, or
41
+ `git bisect` between a good and a bad commit.
42
+ - **Layer:** is the value right when it enters the function? When it leaves?
43
+ At the API boundary? In the database? Log it at each boundary and look,
44
+ rather than reasoning about what it "should" be.
39
45
 
40
- - Run the original reproduction. It must now pass.
41
- - Run the rest of the tests. A fix that breaks two other things is a trade,
42
- and the user gets to make it, not you.
43
- - Write a test that fails without your fix. A bug with no regression test comes
44
- back.
46
+ The value you are sure about is the one under suspicion. Print it.
45
47
 
46
- Then say what the cause actually was, in one or two sentences. If you fixed
47
- something adjacent along the way, say that too.
48
+ ## 4. Know where bugs usually live
49
+
50
+ - **Async:** a missing `await`, a race between two requests, state read before
51
+ it is set, a promise rejection nobody catches.
52
+ - **State:** stale closures in React effects, mutation of shared objects, a
53
+ cache that was never invalidated.
54
+ - **Boundaries:** off-by-one, empty arrays, `null` vs `undefined` vs `''`,
55
+ timezones, number parsing (`'10' + 1`), float rounding.
56
+ - **Data shape:** the API returned something different from the type — an
57
+ error object, a wrapped payload, a string instead of JSON.
58
+ - **Environment:** missing env var, wrong Node version, path case sensitivity,
59
+ Windows vs POSIX paths and line endings, a port already in use.
60
+ - **Build tooling:** a stale build cache, a server/client boundary violation in
61
+ Next.js, a default vs named export mismatch, ESM vs CommonJS.
62
+
63
+ ## 5. State the cause before fixing it
64
+
65
+ Write it in one sentence: *"`score` is `undefined` here because the parser
66
+ returns `{ data: {...} }` and the component reads `result.score`."* If you
67
+ cannot write that sentence, you have not found the cause yet — keep narrowing.
68
+
69
+ ## 6. Fix the cause, not the symptom
70
+
71
+ A `?.` or a `try/catch` that hides the failure leaves the defect in place under
72
+ one more layer. Fix it where it originates. Keep the change as small as the
73
+ cause allows, and do not refactor unrelated code in the same change.
74
+
75
+ ## 7. Prove it
76
+
77
+ - Run the original reproduction. It must pass now.
78
+ - Run the whole test suite. A fix that breaks two other things is a trade the
79
+ user gets to decide on, not you.
80
+ - Add a regression test that fails without the fix and passes with it — then
81
+ briefly revert the fix to confirm the test really catches it.
82
+ - Look for the same mistake elsewhere: `grep` for the same pattern, call, or
83
+ assumption. Bugs come in families.
84
+
85
+ ## 8. Report
86
+
87
+ The cause in one or two sentences, the fix, how you verified it, and anything
88
+ adjacent you noticed but did not change. If you could not fully confirm it,
89
+ say what is still uncertain.
@@ -1,237 +1,300 @@
1
1
  ---
2
2
  name: ui-ux
3
- description: How to design and build an interface that looks deliberate rather than generated — direction, tokens, layout, states, motion, accessibility. Loads itself for any work with a user interface in it.
4
- auto: app, apps, ui, ux, website, web app, webapp, web page, webpage, landing page, dashboard, frontend, front-end, interface, css, tailwind, html, react, vue, svelte, nextjs, next.js, redesign, restyle, responsive, dark mode, ugly, styling, stylesheet, mockup, prototype
3
+ description: Design and build interfaces at the level of a senior product designer who also ships the code — direction, type, colour, layout, components, states, motion, accessibility and performance, verified on screen. Loads itself for any work with a user interface in it.
4
+ auto: app, apps, ui, ux, website, web app, webapp, web page, webpage, landing page, dashboard, frontend, front-end, interface, css, tailwind, html, react, vue, svelte, nextjs, next.js, shadcn, redesign, restyle, responsive, dark mode, ugly, styling, stylesheet, mockup, prototype, component, components, polished, beautiful, good looking, visually
5
5
  ---
6
6
 
7
- # Interfaces
7
+ # Interfaces, done properly
8
8
 
9
9
  The house style of a language model is a centred column, a purple-to-blue
10
- gradient, three equal cards and a lot of empty space. It is recognisable on
11
- sight, and everyone has now seen it a thousand times. Your job is to not
12
- produce it.
10
+ gradient, three identical cards, Inter at every size, and a lot of empty space.
11
+ Everyone has seen it a thousand times, and it reads as generated on sight.
12
+ This skill exists to stop you producing it.
13
13
 
14
- This is not decoration applied at the end. It is the order the work happens in:
15
- direction, then structure, then tokens, then states, then verification.
14
+ Work in this order and do not skip ahead. Most bad interfaces are good CSS
15
+ applied to an undecided design.
16
16
 
17
- ## 1. Decide the direction before writing any CSS
17
+ 1. Direction 2. Structure 3. Tokens 4. Components 5. States 6. Motion
18
+ 7. Accessibility and performance 8. Look at it 9. Report
18
19
 
19
- Answer these to yourself in one line each, then build to the answers:
20
-
21
- 1. **Job** — what does this screen actually do?
22
- 2. **Who** — who opens it, how often, and what do they need first?
23
- 3. **Tone** — pick one and commit: utilitarian, editorial, technical, playful,
24
- industrial, calm, dense. "Modern and clean" is not a tone, it is a way of
25
- avoiding the question.
26
- 4. **One memorable detail** — a colour, a texture, a typographic move, a single
27
- interaction. Exactly one. It is the difference between a design and a
28
- template.
29
-
30
- ## 2. Pick the mode from the surface, not the product
31
-
32
- The mode names what success looks like for the person in front of it. It
33
- decides how much the interface is allowed to perform.
34
-
35
- - **Operate** — they are completing a task. App UI, dashboards, editors,
36
- admin, settings, tools. Scannability, consistency and speed beat expression
37
- every time. The personality lives in precise details, not in the hero.
38
- - **Persuade** — they are deciding whether to act. Landing pages, pricing,
39
- marketing. Here the design *is* the product; earn the attention.
40
- - **Read** — they are trying to understand something. Docs, articles, guides.
41
- Structure for comprehension first, then make reading pleasant enough to stay.
42
- - **Experience** — they are looking at the work itself. Portfolios, galleries.
43
- The artifact leads from the first screen and the interface gets out of the way.
20
+ ---
44
21
 
45
- A tool's landing page is still Persuade. A dashboard is still Operate however
46
- beautiful the brand is. Never put a marketing hero on top of a working tool.
22
+ ## 1. Decide the direction before any code
23
+
24
+ Write these down in one line each, then build to them:
25
+
26
+ - **Job** — what does this screen do, in one sentence a user would say?
27
+ - **Who and when** — who opens it, how often, on what device, in what light?
28
+ A tool opened forty times a day and a page seen once need opposite things.
29
+ - **Mode** — pick one:
30
+ - **Operate**: completing a task (apps, dashboards, tools, settings). Speed,
31
+ scannability and consistency beat expression. Personality lives in details.
32
+ - **Persuade**: deciding whether to act (landing, pricing, marketing). The
33
+ design is the product; it has to earn attention in one screen.
34
+ - **Read**: understanding something (docs, articles). Measure, rhythm and
35
+ hierarchy first; decoration last.
36
+ - **Experience**: looking at the work itself (portfolio, gallery). The
37
+ content leads; the interface gets out of the way.
38
+ - **Tone** — one word you commit to: clinical, warm, editorial, technical,
39
+ playful, industrial, calm, dense. "Modern and clean" is not a tone.
40
+ - **The one memorable thing** — a colour, a type move, a texture, a single
41
+ interaction. Exactly one. It is the difference between a design and a theme.
42
+
43
+ A dashboard stays Operate however loud the brand is. A tool's landing page is
44
+ still Persuade. Never put a marketing hero on top of a working tool.
45
+
46
+ ## 2. Structure: hierarchy before decoration
47
+
48
+ - Decide what the eye lands on first, second, third. Build that with **size,
49
+ weight, colour and position** before reaching for a box, a border or a card.
50
+ Three levels — primary, secondary, muted — is usually all a screen needs.
51
+ - **The primary thing gets disproportionate size.** If a score, a total or a
52
+ status is the point of the screen, make it unmistakably larger than
53
+ everything around it — not 10% bigger, three or four times bigger.
54
+ - Align to a grid and share edges. Ragged left edges are the single most common
55
+ reason a page feels amateur. Pick a max content width (e.g. 1120px for apps,
56
+ 68ch for prose) and hold it.
57
+ - Group by proximity: space **inside** a group must be smaller than space
58
+ **between** groups, or the grouping reads wrong however good the rest is.
59
+ - One job per element. A card that is a link, a form and a menu is three cards.
60
+ - Put actions where the eye already is: primary action at the end of the flow it
61
+ completes, destructive actions separated from safe ones.
47
62
 
48
- ## 3. Tokens first, then never a raw value again
63
+ ## 3. Tokens: set them once, never use a raw value again
49
64
 
50
- Set these at the top and use them everywhere. One-off hard-coded values are
51
- exactly how a design drifts out of alignment with itself.
65
+ Everything below is a starting point to adjust, not a look to ship unchanged.
66
+ The palette in particular must be re-picked for the product's tone.
52
67
 
53
68
  ```css
54
69
  :root {
55
- --font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
56
- --mono: ui-monospace, SFMono-Regular, "Cascadia Code", Consolas, monospace;
57
-
58
- /* One scale. Nothing between the steps. */
59
- --text--1: .8125rem; --text-0: 1rem; --text-1: 1.25rem;
60
- --text-2: 1.5rem; --text-3: 2rem; --text-4: 2.75rem;
61
-
62
- --s1: .25rem; --s2: .5rem; --s3: .75rem; --s4: 1rem;
63
- --s5: 1.5rem; --s6: 2rem; --s7: 3rem; --s8: 4rem;
64
-
65
- /* Neutrals carry a hue. Flat #808080 grey is what makes a UI look dead. */
66
- --bg: #fbfaf9; --surface: #ffffff; --line: #e6e2dd;
67
- --ink: #17161a; --ink-2: #55525c; --ink-3: #8a8792;
68
-
69
- --accent: #2f6fe0; --accent-ink: #ffffff; --accent-soft: #eaf1fe;
70
- --danger: #b42318; --ok: #217a4b; --warn: #b25e09;
71
-
72
- --radius: 10px; --radius-sm: 6px;
73
- --shadow: 0 1px 2px rgb(20 18 24 / .05), 0 8px 24px rgb(20 18 24 / .07);
70
+ /* Type a real scale, fluid between mobile and desktop. */
71
+ --font-sans: "Inter Tight", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
72
+ --font-display: var(--font-sans);
73
+ --font-mono: ui-monospace, "Cascadia Code", "JetBrains Mono", Consolas, monospace;
74
+
75
+ --text-xs: .75rem;
76
+ --text-sm: .875rem;
77
+ --text-md: 1rem;
78
+ --text-lg: clamp(1.125rem, 1rem + .4vw, 1.25rem);
79
+ --text-xl: clamp(1.375rem, 1.1rem + .9vw, 1.75rem);
80
+ --text-2xl: clamp(1.75rem, 1.3rem + 1.6vw, 2.5rem);
81
+ --text-3xl: clamp(2.25rem, 1.5rem + 3vw, 3.75rem);
82
+ --text-hero: clamp(3rem, 2rem + 5vw, 6rem);
83
+
84
+ /* Space one scale, nothing in between. */
85
+ --s-1: .25rem; --s-2: .5rem; --s-3: .75rem; --s-4: 1rem; --s-5: 1.5rem;
86
+ --s-6: 2rem; --s-7: 3rem; --s-8: 4rem; --s-9: 6rem; --s-10: 8rem;
87
+
88
+ /* Colour neutrals carry a hue; flat #808080 grey is what makes a UI look dead.
89
+ OKLCH so lightness steps are perceptually even. */
90
+ --bg: oklch(98.5% .004 90);
91
+ --surface: oklch(100% 0 0);
92
+ --surface-2: oklch(96.5% .006 90);
93
+ --line: oklch(90% .008 90);
94
+ --ink: oklch(22% .01 90);
95
+ --ink-2: oklch(45% .012 90);
96
+ --ink-3: oklch(60% .01 90);
97
+
98
+ --accent: oklch(58% .19 255);
99
+ --accent-ink: oklch(99% 0 0);
100
+ --accent-soft: oklch(95% .03 255);
101
+
102
+ --good: oklch(62% .16 150); --good-soft: oklch(95% .04 150);
103
+ --warn: oklch(72% .16 70); --warn-soft: oklch(96% .05 80);
104
+ --bad: oklch(58% .21 25); --bad-soft: oklch(95% .04 25);
105
+
106
+ --radius-sm: 6px; --radius: 10px; --radius-lg: 16px; --radius-full: 999px;
107
+ --shadow-sm: 0 1px 2px oklch(20% .01 90 / .06);
108
+ --shadow: 0 1px 2px oklch(20% .01 90 / .05), 0 8px 24px oklch(20% .01 90 / .08);
109
+ --shadow-lg: 0 2px 4px oklch(20% .01 90 / .06), 0 24px 48px oklch(20% .01 90 / .14);
74
110
  --focus: 0 0 0 2px var(--bg), 0 0 0 4px var(--accent);
111
+
112
+ --ease-out: cubic-bezier(.22, 1, .36, 1);
113
+ --dur-1: 120ms; --dur-2: 200ms; --dur-3: 320ms;
75
114
  }
76
115
 
77
116
  @media (prefers-color-scheme: dark) {
78
117
  :root {
79
- --bg: #101014; --surface: #17171c; --line: #2a2a33;
80
- --ink: #f2f1f5; --ink-2: #b3b0bd; --ink-3: #807d8a;
81
- --accent: #6fa4ff; --accent-ink: #0f1016; --accent-soft: #16203a;
118
+ --bg: oklch(16% .008 260); --surface: oklch(20% .01 260);
119
+ --surface-2: oklch(24% .012 260); --line: oklch(30% .012 260);
120
+ --ink: oklch(95% .005 260); --ink-2: oklch(76% .01 260); --ink-3: oklch(60% .01 260);
121
+ --accent: oklch(70% .16 255); --accent-ink: oklch(18% .02 260); --accent-soft: oklch(28% .06 255);
82
122
  }
83
123
  }
84
124
 
85
- * { box-sizing: border-box; }
125
+ *, *::before, *::after { box-sizing: border-box; }
126
+ html { -webkit-text-size-adjust: 100%; }
86
127
  body {
87
128
  margin: 0; background: var(--bg); color: var(--ink);
88
- font: var(--text-0)/1.6 var(--font);
89
- -webkit-font-smoothing: antialiased;
129
+ font: var(--text-md)/1.6 var(--font-sans);
130
+ -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;
90
131
  }
91
- h1, h2, h3 { margin: 0; line-height: 1.15; letter-spacing: -.02em; }
92
- :focus-visible { outline: none; box-shadow: var(--focus); }
132
+ h1, h2, h3 { margin: 0; line-height: 1.1; letter-spacing: -.02em; text-wrap: balance; }
133
+ p { text-wrap: pretty; }
134
+ :focus-visible { outline: none; box-shadow: var(--focus); border-radius: var(--radius-sm); }
135
+ .num { font-variant-numeric: tabular-nums; }
93
136
  @media (prefers-reduced-motion: reduce) {
94
- *, *::before, *::after { animation: none !important; transition: none !important; }
137
+ *, *::before, *::after { animation-duration: 1ms !important; transition-duration: 1ms !important; }
95
138
  }
96
139
  ```
97
140
 
98
- **Swap the four palette lines to change direction, and keep everything else:**
99
-
100
- - **Paper** `--bg:#faf9f7; --surface:#fff; --ink:#1a1815; --accent:#b45309`.
101
- Warm, calm, for reading and for tools used daily.
102
- - **Console** `--bg:#0d1117; --surface:#161b22; --ink:#e6edf3; --accent:#2f81f7`
103
- with `--line:#30363d`. Technical and dense, for dashboards and dev tools.
104
- - **Editorial** `--bg:#fffdf8; --surface:#fff; --ink:#141414; --accent:#c2410c`
105
- plus a serif on headings only. For content and landing pages.
106
-
107
- One direction, all the way through. Half Console and half Editorial reads as a
108
- mistake, because it is one.
109
-
110
- **The rules behind the tokens**, for when you need a value that is not in them:
111
-
112
- - **Spacing** every value from the scale. Space *inside* a group must be
113
- smaller than the space *around* it, or the grouping reads wrong however good
114
- the rest is.
115
- - **Type** one family for UI, at most one more for display. Body 15–17px,
116
- line-height 1.5–1.65, measure capped at 65–75ch. Headings tighter: 1.1–1.25,
117
- and `letter-spacing: -.02em` above 28px.
118
- - **Colour** one accent hue, one neutral ramp, semantic red/amber/green. Never
119
- pure `#000` on pure `#fff`.
120
- - **Radius** pick one and derive: inputs and buttons 6–8, cards 10–12,
121
- pills 999. Four unrelated radii look like an accident.
122
- - **Depth**borders and background steps first, shadows last, and only for
123
- things that genuinely float: menus, modals, toasts. A shadow on every card
124
- flattens the hierarchy instead of building it.
125
- - **Dark mode** — swap the variables. Never invert. Surfaces get *lighter* as
126
- they rise, and pure white on near-black is too harsh: use around 90%.
127
-
128
- ## 4. Layout
129
-
130
- - Build hierarchy with size, weight and colour before reaching for a box.
131
- Three levels — primary, secondary, muted is usually all you need.
132
- - Align to a grid and share edges. Ragged left edges are the single most
133
- common reason a page feels amateur.
134
- - Full width is not a layout. Constrain content to what the content needs: a
135
- table wants width, prose does not.
136
- - One job per element. A card that is a link, a form and a menu is three cards.
137
-
138
- ## 5. States are most of the work
139
-
140
- An interface that only handles the happy path is a mockup. For every screen:
141
-
142
- - **Empty** first run, nothing there yet. Say what this is and how to make
143
- the first one. Never a blank box.
144
- - **Loading** — skeletons shaped like the real content, or a spinner on the
145
- control that was pressed. Do not blank the page.
146
- - **Error** — what failed and what they can do about it. Keep their input.
147
- - **Partial** — one row failed and the rest loaded.
148
-
149
- And for every interactive element: `:hover`, `:focus-visible`, `:active`,
150
- `:disabled`, and the selected state. A control with only a default state is
151
- unfinished, not minimal. `outline: none` with no replacement focus ring is a
152
- bug, not a style choice.
141
+ **The rules behind the tokens**, for when you need a value that is not there:
142
+
143
+ - **Type.** One family for UI, at most one more for display. Body 15–17px at
144
+ 1.5–1.65 line height, measure 60–75ch. Headings 1.05–1.2 line height with
145
+ negative tracking above 28px. Weight does more than size for mid-level
146
+ hierarchy: 600 for labels that matter, 400 for body, 500 for UI controls.
147
+ Use `tabular-nums` anywhere numbers line up or change in place.
148
+ - **Pick a typeface with intent.** Inter everywhere is the generated look. Good
149
+ free choices through `next/font/google` or Google Fonts: *Inter Tight,
150
+ Geist, Manrope, DM Sans, Plus Jakarta Sans, Instrument Sans, Space Grotesk,
151
+ IBM Plex Sans* for UI; *Fraunces, Instrument Serif, Bricolage Grotesque,
152
+ Newsreader* for display. Load only the weights you use, `display: swap`.
153
+ - **Colour.** One accent hue, one neutral ramp tinted slightly toward it, and
154
+ semantic good/warn/bad that mean one thing each. Colour is for meaning and
155
+ emphasis, not decoration. Never pure `#000` on pure `#fff`. Count the hue
156
+ families at the end: more than one accent plus the semantics means the
157
+ palette got away from you.
158
+ - **Radius.** Pick one base and derive: controls 6–8, cards 10–16, pills full.
159
+ Four unrelated radii look like an accident.
160
+ - **Depth.** Borders and background steps first; shadows only for things that
161
+ genuinely float (menus, popovers, modals, toasts). A shadow on every card
162
+ flattens the hierarchy it was meant to create.
163
+ - **Dark mode.** Swap variables, never invert. Surfaces get *lighter* as they
164
+ rise. Text around 92–95% lightness, not pure white. Re-check every accent for
165
+ contrast in dark most need to get lighter.
166
+
167
+ ### With Tailwind and shadcn/ui
168
+
169
+ - shadcn ships with a neutral slate look. **Re-theme it** in `globals.css` by
170
+ setting its CSS variables (`--background`, `--foreground`, `--primary`,
171
+ `--muted`, `--accent`, `--destructive`, `--border`, `--ring`, `--radius`) to
172
+ your palette. Shipping the default theme is shipping someone else's design.
173
+ - Use components for behaviour and accessibility (Dialog, Popover, Tabs,
174
+ Select, Tooltip, Toast/Sonner), then style them to the direction. Do not wrap
175
+ every region of the page in a `Card` that is the three-card look again.
176
+ - Extend the Tailwind theme with your tokens rather than scattering arbitrary
177
+ values (`text-[17px]`, `mt-[13px]`). Arbitrary values are the raw values this
178
+ section forbids, in a different syntax.
179
+ - Icons: `lucide-react`, one stroke width throughout, sized to the text beside
180
+ them (16px with 14–15px text, 20px with 16–18px). Never emoji as UI icons.
181
+
182
+ ## 4. Components, with every state built in
183
+
184
+ Every interactive element needs **default, hover, focus-visible, active,
185
+ disabled**, and where it applies **selected, loading, error**. A control with
186
+ only a default state is unfinished, not minimal.
153
187
 
154
188
  ```css
155
189
  .btn {
156
- font: 500 var(--text-0)/1 var(--font);
157
- padding: var(--s3) var(--s5);
190
+ display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2);
191
+ min-height: 44px; padding: 0 var(--s-5);
192
+ font: 500 var(--text-md)/1 var(--font-sans);
158
193
  border: 1px solid transparent; border-radius: var(--radius-sm);
159
194
  background: var(--accent); color: var(--accent-ink);
160
- cursor: pointer; transition: filter .15s, transform .05s;
161
- }
162
- .btn:hover { filter: brightness(1.08); }
163
- .btn:active { transform: translateY(1px); }
164
- .btn:disabled { opacity: .45; cursor: not-allowed; filter: none; }
165
- .btn--quiet { background: transparent; color: var(--ink); border-color: var(--line); }
166
- .btn--quiet:hover { background: var(--accent-soft); }
167
-
168
- .input {
169
- width: 100%; padding: var(--s3) var(--s4);
170
- font: var(--text-0) var(--font); color: var(--ink);
171
- background: var(--surface);
172
- border: 1px solid var(--line); border-radius: var(--radius-sm);
195
+ cursor: pointer; transition: filter var(--dur-1), transform var(--dur-1) var(--ease-out);
173
196
  }
174
- .input::placeholder { color: var(--ink-3); }
197
+ .btn:hover { filter: brightness(1.07); }
198
+ .btn:active { transform: translateY(1px) scale(.99); }
199
+ .btn:disabled, .btn[aria-busy="true"] { opacity: .5; cursor: not-allowed; filter: none; }
200
+ .btn--quiet { background: transparent; color: var(--ink); border-color: var(--line); }
201
+ .btn--quiet:hover { background: var(--surface-2); }
175
202
  ```
176
203
 
177
- ## 6. Motion
204
+ - **Buttons:** verb labels ("Analyze label", not "Submit"). One primary per
205
+ view. Loading state replaces the label's icon with a spinner and keeps the
206
+ width fixed so nothing jumps.
207
+ - **Inputs:** a visible `<label>` always — a placeholder is not a label. Help
208
+ text below, errors below in `--bad` with an icon, `aria-invalid` and
209
+ `aria-describedby` wired up. Validate on blur, re-validate on input once
210
+ an error is showing, never shout on the first keystroke.
211
+ - **File upload:** a real drop zone *and* a click target, keyboard operable,
212
+ showing accepted types and max size before the user tries. Show the chosen
213
+ file (thumbnail for images), let them replace or remove it, validate type and
214
+ size on the client before sending anything.
215
+ - **Scores and metrics:** the number large, with its scale ("7.4 / 10"), a
216
+ label that says what it measures, and a colour band (good/warn/bad) that is
217
+ never the *only* signal — pair it with a word ("Good", "Moderate", "Poor").
218
+ - **Lists of issues or findings:** most severe first; each with the thing, the
219
+ value, why it matters, in one or two lines. If there are none, say so plainly
220
+ once — do not render an empty section header.
221
+ - **Tables:** right-align numbers, left-align text, tabular numerals, sticky
222
+ header on long tables, row hover, and a real empty state.
223
+
224
+ ## 5. States are most of the work
178
225
 
179
- One authored moment, not effects scattered everywhere, and not the same
180
- entrance animation on every section. 120–200ms, on `transform`, `opacity`,
181
- `filter` and colour only — never on `height` or `width`. Ease out from a state
182
- that is already visible. Honour `prefers-reduced-motion` every time.
226
+ An interface that only handles the happy path is a mockup. For every screen
227
+ and every async action:
183
228
 
184
- ## 7. Non-negotiable
229
+ - **Empty / first run** — what this is, and the one action that starts it.
230
+ Illustrated or typographic, never a blank rectangle.
231
+ - **Loading** — skeletons shaped like the real content, or a progress
232
+ indicator on the control that was pressed. Say what is happening if it takes
233
+ more than a second ("Reading the label…"). Never blank the page.
234
+ - **Success** — the result, with a clear next action (try another, share, copy).
235
+ - **Error** — what failed in plain words, what to do next, and the user's input
236
+ preserved. Distinguish "you can fix this" (wrong file type) from "we failed"
237
+ (network, model error) — they need different words and different actions.
238
+ - **Partial** — some parts loaded, one failed; show what worked.
239
+ - **Edge content** — the longest realistic name, a value of zero, a missing
240
+ field, a thousand rows. Design for them, do not discover them.
241
+
242
+ Announce async results to screen readers with an `aria-live="polite"` region.
243
+
244
+ ## 6. Motion
185
245
 
186
- - Contrast 4.5:1 for body text and placeholders, 3:1 for large text and for
187
- the borders of controls.
188
- - Every control reachable and operable by keyboard, in the order it appears.
189
- - Labels on inputs. A placeholder is not a label.
190
- - Touch targets 44px.
191
- - `<button>` for actions, `<a href>` for navigation. Never a `<div>` with an
192
- onClick.
193
- - `aria-label` on any icon-only button; real `alt` text on meaningful images.
194
- - Nothing shifts as content loads: reserve the space, set image dimensions.
195
- - 16px minimum body text on mobile, and `<meta name="viewport" content="width=device-width, initial-scale=1">`.
246
+ One authored moment, not effects scattered everywhere. For a result screen that
247
+ might be the score counting up and the findings staggering in 40–60ms apart.
248
+ Everything else: 120–200ms, ease-out, on `transform`, `opacity`, `filter` and
249
+ colour only never animate `width`, `height`, `top` or `left`. Content must
250
+ be readable with motion off; honour `prefers-reduced-motion` every time.
251
+
252
+ ## 7. Accessibility and performance — non-negotiable
253
+
254
+ - Contrast: 4.5:1 body text and placeholders, 3:1 large text, icons and control
255
+ borders. `--ink-3` on `--bg` is for hints only.
256
+ - Keyboard: every control reachable in visual order, visible focus ring,
257
+ Escape closes overlays, focus returns to the trigger.
258
+ - Semantics: `<button>` for actions, `<a href>` for navigation, landmarks
259
+ (`header`, `main`, `nav`), one `<h1>`, headings in order.
260
+ - Targets 44×44px on touch. `aria-label` on icon-only buttons. Real `alt` text.
261
+ - Viewport meta, 16px minimum input text on mobile (iOS zooms below that).
262
+ - No layout shift: reserve space for images and async content, set image
263
+ dimensions, use `next/image` in Next.js, `font-display: swap`.
264
+ - Ship less JS: server components by default in Next.js, `"use client"` only on
265
+ the parts that are interactive.
196
266
 
197
267
  ## 8. Do not
198
268
 
199
- These are the defaults of the category rather than laws a brief can earn any
200
- of them — but reaching for one *because it was the first thing to hand* means
201
- you were not deciding.
202
-
203
- - Purple-to-blue gradient headers. Gradient text. Glassmorphism as decoration.
204
- - Three identical feature cards with a lorem sentence each. Same-size icon +
205
- heading + text cards used as the whole page structure. Nested cards.
206
- - The hero-metric template: big number, small label, three supporting stats.
207
- - A tracked uppercase eyebrow over every section, or 01 / 02 / 03 section
208
- numbers where the order carries no information.
209
- - Emoji as interface icons. Use an icon set or well-drawn inline SVG.
210
- - Monospace as a costume for "technical" when there is no code or data in it.
211
- - A modal for something that needs neither interruption nor protected focus.
212
- - Centring everything. Long centred paragraphs are genuinely harder to read.
213
- - Inventing a component library when the project already has one. Look for
214
- existing components, tokens and utilities first, and use them.
215
- - Placeholder copy. Write the real words — they are part of the design.
216
-
217
- ## 9. Before you say it is done
218
-
219
- Open it and look at it. Then walk this list and fix what fails. The last three
220
- are the ones that get skipped, so do not skip them:
221
-
222
- 1. **375px wide.** No horizontal scrollbar, nothing overlapping, nothing cut
223
- off. If you wrote no media query at all, you have not done this.
224
- 2. **The longest realistic string** in every label and every cell. Does the row
225
- hold, or does one long title break the layout?
226
- 3. **Empty data.** Is there a real empty state, or a blank rectangle?
227
- 4. **Keyboard only.** Tab through everything. Can you see where you are at
228
- every step?
229
- 5. **Count the accent hues.** More than one family means the palette got away
230
- from you. Put it back.
231
- 6. **Contrast.** `--ink-3` on `--bg` is for hints, never for anything that has
232
- to be read.
233
-
234
- Then say which of these you actually checked and what you found. Do not claim
235
- it works on mobile if you never made it narrow. If it runs in a browser, start
236
- it with `run_command` and `background: true` and open it before you call it
237
- finished.
269
+ Defaults of the category. A brief can earn any of them; reaching for one
270
+ because it was first to hand means you were not deciding.
271
+
272
+ - Purple-to-blue gradients, gradient text, glassmorphism as decoration,
273
+ glowing blobs in the background.
274
+ - Three identical feature cards; icon + heading + sentence cards as the whole
275
+ page; cards inside cards.
276
+ - The hero-metric template: big number, small label, three stats in a row.
277
+ - A tracked uppercase eyebrow over every section; 01/02/03 section numbers.
278
+ - Emoji as icons. Monospace as a costume. Centred long paragraphs.
279
+ - The default shadcn slate theme, unchanged.
280
+ - Lorem ipsum, "Feature 1", "John Doe", placeholder images. Write the real copy
281
+ it is part of the design.
282
+ - A modal for anything that does not need to interrupt.
283
+
284
+ ## 9. Look at it, then report
285
+
286
+ Run it (`npm run dev` starts in the background and returns the URL) and check:
287
+
288
+ 1. **375px wide** — no horizontal scroll, nothing overlapping or clipped. If you
289
+ wrote no responsive rules at all, you have not done this.
290
+ 2. **1440px wide** — the content has a max width and does not stretch into
291
+ unreadable lines.
292
+ 3. **Longest realistic content** in every label, cell and card.
293
+ 4. **Every state** empty, loading, success, error reachable and designed.
294
+ 5. **Keyboard only** tab through everything; focus always visible.
295
+ 6. **Contrast** of body text, muted text and the accent on its background.
296
+ 7. **Hue count** one accent family plus semantics.
297
+ 8. **The first thing the eye lands on** is the thing that matters most.
298
+
299
+ Then say which of these you actually checked and what you found. Never claim it
300
+ works on mobile if you never made it narrow.
@@ -1,47 +1,85 @@
1
1
  ---
2
2
  name: write-tests
3
- description: Write tests that fail for the right reason behaviour over implementation, real edges, and no assertions that can never break.
3
+ description: Write tests that catch real regressions — the right level for each behaviour, real edge cases, deterministic setups, mocks only at the boundaries, and proof that each test can actually fail.
4
+ auto: write tests, add tests, add a test, unit test, unit tests, test coverage, integration test, e2e test, end to end test, testing, vitest, jest, pytest, playwright
4
5
  ---
5
6
 
6
7
  # Writing tests
7
8
 
8
- The test that matters is the one that fails the day someone breaks the thing it
9
+ The test that matters is the one that fails the day someone breaks what it
9
10
  covers. Every other test is overhead with a green tick on it.
10
11
 
11
- ## Test behaviour, not implementation
12
+ ## 1. Find how this project tests
12
13
 
13
- Assert on what a caller can observe: the return value, the state afterwards,
14
- the thing that was written. A test that checks a private helper was called
15
- locks the implementation in place and will need rewriting the first time
16
- anyone refactors while still not proving the feature works.
14
+ - Look for the runner and its config before writing anything: `vitest.config.*`,
15
+ `jest.config.*`, `playwright.config.*`, `pytest.ini`/`pyproject.toml`,
16
+ `go test`, the `test` script in `package.json`.
17
+ - Match the existing style: file location (`__tests__/`, `*.test.ts` beside the
18
+ source, `tests/`), naming, helpers, fixtures. Use what is there.
19
+ - If nothing exists, pick the standard for the stack — Vitest for Vite and
20
+ Next.js, pytest for Python, the built-in `go test` — and add the script.
17
21
 
18
- ## Cover the edges, not five versions of the middle
22
+ ## 2. Pick the right level for each behaviour
19
23
 
20
- For each unit, the ones that actually find bugs:
24
+ - **Unit** pure logic: parsing, scoring, validation, formatting, reducers.
25
+ Fast, many, no I/O.
26
+ - **Integration** — a route handler with its validation and error paths, a
27
+ component with its real children, a module against a real temporary
28
+ database or filesystem.
29
+ - **End to end** — the one or two core user journeys, in a real browser
30
+ (Playwright). Few, because they are slow and brittle.
31
+
32
+ Most value per minute is in unit tests of the logic that makes decisions and
33
+ integration tests of the boundaries where data comes in.
34
+
35
+ ## 3. Test behaviour, not implementation
36
+
37
+ Assert what a caller can observe — the return value, the rendered output, the
38
+ response, the state afterwards. A test that checks a private helper was called
39
+ breaks on every refactor while proving nothing about the feature.
40
+
41
+ For UI, query the way a user finds things: by role, label and text
42
+ (`getByRole('button', { name: /analyze/i })`), not by class names or test IDs
43
+ unless there is no accessible alternative.
44
+
45
+ ## 4. Cover the cases that find bugs
46
+
47
+ For every unit, go through:
21
48
 
22
49
  - empty, one, many
23
- - the boundary: 0, -1, the last index, the maximum
24
- - null, undefined, the missing field, the wrong type
25
- - the failure path: the dependency throws, the network times out, the file is
26
- gone
27
- - the second call: is it idempotent, is state left behind?
28
-
29
- ## Make each test readable on its own
30
-
31
- - The name says the behaviour: `returns null when the session file is missing`.
32
- - Arrange, act, assert, in that order and visibly separated.
33
- - One reason to fail per test. Six assertions in a row means the first failure
34
- hides the other five.
35
- - No shared mutable state between tests, and no dependence on the order they
36
- run in.
37
- - Real values over mocks wherever it is affordable. Mock the network and the
38
- clock; do not mock the thing you are testing.
39
-
40
- ## Prove the test works
41
-
42
- Break the code on purpose and watch the test fail, then put it back. A test
43
- that has never failed is a test you have no reason to trust. If a test cannot
44
- fail an assertion on a constant, a mock asserting itself — delete it.
45
-
46
- Then run the whole suite and report the real numbers, including anything that
47
- was already failing before you started.
50
+ - boundaries: 0, -1, the exact threshold, just over and just under it, the max
51
+ - missing: `null`, `undefined`, missing field, empty string, wrong type
52
+ - malformed input: invalid JSON, a string where a number was expected,
53
+ a reply wrapped in prose
54
+ - failure paths: the dependency throws, times out, returns an error status
55
+ - repetition: the second call, the same input twice, concurrent calls
56
+
57
+ A threshold rule ("sodium over 600mg is flagged") needs a test at 599, 600 and
58
+ 601. That is where the off-by-one lives.
59
+
60
+ ## 5. Keep tests deterministic and independent
61
+
62
+ - Control time (`vi.useFakeTimers()`, a fixed clock) and randomness (a seed).
63
+ - No real network. Mock at the boundary — the HTTP call, the SDK client — and
64
+ never mock the thing under test.
65
+ - Each test sets up what it needs and cleans up after. No reliance on order.
66
+ - Build test data with small factories so each test states only what matters
67
+ to it.
68
+ - One behaviour per test, named for it:
69
+ `flags sodium when it is over the daily threshold`.
70
+ - Arrange, act, assert visibly separated.
71
+ - Snapshots only for stable, small output; a 400-line snapshot gets approved
72
+ without being read.
73
+
74
+ ## 6. Prove each test can fail
75
+
76
+ Break the code on purpose — flip a comparison, delete a branch — and watch the
77
+ test fail, then restore it. A test that has never failed is a test you have no
78
+ reason to trust. Delete tests that cannot fail (asserting a constant, a mock
79
+ asserting itself).
80
+
81
+ ## 7. Run and report
82
+
83
+ Run the whole suite, not only the new file. Report the real numbers — passed,
84
+ failed, skipped — including anything that was already failing before you
85
+ started, and anything you could not cover and why.
package/src/core/loop.js CHANGED
@@ -1053,8 +1053,9 @@ export class Agent {
1053
1053
  .slice(-count);
1054
1054
 
1055
1055
  for (const m of tail) {
1056
- if (m.role === 'user') this.ui.write(`${blue('›')} ${dim(m.content.split('\n')[0])}`);
1057
- else this.ui.assistant(m.content);
1056
+ if (m.role !== 'user') this.ui.assistant(m.content);
1057
+ else if (this.ui.userMessage) this.ui.userMessage(m.content);
1058
+ else this.ui.write(`${blue('›')} ${dim(m.content.split('\n')[0])}`);
1058
1059
  }
1059
1060
  if (tail.length) this.ui.write(dim(' ── picking up here ──\n'));
1060
1061
  }
package/src/ui/screen.js CHANGED
@@ -240,6 +240,34 @@ export class Screen {
240
240
  this.render();
241
241
  }
242
242
 
243
+ /**
244
+ * Something the user said, in the conversation, in the same blue box as the
245
+ * input it was typed into.
246
+ *
247
+ * A long session is mostly the agent's output — tool calls, diffs, answers.
248
+ * Your own messages are the landmarks you scroll back looking for, so they
249
+ * get the frame: every one of them is findable at a glance, and the box
250
+ * matches the one below so it is plain where each came from.
251
+ */
252
+ userMessage(text) {
253
+ const width = this.width();
254
+ const room = Math.max(8, width - 6); // borders, padding, and the caret column
255
+
256
+ const rows = [];
257
+ for (const paragraph of String(text).replace(/\r/g, '').split('\n')) {
258
+ for (const line of wrapAnsi(paragraph, room)) rows.push(line);
259
+ }
260
+
261
+ this.add('');
262
+ this.add(boxTop(width, edge));
263
+ rows.forEach((row, i) => {
264
+ const lead = i === 0 ? blue('›') : ' ';
265
+ this.add(boxRow(` ${lead} ${chalk.white(row)}`, width, edge));
266
+ });
267
+ this.add(boxBottom(width, edge));
268
+ this.render();
269
+ }
270
+
243
271
  /**
244
272
  * A tool call, as it happens: "● Listing src".
245
273
  *
@@ -894,10 +922,9 @@ export class Screen {
894
922
  this.historyIndex = -1;
895
923
  if (text.trim()) {
896
924
  this.history.unshift(text);
897
- // Echo it so the transcript reads as a conversation rather than as
898
- // a series of unprompted answers.
899
- this.push('');
900
- this.push(`${blue('›')} ${chalk.white(text)}`);
925
+ // Answers to a y/N or a numbered pick are not messages, so they are
926
+ // not echoed: the prompt reports its own outcome.
927
+ if (!this.pendingPrompt) this.userMessage(text);
901
928
  }
902
929
  this.render();
903
930
  this.submit(text);