# The agent's half

Paste the block below into your project's `CLAUDE.md` (or the equivalent for
whatever agent you use). Without it the agent has no idea the loop exists and
will not mark anything, which costs you the only feedback you get between
pressing enter and the page changing.

---

## Block to paste

````markdown
## LiGMA — design notes from the browser

The site carries a dev-only annotation overlay (`?grain` on the URL). Notes
written in it land in `apps/<app>/.lab/state.json` and are the primary channel
for design feedback. Treat a note as a task.

**Watch for them.** Keep a persistent watcher running:

```bash
node scripts/watch-notes.mjs
```

Each new note arrives as one line:

```
LAB NOTE  <id>  <selector>  "<the element's own words>"  [<route>]  (1 of N)  <what to change>
          path: <a selector matching that one element>
          shot: <absolute path to a pasted image>
```

A `shot:` line is an image the user pasted into the note — a screenshot of
the problem, or a reference for what they want. **Open it.** It is an absolute
path precisely so you can, and a note with one attached usually says less in
words because the picture is carrying the meaning.

**Act on one immediately, in this order:**

1. `node scripts/lab.mjs working <id>` — **first**, before you read a single
   file. This is what puts a spinner on the note in the user's browser. It is
   the only signal they get that anything is happening, and it costs one
   command.
2. Resolve the element. The quoted **label** is the fastest route into the
   source — grep it. The **path** disambiguates when the label is empty or
   repeated. The **selector alone is not enough**: `(1 of 9)` means nine
   elements share it, and acting on the selector will change the wrong one.
3. Make the change. Run the project's gate.
4. `node scripts/lab.mjs done <id>` — the note ticks itself off in the browser.

**Never** mark a note done you did not act on, and never guess between two
elements that share a selector — if the identity fields do not settle it, say
so and ask.

**A note about a selector shared by many elements is a decision about all of
them.** `.btn: make this rounder` on a shared class is a system change; say so
before making it, and if the user meant one button, the path tells you which.

**Grain entries** in the same file are live experiments, not source. If the
user asks you to "keep" one, move it into the stylesheet as a real rule and
remove the entry — the overlay is not a place where design decisions live.
````

---

## When they ask for variants

A note like _"give me 3 versions of this"_, _"try a few options"_, _"show me
some alternatives"_ is not a request to pick one and ship it. Offer them all:

```bash
node scripts/lab.mjs variants /tmp/set.json
```

```jsonc
{
  "selector": ".ghero-cta .btn-lead", // what the rules target
  "label": "See it in your brand", // from the note, so they know which
  "route": "/",
  "ask": "give me 3 variants of the hero button",
  "options": [
    {
      "key": "a",
      "label": "ember",
      "note": "brand fill, white disc — loudest",
      "css": ".ghero-cta .btn-lead { background-color: var(--color-ember); ... }",
    },
    { "key": "b", "label": "outline", "note": "hairline only", "css": "..." },
    { "key": "c", "label": "ink", "note": "near-black slab", "css": "..." },
  ],
}
```

Rules of thumb that make a set worth looking at:

- **Make them actually different.** Three shades of the same idea wastes the
  one thing this buys you. Vary the decision, not the number.
- **CSS only, and self-contained.** Each `css` is injected unlayered at
  runtime, so it beats `@layer components` — but it cannot change markup. If
  an option needs different markup it is not a variant, it is a proposal; say
  so in a reply instead.
- **`note` is what the chip's tooltip says.** One line on what this one is
  doing differently, not a restatement of its name.
- **Do not include the original as an option.** The panel always adds it.

Then wait. When they choose, the watcher prints:

```
LAB PICK  <id>  <selector>  "<label>"  [<route>]  chose <label>
          ask: <what they asked>
          css: <the ruleset they picked>
```

Write **that** CSS into the real source — properly, in the right file, with
the reasoning in a comment — then `node scripts/lab.mjs drop <id>` to clear
the set. The overlay stops painting a set the moment it is decided, precisely
so the page shows whether your edit actually landed.

A pick of `the original` is a real answer: they looked at three alternatives
and kept what was there. Drop the set and change nothing.

---

