# Notes on AGENTS.md, from building Parley

Written while building `templates/ai-tool/` (an AI chat workspace, nine screens,
sidebar shell, a composer that streams) against AGENTS.md and alongside the
claims-desk, talent-desk, hearth and lodestay notes. Only what cost real time or
forced a judgement the file does not cover.

## What held up unusually well

- **§2's "every figure names the array it comes from"** is the reason this
  product has anything to say. A chat app's numbers are tokens and money, and
  the temptation is to write a usage screen with plausible figures on it.
  Deriving billing from the messages instead — one `turnBilling(thread, index)`
  that sums the messages before a turn — meant sending a message moves the
  context meter, the thread cost, the project cost, the usage headline and one
  bar of the day chart, all without a single stored total. It also produced the
  product's best sentence, which is just the arithmetic said out loud: *every
  turn re-reads the whole thread, so the cost grows with the window, not with
  what you type.*
- **§8.1's refusal to store derived state**, applied to files. `FILE_FLAGS
  .needsAttention` is called by the row tone, the project card's warning, the
  Files page note and the sidebar badge, so reindexing one CSV moves all four
  and the badge cannot lie.
- **§5.4's recession**, applied to prompts nobody has used in two months and to
  files the index could not read. Both keep their row, their status and their
  actions; they just stop competing.

## Gaps and frictions

### 1. §6 counts destinations; a chat app is decided by its list

§6's tie-breaker is destination count, and five destinations points at the
top-nav shell. That would be the wrong shell, for a reason the section does not
have a word for: the Chats destination's within-destination level is thirty
conversations that must stay one click away *while you read one of them*. A top
bar cannot hold that, and putting the list on the page means the reading column
shares its width with a list.

I took the sidebar and wrote the reason in the shell comment. The rule I would
add: **when a destination's within-destination level is a long list the user
switches between constantly, that decides the shell before the count does.**

### 2. §6.3's "nested children" needs a position

§6.3 allows the within-destination level to live "in the shell as nested
children" but does not say where they go. Nested directly under the Chats row is
the letter of it, and it pushes the other four destinations below eight thread
rows and their date group labels — which reads oddly until the nesting is drawn.
A hairline spine and an indent fixed it. Worth a sentence in the file: nested
children need a visible spine, or they read as more destinations.

### 3. §3's commit models do not cover a streaming turn

Send is instant, but it is neither of §3's reversibility tiers: it is not
harmless (it costs money and fills the window) and it is not irreversible (Stop
exists, and it is the *same* control in the *same* place). A confirm dialog on
Send would be absurd; an undo toast on a turn you can watch arrive would be
noise.

I treated Stop as the reversal — the control that replaces Send while the turn
runs — and left the toast off. Proposed wording: **an action the user can watch
happen, with its own stop control in the same place, needs no undo toast.**

### 4. §7.4 says stop and ask when a provider mark is missing

The template ships no `assets/providers/`, so it offers no providers at all and
says so in the auth file's header comment. That is the right outcome, but §7.4
reads as though the missing asset should halt the work. For a template with no
real identity provider behind it, "declare one strategy and offer nothing else"
is the answer, and the file could say so.

### 5. A derived figure that is not checkable by §2's three forms

The context meter is a figure with no rows behind it: it is the sum of message
bodies plus indexed file sizes, and no table lists "tokens per message" because
nobody would ever want one. It passes §2 only under the third form — it names a
predicate whose parts are reachable (the messages are on screen, the files are
listed beneath the meter). Worth making explicit that **a figure summing the
things already visible on the screen is checkable by being visible**, which is
the fourth legal form and the one a reading surface uses.

### 6. A shelf is not a destination, and §6.3 has no word for it

Archived threads are the same records as the live ones, filtered by one
predicate. Made a sidebar destination they would read as a second Chats; made a
tab on the thread screen they would take reading height from the conversation.
What they actually are is a *shelf* of Chats' own within-destination level, so
the route is `/chats/archived` with `parent: "/"` and the entry point is the last
row of the nested list, below the date groups, carrying the count. Proposed
wording: **a filtered shelf of a destination's own list belongs at the foot of
that list, not in the nav and not in tabs.**

### 7. §6.2's search contract has no room for "see them all"

§6.2 is firm that app-scope search is a palette rather than a filter, and it is
right — but a palette answers "take me to one record" and nothing else, so
"eleven chats mention this phrase" has nowhere to land. The fix was one
domain-free addition to `lib/command-palette.js`, copied into all nine products:
an optional `footer` row under the filtered list, holding a destination rather
than a record. Worth a sentence in §6.2: **a palette may carry one persistent
row that is not a result, and a full results screen is a legitimate destination
behind it.**

### 8. Two figures that differ by one predicate need to be said in words

Seats are the clearest case §2 does not cover. `WORKSPACE.seats` is what is
paid for; `seatsUsed` is people plus invites still standing. They are equal in
this fixture, which is exactly when a reader assumes they are the same number —
so the anchor figure reads `10 of 10` and its qualifier decomposes the used
side (`8 people and 2 invites still standing`), and the one expired invite is
named as holding nothing. §2 asks a qualifier to decompose its own figure's set;
this is the case where the figure has two sets behind it and the qualifier has
to say which one it is breaking down.

### 9. An entitlement gate reads better as a disabled control with an arithmetic reason

§2 says entitlements gate controls, not copy, and the plan rows here gave the
rule a good test: Solo is disabled not because of the plan's name but because
`option.seatLimit < seatsUsed(data)`, and the tooltip is the arithmetic — "Solo
covers 3 seats; Northgate is using 10." Same shape on the invite button, whose
reason is the seat figure above it. Both are recoverable by a reader without
trusting the label, which is what makes a disabled control honest rather than
evasive.

## Small things
- `Textarea`, `SidebarInset` and the other primitives are plain function
  components, so a `ref` on them silently does nothing. The composer autofocuses
  through the `autoFocus` attribute and the scroll-to-bottom queries
  `.pl-inset`. Worth a line in the components' notes.
- The design system's stylesheets are appended twice by the DC runtime, the
  second copy after `kit.css`. Every override in `kit.css` that touches a
  component class is therefore written two classes deep — the same note the
  Halyard kit carries.
- `Badge` inside a flex column stretches to the column width. `align-items:
  flex-start` on the record wrapper; easy to miss in a table cell where it looks
  like a full-width bar.
