AGENTS.md · diff

git:20260904.c1458ea to git:20260904.fc93b6e

3 added, 1 removed. Audit B to B.

# AGENTS.md — vibe-replay
This is the single source of truth for agent instructions in this repo. Every
coding agent reads it, directly or through a shim — see
[Agent setup](#agent-setup) at the bottom. Put project knowledge here, not in a
tool-specific file.
## What is this
- vibe-replay turns AI coding sessions into animated, interactive web replays as self-contained HTML files. Supports Claude Code, Claude Desktop, Claude Cowork, Cursor, Codex, OpenCode, Hermes, and Pi. The editor's AI Studio uses the embedded Pi provider registry and agent loop rather than requiring a separate AI CLI.
+ vibe-replay turns AI coding sessions into animated, interactive web replays as self-contained HTML files. Supports Claude Code, Claude Desktop, Claude Cowork, Cursor, Codex, OpenCode, Hermes, Pi, and Grok Bot. The editor's AI Studio uses the embedded Pi provider registry and agent loop rather than requiring a separate AI CLI.
pnpm monorepo: `packages/cli` (npm: `vibe-replay`), `packages/viewer` (React → single HTML), `packages/types` (shared types), `website/` (Astro), `cloudflare/` (Workers API).
## Commands
```bash
pnpm install # Install deps
pnpm build # Full build: viewer → cli
pnpm start # Build + run interactive picker
pnpm dev # Viewer (Vite HMR) + CLI (tsx + Node fs.watch restarts) together
pnpm dev:dashboard # Dev mode with dashboard flag (-d)
pnpm dev:website # Website (Astro HMR) + Viewer (Vite HMR) together
pnpm test # Run unit tests
pnpm test:cloudflare # Run Cloudflare worker tests
pnpm test:e2e # Run E2E tests (requires pnpm build first)
pnpm lint # Lint + format (auto-fix)
pnpm lint:check # Lint check (no fix, for CI)
pnpm typecheck # Strict TypeScript + website checks
pnpm verify # Sequential pre-PR gate: lint, types, tests, build
```
When to use which:
- `pnpm start` — validate full user flow (build + run)
- `pnpm dev` — daily iteration with full HMR: viewer auto-reloads via Vite, and the CLI auto-restarts through the launcher's Node `fs.watch` watcher rather than `tsx watch` (which would intercept interactive stdin)
- `pnpm dev:website` — website + viewer iteration: Astro HMR + `/view/` redirects to Vite viewer
- Dev launchers reserve ports for their entire lifetime. If one component's port is explicitly overridden, automatic selection skips that port for the other component as well. Use `VIBE_API_PORT`/`VIBE_VIEWER_PORT` for `pnpm dev` and `VIBE_VIEWER_PORT`/`VIBE_WEBSITE_PORT` for `pnpm dev:website`.
## Database (D1 + Drizzle)
Schema is managed by **Drizzle ORM** — single source of truth in `cloudflare/src/db/schema.ts`.
**When you change the schema:**
```bash
cd cloudflare
pnpm db:generate # Drizzle reads schema.ts, generates SQL migration in drizzle/
pnpm db:migrate:local # Apply to local D1
pnpm db:migrate:remote # Apply to production D1 (requires auth)
```
- All tables (replays + Better Auth auth tables) are defined in `cloudflare/src/db/schema.ts`
- Migration files live in `cloudflare/drizzle/` — commit them to git
- **Never hand-edit migration files** — always use `drizzle-kit generate`
- `schema.sql` is kept as a reference but migrations are the source of truth
- Better Auth uses `drizzleAdapter` — it reads/writes auth tables through the same Drizzle instance
## Gotchas
- **`</` escaping**: JSON in `<script>` tags MUST escape `</` as `<\/` — browsers close the tag otherwise (see `generator.ts`)
- **`lastIndexOf("</head>")`**: Use `lastIndexOf`, not `indexOf` — minified JS in the viewer bundle may contain the string `</head>`
- **Shared types**: `Scene`, `Annotation`, `DataSourceInfo`, `ReplaySession` live in `packages/types` (`@vibe-replay/types`). CLI and viewer re-export from there. Provider-specific and viewer-specific types remain in their respective packages.
- **Viewer bundle**: The viewer ships as a self-contained HTML artifact. There is no hard size cap; keep an eye on bundle size and runtime performance as features grow.
- **Viewer styling**: Tailwind **v4** via the `@tailwindcss/vite` plugin (no `postcss.config`/autoprefixer). The terminal theme lives in `packages/viewer/tailwind.config.js`, loaded from `src/styles/index.css` via `@config` (v4's legacy-config bridge). The website is also on v4.
- **Self-contained HTML**: Output must make zero automatic external requests. Viewer assets are inlined; remote replay images follow the explicit-consent rule below.
- **Remote replay images**: `ReplayImage` renders `data:image/*` sources immediately, but HTTP(S) images require an explicit per-image click and use `no-referrer`. Never attach an external URL to `<img src>` before consent.
- **Multi-file sessions**: Claude Code `/resume` creates new JSONL files. Parser accepts `string | string[]` and merges by slug+project.
- **Cursor tri-source**: Sessions come from SQLite `store.db` (primary), `globalStorage/state.vscdb`, or JSONL (fallback). Discovery merges all sources. DB data is source of truth; JSONL supplements missing thinking/images.
- **Cursor SDK**: SDK agents (TypeScript `@cursor/sdk`) write to `~/.cursor/projects/<workspace>/sdk-agent-store/<projectHash>/index.db` (tables: `agents`, `runs`, `run_events`) and a parallel JSONL transcript at `agent-transcripts/<agentId>/<agentId>.jsonl`. The transcript is the source of user prompts (SDK doesn't store them in events) and the SDK index.db supplies tool *results*, structured per-run timing, and per-turn model. See `packages/provider-cursor/src/cursor/sdk-reader.ts`. Detection is by sessionId prefix `agent-` — IDE chat sessions (UUID-only) skip the SDK SQLite probe. Newer stores add `runs.usage_json` (camelCase: `inputTokens`, `outputTokens`, `cacheReadTokens`, `cacheWriteTokens`) and it is the only token source for SDK sessions; optional columns are probed with `PRAGMA table_info` so older stores still parse. Tool duration comes from the first/last `run_events.created_at` of a call. Tool enrichment pairs positionally but validates the tool name (Edit/Write/MultiEdit count as one family) and prefers a matching path/command, leaving a block unenriched rather than attaching another tool's result.
- **Cursor subagents**: IDE delegation trajectories live beside the parent transcript at `agent-transcripts/<sessionId>/subagents/<subAgentId>.jsonl`. Parent `Task`/`Subagent` calls do not carry the file ID, so `packages/provider-cursor/src/cursor/parser.ts` links them by normalized delegated-prompt containment and intentionally leaves unmatched files unattached rather than guessing by completion order. Current files contain tool calls and assistant text/reasoning, but usually no tool IDs/results, timestamps, token usage, or actual subagent model.
- **Cursor structured subagents**: Global-state `composer.composerHeaders` (stored in `ItemTable`) may identify child composers through `subagentInfo.parentComposerId/toolCallId`. Discovery hides those children from the top-level session list, and parsing links them to the exact parent `Agent` tool call. Structured IDs take precedence; transcript prompt containment remains the legacy fallback.
- **Cursor project paths**: `~/.cursor/projects/<encoded>` encodes the workspace path by replacing `/` with `-` **and dropping each segment's leading dot**, so `~/.cursor` arrives as `Users-me-cursor`. `decodeProjectDir` resolves the ambiguity by walking the real filesystem, trying `<candidate>` then `.<candidate>` at each level. The walk now returns the *deepest* directory it could confirm: once a real parent is found, everything left over is kept as one segment rather than split on `-`, because these workspaces are routinely deleted and splitting shredded their run ids (a scratch dir ending in a UUID became five path segments and a project named after the UUID's tail). Only when nothing below the root resolves does it fall back to replacing every `-` with `/`.
- **Agent run workspaces**: Automation creates one scratch workspace per run (Cursor SDK artifacts, PR-review worktrees, `/var/folders` temp dirs), each its own project. `agentRunWorkspaceParent` in `dashboard-utils.ts` spots a run id at the end of a directory name — a UUID or a hex digest of 12+ chars, short enough digests are left alone so `ros-4` and PR numbers don't match — and rolls the session up under the directory that holds it. `rollupProject` applies that alongside the Claude `.claude/worktrees` rule, so every surface gets it. Sessions, Replays, and Projects hide these workspaces by default (`agentRuns=true` in the URL shows them); the filter runs on the raw project path, since the rollup produces a parent that no longer looks like a run workspace.
- **Project identity**: `packages/types/src/project-identity.ts` is the shared classification layer for raw workspace paths. It preserves the generic run-id safeguards, recognizes Cursor SDK automation roots/workflows (including hyphen/underscore path variants), carries repository/PR metadata, and exposes canonical keys consumed by scanner aggregation and viewer rollups. Raw top-project entries remain available so the Projects `agentRuns=true` toggle can reveal individual workspaces.
- **Cursor duplicate transcripts**: Discovery coalesces duplicate transcript copies by session ID without adding their byte counts, and parsing removes identical records only across distinct files. Inline tool blocks are authoritative; mtime sidecars may fill unresolved tools but must not create duplicate calls.
- **Claude Cowork replay duplicates**: Cowork `audit.jsonl` can contain a host-loop user record plus an `isReplay: true` copy of the same prompt. Discovery excludes replay copies that match an original by content/UUID, and rich scans use the Cowork parser so dashboard prompt analytics match replay output.
- **Cowork `result` billing**: each `type: "result"` record is one completed host-loop run's final bill — `usage`, `modelUsage`, `total_cost_usd` and `duration_ms` are per-run, not cumulative. Session totals are the sum over UUID-deduplicated results; never mix them with assistant `message.usage` snapshots (those are partial streams and undercount output badly), and never add `usage` to `modelUsage` or `total_cost_usd` to `modelUsage.*.costUSD`. Audits with no `result` records fall back to assistant snapshots plus the timestamp-gap duration estimate. `system/api_retry` maps to `apiErrors` with attempt metadata only — the raw error text is never stored.
- **Provider-reported cost**: `ProviderParseResult.reportedCostUsd` wins over the local pricing table in `transform.ts`, which is what makes cost available for model generations `pricing.ts` does not know.
- **AI Studio runtime**: `packages/cli/src/ai-runtime.ts` owns the embedded Pi `Models` registry, provider-owned API-key/OAuth flows, and an atomic 0600 credential file with a cross-process lock outside replay directories. `feedback.ts` runs Coach, Translate, and Tone through `@earendil-works/pi-agent-core` with a single structured result tool and no filesystem, network, or MCP tools. The initial registry contains OpenAI API key, ChatGPT/Codex OAuth, OpenRouter API key/OAuth, and OpenCode Zen API key providers. The editor can also configure one OpenAI-compatible proxy (`custom-openai`), discover models from `/models` or `/model`, and send Chat Completions requests through Pi. Shared provider settings live in `packages/viewer/src/components/AiProviderSettings.tsx` and render the same provider/model/auth/custom-endpoint controls inline in Settings or inside the AI Studio modal; model choices are searchable and the selected provider/model is remembered automatically in browser-local storage, never as a credential. Endpoint metadata lives in a separate atomic 0600 `~/.vibe-replay/ai-providers.json` file (`VIBE_REPLAY_AI_CONFIG` can override it); custom API keys remain in the credential store and are never returned by provider discovery. HTTP custom endpoints are limited to loopback; remote endpoints must use HTTPS. Provider setup is intentionally separate from replay/session credentials.
- **opencode**: Sessions live in a SQLite DB at `~/.local/share/opencode/opencode.db` (`%LOCALAPPDATA%\\opencode` on Windows, `OPENCODE_DATA` env var wins). Tables: `session`/`message`/`part`; role/user and tool payloads are JSON in `message.data`/`part.data`. Discovery writes `<dbPath>#session:<id>` marker paths. Parse is SQLite-backed (`parseSessionFromDb`), so background scan uses a lightweight path from discovery-computed stats (`buildLightweightOpencodeScanResult` in `scanner.ts`) to avoid opening the DB per session. Synthetic user messages carrying a `compaction` part are compaction events even when they also carry text; discovery must exclude that text from prompt counts and `firstPrompt`, matching the parser's early compaction branch. `sql.js` named-param binds need their `:`-prefix in the key — this provider uses positional `?` params instead. The `session.cost` column (populated only by some opencode versions) feeds `reportedCostUsd` when positive. See `packages/provider-opencode/`.
- **Hermes**: Sessions live in SQLite at `~/.hermes/state.db` (FTS5-backed), plus one DB per named profile at `~/.hermes/profiles/<name>/state.db`. Discovery scans all of them (`hermesDbPaths()` in `packages/provider-hermes/src/hermes/sqlite.ts`), dedups by session id, and writes `<dbPath>#session:<id>` marker paths; parse resolves the right DB from the marker before falling back to a scan. `hermesRootDir()` mirrors Hermes's `get_default_hermes_root()`: `HERMES_HOME` inside `~/.hermes` is profile mode (root stays `~/.hermes`); outside it, that path itself is the root. Caveat: sql.js reads raw file bytes and cannot replay un-checkpointed `-wal` frames, so sessions still sitting in the WAL (long-running gateway) are invisible until Hermes checkpoints. Tables: `sessions` (token/cost/git columns maintained by Hermes) + `messages` (OpenAI-style: assistant rows carry `tool_calls` JSON + `reasoning`; tool rows carry `tool_name` + `tool_call_id` + result content). Discovery writes `<dbPath>#session:<id>` marker paths and reads `~/.hermes/.update_check` for the version. Context compaction is recorded two ways: `compacted=1` rows (pre-compaction history, kept in full) and a user row prefixed `[CONTEXT COMPACTION` (→ `subtype: "compaction-summary"`, mirroring claude-code); every `compacted=1` run boundary emits its own compaction event, summary-only stores fall back to marker-derived compaction metadata, and discovery excludes those marker rows from prompt counts and firstPrompt. Provider cost: `reportedCostUsd` comes from `sessions.actual_cost_usd`, falling back to `estimated_cost_usd` when positive (`cost_status` "included"/"unknown" store 0). Parse is SQLite-backed, so background scan uses `buildLightweightHermesScanResult` in `scanner.ts`. Tool names map to the viewer vocabulary in `tool-mapping.ts`. Bot Mode: each Bot is a profile (`~/.hermes/profiles/<bot>/`), its canonical **Bot Chat** is a `hidden=1` session (`title: "Bot Chat"`, `profile_name: "<bot>"`, `cwd: ""`) with the same storage shape; discovery treats `cwd`-less sessions as `~/.hermes/profiles/<bot>` so bots appear as distinct projects, and parse infers per-tool `*_durationMs` from `assistant.timestamp → tool.timestamp` so activity timelines no longer show large unattributed gaps. See `packages/provider-hermes/`.
+ - **Grok Bot**: Cloud-box JSONL at `/home/box/agent-data/agent-transcripts` (and `sand-data`); env `GROK_BOT_TRANSCRIPTS_DIR` / `VIBE_REPLAY_GROK_BOT_DIR`; optional `~/.grok-bot/agent-transcripts`. Skip `[SAND_HIDDEN_PROMPT]`, strip `[t0u]` prefixes, promote `send_message` to assistant text. Details: `packages/provider-grok-bot/AGENTS.md`.
- **Skip `progress` lines**: These are subagent streaming artifacts in JSONL.
- **sql.js (WASM)**: Used instead of native SQLite bindings for portability — no C++ compiler needed.
- **Session discovery cache**: CLI picker + local dashboard use file cache at `~/.vibe-replay/cache/*.json` (stale-while-refresh UX). Cache validity is tied to CLI release version (`CLI_VERSION`) plus envelope version, so caches auto-invalidate across releases. Keep cache writes best-effort and never block generation/parsing on cache failures.
- **Replay compaction snapshots**: Generated replay cards must use only the replay summary's own `compactionCount`; a missing legacy field means zero. Never fall back to the current source scan, or a replay generated before a later source compaction is mislabeled.
- **SSH sources**: `~/.vibe-replay/config.json` may define generic OpenSSH targets under `remoteSources`. `packages/cli/src/remote.ts` indexes remote Codex, Claude Code, and Pi JSONL files with the system `ssh` client, stages them in a per-target read-only cache through manifest-sized bounded streams, and reuses the existing providers. SSH credentials stay in the user's OpenSSH configuration/agent; do not add keys or passwords to the vibe-replay config. Remote Codex `/resume` titles prefer the latest explicit `thread_name` from `session_index.jsonl`, then fall back to a read-only query of `state_5.sqlite`; the live database and WAL are never copied, and Python `sqlite3` plus a schema-aware `sqlite3` CLI fallback are supported. Metadata-only, missing, or unreadable sources remain visible with `no-prompts` or `unreadable` status and cannot generate an empty replay. Remote repository identity is local dashboard metadata and is stripped from share/export payloads. Remote Live mode is disabled. Remote source identity is carried separately from provider/data-source identity so equal session IDs on different targets do not merge.
- **Dashboard Settings**: The editor dashboard's Settings tab manages the same `remoteSources` entries through local `/api/settings` routes. It validates ids, hosts, providers, and timeouts, writes the config atomically while preserving unrelated keys, and offers a bounded SSH probe. Never add passwords or private keys to this UI or config.
- **SSH insight privacy**: Remote sessions may contribute to local dashboard insights, but `aggregateDailyInsights` excludes SSH locations from the optional cloud sync so remote project/provider aggregates stay on the machine.
- **Windows support**: Claude Desktop and Cowork search standard `%APPDATA%`/`%LOCALAPPDATA%` roots plus MSIX package storage under `%LOCALAPPDATA%\Packages\Claude_*\LocalCache\Roaming`; Claude project-memory lookup encodes drive paths as `C--Users-...`; Cursor encodes workspace dirs as `C:\a\b` → `C-a-b` (drive colon dropped, separators → `-`); `decodeProjectDir` has a `win32` branch that resolves these against the real filesystem (POSIX uses `/` root, Windows uses the drive root). Cursor on Windows stores IDE chats only in the `globalStorage/state.vscdb` under `%APPDATA%\Cursor` (there is no `~/.cursor/chats` dir). Replay output normalizes file paths to `/` for cross-platform display via `redactFilePath` in `transform.ts` — never apply that to prose. Build scripts shell out to `scripts/copy-file.mjs` instead of `mkdir -p`/`cp` (not available in PowerShell). `.gitattributes` forces `eol=lf` so Windows clones don't trip `oxfmt --check` with CRLF.
- **Pi harness tools**: Pi sessions use both native tool names (`bash`, `edit`, `write`) and harness names (`exec_command`, `apply_patch`). The Pi provider maps `exec_command` to replay `Bash` and parses `apply_patch` into replay `Edit` inputs (including all touched `file_paths`, with the first file represented by the single-diff viewer). Harness tools report failure through `details.exit_code` rather than the `isError` flag native tools use, so a non-zero exit code marks the result as an error. Native `edit` can carry several replacements; all of them are joined into the single `old_string`/`new_string` diff the viewer renders, and the tool stays named `Edit`.
- **Pi token accounting**: `compaction` and `branch_summary` entries carry the usage of a *separate* summarization call, so their `usage` adds to session totals (matching Pi's own billed totals). `tokensBefore` is context size, not usage, and only feeds `compactions[].preTokens`. Summary usage never enters `turnStats`. Session `model` is the last selected model, which is what discovery reports.
- **Pi compaction diagnostics**: `packages/provider-pi/src/pi/parser.ts` preserves successful `compaction` entries and emits privacy-safe `diagnostics` events. A successful compaction is labeled `automatic-context` only as an inferred classification when durable evidence (such as a preceding assistant `stopReason: "length"`) supports it; Pi v3 JSONL does not persist the `compaction_start`/`compaction_end` or `session_compact_failed` lifecycle events, so missing failure events are inconclusive. Explicit persisted messages such as `Auto-compaction failed: ...` become failed compaction diagnostics, while unrelated `stopReason: "error"` assistant messages remain `assistant-api-error` events. `get_compaction_diagnostics` in `packages/cli/src/local-assistant.ts` and the replay `StatsPanel` expose these distinctions without exposing raw error text in metadata.
- **Pi replay metric scope**: Assistant records with usage but no visible content remain in aggregate token/cost totals and are reported as a data-quality note, while still producing no empty replay scene. Viewer per-turn joins use `TurnStat.turnIndex`; sparse rows remain no-data rather than shifting later prompts. Context charts use the configured `meta.contextLimit` when available and distinguish recorded compactions from inferred context drops; the chart's prompt-footprint values are provider-reported accounting, not guaranteed actual model context size.
- **Usage index (tool / MCP / skill)**: `scanner.ts` derives one `UsageEvent` per invocation (name, turn, timestamp, duration, status, subagent) plus a per-session `SessionUsageSummary`; only the latest 100 detail events are retained, and tool inputs/results are never stored. MCP naming differs per provider and is normalized in `parseMcpUsage`: `mcp__<server>__<tool>` (Claude/Codex), `CallMcpTool {server, toolName}` (Cursor SDK), `mcp-<server>-<tool>` (Cursor IDE — split at the *last* dash because server IDs are kebab-case, and the normalized input's `server`/`tool_name` wins), `mcp_<server>_<tool>` (older Cursor — split at the *first* underscore because tool names are snake_case, with `mcp_auth`/`mcp_get_tools`/`mcp_meta_tool*` excluded as MCP management tools), and Pi's single `mcp` tool (`server` field, or `<server>_<tool>` in `tool`). An MCP call is counted under `mcpServers`/`mcpTools` only — never also as a tool — so the Tool facet lists real tools instead of repeating the MCP facets. Cowork names servers by UUID, so its parser exposes `mcpServerNames` from the sibling `local_{id}.json` `remoteMcpServersConfig`. OpenCode/Hermes and deferred Cursor scans emit no usage events — they say so in `dataQualityNotes` rather than looking like zero usage. Cursor reports the same server under several ids (`user-<name>`, `<name>::mcpScope:profile:...:cfg:...`); `stripCursorServerScope` folds them so one server is one facet. `/api/scan/results` strips events; per-session events come from `/api/usage/events`, and `/api/usage/rollup` serves the compact per-session `{ startTime, usage }` projection the Insights page aggregates client-side (`engine/usage-rollup.ts`, range-filtered by instant so switching 7d/30d/90d costs no request). The dashboard renders Tool and MCP server facets plus a per-session breakdown (`engine/session-usage.ts`) built from the summary alone, so expanding a card costs no request; the MCP tool facet is a drilldown that only appears once a server or tool is selected, and long facet lists scroll inside their section so the ones below stay reachable. Context compaction counts are indexed with each scan result (OpenCode/Hermes compute them during lightweight discovery), exposed as a URL-synced dashboard signal filter, and shown directly on session/replay cards. The scan-results cache key carries `SCANNER_VERSION`, so a bump can't serve results in the previous shape. Insights renders a "Tools & MCP" card (top tools / MCP servers / MCP tools / skills, each with calls and session reach). Cursor SQLite sessions are scanned twice: a fast pass with rich parsing deferred, then a background `backfillDeferredUsage` pass in `server.ts` that indexes their usage and rewrites the scan cache (progress in `/api/scan/status` as `usageBackfill`).
- **Context composition privacy**: `ContextBreakdown` contains aggregate counts, UTF-8 byte sizes, and provider-estimated tokens only—never raw system prompts, tool descriptions, schemas, or conversation text. Cursor reads the latest `promptTokenBreakdown` snapshot; Cowork counts its persisted system prompt and enabled MCP definitions; Codex counts rollout base/developer instructions; Pi counts unique MCP descriptors observed in active-branch tool-result metadata. Byte measurements are not token counts, Cursor snapshots are not cumulative usage, and Pi observations may be incomplete. Keep these distinctions in UI copy and preserve the field through parser → replay/scan → durable local insights.
## Rules
- **Always use pnpm** — never npm/yarn
- **TypeScript strict mode**, ESM throughout
- **oxlint** for linting, **oxfmt** for formatting. The lefthook pre-commit hook runs both on staged files for every agent. Claude Code additionally fixes each file right after editing it; other agents should run `pnpm lint` themselves before finishing.
- **Before commit**: run `pnpm lint:check` and fix any errors. Do NOT commit code that fails lint.
- **Before PR**: run `pnpm verify`. Keep its stages sequential; concurrent full checks can cause integration-test timeouts.
- **Before commit**: security review — check for leaked secrets, API keys, tokens, credentials, .env files
- **Never bump versions or publish** without explicit user confirmation
- **After changes**: update AGENTS.md / README.md / CONTRIBUTING.md if anything becomes outdated. Never edit `CLAUDE.md` to record project knowledge — it is a shim (see [Agent setup](#agent-setup)).
- **Viewer changes** → `pnpm build` (rebuilds both packages)
- **CLI-only changes** → `pnpm --filter vibe-replay build`
- **Shared types changes** → edit the relevant source under `packages/types/`; both CLI and viewer pick the changes up automatically
- Test with both small (~30 scenes) and large (~500 scenes) sessions
- **Test modification policy** — see `packages/cli/test/README.md` before changing any test
## Release checklist (important)
When creating a release for npm/GitHub, do this in order:
1. Confirm with user first (no autonomous publish/version bump).
2. Bump `packages/cli/package.json` `version` to the target release version.
3. Build CLI: `pnpm --filter vibe-replay build`.
4. Verify displayed CLI version matches package version:
- `node packages/cli/dist/index.js --version`
- Note: startup banner `vX.Y.Z` comes from `packages/cli/src/version.ts` reading `packages/cli/package.json`.
5. Only then create tag/release/publish for that same version.
If tag/release is updated but `packages/cli/package.json` is not, CLI will still show the old version.
## Key files
| What | Where |
|------|-------|
| CLI entry | `packages/cli/src/index.ts` |
| Shared types | `packages/types/src/index.ts` |
| CLI types | `packages/cli/src/types.ts` |
| Transform (turns → scenes) | `packages/cli/src/transform.ts` |
| HTML generation | `packages/cli/src/generator.ts` |
| Editor server | `packages/cli/src/server.ts` |
| AI provider/runtime | `packages/cli/src/ai-runtime.ts` / `packages/cli/src/feedback.ts` |
| Provider interface | `packages/provider-contract/src/index.ts` (contract) / `packages/providers-default/src/index.ts` (registry) |
| Cursor SDK reader | `packages/provider-cursor/src/cursor/sdk-reader.ts` |
| opencode provider | `packages/provider-opencode/src/opencode/` |
| Hermes provider | `packages/provider-hermes/src/hermes/` |
+ | Grok Bot provider | `packages/provider-grok-bot/src/grok-bot/` |
| Viewer entry | `packages/viewer/src/App.tsx` |
| Playback engine (pure) | `packages/viewer/src/engine/` |
| Playback hook | `packages/viewer/src/hooks/usePlayback.ts` |
| Session loading | `packages/viewer/src/hooks/useSessionLoader.ts` |
| View preferences | `packages/viewer/src/hooks/useViewPrefs.ts` |
| DB schema (all tables) | `cloudflare/src/db/schema.ts` |
| Auth config | `cloudflare/src/auth.ts` |
| Worker (Hono routes) | `cloudflare/src/worker.ts` |
| Drizzle config | `cloudflare/drizzle.config.ts` |
| Drizzle migrations | `cloudflare/drizzle/` |
| E2E test helpers | `e2e/helpers.ts` |
| E2E: generated HTML | `e2e/generated-html.test.ts` |
| E2E: editor server | `e2e/editor-server.test.ts` |
| E2E: CLI smoke | `e2e/cli-smoke.test.ts` |
| E2E: auth worker | `e2e/auth-worker.test.ts` |
| Agent instruction wiring | `AGENTS.md`, `CLAUDE.md`, `.agents/skills/`, `.cursor/rules/` |
## Agent setup
Every agent reads this file. Only the plumbing differs.
| Agent | Instructions | Skills |
|-------|--------------|--------|
| Codex | `AGENTS.md` (native) | `.agents/skills/` (native) |
| Cursor | `AGENTS.md` (native) + `.cursor/rules/` | — |
| Pi | `AGENTS.md` (native, preferred over `CLAUDE.md`) | `.agents/skills/` (native) |
| opencode | `AGENTS.md` (native) | — |
| Claude Code | `CLAUDE.md` (`@AGENTS.md` + Claude-only notes) | `.claude/skills/` (native) |
Shims exist because Claude Code reads `CLAUDE.md` and not `AGENTS.md`, and
because it discovers skills under `.claude/skills/` while Codex and Pi use
`.agents/skills/`:
- `CLAUDE.md` is a real file whose first line is `@AGENTS.md`. An import is used
rather than a symlink because Windows symlinks need Administrator or Developer
Mode, and this repo supports Windows contributors.
- The replay skill has exactly one real copy, at `skills/replay/` — the published
path that `.claude-plugin/plugin.json` and the README install command point at.
`.agents/skills/replay` and `.claude/skills/replay` are both symlinks into it.
On a Windows clone, run `git config core.symlinks true` and re-checkout before
running the tests; the wiring guard intentionally rejects a copied directory.
`test/agent-instructions.test.ts` in `packages/cli` guards this wiring. It fails
if `CLAUDE.md` stops importing `AGENTS.md`, if either skill symlink breaks or
turns back into a copy, or if `AGENTS.md` outgrows the size limit below.
### Size limit
Keep `AGENTS.md` **under 32 KiB** — Codex's default `project_doc_max_bytes`. Past
it Codex truncates the file and silently stops reading later sections. The guard
test enforces this. **Headroom is currently roughly 3 KiB**, so a large addition
needs a split rather than an append.
To split, move a package's gotchas into a nested pair inside that package:
```
packages/provider-cursor/AGENTS.md # the content
packages/provider-cursor/CLAUDE.md # one line: @AGENTS.md
```
Both files are needed. Codex, Cursor, Pi, and opencode layer nested `AGENTS.md`;
Claude Code only layers nested `CLAUDE.md`, so it needs the sibling shim to see
the same text. Nested files load when an agent touches that subtree and cost
nothing elsewhere. Do **not** use `.claude/rules/` for this — only Claude Code
reads it, which reintroduces exactly the split this layout removes.
### Adding a skill
Create it at `skills/<name>/SKILL.md`, then link both discovery locations:
```bash
ln -s ../../skills/<name> .agents/skills/<name> # Codex, Pi
ln -s ../../skills/<name> .claude/skills/<name> # Claude Code
```
Never copy a skill into a discovery directory — the two locations drift silently
and nothing catches it until an agent acts on a stale version.
See [CONTRIBUTING.md](./CONTRIBUTING.md) for full architecture details.