## Why `working` matters more than it looks

The window between the user pressing enter and the page hot-reloading is where
this tool either feels magic or feels broken. During it, nothing on their
screen changes. If the note just sits there inert, the reasonable conclusion is
that the tool dropped it — and the user writes it again, which you then
implement twice.

That happened. Twice, verbatim, from the same user in the same minute:

```
.lg:col-span-7  lets completely redesign this please
.lg:col-span-7  lets redesign it completely please
```

One `lab.mjs working <id>` at the top of the task prevents it. The spinner, the
lit edge and the "claude is on a note" line in the panel bar all come from that
one field.

---

## Ordering

The panel sorts notes **in flight → open → done**, stable within each group, at
render time only — the file keeps its write order. The user is always looking
at the one you are on; it cannot end up below a scroll.

---

## What a good note looks like from your side

```
LAB NOTE  mu7k6bf1  .btn  "Contact"  [/]  (1 of 4)  lets redesign this button
          path: #root > header > div > span:nth-of-type(2) > a
```

Everything you need is on those two lines: the class tells you which stylesheet
block, the label tells you which call site, the route tells you which page, the
`(1 of 4)` warns you the class is shared, and the path settles it if the label
does not. You should not need to open `state.json` at all.

If you find yourself opening it, the watcher is not passing enough through —
fix the watcher, not your workflow.

## Threads: one agent each

A **note** is one instruction, ticked off once. A **thread** is a conversation
pinned to a component, and it is meant to be worked by **one agent on its own**
— that is the whole shape of the feature.

The watcher announces each new line as:

```
LAB TALK  t<id>  .btn  "Contact"  [/]  (open)  this should open the venue
          path: #root > header > a
          claim: node scripts/lab.mjs take t<id> <agent>
```

The loop for whoever is orchestrating:

1. `LAB TALK … (open)` arrives — a component needs work.
2. **Claim it FIRST**: `node scripts/lab.mjs take <id> <agent-name>` — before
   reading a single file, not once you have the answer.

   The claim no longer starts the field — that was the old arrangement and it
   failed the obvious way. An agent that answers in one pass never calls
   `take` at all, so the component sat looking untouched for the whole minute
   it took to write five variants, and it was reported as the animation being
   broken. It was not broken; it was waiting to be told, by the one party with
   every reason to forget. The field now runs on whose turn it is, which the
   thread knows on its own: you spoke last, nothing has answered, no chips are
   on the table. See `Pins` in `Lab.tsx`.

   What the claim still does is put a NAME on it — the pin reads `thinking`
   until something claims it and its agent name after — and it fails with a
   non-zero exit if another agent already has the thread. That failure is the
   point, not an inconvenience: two agents editing one component is exactly
   what the claim exists to prevent, and several agents may be watching the
   same file with nothing else shared between them.

   So still claim first. The cost of forgetting is now a pin that says
   `thinking` instead of your name, rather than a page that says nothing.

3. Spawn one agent for that thread and give it the `path`, the `selector` and
   the messages. Its brief is that component and nothing else.
4. **Say what you are doing, as you do it**:
   `node scripts/lab.mjs step <id> "reading Header.tsx"`. Each one appends a
   line to the thread's own progress list in the bubble. A claim only tells the
   person on the page that the thread is taken; on work that runs for minutes
   they are also asking whether anything is moving, and this is the only thing
   that answers that. One line per real step — not a running commentary.
5. The agent reports back **on the page**, where the question was asked:
   `node scripts/lab.mjs reply <id> "pointed it at /games.html"`.

   **Five short sentences, maximum, and the command enforces it at 400
   characters.** The bubble is 19rem wide and sits ON the component being
   judged, so a long reply covers the thing the person is looking at. Say what
   changed and the one thing they cannot see for themselves. Name variants by
   number; do not explain each one. The reasoning goes in a code comment next
   to the change, where whoever reads it next is already looking.

6. `node scripts/lab.mjs finish <id>` when it is done.

`LAB TAKEN` is emitted when a thread's status changes under you — another
watcher's agent got there first. Nothing to do but leave it alone.

