Authoring round: 6 terms per category
One round takes the site up to 54 terms forward: roster from the pool, briefs with
demo hints, one parallel author agent per category plus one verify agent, then
e2e centrally, then a single commit. Eleven rounds have run this way; the process
below is the distilled shape, including every failure mode they taught.
A round is 6 per category only while every category still has 6. The pool is
now uneven and draining unevenly: interaction was exhausted before round 16, and
round 16 emptied color and aesthetic (1 candidate each) while authoring 6 apiece
elsewhere, for 38 terms across 8 agents. Take the whole of a nearly-empty category
rather than skipping it, size the agent list to the categories that still have
terms, and tell a 1-term agent it has one term so it does not pad. Check the real
shape with pool-remaining.ts before promising the user a number.
Files in this skill directory:
pool-remaining.ts: lists the unauthored pool per category
make-briefs-template.ts: briefs generator to copy and fill
workflow-template.js: the two-phase agent orchestration to copy and fill
0. Preconditions
- Read SPEC.md and AGENTS.md first if this session has not.
- Clean git tree, on
main (commits go directly to the current branch; never branch).
- The USER runs the dev server on port 4321. Confirm with a GET request to
http://localhost:4321/ (Astro dev answers HEAD with nothing, so a HEAD probe
looks exactly like a server that is down). Never start, stop, or restart any dev server; if it is
down, ask the user rather than launching one.
- Agent quirk: Astro 7.2 force-backgrounds
astro preview when it detects an agent, so
Playwright aborts with "webServer exited early" and runs nothing. playwright.config.ts
now sets ASTRO_PREVIEW_BACKGROUND=1; set it by hand for any ad-hoc playwright run.
Piped through a buffering pipeline this reads as a HANG with an empty log, because the
orphaned preview holds stdout open. Run e2e detached to a log file and never under a
tool timeout: the full suite is ~18 min and a 10 min cap kills it mid-run.
1. Roster (main session, ~15 min)
bun .claude/skills/authoring-round/pool-remaining.ts for the per-category pool
AND the stub backlog, which it now prints as its own list. Stubs outrank the
pool. A stub is a published page carrying a definition and nothing else, already
linked to by six to nine articles, so promoting one repays a debt the site is
already visibly carrying. If the stub list is non-empty, the round is a promotion
round (see §6) unless the user asked for new terms specifically.
Pick 6 per category, or the whole remainder where fewer are left. Selection
principles, in priority order:
- Pay prose IOUs: grep recent articles for "enumerated separately" and named
cross-references to unauthored terms; an on-site article that NAMES a candidate
is a debt the candidate's article repays by linking back.
- Prefer head/core priority; take tail terms when they complete a family or pair.
- Build contrast pairs into one round (fake-urgency/fake-scarcity,
frame-rate/jank): round-mates get told to contrast each other in one sentence
each way, and the demos must look different. Only where the pair is REAL, and
that is a decision made at roster time, not at batching time: batching terms
together for coordination (shared context, one owner for both sides of an edge,
lookalike avoidance) must never come with an instruction to write an edge
between them. Round 23 batched trace-viewer with diff-viewer purely because both
are dense developer views, told the agent to author them "as a pair, one edge
between them written on both sides", and got exactly that: a seeAlso each way
plus a sentence in each article inventing a kinship ("both are dense two-pane
developer views", "a list beside a detail is what they have in common"). The
agent had already half-caught it, downgrading contrastWith to seeAlso because
nobody describing a trace viewer reaches for the word diff viewer, which is the
discrimination test correctly applied and should have ended in NO edge at all.
The user caught the rest. Say "these two are yours, keep them from looking
alike" and let the relation earn its place or not exist.
- Avoid near-duplicates of existing specimens (a demo that would look like an
existing one is a smell even when the term is distinct).
- Rotate and >2-contact terms wait: the stage speaks single-contact touch
(data-touch persona, hold step, pressure; STAGE_NEWS law 17) and the
two-contact pinch (pinch step, pinchSpread, Ctrl+drag takeover; law 21), so
pressure, long-press, tap, swipe, and pinch terms are authorable. Rotate and
gestures past two contacts stay out of rosters until the vocabulary grows
them (the fake-touch entry in the specimen-sweep ledger tracks it).
Collision-check every pick AND its likely aliases against
src/content/terms/ (name: lines catch both term names and alias names).
A candidate whose name is already an alias of an existing term is DEAD: swap it
and note nothing. Claimed aliases a surviving pick must not take become fences
in its brief hint.
This is now the highest-value step in the roster, not a formality. Do it as a
script over the whole pool rather than per pick: slugify every alias name in
every term file into a claimed-slug map, then test each candidate slug against
it. Round 16 found 19 dead candidates in a 214-term pool, and they cluster
in exactly the wrong place: both of accessibility's core candidates
(landmark-region, tab-order) and two of motion's three head/core
(shared-element-transition, layout-animation). Priority is no defence.
Also eyeball the survivors for near-duplicates the slug test cannot see: a
candidate whose display name equals an existing term's name is just as dead
(motion's reduced-motion is the on-site prefers-reduced-motion), and the
pool's own notes field often flags merge candidates. The dead list is worth
reporting to the user, since candidates.json still carries them.
Round 17 found 20 dead this way, then found four more the slug test cannot
see and one that escaped the roster entirely. Two failure modes to add to the
sweep, both cheap:
- Affix variants. A candidate that is an existing term plus or minus a
trailing noun shares no slug with it:
back-to-top-button against the
published back-to-top. This one reached an author agent, which refused it
as a duplicate and cost the round a slot. Test each candidate slug with its
trailing -button, -bar, -menu, -view, -pattern stripped, and with
the existing slugs' own affixes added.
- Definition duplicates. Read the DEFINITION of the nearest existing term,
not just its name.
bulk-action-bar died because bulk-actions' definition
already says "a bar that appears once a selection exists and reports its
count"; session-timeout died to timeout-warning, slash-command-menu to
slash-command, color-alone to use-of-color. Four picks, all with distinct
slugs, all already written. Grep the candidate's own definition keywords
against existing definitions before promising the pick.
2. Briefs (main session, ~30 min)
Copy make-briefs-template.ts to a scratch directory, fill PLAN with the round's
slugs and hints (hint anatomy is documented in the template; drop the key for any
category with no terms left), set OUT to an absolute scratch path, run it with
bun from the repo root. It hard-fails on any
slug already on site or missing from the pool.
It also prints a const ROSTER = {...} line. Keep that output: it goes into the
workflow script in step 3 verbatim, and it is what lets the verify gate tell a finished
round from an interrupted one.
3. Author + verify (agents, ~30-45 min)
Copy workflow-template.js, fill the placeholders:
BRIEFS: the absolute path from step 2.
ROSTER: the line step 2 printed, pasted verbatim. It is the round's contract in
three places: each author is told its exact slugs and refuses to guess if its briefs
disagree, the log names any slug no agent reported, and the verify gate is told the
shape of the round rather than inferring it from what survived. A workflow script has
no filesystem access, so this literal is the only way the script can know it.
CATEGORY_GUIDANCE: per-category exemplars, IOUs, and alias fences (anatomy
documented in the template). This is where roster knowledge becomes agent
knowledge; skimping here costs e2e failures later.
- Never trim
STAGE_NEWS; append new laws as rounds teach them, and mirror any
mechanically-checkable new law into scripts/validate-terms.ts as a gate.
Smoke-test the filled script before running it, because a script that fails to parse
burns the whole launch and reports it as a bare SyntaxError with no round attached:
node -e 'const s=require("fs").readFileSync(process.argv[1],"utf8").replace(/^export const meta/m,"const meta");new Function(`return (async () => {${s}})()`);console.log("parses")' -- /abs/path/to/rN.js
node --check is the wrong tool here, and worse than nothing. A workflow script's real
evaluation context is an async function body: top-level await and a top-level return
are both legal in it, and both are errors to --check (as CJS it rejects the await, as
.mjs it rejects the return). So --check fails on every correct script, which trains
you to dismiss its output, which is how round 25 launched a script whose prompt strings
contained unescaped backticks. Wrapping the source in a function body parses it the way
the runner will and reports only real faults. It earned its keep immediately: the first
run of it over the edited template caught a missing comma in a prompt array.
Run it with the Workflow tool if available (one author agent per stocked category, in parallel, then the
verify agent). Without a Workflow tool, run the same prompts as parallel subagents
and the verify prompt as one subagent after all authors return. Authors do their
own visual pass against the user's dev server; the verify agent runs
validate/typecheck/biome once and fixes what fails. Nobody runs e2e but you.
Authors also run AUDIT_PASS: vd-stage.audit() plays a specimen's choreography
through the real attract player on the dev server and reports the same triple
e2e/choreography.e2e.ts asserts (no failures, exactly one subject, not
interrupted). Round 17 is why it is in the template: three of seven agents found
the API unprompted, audited their own specimens, and the round came back
96/96 green on the first test:e2e:new, with zero behavioral fixes needed
for the first time in the process. The other four flew blind and got lucky. It is
a dev-server pre-check, not the gate (dev injects CSS through JS, so a specimen
can mount before the stage has its height), so a failure there is always real
while a pass is only very likely: you still run the full suite.
Refused terms (STAGE_NEWS law 18): an author who finds a term needs input or
stage machinery the player does not have returns it unauthored with
needs stage primitive: <what> instead of faking it with a simulation control.
Collect these in the report, tell the user (building the primitive is a main-session
or dedicated-session job, like hold and the touch persona were), and keep the term
out of rosters until the primitive lands. Faking the player is exactly what the
fake-touch sweep is un-doing; never mint new offenders.
Interrupted-agent recovery (three times now: twice a spend limit, once an API
connection lost mid-response): inventory which slugs have all three files
(terms/<slug>.mdx, demos/<slug>/demo.ts, demos/<slug>/choreography.ts), then
relaunch a finish variant: per-category agents get only their REMAINING slugs, are told
which batch-mates are DONE (read them for idiom, do not rewrite), treat partial files as
drafts from their earlier self, and run the visual sweep over the WHOLE category batch
(dead agents may not have visually checked their own later terms; the sweep has caught
real overflow bugs in "complete" specimens every time). The verify agent then gates the
whole round.
An agent that dies mid-response returns NOTHING, so its finished terms are absent from
completed and invisible to anything reading only the agents' reports. That is what the
ROSTER fill is for. Round 25 is the worked example: an author died having written
wireframe of a three-term fidelity ladder, and the verify gate, told only what the
surviving agents reported, found a published term contrasting two slugs that did not
exist. It did the SPEC 2.3-sanctioned thing and minted mockup and prototype as
STUBS, which is right in isolation and is exactly the debt the family was batched whole
to avoid. The gate now knows the roster and is told the opposite rule for slugs on it:
leave the dangling relation dangling, let bun validate fail, and report the gap in its
incomplete field, because a stub there converts a recoverable interruption into
published debt signed by the gate. A stub is still correct for a slug the round never
planned, and the gate says so explicitly when it mints one.
4. e2e and commit (main session, ~30 min)
bun run test:e2e:new plays only specimens without a committed subject
snapshot; first run fail-writes the new snapshots. Ask before starting it, and
never run the full bun run test:e2e unasked: nothing runs it automatically any more
(ci.yml is static gates only, e2e is manual in e2e.yml), so a 45-minute pass is
45 minutes of the user's session. Fix behavioral failures and re-run targeted until
green.
Run it in the round that authored the specimens, even if the round is otherwise
done. The gate finds its work by the ABSENCE of a snapshot, so skipping it leaves
no trace of the omission: nothing fails, nothing warns, and the round commits
looking complete. Round 24 shipped 38 specimens with no record anywhere of what any
of them identifies as, and the only reason anyone found out is that round 25's run
came back reporting 47 new specimens instead of nine, at four times the wall clock.
If the user declines the run, say in the report that the round's specimens are
unproven and that the next round will pay for it.
The failure taxonomy so far, most common first:
- an assert timed to the edge of a state window (give it room, aim mid-window)
- an assert on evidence inside a popup the action just closed (mirror onto trigger)
- a mount-time assert with no room for kit fades (open with a wait)
- invalid selector syntax and hairline subjects (now validate-gated)
- an assert on a wrapper with no box: a group whose children are all absolutely
positioned, or one held at
height: 0 to overlay without shifting, measures zero
and reads as absent just like a hairline does
- a demo that answers input by synthesizing more input (
btn.click() inside a
click handler): the choreography passes and the TAKEOVER pass fails, because
it counts the clicks reaching the specimen and wants exactly one
- a mid-flight assert on an element that is fading OUT (STAGE_NEWS law 28). The
tell is an identical claim passing inbound and failing outbound, so it reads
as flake rather than as a rule; it is not one, and widening the window cannot
fix it. Claim the exit through something that stays visible.
- an assert whose meaning does not match the element's (STAGE_NEWS law 29): a
cumulative claim aimed at a "last event" readout. Look for its twin while
fixing it, an assert that passes for a reason the demo's state axis never
touched, which is invisible precisely because it is green.
Review the new subject snapshots (e2e/__snapshots__/<slug>-subject.txt):
every one should be scope: element with a sensibly narrow subject; every
data-pose carrier must mount in a state satisfying its pose (the attributes in
the snapshot show both). A whole scene scope withdraws identify: verify the
agent noted that decision deliberately.
No full bun run test:e2e, locally or in CI. If the round's specimens should be
proven before release, say so in the report and let the user trigger the manual
Specimens workflow, scoped with grep if only some terms changed.
Check for stray files (authors sometimes leave shot-*.png or probe scripts at
the repo root); move them out, never commit them.
bun run og --build shoots the share images (SPEC §10). It builds, then
photographs every specimen that has no image yet, which after a round is exactly
the new terms: seconds of shooting on top of the build. Not optional. A term page
names /og/<slug>.png whether or not the file exists, so a new term without one
ships a broken link preview, and nothing anywhere checks for it.
Commit everything as Author an Nth round of 6 terms per category. The tree
must be clean after.
5. Report
Tell the user: terms count before/after (from bun validate's summary), e2e check
count, behavioral failures found and how they were fixed (each is a candidate new
law for STAGE_NEWS), notable agent decisions (subject choices, dropped aliases,
deviations from briefs), and the remaining pool size.
The consolidated relations pass has run (2026-08-21), so a new term no longer ships
with empty relations: author its edges in the round, against the discrimination
test in SPEC §2.3, and give a term whose family already has a head term a variantOf
rather than a new contrast. Tags are authored too, since the corpus backfill
(2026-08-29): give every term the tags it belongs to FROM THE ENUM AS IT STANDS, read
off how the same facet was applied to that term's own family rather than from the tag
name alone. What a round may not do is grow the enum: a tag invented while authoring
nine terms is a tag chosen without knowing who else wants it, so record the candidate
in research/enumeration/canonicalize-notes.md and let it be settled by a pass over the
whole vocabulary (SPEC §2.5).
6. Promotion rounds
A promotion round finishes stubs instead of authoring new terms. Everything above
applies, with five differences that all come from the same fact: the term file
already exists and is already linked to.
- Roster. The stub list from step 1 IS the roster, so there is no pool selection,
but the collision sweep matters MORE rather than less: a stub written as a relation
target a year of rounds ago may since have been authored under another name.
Check each stub's definition against its nearest published neighbours, not just its
slug, and be willing to return one as a merge (an alias on the neighbour) rather
than force a page. The 2026-08-21 round checked 24 and merged none, but two
(
zero-state against empty-state, semantic-color against color-role) needed the
existing graph consulted before they survived.
- Briefs. Per term they carry, beyond the usual: the MANDATORY reciprocations
(every symmetric edge a published term already declares at the stub, which symmetry
stopped requiring only because it was a stub, and which the gate demands the moment
it publishes), the prose IOUs (the articles already linking in, which the new
article must agree with and link back to), and a per-term lookalike fence naming the
neighbour demo it must not duplicate. Compute the reciprocations mechanically; they
are not guessable.
- Relations are declared one-sided, on purpose. Authors write relations ONLY in
their own files and never touch another term's frontmatter, because seven agents
editing shared neighbours is a lost-write race.
bun validate then reports one
symmetry error per new outbound edge, which is expected, not a failure: the main
session closes them centrally afterwards (the closure script is trivial and worth
keeping). Check the eight-contrast hub cap after closure, not before: the
2026-08-21 round pushed select to nine and had to drop its weakest new edge.
- Tags are the main session's, not the authors'. Assign them centrally, matching how
the facet was applied to that term's own family (read the neighbours' tags), rather than
letting each agent invent its own reading of the enum.
- Editing in place. Keep
name, slug and category byte-identical: other pages
link to that slug. A definition may be sharpened only after reading the articles
that link in, because their prose was written against it, and a sharpening that
makes them false is a regression. Have the verify agent diff all three fields
against git show HEAD: and report drift.
1---2name: authoring-round3description: Run one vocab.design authoring round, up to 6 new terms per category (54 at full width, fewer as categories empty), from roster selection through briefs, parallel author agents, gates, e2e, and commit. Use when asked to author a round/batch of terms, "do another round of 6 per category", or to continue filling the candidate pool.4---56# Authoring round: 6 terms per category78One round takes the site up to 54 terms forward: roster from the pool, briefs with9demo hints, one parallel author agent per category plus one verify agent, then10e2e centrally, then a single commit. Eleven rounds have run this way; the process11below is the distilled shape, including every failure mode they taught.1213**A round is 6 per category only while every category still has 6.** The pool is14now uneven and draining unevenly: interaction was exhausted before round 16, and15round 16 emptied color and aesthetic (1 candidate each) while authoring 6 apiece16elsewhere, for 38 terms across 8 agents. Take the whole of a nearly-empty category17rather than skipping it, size the agent list to the categories that still have18terms, and tell a 1-term agent it has one term so it does not pad. Check the real19shape with `pool-remaining.ts` before promising the user a number.2021Files in this skill directory:2223- `pool-remaining.ts`: lists the unauthored pool per category24- `make-briefs-template.ts`: briefs generator to copy and fill25- `workflow-template.js`: the two-phase agent orchestration to copy and fill2627## 0. Preconditions2829- Read SPEC.md and AGENTS.md first if this session has not.30- Clean git tree, on `main` (commits go directly to the current branch; never branch).31- The USER runs the dev server on port 4321. Confirm with a GET request to32 `http://localhost:4321/` (Astro dev answers HEAD with nothing, so a HEAD probe33 looks exactly like a server that is down). Never start, stop, or restart any dev server; if it is34 down, ask the user rather than launching one.35- Agent quirk: Astro 7.2 force-backgrounds `astro preview` when it detects an agent, so36 Playwright aborts with "webServer exited early" and runs nothing. `playwright.config.ts`37 now sets `ASTRO_PREVIEW_BACKGROUND=1`; set it by hand for any ad-hoc playwright run.38 Piped through a buffering pipeline this reads as a HANG with an empty log, because the39 orphaned preview holds stdout open. Run e2e detached to a log file and never under a40 tool timeout: the full suite is ~18 min and a 10 min cap kills it mid-run.4142## 1. Roster (main session, ~15 min)43441. `bun .claude/skills/authoring-round/pool-remaining.ts` for the per-category pool45 AND the stub backlog, which it now prints as its own list. **Stubs outrank the46 pool.** A stub is a published page carrying a definition and nothing else, already47 linked to by six to nine articles, so promoting one repays a debt the site is48 already visibly carrying. If the stub list is non-empty, the round is a promotion49 round (see §6) unless the user asked for new terms specifically.502. Pick 6 per category, or the whole remainder where fewer are left. Selection51 principles, in priority order:52 - **Pay prose IOUs**: grep recent articles for "enumerated separately" and named53 cross-references to unauthored terms; an on-site article that NAMES a candidate54 is a debt the candidate's article repays by linking back.55 - Prefer head/core priority; take tail terms when they complete a family or pair.56 - **Build contrast pairs into one round** (fake-urgency/fake-scarcity,57 frame-rate/jank): round-mates get told to contrast each other in one sentence58 each way, and the demos must look different. Only where the pair is REAL, and59 that is a decision made at roster time, not at batching time: batching terms60 together for coordination (shared context, one owner for both sides of an edge,61 lookalike avoidance) must never come with an instruction to write an edge62 between them. Round 23 batched trace-viewer with diff-viewer purely because both63 are dense developer views, told the agent to author them "as a pair, one edge64 between them written on both sides", and got exactly that: a seeAlso each way65 plus a sentence in each article inventing a kinship ("both are dense two-pane66 developer views", "a list beside a detail is what they have in common"). The67 agent had already half-caught it, downgrading contrastWith to seeAlso because68 nobody describing a trace viewer reaches for the word diff viewer, which is the69 discrimination test correctly applied and should have ended in NO edge at all.70 The user caught the rest. Say "these two are yours, keep them from looking71 alike" and let the relation earn its place or not exist.72 - Avoid near-duplicates of existing specimens (a demo that would look like an73 existing one is a smell even when the term is distinct).74 - **Rotate and >2-contact terms wait**: the stage speaks single-contact touch75 (data-touch persona, hold step, pressure; STAGE_NEWS law 17) and the76 two-contact pinch (pinch step, pinchSpread, Ctrl+drag takeover; law 21), so77 pressure, long-press, tap, swipe, and pinch terms are authorable. Rotate and78 gestures past two contacts stay out of rosters until the vocabulary grows79 them (the fake-touch entry in the specimen-sweep ledger tracks it).803. **Collision-check every pick AND its likely aliases** against81 `src/content/terms/` (`name:` lines catch both term names and alias names).82 A candidate whose name is already an alias of an existing term is DEAD: swap it83 and note nothing. Claimed aliases a surviving pick must not take become fences84 in its brief hint.8586 This is now the highest-value step in the roster, not a formality. Do it as a87 script over the whole pool rather than per pick: slugify every alias name in88 every term file into a claimed-slug map, then test each candidate slug against89 it. Round 16 found **19 dead candidates in a 214-term pool**, and they cluster90 in exactly the wrong place: both of accessibility's core candidates91 (landmark-region, tab-order) and two of motion's three head/core92 (shared-element-transition, layout-animation). Priority is no defence.93 Also eyeball the survivors for near-duplicates the slug test cannot see: a94 candidate whose *display name* equals an existing term's name is just as dead95 (motion's `reduced-motion` is the on-site `prefers-reduced-motion`), and the96 pool's own `notes` field often flags merge candidates. The dead list is worth97 reporting to the user, since `candidates.json` still carries them.9899 Round 17 found 20 dead this way, then found **four more the slug test cannot100 see and one that escaped the roster entirely**. Two failure modes to add to the101 sweep, both cheap:102 - **Affix variants.** A candidate that is an existing term plus or minus a103 trailing noun shares no slug with it: `back-to-top-button` against the104 published `back-to-top`. This one reached an author agent, which refused it105 as a duplicate and cost the round a slot. Test each candidate slug with its106 trailing `-button`, `-bar`, `-menu`, `-view`, `-pattern` stripped, and with107 the existing slugs' own affixes added.108 - **Definition duplicates.** Read the DEFINITION of the nearest existing term,109 not just its name. `bulk-action-bar` died because bulk-actions' definition110 already says "a bar that appears once a selection exists and reports its111 count"; `session-timeout` died to timeout-warning, `slash-command-menu` to112 slash-command, `color-alone` to use-of-color. Four picks, all with distinct113 slugs, all already written. Grep the candidate's own definition keywords114 against existing definitions before promising the pick.115116## 2. Briefs (main session, ~30 min)117118Copy `make-briefs-template.ts` to a scratch directory, fill `PLAN` with the round's119slugs and hints (hint anatomy is documented in the template; drop the key for any120category with no terms left), set `OUT` to an absolute scratch path, run it with121bun from the repo root. It hard-fails on any122slug already on site or missing from the pool.123124It also prints a `const ROSTER = {...}` line. **Keep that output**: it goes into the125workflow script in step 3 verbatim, and it is what lets the verify gate tell a finished126round from an interrupted one.127128## 3. Author + verify (agents, ~30-45 min)129130Copy `workflow-template.js`, fill the placeholders:131132- `BRIEFS`: the absolute path from step 2.133- `ROSTER`: the line step 2 printed, pasted verbatim. It is the round's contract in134 three places: each author is told its exact slugs and refuses to guess if its briefs135 disagree, the log names any slug no agent reported, and the verify gate is told the136 shape of the round rather than inferring it from what survived. A workflow script has137 no filesystem access, so this literal is the only way the script can know it.138- `CATEGORY_GUIDANCE`: per-category exemplars, IOUs, and alias fences (anatomy139 documented in the template). This is where roster knowledge becomes agent140 knowledge; skimping here costs e2e failures later.141- Never trim `STAGE_NEWS`; append new laws as rounds teach them, and mirror any142 mechanically-checkable new law into `scripts/validate-terms.ts` as a gate.143144**Smoke-test the filled script before running it**, because a script that fails to parse145burns the whole launch and reports it as a bare `SyntaxError` with no round attached:146147```bash148node -e 'const s=require("fs").readFileSync(process.argv[1],"utf8").replace(/^export const meta/m,"const meta");new Function(`return (async () => {${s}})()`);console.log("parses")' -- /abs/path/to/rN.js149```150151`node --check` is the wrong tool here, and worse than nothing. A workflow script's real152evaluation context is an async function body: top-level `await` and a top-level `return`153are both legal in it, and both are errors to `--check` (as CJS it rejects the `await`, as154`.mjs` it rejects the `return`). So `--check` fails on every correct script, which trains155you to dismiss its output, which is how round 25 launched a script whose prompt strings156contained unescaped backticks. Wrapping the source in a function body parses it the way157the runner will and reports only real faults. It earned its keep immediately: the first158run of it over the edited template caught a missing comma in a prompt array.159160Run it with the Workflow tool if available (one author agent per stocked category, in parallel, then the161verify agent). Without a Workflow tool, run the same prompts as parallel subagents162and the verify prompt as one subagent after all authors return. Authors do their163own visual pass against the user's dev server; the verify agent runs164validate/typecheck/biome once and fixes what fails. Nobody runs e2e but you.165166Authors also run `AUDIT_PASS`: `vd-stage.audit()` plays a specimen's choreography167through the real attract player on the dev server and reports the same triple168`e2e/choreography.e2e.ts` asserts (no failures, exactly one subject, not169interrupted). Round 17 is why it is in the template: three of seven agents found170the API unprompted, audited their own specimens, and the round came back171**96/96 green on the first `test:e2e:new`**, with zero behavioral fixes needed172for the first time in the process. The other four flew blind and got lucky. It is173a dev-server pre-check, not the gate (dev injects CSS through JS, so a specimen174can mount before the stage has its height), so a failure there is always real175while a pass is only very likely: you still run the full suite.176177**Refused terms** (STAGE_NEWS law 18): an author who finds a term needs input or178stage machinery the player does not have returns it unauthored with179`needs stage primitive: <what>` instead of faking it with a simulation control.180Collect these in the report, tell the user (building the primitive is a main-session181or dedicated-session job, like `hold` and the touch persona were), and keep the term182out of rosters until the primitive lands. Faking the player is exactly what the183fake-touch sweep is un-doing; never mint new offenders.184185**Interrupted-agent recovery** (three times now: twice a spend limit, once an API186connection lost mid-response): inventory which slugs have all three files187(`terms/<slug>.mdx`, `demos/<slug>/demo.ts`, `demos/<slug>/choreography.ts`), then188relaunch a finish variant: per-category agents get only their REMAINING slugs, are told189which batch-mates are DONE (read them for idiom, do not rewrite), treat partial files as190drafts from their earlier self, and run the visual sweep over the WHOLE category batch191(dead agents may not have visually checked their own later terms; the sweep has caught192real overflow bugs in "complete" specimens every time). The verify agent then gates the193whole round.194195An agent that dies mid-response returns NOTHING, so its finished terms are absent from196`completed` and invisible to anything reading only the agents' reports. That is what the197`ROSTER` fill is for. Round 25 is the worked example: an author died having written198`wireframe` of a three-term fidelity ladder, and the verify gate, told only what the199surviving agents reported, found a published term contrasting two slugs that did not200exist. It did the SPEC 2.3-sanctioned thing and minted `mockup` and `prototype` as201STUBS, which is right in isolation and is exactly the debt the family was batched whole202to avoid. The gate now knows the roster and is told the opposite rule for slugs on it:203leave the dangling relation dangling, let `bun validate` fail, and report the gap in its204`incomplete` field, because a stub there converts a recoverable interruption into205published debt signed by the gate. A stub is still correct for a slug the round never206planned, and the gate says so explicitly when it mints one.207208## 4. e2e and commit (main session, ~30 min)2092101. `bun run test:e2e:new` plays only specimens without a committed subject211 snapshot; first run fail-writes the new snapshots. **Ask before starting it**, and212 never run the full `bun run test:e2e` unasked: nothing runs it automatically any more213 (`ci.yml` is static gates only, e2e is manual in `e2e.yml`), so a 45-minute pass is214 45 minutes of the user's session. Fix behavioral failures and re-run targeted until215 green.216217 **Run it in the round that authored the specimens, even if the round is otherwise218 done.** The gate finds its work by the ABSENCE of a snapshot, so skipping it leaves219 no trace of the omission: nothing fails, nothing warns, and the round commits220 looking complete. Round 24 shipped 38 specimens with no record anywhere of what any221 of them identifies as, and the only reason anyone found out is that round 25's run222 came back reporting 47 new specimens instead of nine, at four times the wall clock.223 If the user declines the run, say in the report that the round's specimens are224 unproven and that the next round will pay for it.225226 The failure taxonomy so far, most common first:227 - an assert timed to the edge of a state window (give it room, aim mid-window)228 - an assert on evidence inside a popup the action just closed (mirror onto trigger)229 - a mount-time assert with no room for kit fades (open with a wait)230 - invalid selector syntax and hairline subjects (now validate-gated)231 - an assert on a wrapper with no box: a group whose children are all absolutely232 positioned, or one held at `height: 0` to overlay without shifting, measures zero233 and reads as absent just like a hairline does234 - a demo that answers input by synthesizing more input (`btn.click()` inside a235 click handler): the choreography passes and the TAKEOVER pass fails, because236 it counts the clicks reaching the specimen and wants exactly one237 - a mid-flight assert on an element that is fading OUT (STAGE_NEWS law 28). The238 tell is an identical claim passing inbound and failing outbound, so it reads239 as flake rather than as a rule; it is not one, and widening the window cannot240 fix it. Claim the exit through something that stays visible.241 - an assert whose meaning does not match the element's (STAGE_NEWS law 29): a242 cumulative claim aimed at a "last event" readout. Look for its twin while243 fixing it, an assert that passes for a reason the demo's state axis never244 touched, which is invisible precisely because it is green.2452. **Review the new subject snapshots** (`e2e/__snapshots__/<slug>-subject.txt`):246 every one should be `scope: element` with a sensibly narrow subject; every247 `data-pose` carrier must mount in a state satisfying its pose (the attributes in248 the snapshot show both). A `whole scene` scope withdraws identify: verify the249 agent noted that decision deliberately.2503. No full `bun run test:e2e`, locally or in CI. If the round's specimens should be251 proven before release, say so in the report and let the user trigger the manual252 Specimens workflow, scoped with `grep` if only some terms changed.2534. Check for stray files (authors sometimes leave `shot-*.png` or probe scripts at254 the repo root); move them out, never commit them.2555. `bun run og --build` shoots the share images (SPEC §10). It builds, then256 photographs every specimen that has no image yet, which after a round is exactly257 the new terms: seconds of shooting on top of the build. Not optional. A term page258 names `/og/<slug>.png` whether or not the file exists, so a new term without one259 ships a broken link preview, and nothing anywhere checks for it.2606. Commit everything as `Author an Nth round of 6 terms per category`. The tree261 must be clean after.262263## 5. Report264265Tell the user: terms count before/after (from `bun validate`'s summary), e2e check266count, behavioral failures found and how they were fixed (each is a candidate new267law for STAGE_NEWS), notable agent decisions (subject choices, dropped aliases,268deviations from briefs), and the remaining pool size.269270The consolidated relations pass has run (2026-08-21), so a new term no longer ships271with empty `relations`: author its edges in the round, against the discrimination272test in SPEC §2.3, and give a term whose family already has a head term a `variantOf`273rather than a new contrast. Tags are authored too, since the corpus backfill274(2026-08-29): give every term the tags it belongs to FROM THE ENUM AS IT STANDS, read275off how the same facet was applied to that term's own family rather than from the tag276name alone. What a round may not do is grow the enum: a tag invented while authoring277nine terms is a tag chosen without knowing who else wants it, so record the candidate278in research/enumeration/canonicalize-notes.md and let it be settled by a pass over the279whole vocabulary (SPEC §2.5).280281## 6. Promotion rounds282283A promotion round finishes stubs instead of authoring new terms. Everything above284applies, with five differences that all come from the same fact: **the term file285already exists and is already linked to.**286287- **Roster.** The stub list from step 1 IS the roster, so there is no pool selection,288 but the collision sweep matters MORE rather than less: a stub written as a relation289 target a year of rounds ago may since have been authored under another name.290 Check each stub's definition against its nearest published neighbours, not just its291 slug, and be willing to return one as a merge (an alias on the neighbour) rather292 than force a page. The 2026-08-21 round checked 24 and merged none, but two293 (`zero-state` against empty-state, `semantic-color` against color-role) needed the294 existing graph consulted before they survived.295- **Briefs.** Per term they carry, beyond the usual: the MANDATORY reciprocations296 (every symmetric edge a published term already declares at the stub, which symmetry297 stopped requiring only because it was a stub, and which the gate demands the moment298 it publishes), the prose IOUs (the articles already linking in, which the new299 article must agree with and link back to), and a per-term lookalike fence naming the300 neighbour demo it must not duplicate. Compute the reciprocations mechanically; they301 are not guessable.302- **Relations are declared one-sided, on purpose.** Authors write relations ONLY in303 their own files and never touch another term's frontmatter, because seven agents304 editing shared neighbours is a lost-write race. `bun validate` then reports one305 symmetry error per new outbound edge, which is expected, not a failure: the main306 session closes them centrally afterwards (the closure script is trivial and worth307 keeping). Check the eight-contrast hub cap after closure, not before: the308 2026-08-21 round pushed `select` to nine and had to drop its weakest new edge.309- **Tags are the main session's, not the authors'.** Assign them centrally, matching how310 the facet was applied to that term's own family (read the neighbours' tags), rather than311 letting each agent invent its own reading of the enum.312- **Editing in place.** Keep `name`, `slug` and `category` byte-identical: other pages313 link to that slug. A definition may be sharpened only after reading the articles314 that link in, because their prose was written against it, and a sharpening that315 makes them false is a regression. Have the verify agent diff all three fields316 against `git show HEAD:` and report drift.