Skip to content

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 10 Oct 19:00
· 104 commits to main since this release
622929e

[0.5.0] - 2026-10-10

"Spend in the office": per-agent usage badges and a per-call detail panel on the canvas, fed only by host props (#76, #77); the portal reading those same figures from the real usage ledger for canvas badges, the agent inspector and the call-detail modal (#78); cost per meeting and per tool computed on the server and shown in the Meetings panel and tool tooltips (#80, #81); Model Ops's Matrix, Agents and Feed tabs reconciling call by call against the ledger instead of local counters (#79); and the library guide's four privacy rules written out in one place for integrators: the host computes, the component only displays; usage UI stays hidden until the host opts in; a missing figure is always "unknown", never 0; and the call detail panel is metadata only, never prompt, completion or tool-argument text. Full reference: the usage chapter of the library guide (español).

Added

  • Crew: its own audio engine, with music in the Lounge room (#150, epic #114): Crew gets a real audio engine (src/crew/crewAudioEngine.ts), independent of the Cartoon mode sound effects (src/engine/soundEffects.ts); separate code, separate state, separate preference. Every sound is synthesized at runtime with Web Audio API oscillators, so there is no recorded audio file to license: origin and license (CC0, original code, no third-party IP) for each sound are documented in src/crew/crewAudioAssets.ts (CREW_AUDIO_MANIFEST, the canonical source) and in docs/crew/AUDIO_LICENSES.md (human-readable table and rationale). Music is off by default and never starts on its own: CrewStage's accessibility-and-audio panel gets a Play/Pause button that only a click or keyboard activation can trigger; selecting the Lounge room and pressing Play starts its ambient loop, moving to any other room (Development, QA, the CEO office, etc.) fades it out and requires a fresh Play press to resume, so Lounge music can never play by accident elsewhere. CREW_ROOM_MUSIC maps every room explicitly (null = silence; only lounge has a track today), so adding a sound to another room is a one-line, reviewable change. The engine fades in/out on room changes and on manual pause, pauses on window/tab blur and visibilitychange and resumes only if it was already authorized to play (never a new autoplay), and dispose()s fully on unmount (every oscillator stopped, every node disconnected, the AudioContext closed, no listener left behind): switching to Cartoon mode unmounts CrewStage and so cleans up Crew's audio without touching Cartoon's own sound preference. useCrewAudio (src/crew/useCrewAudio.ts) wires the engine to React's lifecycle and already-existing mute/volume preferences (crewPreferences.ts, wired but inert since #157); a browser blocking autoplay (AudioContext.resume() rejecting) is caught and surfaced as its own blocked state with a caption, never thrown. New subtitles for every audio state change (playing/paused/blocked) reuse the existing room/camera caption mechanism (subtitles preference, #157), satisfying the same textual-equivalent requirement for this sound. New tests/lib/crewAudioEngine.test.ts, tests/lib/useCrewAudio.test.tsx (mocked Web Audio API, autoplay-blocked handling, focus/visibility pause-resume, StrictMode-safe cleanup) and tests/lib/crewAudioAssets.test.ts (license manifest completeness, room isolation). All new English/Spanish UI text follows CrewStage's existing inline-ternary pattern. Deliberately out of this change's scope, per the issue's own split into sub-topics: the phone ring (#145) and the coffee sound (#146) are separate issues and are not wired to any room; the engine already exposes the primitive (playCue) and the per-room mapping those issues need, with no further change expected here. A per-room volume slider was not added: CrewPreferences.volume/muted (#157) stay global, and room isolation is achieved architecturally (each room's track is independent and fades out when left), matching the issue's "no sala de Lounge por accidente" acceptance criterion without inventing a second, undocumented preference axis.

  • Example: library-host, a worked host app reading the rollup/calls API (#260, v0.5.0 milestone): new examples/library-host/, the runnable example #82's guide points to for "getting figures from the server". usageFromRollup.ts declares the server's rollup/calls response shapes and two pure mappers, rollupToUsage and callsToDetail, that only copy or null a figure, never compute one (locked by a token-scanner AST-style check in tests/lib/libraryHost.test.tsx, since the whole point of this example is showing the one right way to feed the library real spend). HostOffice.tsx fetches both endpoints on mount and every 15 seconds (abortable, never overlapping) through a small node:http proxy (proxy.ts, about 70 lines) that injects Authorization: Bearer $AGENT_VIEWER_API_TOKEN server side; the token variable is referenced only there and in the example's own README.md (tests/library-host-proxy.test.mjs checks this statically, plus that the proxy forwards only allowlisted GET paths, strips a client-sent Authorization, never echoes the token, and refuses to start empty). main.tsx/index.html/vite.config.ts give it its own dev server, unrelated to the root build. seed.jsonl/seed.sh seed five agents covering a known cost, mixed currencies, an explicitly unknown cost and a failed call (agent-viewer send cannot send llm.usage, so seed.sh posts the batch endpoint directly, taking the server token as an argument rather than by the same environment variable name, so it stays outside that "only two files" rule too).
    Reconciled against the real shipped #66, not the issue's illustrative sketch: the shipped rollup response has no single totalTokens field, only a per-kind breakdown already summed server-side (tokens.input.sum, ...); computing one from input.sum + output.sum here would be exactly the client-side arithmetic this mapper must not do, so totalTokens reads "unknown" on the compact badge in this example until the rollup API grows a combined field, while the per-kind breakdown, cost and the call detail panel all show real figures. Likewise, an agent absent from the rollup gets no byAgent entry, which the already-shipped #76 badge and accessible list render as no usage text at all (not a literal "unknown" badge); this example's tests check that real, shipped behavior rather than the issue's pre-#76 wording. See examples/library-host/README.md and the module doc comment in usageFromRollup.ts for the full reasoning on both.

  • Docs: reproducible, script-verified role palette measurement for the Crew visual style contract (#116 partial, epic #114): re-audited #116 and its own blocking foundations (CREW-001..008: #172/#173/#166/#168/#171 closed, #167/#170 still open) and confirmed the issue's core remains genuinely blocked, as it requires final art and real walk animation for all six roles that only exists, as a prototype, for the CEO; that is art direction, not something this change can decide. Found one real, bounded gap inside the already-merged contract itself: section 6 of docs/crew/visual-style-contract.md/.es.md described the five non-CEO roles' outerwear swatches as "measured automatically... 16-level color quantization, alpha >= 200..." but no script in the repository ever implemented that measurement; the documented values (~#005060 and similar) were hand-eyeballed approximations, not reproducible ones. New scripts/crew-palette-measure.mjs (a from-scratch PNG decoder built only on node:zlib, no new dependency, since the project has no PNG-decoding library) measures each role's idle-front prototype PNG with that exact method, explicitly excluding hair/outline and light-skin pixels by hue and lightness so they cannot be mistaken for the outerwear color. The five swatches are now exact and script-verified (#085868, #1848C8, #283838, #482888, #C85838), replacing the old ~-prefixed guesses in both documents, and locked by new tests/crew-palette-measure.test.mjs (npm run crew:palette:measure reproduces them against the real bank PNGs) so the table cannot silently drift from the art it describes. This does not ratify any palette: every swatch still reads unratified, and section 13's open follow-up ("art-director ratification... ") is explicitly left standing, since that is a human art-direction call this change does not make. No PNG/SVG/manifest asset was touched. See the #116 row in docs/crew/IMPLEMENTATION-STATUS.md for the full accounting.

  • Crew: real desk and plant artwork from the asset bank in the CEO office (#115 partial, epic #114): closes the gap the issue itself flagged as pending after #172/#194 ("this rescue does not yet integrate art into the rooms") for a small, honest slice. The executive desk and the floor plant (furniture.desk-executive, furniture.plant-floor, both still status: prototype, never approved) now render as the real original SVG in the independent Crew renderer, not the usual flat-shaded 2.5D box, only for the ceo-desk/ceo-plant furniture of the CEO office; the other 51 bank assets and the other ten rooms are untouched. New src/crew/crewPropImages.ts (load/dispose, mirroring the existing CEO sprite loader) and src/crew/useCrewPropImages.ts (per-room load/cleanup, same lifecycle as useCrewSprite), with crewPropsLayer.ts/renderCrewRoom.ts drawing the resolved image as a flat billboard instead of the polygon box when one is available, falling back to the box (with its own English/Spanish status text, data-testid="crew-prop-art-status") if the image fails to load. Two known, documented limits: the bank has no per-angle verified views for any furniture piece yet, so the same single image is reused across all four cameras rather than four distinct views; and because both SVGs are under Vite's 4 KB inline threshold, they ship as a data:image/svg+xml URI inside their own lazy chunk instead of a hashed file (unlike the CEO PNG pilot), with no change to their original bytes. New tests/lib/crewPropImages.test.ts plus cases added to crewPropsLayer.test.ts/renderCrewRoom.test.ts, and a new tests/e2e/crew-prop-images.spec.ts proving the real image renders from all four cameras in the CEO office, is absent in every other room, falls back correctly, and that Cartoon mode has no regression. Not closing the issue: 9 of 11 rooms and 51 of 53 bank assets still render with no image, and per-angle art for these two pieces is still unverified; see the #115 row in docs/crew/IMPLEMENTATION-STATUS.md for the full accounting.

  • Docs: audit and close #173, Crew camera ADR (v0.3.0 milestone, epic #114): re-read the issue and the real code in src/crew/ on main against its own five literal acceptance criteria, instead of trusting this file's stale historical note or the ADR's self-imposed "requires G1" status (that G1 bar belongs to the full epic #114, not to this child issue). All five already hold today: the ADR (merged by #174), the per-room camera contract with explicit front/right/back/left presets and continuous zoom (src/crew/crewCamera.ts), the dedicated tests/e2e/crew-facing.spec.ts proving that rotating the camera never leaves characters facing forward by mistake, documentation that never conflates 2D scene rotation with real 3D perspective, and isolated single-office selection with a fluid desktop/mobile experience, all re-verified today (npm run lint, 15 Vitest, 5 Chromium E2E, all green). No product code changed; see docs/crew/IMPLEMENTATION-STATUS.md (#173 row) and docs/adr/ADR-001-crew-camera.md. The epic's own G1 certification (final illustrated art, physical-device QA) remains open and is owned by other epic issues, not #173.

  • Portal: meeting and tool spend in the office (#81, v0.5.0 milestone): the Meetings panel and the tool chip tooltips show real cost and token figures from the usage ledger instead of the fake 0/$0.000 a live meeting always showed before. Both are thin, read-only views over GET /api/v1/usage/rollup?groupBy=meeting|tool (#80), through a src/integrations/ledgerClient.ts extension (meeting/tool dimensions, attribution, sessionIds/sessionCount) and new portal-only modules under src/components/usage/ (useMeetingUsage, useToolUsage, ToolSpendTooltip, spendFormat), neither reachable from src/lib (tests/lib/libraryIsolation.test.ts). Reconciled against the real shipped server, not the issue's pre-implementation sketch: there is no dedicated /usage/meetings//usage/tools endpoint, tokens come per kind (input/output/cacheRead/cacheWrite/reasoning, each already summed server-side) rather than one total, and "unattributed" is the rollup's own meeting-dimension group with key.meetingId: null (no per-meeting time-overlap heuristic exists on the server, by design). The Meetings panel never reads the deprecated Meeting.tokensAccumulated/costAccumulated in live mode, shows "unknown" for a meeting with no ledger rows yet, and adds a "Spend per meeting" table listing every portal meeting plus any server-only row plus the unattributed bucket, with no totals row. Demo mode (no server connected) keeps the old simulated figures, now tagged "Simulated", and makes no request. Tool chips in AgentInspector and AgentDetailModal (current tool and per-task tools) get an accessible tooltip (hover and keyboard focus to open, blur and Escape to close, hoverable per WCAG 1.4.13, aria-describedby) showing that tool's attributed calls, tokens and cost, or "no figures from the server" for a chip with no matching row. Library: AgentOffice gains the optional meetingUsage prop (MeetingUsage/MeetingUsageFigures, extending UsageFigures with calls, callsWithoutCost and unattributedCalls, new formatMeetingUsage), shown next to the active meeting (or the latest concluded one with an entry) only while showUsage is also on, in a new dl.av-meeting-usage panel and a second accessible "Meetings" list; nothing is summed across meetings, and an unmatched meeting id renders nothing. Meeting.tokensAccumulated/costAccumulated are now @deprecated (removal deferred to 1.0, a breaking change). Five new portal meetings.spend.* and tool.spend.* message groups and five new library usage.* keys, in English and Spanish.

  • Portal: ledger-backed spend badges, per-call detail and display preferences (#78, v0.5.0 milestone): in live mode with a ledger-capable server, each agent on the office canvas shows a compact spend badge (agentBadges, fed by the src/integrations/ledgerToUsage.ts mapper over GET /api/v1/usage/rollup?groupBy=agent, #66), and the top-bar chip reads rollup.totals instead of summing the SSE stream it happened to receive. A Last hour/Today/7 days/All selector next to the chip (src/integrations/usageWindow.ts, persisted as agent-viewer:usage-window) controls the window for both; figures survive a reload and never contradict a badge: once the ledger has answered, the canvas's own locally-summed telemetry (usageTelemetry) turns off. The mapper reads only, never invents: a token or cost figure the server did not fully report for every call stays "unknown", never 0; an agent with no row in the rollup gets no badge at all. useLedgerAgentUsage refetches on mount, on a window change, on a debounced new llm.usage/llm.failed event and on a 60 s clock tick for a rolling window; demo mode and a server without the ledger endpoints make no request.
    This item completes the rest of the issue on top of that foundation. AgentInspector, LiveTimelineSidebar and AgentDetailModal's metrics tab now read the agent's own rollup row (usageFigures/ledgerReady props, formatTokens/formatCost from src/lib/usage.ts, never a hardcoded $) instead of the agent's local accumulators whenever the ledger has answered, falling back to the local counters exactly as before otherwise; the metrics tab also gains cache-read and cache-write token cards. The inspector's new "View calls" button opens AgentDetailModal directly on its metrics tab (onOpenDetailModal now takes an optional tab, App.tsx's detailModalInitialTab), which lists that agent's calls newest first through a new cursor-paginated src/components/usage/useAgentCalls.ts hook (GET /api/v1/usage/calls, mirroring Model Ops' #79 useModelOpsLedger feed pattern: 50 rows per page, dedup by eventId, a debounced refetch on a new usage/failed event for that agent, invalid-cursor recovery, a hard 500-row display cap with a cap hint) and a new presentation-only src/components/usage/AgentCallsTable.tsx (time with receivedAt in its tooltip, model, status with errorCode, input/output/cache-read/cache-write/reasoning tokens, latency, cost with an "estimated"/"source unknown" tag, and requestId). CallsQueryParams/fetchCalls (src/integrations/ledgerClient.ts) gained from/to, the same pass-through convention RollupQueryParams already had. Two new persisted display preferences (src/integrations/displayPreferences.ts, agent-viewer:mask-secrets and agent-viewer:show-usage-badges, both default on, lifted out of SettingsModal's own state into controlled props): masking shortens a call's requestId with a copy-to-clipboard button (full value still copyable); hiding usage badges clears the canvas badge map while the top-bar total keeps showing. A requestId matching the server's [REDACTED:<rule>] marker (new exported REDACTION_MARKER_PATTERN in src/integrations/redaction.ts, purely additive) always renders as a distinct "redacted" chip regardless of the mask preference. A failed refetch that keeps the last good rollup now shows as a distinct dimmed "stale" state, with a clock marker and an accessible note, in the top-bar chip, the inspector and the detail modal, instead of looking identical to a confirmed-fresh reading. New agentDetail.calls.*, inspector.viewCalls/noCallsInWindow/stale, settings.usageDisplayTitle/showUsageBadges* and usage.stale message keys, in English and Spanish.
    Deferred, out of this issue's scope on purpose: the warning for "server reports redaction is disabled" is not implemented. No endpoint in this codebase (GET /api/v1/usage/calls, the rollup, or any other) reports whether redaction is active; wiring that signal would mean adding a new field to the server's usage contract, which issue #78 itself lists as out of scope ("redaction rules ... #68"). The [REDACTED:...] chip above still renders correctly whenever the server does apply its marker; only the "redaction is off" notice itself needs that future contract change.

  • Library: per-call detail panel from host props (#77, v0.5.0 milestone): an agent's usage total answers "who spent what"; clicking that agent now answers "which calls made up that number". showCallDetails (AgentOfficeProps, off by default, independent of showUsage/showUsageBadges) opens a read-only <section class="av-call-details"> panel next to the selected agent, fed only by the new agentCallDetails prop (AgentCallDetails = Record<string, readonly AgentCallDetail[]>, new src/lib/callDetails.ts, also exporting AgentCallTokens, AgentCallStatus and AgentCallCostSource). The component never fetches, sums, prices, sorts or deduplicates: every call renders in the host's order with 12 fixed rows (provider, model, status, latency, request id, input/output tokens and cost source/cost always shown; cache read, cache write and reasoning tokens only when that key is present in tokens, reusing the usage.cacheReadTokens/usage.cacheWriteTokens/usage.reasoningTokens/usage.costSource catalog keys and formatCostSource from #76). AgentCallDetail is closed on purpose: no field can carry a prompt, a completion or any other free text, provider/model/requestId are capped at 128 Unicode code points (cut with an ellipsis, in the text and in title), unrecognized status/costSource values and non-string/empty/whitespace-only text read as "unknown" rather than the raw value, and the renderer (src/lib/CallDetailsPanel.tsx) picks each field explicitly instead of spreading the row, so an extra host key is never rendered. The panel opens for a click on the canvas, a choice in the keyboard agent list, or a controlled selectedAgentId, reusing the existing onSelectAgent callback with no new prop added for it; it renders nothing for a selected id outside the current snapshot. Close it with its close button or Escape (focus inside the panel), both routed through onSelectAgent(null). An agent with no entry, or an empty array, shows "No call details provided", never "0 calls". New :where-scoped CSS classes with no transition or animation (av-call-details and its -header/-close/-empty/-list/-item/-row/-request-id children), positioned bottom-right of the stage so it does not collide with the usage panel (top-right) or the toolbar (left). Not drawn into recordReplay's video export, which has no option for it. Thirteen new calls.* message keys in English and Spanish. Out of scope here, left for later issues: real ledger data from the calls API (#67) and the portal wiring that feeds it from the server (#78).

  • Library: per-agent usage badges on the office canvas, fed only by host props (#76, v0.5.0 milestone): showUsageBadges (AgentOfficeProps, off by default, independent of showUsage) draws a compact spend badge on every agent card from usage.byAgent, display only: the embedded component still never invents, sums or prices usage. An agent with no usage.byAgent entry gets no badge; an entry with missing or null fields shows "unknown" for those. UsageFigures (src/lib/usage.ts) gains cacheReadTokens, cacheWriteTokens, reasoningTokens, costSource ('provider-reported' | 'estimated' | 'unknown') and failedCalls, all optional and backward compatible; formatUsage adds the matching rows only when the host sends those keys. New formatUsageBadge(figures, locale, translate) (exported along with UsageBadge and UsageCostSource) formats the compact card text: tokens under 10,000 exact and compact from there (12.3K), a reported zero cost as $0.00, a positive cost under half a cent as <$0.01 (never $0.00), an est. mark for an estimated cost source, and a N failed chip. summarizeUsage fills the new fields for hosts with no usage service of their own: the three breakdown token fields are omitted when no counted event reports them, summed when every one does, null when only some do (and read the deprecated cachedTokens as cacheReadTokens); costSource is omitted until some event sends it, then the common value or null on disagreement; failedCalls counts llm.failed events independently of llm.usage, giving an agent with only failed calls its own entry. The canvas (drawAgentOverlays in src/engine/canvasRenderer.ts) receives only pre-formatted strings through the new agentBadges prop (OfficeCanvas, RenderContext), never a number, and the badge path imports nothing from modelOps: the card grows to fit the badge on one row up to the existing 190px cap, then wraps (52/66/80px for 1/2/3 rows), staying clear of heads, plaques and other cards via the existing placeOverlay layout. The accessible agent list is now fed by showUsage || showUsageBadges, so the screen-reader text always carries the same exact figures as a visible badge. Ten new usage.* message keys in English and Spanish. Out of scope here, left for later issues: the per-call detail panel (#77), server/portal wiring of real ledger figures (#78) and budget/limit comparison (#87).

  • Cost per meeting and per tool on the server (#80, v0.5.0 milestone): GET /api/v1/usage/rollup gains two new groupBy dimensions, meeting and tool, so an auditor can answer "how much did this meeting cost" and "which tool is the expensive one" from honestly attributed figures, never from time overlap. Attribution is explicit only: a usage row is attributed to a meeting only through its own meetingId, and to a tool only through its own toolCallId matched against a tool.started event in the exact same (sessionId, agentId, toolCallId) scope (attributed/unresolved/ambiguous/unattributed, see server/usage/attribution.ts). New row-level filters meetingId, toolCallId, tool, meetingAttribution, toolAttribution; meeting groups also carry sessionIds/sessionCount. Non-attributed groups are never dropped by limit, and groups sort by attribution rank (attributed first) before anything else. New SQLite migration 0011-rollup-attribution adds the derived tool_calls/meeting_labels tables and the tool_call_resolution view (backfilled from events.payload, rebuilt automatically if either table is ever dropped); memory mode keeps the equivalent index in ServerState (server/serverState.ts). The Python and TypeScript SDKs already supported toolCallId on usage()/tool*() calls (issue #64); this item only adds the server-side aggregation. Library and portal are unchanged (#81 builds the panels on top of this). See tests/usage-rollup-attribution.test.mjs for the full hand-computed fixture.

  • Crew accessibility preferences (#157): CrewPreferences (src/crew/crewPreferences.ts, re-exported from @warlockcode/agent-viewer) gains muted, volume, highContrast, subtitles, hudSize and accessibilityPreset, additive and backward compatible with the #171 shape (only reducedMotion). New applyCrewAccessibilityPreset() sets several fields at once for a "screen reader" or "low vision" preset. CrewStage wires checkboxes/select/range controls for these preferences, a collapsed-by-default <details> disclosure to keep the toolbar usable on narrow viewports, visible camera/room-change captions gated by subtitles (screen-reader-only otherwise), and a high-contrast canvas palette (src/crew/crewContrast.ts, with a WCAG contrast-ratio helper and unit tests). Mute/volume are wiring only: Crew has no audio engine yet (#150 depends on this issue), so they are a documented no-op until real audio exists.

  • Model Ops on real ledger data (#79): the demo app's Model Ops console gets a ledger mode, used whenever the portal resolves an API base. The Matrix, Agents and Feed tabs become a thin, read-only view over the server's usage-ledger rollup and calls read APIs (#66, #67): per-model and per-agent figures now reconcile call by call instead of being rebuilt from agent-level counters (aggregateModelUsage, now @deprecated demo-only, stays for simulated mode and the office plaque only), a metric the ledger never reported renders "n/a" (never 0), a partially reported one carries a "partial" marker, cost is shown per currency and cost source as separate chips (never summed across either), cache read and cache write are separate columns, and failed calls are counted and filterable. The Feed tab lists real calls, paginated by cursor, with no fabricated entry, no prompt text and no invented "200 OK"/"CACHED" status. The Simulator tab keeps its permanent "Simulation" banner, offers only the demo price catalog's own models (never a model only observed on the ledger, whose price it does not know), and never makes a network request. Seven empty/error states (loading, server too old, unauthorized, network error, ledger empty with copy-ready snippets, filters match nothing, rollup truncated) cover every gap, in English and Spanish. New, portal-only modules: src/integrations/ledgerClient.ts and src/components/modelOps/*; neither is reachable from src/lib (enforced by tests/lib/libraryIsolation.test.ts). simulated mode (no API base, for example a plain npm run dev) keeps the pre-ledger behavior behind a permanent "Simulated demo data" banner. No server, contract or SDK change.

  • Docs: library guide rewrite for usage badges, call detail and the four privacy rules (#259, v0.5.0 milestone): docs/library.md/docs/library.es.md now cover version 0.5.0 (header and install URL). A new "The four rules" box states, together and in both languages: the host computes and the component only displays; usage UI stays hidden until the host opts in (showUsage/showUsageBadges/showCallDetails); a missing figure is always "unknown", never 0; and the call detail panel is metadata only, with no field for prompt, completion or tool-argument text. Added the previously undocumented calls.* (12 keys, from #77) and crew.* (3 keys, demo/Crew-renderer-only) message-key tables, matching OFFICE_MESSAGES exactly, and corrected the stale "116 keys" count to the real 156. New accessibility bullets describe the call details panel's actual focus and aria-live behavior (no focus trap on open, no announcement on open or update); two new isolation-guarantees bullets state that no usage figure is ever persisted and that the usage UI renders nothing while its opt-in prop is off. New tests/docs-parity.test.mjs (heading sequence, line count, code-block count and em-dash guard between the two guides) and tests/docs-usage-keys.test.mjs (the usage.*/calls.* tables cross-checked against OFFICE_MESSAGES, in both languages). Split out of #82 as its own sub-issue, per .claude/agent-policy.md; the example host (#260) and the version bump/CHANGELOG/release prep (#261) are separate, still-open sub-issues.