# Notes on AGENTS.md, from building Talent Desk

Written while building `templates/talent-desk/` (a creator talent agency's desk,
seven screens, top-nav shell) against AGENTS.md and alongside
`templates/claims-desk/AGENTS-notes.md`. Each note is something that cost real time or
forced a judgement the file does not cover. Where a claims-desk note is
confirmed, it says so — a second product hitting the same wall is the strongest
argument for fixing it.

## What held up unusually well

- **§2 "every figure names the array it comes from", again.** `metrics.js`
  existed before any screen, which is why raising one creator's follower count
  moves the roster band, the audience axis on the match screen and the
  performance figures together, and why the invoice and the payout cannot
  disagree about what a deal is worth — neither of them stores an amount.
- **§2 "a committing action writes into the fixture and re-renders."** Two
  commits in this product move more than one record, and the rule is what made
  that obvious rather than clever: recording a brand's payment also releases the
  creator's payout from `awaiting_funds`, and marking a post published writes its
  metrics so the promise wall moves while you watch. Those two are the most
  convincing moments in the template and they exist because of that line.
- **§1.2 "one act-ability predicate at every altitude."** Three predicates, three
  badges, three section headings, no drift. Still the most valuable line in the
  file.
- **§9 "creativity goes into composition, hierarchy, rhythm and density."** Seven
  screens with seven spatial signatures, no new colour, no type size off the
  scale, no new component that is not layout over existing primitives. The
  constraint did the work.

## Gaps and frictions

### 1. A record whose parent is not a destination leaves no destination active

§6.6 requires exactly one active destination. §5 says a namespace parent with no
screen renders as plain text in a breadcrumb. Put those together and a deal
record — reached from a brief, a deliverable, an invoice or a payout, with no
deals list anywhere — has a legal breadcrumb (`Deals › DEAL-26-019`, "Deals" as
plain text) and *no* active destination, which §6.6 forbids. My first build had
the shell force Roster active on a deal screen, which was simply a lie.

I resolved it by putting `/deals/:id` under `/briefs` in the route table: a deal
is the outcome of a brief, so Briefs stays active and the crumb resolves without
a phantom namespace.

**Suggested addition to §6:** "A screen's active destination is its nearest
ancestor in the route table that is a destination. A registered screen with no
such ancestor is a bug in the route table, not a case for two actives or none."

### 2. The primary transition can depend on a derived figure

§8.2 says "one transition is primary — the record's normal path". For an inbound
brief the normal path is to match it to the roster. For a brief that **nobody on
the book fits**, the normal path is to say no — and which of those is true is a
derived figure, not a stored status.

Re-weighting the buttons on the screen would have put the decision in the wrong
place (§8.2 wants the set derived once, product-wide). I passed the fit count
into the transition function instead: `briefTransitions(brief, fitCount)`.

**Suggested addition to §8.2:** "A transition's primacy may depend on a derived
figure. When it does, the entity's transition function takes that figure as an
argument — the screen never re-weights the set it was given."

### 3. §5.4 recession has no category for a hazard

Recession is defined for work the user cannot act on. An exclusivity conflict is
not that. The creator is available, the fee fits, the audience fits — and pitching
her is a breach of a contract we signed. Applied literally, §5.4 would have
recessed the single most important fact on the match screen into grey.

I took it the other way: the conflict is stated three times, at three altitudes —
a full-width alert above everything, a marked row in "Cannot take it" with a red
rule and a tinted wash, and the reason on the disabled control itself. §1.2's
"disable it, with the reason stated" covers the control but nothing in the file
covers the page-level treatment.

**Suggested addition to §5.4:** "A blocker with contractual or legal consequence
is not recession. It is stated at every altitude it appears and never muted;
recession is for work that is merely waiting on somebody."

### 4. Suppression needs a second form: not-yet, as well as never

