Build agent notes per the implementation-notes SOP. Spec: 2026-07-30-onoma-build-spec.md. Scope: v0 only. No key or secret appears in this file or in app/.
Decisions
- API surface (claude-api skill + live verification): raw
fetchtoPOST https://api.anthropic.com/v1/messageswith headerscontent-type,x-api-key,anthropic-version: 2023-06-01,anthropic-dangerous-direct-browser-access: true. The browser-direct header is NOT documented in the claude-api skill (grepped all skill files); it rested on [[2026-07-21-ink-build-spec|Ink's]] 2026-07-21 live verification. Per parent ruling (2026-07-30), re-verified live once from this build: OPTIONS preflight withAccess-Control-Request-Headers: content-type,x-api-key,anthropic-version,anthropic-dangerous-direct-browser-accessreturned 200 withaccess-control-allow-headersechoing all four andaccess-control-allow-origin: *; one POST with a garbage key (sk-ant-onoma-dummy-not-a-real-key) returned401 {"type":"error","error":{"type":"authentication_error","message":"invalid x-api-key"}}(request_id req_011CdYKYcfN2kHULG4BJPfrZ). That was the only live call in the build; no real key was ever present. - Model:
claude-opus-5(spec-locked; confirmed a current model ID by the skill). Notemperature/top_p/top_k/thinkingparams sent — sampling params 400 on Opus 5; omittingthinkingruns adaptive (the default).max_tokens: 8192because on Opus 5 thinking is on by default andmax_tokenscaps thinking + JSON output together. - Structured output (from skill):
output_config: {format: {type: "json_schema", schema: ...}}on the request. Schema encodes the spec's shape exactly, withadditionalProperties: false+ fullrequiredon every object and enums forroleandconfidence. - Schema cannot express the compression/parts length invariant (JSON Schema has no cross-field constraints, and the API's structured-output subset excludes complex array constraints). The invariant is stated in the schema
description, restated in the system prompt, and asserted in the render: on mismatch the three-beat + reading are withheld and an honest "misaligned reading" notice is shown while parts + synthesis render as received. This is a real tested render state (mismatch.json fixture), not just an assertion. - Palette: v2 Tampa Bay sky+sun per parent ruling 2026-07-30 (spec originally said v1 cream/terracotta; DESIGN-rdco.md marks that legacy-only; spec has been corrected at source by parent).
rdco-sample.htmlused for layout/voice feel only, not palette. - Few-shot exemplars span three types (person: Benjamin Andrew Wilson from spec; place: Brasstown — the settlers-translated-the-wrong-Cherokee-word detail, confidence
mixedwith a Mooney-sourced caveat so the model sees the honesty machinery used; organization: Sony — sonus×sonny, chosen to mean nothing in Japanese, plus the son-ni "to lose money" clipping detail). Each note is a genuinely non-obvious fact, not a root restatement; all three validate mechanically against the schema (checked in Node, including compression/parts alignment). - File layout: classic scripts, no build step (quick-sites ships verbatim):
index.html,onoma.css,js/api.js,js/render.js,js/app.js,fixtures/{benjamin,place,mismatch}.json. Cache-bust?v=01on all subresources (Ink false-verdict trap noted; bump per edit round). - Key handling:
localStorage['onoma.apikey']only; separate from any other state; read insidedecompose()at call time and nulled after the fetch call is issued; input istype="password"autocomplete="off"; never logged, never in URLs, never serialized into errors. Error rendering shows status + the API's ownerror.messageonly (e.g. "invalid x-api-key" — contains no key material); request headers are never echoed anywhere. - Response handling:
stop_reasonchecked before reading content (refusalandmax_tokenseach get honest distinct notices, per skill guidance that Opus 5 classifiers return 200 +stop_reason: "refusal"). Model output enters the DOM viatextContentonly — no innerHTML of model data anywhere. - Fixtures are full API message envelopes (id/model/stop_reason/content/usage) with the payload as a stringified text block, so the stubbed fetch exercises the real parse path end to end.
Tradeoffs
- No streaming: structured-output responses here are small and layout needs the whole object; non-streaming keeps the call layer trivial. 8192 max_tokens stays under SDK-timeout territory.
- Six radio chips are plain radios styled as pills — no JS state beyond reading
:checked, keeping DONE #3 trivially verifiable. - The hero doodle is inline SVG (navy wobble stroke + one sun accent) in a sky-lift field — hidden below 760px rather than reflowed, keeping mobile clean.
Open questions (→ founder / v1)
- Live-output quality (voice bar on real calls) is untested by design — parent runs the LIVE-OUTPUT ACCEPTANCE with a real key after deploy.
other-type inference wording: the system prompt tells the model to replaceotherwith the inferred category inname_type; the render shows it in the eyebrow. If founder prefers "inferred: X" phrasing, one-line change in render.- No lookup history / permalink per spec out-of-scope list.
Design-critic iteration (round 1 — ITERATE verdict, all findings addressed)
Fresh-eyes critic subagent (design-critic skill, run as a zero-context agent per the skill's subagent mandate) returned ITERATE: 5/5 tells, doodle aesthetic 4/4, hero-character check PASS, zero console/network issues — held back on proportion: navy secondary at ~0.1% of canvas (page read as one pale-blue field), sun accent ~1-2% (below the 5% floor), the motto's mono-caps line a verbatim duplicate of the serif beats, doodle hidden on mobile, gear 24×32px tap target, chips 41px.
Fixes applied (cache-bust ?v=02 → 04 across two edit rounds):
- Navy secondary band ×2: the compression section is now a full navy
#0D2438band (sky-soft serif beats, sun separators) and the footer is a full-bleed navy band (100vw breakout, column-aligned padding). - Sun promotion: wave-sun banker's-note underline on the hero
<em>(inline data-URI SVG — the staged wave asset files weren't shipped into the app folder; same visual contract, self-contained), sun-deep 3px left rails on each card's literal-gloss line, sun beat separators in the band. - Motto duplicate removed:
readingis retained in the payload but no longer displayed — the critic flagged it as a near-verbatim restatement of the beats in a second register. The band's mono caption is now the root chain (parts[].rootjoined with ·) in paperline mono. Deviation from the spec's "compression + reading are the three-beat payoff" phrasing — reading is data-present, display-suppressed; one-line revert if founder wants it back. - Mobile: hero doodle now shown compact (150px, centered, hero-row wraps) instead of
display:none; gear enlarged to 44×44; chips to 44px min-height. Verified: gear 44×44, chip 44px, no horizontal overflow at 393px. - Contract catch during re-verify (own audit, not critic): the root chain first used bay
#4d8aacas text on navy (~3.4:1; contract marks bay decorative-only) — switched to paperline#b8c8d6(~8.5:1 on navy).
Design-critic round 2 — DID NOT COMPLETE (UNVERIFIED)
A round-2 re-critic was dispatched to the same fresh-eyes agent after the fixes; its transcript shows it mid-capture (mobile checks) with no verdict emitted before the session ended. Round 2 is therefore UNVERIFIED — the round-1 verdict (ITERATE, 5/5 tells, findings all addressed above) is the last completed critic pass. Parent owns the critic loop from here. All round-1 findings were verified fixed mechanically (gear 44×44, chips 44px, doodle visible on mobile, navy bands + root chain present in the DOM and in read screenshots), but no fresh-eyes agent has re-scored the iterated page.
Verification log (Playwright, localhost:8791, cache-busted per-round query params, JPEG screenshots — read, not just taken)
- Console: 0 errors / 0 warnings on every load after the first (the single session error was a favicon 404 on the very first load, fixed with a data-URI icon in the same round; final loads clean).
- DONE 3: 6 radios render (
person,place,organization,vessel,work,other); for each, a stubbed-fetch submit captured the request —Type:line in the user message tracks the selection for all six; every captured request had modelclaude-opus-5,output_config.format.type: "json_schema", endpointhttps://api.anthropic.com/v1/messages, and exactly the four headers (content-type, x-api-key, anthropic-version, anthropic-dangerous-direct-browser-access). - DONE 4: benjamin fixture (3 parts) → 3 cards (Benjamin/Andrew/Wilson) with role/language mono meta, root chip, italic gloss, note; navy band with large serif three-beat "Right hand. · Courage. · Son of will." + root chain; synthesis as body prose; no confidence footnote (high + empty caveat = correctly quiet).
verification/onoma-benjamin-render.jpeg(desktop 1440) read and confirmed. - DONE 5: place fixture (2 parts) → 2 cards (Amstel/dam), 2-beat band, eyebrow "Reading · place · 2 parts" — render not hardcoded to 3 parts or person shape.
onoma-place-render.jpeg. - Mismatch fixture (3 parts / 2 beats): three-beat and reading withheld, honest coral-railed "misaligned reading" notice shown, cards + synthesis still rendered.
onoma-mismatch-notice.jpegread and confirmed. (Coordinator hold-point 1: tested as a real render state.) - DONE 6: no-key submit → "no key" notice + settings auto-open + zero requests to anthropic (performance entries checked); save →
localStorage['onoma.apikey']only, input cleared; persists across cache-busted reload; clear control removes it; status line tracks state. - DONE 7: stubbed 401 → "key rejected"; 429 → "rate limited"; malformed 200-body → "unreadable reply";
stop_reason: refusalenvelope → "declined". All four distinct, visible, honest; DOM contains no key material and no header names after all error paths. - DONE 2:
grep -rE 'sk-ant|ANTHROPIC_API_KEY' app/→ no matches (two UI-copy literals of "sk-ant" were reworded to keep the gate mechanical). - Live header verification (the one live call, per parent ruling): recorded under Decisions above.
- Mobile (393×852): full flow renders, no horizontal overflow, doodle visible, root chain present, tap targets ≥44px.
onoma-mobile-render.jpeg. - Fixtures sanity (Node): all three fixtures parse as API envelopes; payload parts/compression counts 3/3, 2/2, 3/2 (mismatch) as designed. Few-shot exemplars validated mechanically against the schema (keys, enums, no extras, compression alignment); three types represented.