---
description: CLI, MCP, and share must pass the same format opts (tz, locale, flags)
globs: packages/{cli,mcp,core,plugin}/**/*
alwaysApply: false
---

# Surface parity (CLI / MCP / share)

Any user-visible formatter change must land on **all three surfaces** before merge:

| Surface | Verify |
|---------|--------|
| CLI | `packages/cli` command + `packages/cli/test/` |
| MCP | `toolGet*` in `packages/mcp/src/tools.ts` + `packages/mcp/test/tools.test.ts` |
| Share | `formatShare*` + `packages/mcp/test/share.test.ts` |

One stated exception: the match timeline (core `timelineRows`) is CLI `match` (its text and `--json`'s `lines`) and MCP
`get_match` only (the plugin's match pane draws `match --json`'s `rows`, with `lines` for an older CLI, so it shows the same rows); a share card prints
none.

## Knockout surfaces live-resolve — never read the skeleton (the #1 recurring bug)

**Rule:** every team-facing surface must reach the live overlay (`getBracket` / `getNextFixtureForTeam` — on the bundled competition, the same `fetchWindow` the bracket uses; off it `next` reads discovery's schedule ahead and `bracket` makes no request), fail closed on outage, and attribute the provider only when the overlay served the chosen fixture.

| Surface | Entry point | Must |
|---------|-------------|------|
| CLI `bracket` / `next` | `cmdBracket` / `cmdNext` | call `getBracket` / `getNextFixtureForTeam` (a saved pin: `nextFixtureForPin`, which on the bundle IS `getNextFixtureForTeam` by code) |
| CLI `share bracket` / `share next` | `cmdShare` | same |
| MCP `get_bracket` / `get_next_fixture` | `toolGetBracket` / `toolGetNextFixture` | same (no `team`: the server's `CLAUDINHO_TEAM`, else the pin through `nextFixtureForPin`) |
| MCP `get_share_snippet{bracket,next}` | `toolGetShareSnippet` | same |
| **statusline** | `renderPrompt` + `runRefresh` | hot path, NO network — indirect: refresher caches `getKnockoutFixtures` → `CacheState.fixtures`; `renderPrompt` merges them, **failing closed to `⚽ —`** when the cache lacks the pairing; unresolved placeholders dropped in syncing (never 🏳️). Empty successful fetch uses a **short TTL** (~60s); provider errors keep prior cache. **`cmdPrompt` and `cmdHook`** both spawn fixtures refresh in knockout phase. |
| **`ambient --json`** | `cmdAmbient` → `ambientView` | the statusline's cache through the same reader: its `line` and `next` come from the resolved pairings and fail closed the same way (`⚽ —`, `next: null`); hot path, NO network, the same refresher trigger as `cmdPrompt`. |
| **Claude Code plugin's band, `/claudinho` and the match pane** | `packages/plugin/hooks/register.tsx` (`ui.render` on `AbovePrompt`; `command.run` on `claudinho`; `ui.render` on `Pane`, `requestId` `match`) | the band renders `ambient --json`'s `lineParts` (its `strong` part bold) when their texts join to `line`, else `line`; the toasts and the celebration read the view's `live`, `finished`, `pinnedSide`, `names` and `lastGoal`, today's text for a view without `names`; `/claudinho` relays its `line`, then its `attribution` and `disclaimer` from a view; `/claudinho match` binds the view's first live record (else its `next`) and its competition, and the pane draws `match <id> --json`'s `rows` (with `lines` verbatim for an older CLI; the run names no competition, so the CLI's mode row says where the person's choice came from; once an answer names another competition, or none, the binding is pinned and every read passes `--competition <slug>`), then its `attribution` and `disclaimer`; all read no skeleton (no cache, no bundle: the CLI's objects only), so they fail closed where `ambient --json` and `match --json` do; its coverage is the kit (`packages/plugin/hooks/*.test.tsx`, `claude plugin test`), not the two coverage tests. |

**Adding a team-facing surface? Add it to BOTH coverage tests and this table** (the plugin excepted: it reads no skeleton, and its coverage is its kit, as its row says).

## Format opts invariant

If a core formatter accepts `tz`, `locale`, `flags`, or `date`, **every wrapper** passes them from `CliConfig` / `CommonOpts`; a formatter that renders relative to now (a countdown) takes the request's ONE clock (`ctx.now` / `args.now`, else the adapter's read clock) as a required parameter, never a `new Date()` of its own.

The rest is `AGENTS.md` "Conventions" (canonical) and the guides that hold what this rule stated that no bullet does: `guides/conventions/surfaces.md` (the flair slot, what `release:qa` renders, the format opts example, the regression tests), `guides/conventions/selection.md` (the mode line and the pinned team), `guides/conventions/verdicts.md` (verdict forwarding), `guides/conventions/bundle-and-bracket.md` (the knockout live-resolve history and its exceptions) and `guides/conventions/stage-and-teams.md` (clubs and stages).