Claims note #4 asked for "a derived figure is suppressed with a word when the
record's state makes it meaningless". This product needed the same rule on a
clock rather than a state. A campaign one post into five is at 35% of its
promised reach. That is arithmetically true and operationally a lie, and it is
not covered by "the record's state makes it meaningless" — the state is fine, it
is just early.

`deliveryIndex` returns null unless every deliverable is live, and the screens
print "In flight" or "Not yet measurable" instead.

**Suggested wording, extending the claims-desk suggestion:** "…or when the record
is not finished and the figure only means anything at the end. Say which, in
words, on the row."

### 5. §3's staged/instant rule decides information architecture, not just controls

Confirms claims note #7, with a sharper consequence. The match screen is
genuinely staged — ticking creators builds a pitch that does not leave the desk
until you send it. But the natural header actions for a brief ("Chase the brand",
"Reopen brief") are instant, and a staged screen may not hold instant controls.

Moving them to the briefs list is defensible and I did it, but note what happened:
a rule about commit models silently decided which screen owns two actions. The
carve-out claims-desk asked for (navigation and dialog-openers are not controls
for this rule) does not cover it, because these are real transitions.

**Suggested:** extend the carve-out to "controls that act on a *different record*
than the one the form edits", or say explicitly that a staged screen exports its
record's instant transitions to the list that owns it — either is fine, but the
file should choose, because right now two authors will choose differently.

### 6. §8.3 again, and delivered rows are the sharper case

Claims note #9 covered near-future dates. This product hit it on rows where the
clock has *stopped*: a deliverable submitted on time, due next Wednesday, rendered
"Due Wed 10:00" — indistinguishable from last Wednesday, on a row whose whole
point is that we already handed it over.

Resolution here is one cell rather than the claims desk's two columns: the due
column is declared forward-looking, so it carries the countdown spelling while
the clock is still running and the absolute date once the work has been handed
over, with the full timestamp on hover either way.

**Suggested:** allow a column to declare itself forward-looking, and define it as
"countdown while the clock runs, absolute date once it stops". That is one rule
that fixes both this and the claims desk's workaround.

### 7. §4's file table is still missing `fixtures` and `metrics`

Confirms claims note #6 exactly. I named them by copying the neighbouring
product, which is the whole argument: two products agreeing by imitation is not
the same as the file saying so. Both files are effectively mandated by §2.

### 8. "One record, one fixture" needs a line for records outside the product

Three creators on this roster are booked. One is booked on a deal this desk owns,
so the deal owns the brand name and the booking reads it (`bookingBrandOf`).
Two are booked on campaigns that predate the desk, so the brand name is the
booking's own fact and there is nothing to derive it from.

§2's "when two records describe the same money, one owns it and the other reads
it" assumes both records are in the product. The interesting case is the mixed
one, and getting it wrong means either a literal that silently drifts or a
dangling reference.

**Suggested:** "Where the owning record is outside the product, the reference
carries the value itself and says so in a comment. One accessor resolves both
cases, so no screen chooses."

### 9. A product with no destructive action is a missing enum value, not an exemption

§1.5 says every destructive action opens a confirm dialog. It says nothing about
a product that has none — and my first pass had none, because every transition in
a talent agency is reversible: you can un-pitch, un-chase, re-draft.

Rather than ship a template that never exercises the pattern, I applied claims
note #8 in the positive: voiding a deal had nowhere to land, so `void` was
missing from the contract enum. Adding it (with its tone reason and a coverage
note) gave the product one genuinely irreversible action, offered only while the
paper is unsigned and nothing is live.

**Suggested addition to §1.5 or §8.1:** "If a product appears to have no
destructive action, check whether an outcome has nowhere to land before
concluding the pattern does not apply."

### 10. Practical: the template's own stylesheet loads *before* the design system's

The scaffold puts `<script src="./ds-base.js">` and `<link href="./kit.css">` in
the same `<helmet>`, in that order — but `ds-base.js` appends its stylesheet links
when it executes, which is *after* `kit.css` is already in the head. So the
design system's rules land last, and any product rule that retones a design-system
class at equal specificity loses.