A reply from the page **reopens** a finished thread. Somebody has just said the
answer was not the end of it, and a `done` thread is one no agent looks at
again.

### Offering variants on a thread

When the answer is "here are four and you pick", put `"thread": "<id>"` in the
variant spec:

```json
{ "thread": "t1a2b3c", "ask": "four ways in", "options": [ … ] }
```

The chips then render **inside that thread's bubble**, under the element they
are about, instead of in the panel's variants tab across the screen. The
selector, the label and the route are inherited from the thread — which element
it is on is the reason the thread exists — so a spec that names one needs
nothing but `ask` and `options`.

A set that already exists moves onto a thread with
`node scripts/lab.mjs attach <set id> <thread id>` — for when it was offered
before the thread existed, which is the common case while this is new and the
person is staring at the bubble wondering where the answer went.

A set with no `thread` behaves exactly as before and shows in the panel. That
is still right for a set asked for in a plain note, which has no pin to sit
under.

The choice comes back in the same place either way: `chosen` on the set.

**Offering a set suspends the working indicator.** While any set on the thread
is undecided the component stops dithering and its marker reads `your call` —
you are not editing it, you are waiting, and a stipple over the element makes
the chips impossible to compare. The claim is untouched, so nothing else picks
the thread up. Answering the set starts it again.

### Attachments that are drawings, not screenshots

A composer can attach two kinds of image and they mean opposite things. A
pasted screenshot is evidence — this is what it looks like now. A drawing from
the pad is intent — this is what it should look like, and it is a sketch, so
read it for what it is pointing at rather than for its measurements. Arrows,
boxes and scribbled placeholder text are the vocabulary; nobody drew 14px of
padding with a trackpad.

Both land in `.lab/shots/` and both appear as `shots` on the message, so the
only way to tell them apart is to look. A drawing is on light paper — often
with a faint dot grid — and a screenshot is the page.

The pad has a pen, straight lines, arrows, eight shapes (rectangle, rounded,
ellipse, triangle, diamond, pentagon, hexagon, star), text and an eraser, in
five colours and three weights. So a drawing can be more precise
than a scribble, and the vocabulary is worth reading properly: an ARROW points
at where something should go, a BOX marks an area, and TEXT on the drawing is
usually a label for a thing rather than copy to set. Shift constrains while
drawing, so a square really is a square and a line at 45° was meant to be at
45° — but a proportion eyeballed on a 1200×780 sheet is still eyeballed.

### A message that starts `Undo —`

Sent by the undo control under the history scrubber. It means someone dragged
an element back through its past, found a moment they wanted, and asked for it
back. The message carries the exact drift — every tracked property that
changed, written `now → then` — so you do not have to work out what moved.

Two things it is not:

- **It is not a revert you can do with an override.** The message says which
  computed values to restore, not which rule produced them. Find the rule and
  change it, the same as any other request. A fresh `.foo { text-align: left }`
  bolted on top passes the eye test and leaves two rules arguing.
- **It is not necessarily a whole commit.** The trail records one moment per
  change that reached the browser, so a moment can be part of one commit or
  span several. `git log -p` on the file is the check, and the timestamp in
  the message is what to line up against.

The scrub is dropped when the request is sent, so the element on screen is
showing its real current state, not the target. That is deliberate: a page
pinned to the past would show the undo as already done.

### Clearing up after yourself

`node scripts/lab.mjs stale` prints everything still open on the page and the
command to clear each one: threads answered but never closed, threads waiting
on a pick, and sets nobody decided.

**Nothing expires on its own.** Deleting somebody's note because it got old is
the one thing this tool must never do. The cost is that leftovers accumulate —
an answered thread nobody closed keeps drawing its outline on the component
for as long as the page is open, which is how you get "there are stale
pickers" pointed at a card whose work finished an hour ago.

So the habit is: `finish` the thread when the work lands, and `drop` a set
once its choice is in the source. `stale` is for when that slipped.

### What the tool does not do

It does not spawn agents. It writes to `.lab/state.json` and prints lines; the
orchestration — how many agents, which model, whether they run in parallel —
belongs to whoever is reading the watcher, because that is the side that knows
what an agent costs.