This cost me a build: `.mq-portrait { border-radius: var(--radius-sm) }` on an
`Avatar` was beaten by `.o-avatar { border-radius: var(--radius-full) }`, and
squared portraits are the roster's whole visual signature. The fix is to write
such rules two classes deep (`.o-avatar.mq-portrait`), which I have done and
commented — but nothing warns you, and the symptom looks like a typo.

**Suggested:** state it wherever product scaffolding is described, alongside
claims note #11's two loader traps — either have `ds-base.js` prepend its links,
or tell authors that product overrides of design-system classes are written at
two classes deep.

### 11. Practical: a component that carries its size inline cannot be shrunk by a breakpoint

`Avatar` takes `size` as a prop and writes `width`/`height` inline. My roster band
narrowed its portrait column at 1280px, and the 104px avatar simply overflowed
into the creator's name — inline styles beat every media query. The fix is
trivial (never narrow that track) but the failure mode is invisible until you
look at the right width.

**Suggested, for §6.5:** "A grid track holding a component that carries its size
inline is not responsive. Reflow around it or change the component's size prop —
narrowing the track only causes overflow."

## Notes from the second pass (brands, records, search, recovery)

Added eight screens — brand, deliverable, statement, wrap report, inbox, search
results, team, and the password-recovery set. Three new frictions.

### 12. A name repeated on many records is an entity, and §2 does not say so

Brand names lived as string literals on briefs, deals and invoices: eleven
records each carrying `brand: "Tessellate Home"`. Every figure about them
reconciled, so §2's checklist passed — and it was still the one place in the
product where a rename would have had to be made in eleven places, and where no
screen could exist for a thing every screen named.

Fixing it did not need a schema: records carry `brandId`, `BRANDS` holds the
name once, and `fixtures.js` derives `record.brand` in a loop at the bottom, so
forty-odd existing call sites kept working unchanged.

**Suggested addition to §2:** "A value repeated verbatim across records is an
entity that has not been written yet. Give it an id and derive the label — a
noun that appears on three screens needs a record behind it before it needs a
figure."

### 13. §6.2 makes app-scope search a palette, and stops there

"App-scope search is a palette, not a filter" is right, and a palette holding
six groups of records runs out of room. The results screen this needed is not a
list screen (§5's filter-bar architecture does not fit — there is no one entity
to filter) and not a destination (it has no place in the bar). It ended up a
registered off-nav screen reached from a footer row in the palette.

The practical snag: `lib/command-palette.js` owns the query and must stay
byte-identical between products, so the footer cannot hand its query to the
screen. The screen's own field takes over instead, which is honest but means the
reader types twice.

**Suggested:** say whether a palette may overflow into a results screen, and if
so, that the palette's own query is part of the contract — the shared component
should pass it to the footer.

### 14. §7.2's rule needs its converse

"Every entry point rendered has a screen behind it" got the reset flow built.
What it does not say is that a *registered* auth screen has to be reachable
from the flow — and `/account-locked` is the one nothing naturally links to,
because the thing that leads there is a count of failures rather than a control.
Leaving it as a route the preview menu could reach would have been a screen with
no way in. Sign-in now counts rejections against `AGENCY_POLICY.signInAttempts`
and navigates there on the last one.

**Suggested addition to §7.2:** "And every screen registered has a state that
reaches it. A designed error state with no path to it is documentation, not a
screen."

## Not a rule, but a token gap

§9 forbids a type size off the scale, and the scale tops out at `--text-5xl`
(48px). For a product whose brief asks for editorial hierarchy and real scale
contrast, 48px is the ceiling for a page title *and* for a brand name on an
inbound brief, which flattens the two most important sizes on the product
together. I used 5xl for page titles and 4xl for brand names and it works, but a
design system that wants to serve editorial products will eventually want one
display step above the UI scale.
