{"owner":"JuliusBrussee","repo":"caveman","hasSkills":true,"totalSkillsCount":99,"totalTokensCount":137303,"categories":["marketplace","plugin-manifest","root-instruction","claude-rule","anthropic-skill"],"hasMcp":false,"mcpConfig":null,"found":[".claude-plugin/marketplace.json",".claude-plugin/plugin.json",".cursor-plugin/plugin.json","AGENTS.md","CLAUDE.md","browse/CLAUDE.md","engine/AGENTS.md","engine/CLAUDE.md","evals/README.md","extension/AGENTS.md","extension/CLAUDE.md","integrations/CLAUDE.md","mcp/AGENTS.md","mcp/CLAUDE.md","mem/AGENTS.md","mem/CLAUDE.md","packages/cli/AGENTS.md","packages/cli/CLAUDE.md","packages/cli/README.md","packages/middleware/python/README.md","packages/middleware/typescript/README.md","packages/pi-extension/README.md","packages/sdk/parity/AGENTS.md","packages/sdk/parity/CLAUDE.md","packages/sdk/python/AGENTS.md","packages/sdk/python/CLAUDE.md","packages/sdk/python/README.md","packages/sdk/typescript/AGENTS.md","packages/sdk/typescript/CLAUDE.md","packages/sdk/typescript/README.md","packages/shared/contracts/AGENTS.md","packages/shared/contracts/CLAUDE.md","packages/shared/contracts/conformance/README.md","packages/shared/contracts/openapi/middleware.openapi.json","packages/subagent-tax/README.md","plugins/caveman/.codex-plugin/plugin.json","plugins/caveman/skills/cavecrew/SKILL.md","plugins/caveman/skills/caveman-compress/SKILL.md","plugins/caveman/skills/caveman-stats/SKILL.md","plugins/caveman/skills/caveman/SKILL.md","plugins/caveman/skills/caveman/agents/openai.yaml","plugins/caveman/skills/megacave/SKILL.md","plugins/caveman/skills/ultracave/SKILL.md","proxy/AGENTS.md","proxy/CLAUDE.md","shrink/AGENTS.md","shrink/CLAUDE.md","skills/cavecrew/README.md","skills/cavecrew/SKILL.md","skills/caveman-commit/README.md","skills/caveman-commit/SKILL.md","skills/caveman-compress/README.md","skills/caveman-compress/SECURITY.md","skills/caveman-compress/SKILL.md","skills/caveman-discover/SKILL.md","skills/caveman-evidence-review/SKILL.md","skills/caveman-explore/SKILL.md","skills/caveman-explore/package.json","skills/caveman-help/README.md","skills/caveman-help/SKILL.md","skills/caveman-learn/CLAUDE.md","skills/caveman-learn/README.md","skills/caveman-learn/SKILL.md","skills/caveman-learn/package.json","skills/caveman-manage/SKILL.md","skills/caveman-optimize/SKILL.md","skills/caveman-review/README.md","skills/caveman-review/SKILL.md","skills/caveman-setup/SKILL.md","skills/caveman-stats/README.md","skills/caveman-stats/SKILL.md","skills/caveman/README.md","skills/caveman/SKILL.md","skills/engine-mcp-tools.json","skills/generated/aider/pack.json","skills/generated/claude/pack.json","skills/generated/codex/pack.json","skills/generated/gemini/pack.json","skills/generated/hermes/pack.json","skills/generated/opencode/pack.json","skills/investigate-first/SKILL.md","skills/investigate-first/agents/openai.yaml","skills/lean-build/SKILL.md","skills/lean-build/agents/openai.yaml","skills/megacave/README.md","skills/megacave/SKILL.md","skills/migration/SKILL.md","skills/migration/agents/openai.yaml","skills/native/native-core.md","skills/registry.json","skills/safe-refactor/SKILL.md","skills/safe-refactor/agents/openai.yaml","skills/surgical-patch/SKILL.md","skills/surgical-patch/agents/openai.yaml","skills/ultracave/README.md","skills/ultracave/SKILL.md","skills/verify-and-stop/SKILL.md","skills/verify-and-stop/agents/openai.yaml","src/plugins/opencode/README.md"],"skills":{".claude-plugin/marketplace.json":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=.claude-plugin%2Fmarketplace.json -->",".claude-plugin/plugin.json":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=.claude-plugin%2Fplugin.json -->",".cursor-plugin/plugin.json":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=.cursor-plugin%2Fplugin.json -->","AGENTS.md":"Read `CLAUDE.md` before repository work. Binding routing: this repo owns\nskills, Engine, and MV3 extension; Agent SDK work belongs in\n`/Users/julb/Desktop/GitHub/caveman-agent-sdk`; Browse driver/MCP/benchmark/plugin\nwork belongs in `/Users/julb/Desktop/GitHub/caveman-browse`; proprietary Pebble\nproduct work belongs in `/Users/julb/Desktop/GitHub/caveman-coding-agent`.\n\nVisibility: this repo and `caveman-browse` are public now;\n`caveman-agent-sdk` is private during development with planned public release\nafter its gates; `caveman-coding-agent` and Caveman-Cloud remain private\ncommercial source.\n\n@./skills/caveman/SKILL.md\n@./skills/caveman-commit/SKILL.md\n@./skills/caveman-review/SKILL.md\n@./skills/caveman-compress/SKILL.md\n","CLAUDE.md":"# CLAUDE.md — caveman\n\n## README is a product artifact\n\nREADME = product front door. Non-technical people read it to decide if caveman worth install. Treat like UI copy.\n\n**Rules for any README change:**\n\n- Readable by non-AI-agent users. If you write \"SessionStart hook injects system context,\" invisible to most — translate it.\n- Keep Before/After examples first. That the pitch.\n- Install table always complete + accurate. One broken install command costs real user.\n- What You Get table must sync with actual code. Feature ships or removed → update table.\n- Preserve voice. Caveman speak in README on purpose. \"Brain still big.\" \"Cost go down forever.\" \"One rock. That it.\" — intentional brand. Don't normalize.\n- Benchmark numbers from real runs in `benchmarks/` and `evals/`. Never invent or round. Re-run if doubt.\n- Adding new agent to install table → add detail block in `<details>` section below.\n- Readability check before any README commit: would non-programmer understand + install within 60 seconds?\n\n---\n\n## Project overview\n\nCaveman makes AI coding agents respond in compressed caveman-style prose while preserving technical substance, code, commands, and exact errors. Publish no reduction or quality-equivalence percentage without a committed reviewed benchmark. Ships as Claude Code plugin, Codex plugin, Gemini CLI extension, and agent rule files for Cursor, Windsurf, Cline, Copilot, and other profiles via `npx skills`.\n\n## Repository routing\n\nThis repo is source of truth for Caveman skills, Engine, and MV3 directive\nextension. Agent SDK + initializer work belongs in\n`/Users/julb/Desktop/GitHub/caveman-agent-sdk`\n(`JuliusBrussee/agent-sdk`). Proprietary Pebble runtime, policy, sessions, TUI,\ndistribution, and conformance work belongs in\n`/Users/julb/Desktop/GitHub/caveman-coding-agent`\n(intended `JuliusBrussee/caveman-coding-agent`). Browse driver/MCP/benchmark/plugin work\nbelongs in `/Users/julb/Desktop/GitHub/caveman-browse`\n(`JuliusBrussee/caveman-browse`). The matching `browse/` directory here is a\nconsumer copy (it still builds the `caveman-browse` release binary); edit only\nfor pinned integration, migration/removal, or an explicitly requested\ncross-repo sync.\n\nVisibility is separate from ownership: this repo and `caveman-browse` are\npublic now; `caveman-agent-sdk` is private during development and planned for\npublic release after its release gates; `caveman-coding-agent` and\nCaveman-Cloud remain private commercial source.\n\n---\n\n## What lives where\n\nPost-cleanup layout. Sources of truth at the top, distribution mirrors below, build outputs in `dist/`, human docs alongside each skill.\n\n```\ncaveman/\n├── README.md                    # Front door (product pitch)\n├── INSTALL.md                   # Per-agent install commands\n├── CONTRIBUTING.md              # Dev guide\n├── CLAUDE.md                    # This file (maintainer instructions)\n├── AGENTS.md / GEMINI.md        # Autodiscovery files (must stay at root)\n│\n├── install.sh / install.ps1     # 30-line shims → installer/install.js\n│\n├── installer/                   # Unified installer\n│   ├── install.js               # Single source for all 30+ agents (PROVIDERS array)\n│   └── lib/settings.js          # JSONC-tolerant settings.json reader/writer\n│\n├── skills/                      # ALL skills, single source of truth\n│   ├── caveman/{SKILL.md, README.md}\n│   ├── caveman-commit/{SKILL.md, README.md}\n│   ├── caveman-review/{SKILL.md, README.md}\n│   ├── caveman-help/{SKILL.md, README.md}\n│   ├── caveman-stats/{SKILL.md, README.md}\n│   ├── caveman-compress/{SKILL.md, README.md, scripts/}\n│   └── cavecrew/{SKILL.md, README.md}\n│\n├── agents/                      # cavecrew subagents (single source — kept at root for plugin auto-discovery)\n│   └── docs/                    # profile-registry docs — NOT agents (see note below)\n├── commands/                    # Codex/Gemini TOML command stubs (root for plugin auto-discovery)\n│\n├── src/                         # Internal source — not auto-discovered by plugin\n│   ├── hooks/                   # Claude Code hooks (installer reads here)\n│   ├── rules/                   # Auto-activation rule body (single source)\n│   ├── tools/                   # caveman-init.js (per-repo rule writer)\n│   └── mcp-servers/             # caveman-shrink npm-published MCP middleware\n│\n├── packages/                    # Current public packages\n│   ├── cli/                     # @caveman-ai/cli\n│   ├── pi-extension/            # @caveman-ai/pi — native Pi extension (bundled into the CLI, published on `pi-v*` tags)\n│   ├── sdk/                     # TypeScript + Python gateway clients\n│   ├── subagent-tax/            # Local harness-prefix benchmark\n│   └── shared/                  # Contracts + binary installer\n├── engine/ · proxy/             # Local compression runtime + provider proxy\n├── mcp/ · mem/ · shrink/        # Recovery tools, memory, output compression\n├── browse/                      # consumer copy; source = caveman-browse\n├── extension/                   # MV3 extension source\n├── shared/                       # Provider catalog + platform libraries\n│\n├── .claude-plugin/              # Claude Code plugin manifest (REQUIRED at root)\n├── .cursor-plugin/plugin.json   # Cursor plugin: skills/, hooks/hooks-cursor.json, agents/ (default scan)\n├── hooks/hooks-cursor.json      # Cursor plugin sessionStart hook ONLY. Never add hooks/hooks.json:\n│                                #   Claude Code and Gemini auto-load it from the plugin/extension root\n├── plugins/caveman/             # Claude Code plugin distribution (CI-mirrored)\n│   ├── skills/                  # ← from skills/\n│   └── agents/                  # ← from agents/\n│\n├── dist/                        # Build artifacts (gitignored)\n│   └── caveman.skill            # ZIP of skills/caveman/, rebuilt by CI\n│\n├── tests/                       # All tests (Node + Python)\n├── benchmarks/                  # Real token measurements through Claude API\n├── evals/                       # Three-arm eval harness\n├── docs/                        # User-facing docs site\n└── .github/workflows/           # CI sync\n```\n\n---\n\n## File structure and what owns what\n\n### Single source of truth files — edit only these\n\n| File | What it controls |\n|------|-----------------|\n| `skills/caveman/SKILL.md` | The caveman voice: rules, auto-clarity, persistence. Sibling skills `skills/ultracave/SKILL.md` (grammar stripped) and `skills/megacave/SKILL.md` (文言文) are self-contained and each map 1:1 to a stored mode. Edit these three for behavior changes. |\n| `src/rules/caveman-activate.md` | Always-on auto-activation rule body, consumed by `src/tools/caveman-init.js` (per-repo IDE rule files) and by the opencode `AGENTS.md` block. GENERATED: `skills/compile.mjs` derives it from `skills/caveman/SKILL.md` (thesis line + rule headlines) plus the fixed tail in `skills/activation-rule.mjs`, and rewrites the `RULE_BODY` fallback in `caveman-init.js` to match. Edit the skill or the tail, rerun `node packages/cli/scripts/compile-registries.mjs`; `tests/installer/rule-copies.test.mjs` fails on drift. |\n| `src/rules/caveman-openclaw-bootstrap.md` | Marker-fenced bootstrap snippet appended to `~/.openclaw/workspace/SOUL.md` by `installer/lib/openclaw.js`. Drives always-on caveman through the OpenClaw gateway. Must include the SENTINEL `Respond terse like smart caveman` and stay well under OpenClaw's 12K-per-bootstrap-file cap. |\n| `.codex/codex-sessionstart.js` | Repo-local Codex SessionStart hook. Resolves the configured default mode through `src/hooks/caveman-config.js` and emits the active skill's ruleset whole, replacing the hardcoded `full`-level echo `.codex/hooks.json` used to carry — so `CAVEMAN_DEFAULT_MODE`, a repo-local `.caveman.json` and a user-config `defaultMode: \"off\"` all take effect on Codex. Also the user-level always-on hook (#573): `installer/install.js --only codex` copies it (shipped via `package.json` `files`) into the owned payload `$CODEX_HOME/caveman/hooks/` beside `caveman-config.js` and `package.json`, with `caveman/skills/{caveman,ultracave,megacave}/SKILL.md`, and merges one SessionStart entry into `$CODEX_HOME/hooks.json`, identified by that script path so foreign and caveman-CLI `native-hook` entries are never touched. `--no-hooks`/`--minimal` skip it; uninstall unmerges before removing the payload. Codex asks the user to trust a new hook via `/hooks`; never pre-trust it. Carries a hand-copied `FALLBACK_VALID_MODES` for when the shared resolver is absent; `tests/hooks/codex-sessionstart.test.mjs` fails if it drifts from `VALID_MODES`. |\n| `installer/lib/openclaw.js` | OpenClaw install/uninstall helper. Frontmatter merge (`version`, `always: true`), SOUL.md marker append/strip, idempotent. Shared by `installer/install.js` and `src/tools/caveman-init.js`. |\n| `skills/caveman-commit/SKILL.md` | Caveman commit message behavior. Fully independent skill. |\n| `skills/caveman-review/SKILL.md` | Caveman code review behavior. Fully independent skill. |\n| `skills/caveman-help/SKILL.md` | Quick-reference card. One-shot display, not a persistent mode. |\n| `skills/caveman-compress/SKILL.md` | Compress sub-skill behavior. |\n| `skills/cavecrew/SKILL.md` | Cavecrew decision guide — when to delegate to caveman subagents vs vanilla. Edit only here. |\n| `skills/{caveman-setup,caveman-discover,caveman-learn,caveman-manage,caveman-optimize,caveman-explore,caveman-evidence-review}/SKILL.md` | Engine/proxy driver skills. Ship with the plugin (see the auto-discovery note below). |\n| `skills/{investigate-first,lean-build,surgical-patch,safe-refactor,migration,verify-and-stop}/SKILL.md` | Token-discipline work patterns — same goal as caveman prose (fewer output tokens) applied to code volume rather than wording. Deliberately un-branded so they read as generic patterns to the model. Ship with the plugin. |\n| `agents/cavecrew-investigator.md` | Read-only locator subagent (haiku). Output contract: `path:line — symbol — note`. |\n| `agents/cavecrew-builder.md` | Surgical 1-2 file editor subagent. Refuses 3+ file scope. |\n| `agents/cavecrew-reviewer.md` | Diff/file reviewer subagent (haiku). One-line findings with severity emoji. |\n| `src/plugins/opencode/plugin.js` | opencode native plugin. ESM Bun module — `session.created` writes flag, `tui.prompt.append` parses slash/natural-language activation and appends per-prompt reinforcement. Reuses `caveman-config.js` via `createRequire`. |\n| `src/plugins/opencode/commands/*.md` | Six opencode slash-command prompt templates (`/caveman`, `/caveman-{commit,review,compress,stats,help}`). |\n\n### `skills/` is auto-discovered wholesale — every subdirectory ships\n\n`.claude-plugin/marketplace.json` sets `\"source\": \"./\"`, so the plugin root IS\nthe repo root, and Claude Code auto-discovers **every** `skills/*/SKILL.md`\nwith no `skills` key in `plugin.json` to gate it. Adding a directory under\n`skills/` therefore installs it into every plugin user's agent, where its\n`description` competes for activation on ordinary work.\n\nThere is no allowlist. Before adding a directory here, decide whether it\nshould reach end users; if not, it belongs under `packages/` or another root.\nKeep the two tables above and the README \"What you get\" table in sync with the\nactual directory listing — `ls skills/` is the source of truth.\n\n### `agents/*.md` is auto-discovered too — and `plugin.json` must NOT list them\n\nSame mechanism, sharper edge. `plugin.json` used to carry an explicit\n`\"agents\": [\"./agents/cavecrew-*.md\"]` array. On Claude Code 2.1.235 that array\nloads **zero** agents — `claude plugin details caveman` reports `Agents (0)`\nwith it and `Agents (3)` without it, so the three cavecrew subagents the\n`cavecrew` skill delegates to did not exist for any plugin user. The docs say\n`string | string[]` is valid and a directory string is rejected outright\n(`agents: Invalid input`), so the default `agents/` scan is the only path that\nworks. Do not re-add the key.\n\nConsequence: **every `.md` anywhere in `agents/` becomes a subagent**, named\nfrom its frontmatter or filename. Maintainer documentation belongs outside that\ntree; profile-registry orientation lives in\n`docs/technical/agent-profile-registry.md`. `tests/verify_repo.py` fails build\nif any markdown beyond three intended cavecrew agents appears under `agents/`.\n\n### `commands/*.md` shadows same-named skills — keep the `.md` stubs unique\n\nClaude Code loads `commands/*.md` as flat skills alongside `skills/*/SKILL.md`,\nso a stub sharing a skill's name registers that name twice. `caveman.md`,\n`caveman-commit.md`, `caveman-review.md` and `caveman-stats.md` each duplicated\na real skill — a 3-line stub competing with the full ruleset for the same slash\ncommand — and were removed. The `.toml` stubs are Codex/Gemini-only and are not\nscanned, so `commands/` keeps one `.md`: `caveman-init.md`, which has no skill\ntwin. Guarded by `tests/verify_repo.py`.\n\n### Auto-generated / auto-synced — do not edit directly\n\nWe removed the agent-specific dotdir mirrors at the repo root (`.cursor/`, `.windsurf/`, `.clinerules/`, `.github/copilot-instructions.md`, root `caveman/SKILL.md`). They were never read by the installer — only used to self-apply caveman to this repo when a maintainer opened it in Cursor/Windsurf/Cline. Devs who want caveman in their editor while editing this repo should run `npx -y github:JuliusBrussee/caveman -- --with-init` once (writes per-repo rule files from `src/rules/caveman-activate.md` via `src/tools/caveman-init.js`). For per-user installs through the upstream skills CLI, `npx -y github:JuliusBrussee/caveman -- --only <agent>` runs `npx skills add ... -a <profile>`.\n\nA handful of dotdir leftovers (`.junie/`, `.kiro/`, `.roo/`, `.agents/`) still hold a stale `cavecrew/SKILL.md` mirror from before the cleanup. They aren't read by anything in the current install path; remove on sight, no migration needed.\n\nWhat's left is the Claude Code plugin distribution (required by the plugin loader) and the release ZIP.\n\n| File | Synced from |\n|------|-------------|\n| `plugins/caveman/skills/caveman/SKILL.md` | `skills/caveman/SKILL.md` |\n| `plugins/caveman/skills/caveman-compress/SKILL.md` (+ `scripts/`) | `skills/caveman-compress/SKILL.md` (+ `scripts/`) |\n| `plugins/caveman/skills/cavecrew/SKILL.md` | `skills/cavecrew/SKILL.md` |\n| `plugins/caveman/agents/cavecrew-*.md` | `agents/cavecrew-*.md` |\n| `dist/caveman.skill` | ZIP of `skills/caveman/` directory (gitignored; rebuilt by CI on release) |\n\nSkills not in this table (`caveman-commit`, `caveman-review`, `caveman-help`, `caveman-stats`) are not mirrored into the Claude Code plugin distribution by CI. They reach Claude Code through the standalone hook + skill install path, and reach other agents via `npx skills add`. A `plugins/caveman/skills/caveman-stats/` directory is currently checked in as a hand-committed copy; the sync workflow does not touch it, so don't rely on edits there to propagate.\n\n---\n\n## CI sync workflow\n\n`.github/workflows/sync-skill.yml` triggers on main push when `skills/**/SKILL.md` or `agents/cavecrew-*.md` changes.\n\nWhat it does:\n1. Copies `skills/caveman/SKILL.md` and `skills/cavecrew/SKILL.md` into their `plugins/caveman/skills/<name>/` mirrors so the Claude Code plugin loader sees the latest behavior.\n2. Copies `skills/caveman-compress/SKILL.md` and its `scripts/` into `plugins/caveman/skills/caveman-compress/`.\n3. Copies `agents/cavecrew-*.md` into `plugins/caveman/agents/`.\n4. Rebuilds `dist/caveman.skill` (ZIP of `skills/caveman/`) for the release artifact.\n5. Commits and pushes with `[skip ci]` to avoid loops.\n\nCI bot commits as `github-actions[bot]`. After PR merge, wait for workflow before declaring release complete.\n\nThe old steps that mirrored SKILL.md and rules into root dotdirs (`.cursor/`, `.windsurf/`, `.clinerules/`, `.github/copilot-instructions.md`) are gone — those mirrors no longer exist. The old `caveman-compress/` → `skills/compress/` rename-on-sync is also gone now that compress lives at `skills/caveman-compress/`.\n\n---\n\n## Hook system (Claude Code)\n\nFour hooks in `src/hooks/` plus a `caveman-config.js` shared module, a `caveman-parse.js` shared mode-change parser and a `package.json` CommonJS marker.\n\n**Mode state is per session.** Each session's mode lives in `$CLAUDE_CONFIG_DIR/.caveman-sessions/<session_id>.mode`, keyed by the `session_id` Claude Code puts in every hook payload *and* in the statusline's stdin JSON. `$CLAUDE_CONFIG_DIR/.caveman-active` survives as a last-write-wins compat mirror (falls back to `~/.claude/`).\n\n```\nSessionStart hook ──┐                                        ┌── UserPromptSubmit hook\n  (session_id,      │                                        │     (session_id, prompt)\n   source)          ▼                                        ▼\n        $CLAUDE_CONFIG_DIR/.caveman-sessions/<session_id>.mode\n                             │             │\n                          mirrors       reads\n                             ▼             ▼\n              .caveman-active      caveman-statusline.sh ◀── session JSON on stdin\n           (last-write-wins,        [CAVEMAN] / [ULTRACAVE] / [MEGACAVE]\n            compat only)\n```\n\nTwo invariants that are load-bearing, not stylistic:\n\n- **The legacy mirror never holds the literal `off`.** Deactivation deletes it. `off` is in `VALID_MODES`, so an older `caveman-mode-tracker.js` reading `off` from that path would pass its `!INDEPENDENT_MODES.has(...)` check and inject \"CAVEMAN MODE ACTIVE (off)\", and an older `caveman-statusline.sh` would render `[CAVEMAN:OFF]`. Mixed-version installs are real: plugin hooks and standalone hooks can both be registered at once, and `statusLine` holds an absolute path baked in at install time.\n- **`recordModeChange` compares against the session's own state, never the mirror.** That's why `readSessionModeRaw` exists alongside `resolveActiveMode` — diffing against the mirror would compare one session's transition to whatever another session wrote last and spam the log with phantom entries.\n\nBoth spellings of off are READ everywhere: a missing file (old semantics) and a literal `off` (new, durable).\n\n`src/hooks/package.json` pins the directory to `{\"type\": \"commonjs\"}` so the `.js` hooks resolve as CJS even when an ancestor `package.json` (e.g. `~/.claude/package.json` from another plugin) declares `\"type\": \"module\"`. Without this, `require()` blows up with `ReferenceError: require is not defined in ES module scope`.\n\nAll hooks honor `CLAUDE_CONFIG_DIR` for non-default Claude Code config locations.\n\n### `src/hooks/caveman-config.js` — shared module\n\nExports:\n- `getDefaultMode()` — resolves default mode in order: `CAVEMAN_DEFAULT_MODE` env var → repo-local config (`<cwd>/.caveman/config.json` or `<cwd>/.caveman.json`, walking up to the filesystem root; the first file that names a `defaultMode` wins, so a CLI-only `.caveman/config.json` does not hide a `.caveman.json`) → user config (`$XDG_CONFIG_HOME/caveman/config.json` / `~/.config/caveman/config.json` / on Windows `%APPDATA%\\caveman\\config.json`, then `~/.config/caveman/config.json`) → `'caveman'`. Config files may carry a BOM, be UTF-16LE, or hold comments and trailing commas. The env var short-circuits before any cwd walk. Repo-local config lets a team check in a per-project default without polluting every contributor's env or user config.\n- `findRepoConfigPath(start, accept)` — walks up from `start` (default `process.cwd()`) looking for the first `.caveman/config.json` or `.caveman.json` (that `accept(path)` approves, when given). Bounded to 64 ancestors. Refuses symlinked files (symmetric with `safeWriteFlag` / `readFlag`).\n- `safeWriteFlag(flagPath, content)` — symlink-safe flag write. Refuses if flag target or its immediate parent is a symlink. Opens with `O_NOFOLLOW` where supported. Atomic temp + rename. Creates with `0600`. Protects against local attackers replacing the predictable flag path with a symlink to clobber files writable by the user. Used by both write hooks. Silent-fails on all filesystem errors.\n- `validateSessionId(id)` — returns the id or `null`. Whitelist `^[A-Za-z0-9_-]{1,128}$`. **A session id becomes part of a filesystem path, so nothing may interpolate one without passing it through here first.** The symlink hardening in `safeWriteFlag` does not cover traversal.\n- `resolveActiveMode(claudeDir, sessionId)` — what mode is in effect: session file → legacy mirror, with both a missing file and a literal `off` collapsing to `null`. The reader every hook should use.\n- `readSessionModeRaw(claudeDir, sessionId)` — literal stored value for **this** session, no legacy fallback. Two callers only: `recordModeChange` (see the invariant above) and the SessionStart continuation branch, which has to tell a stored `off` apart from \"nothing stored yet\".\n- `writeSessionMode(claudeDir, sessionId, modeOrNull)` — the single writer. Writes `off` literally to the session file, unlinks the legacy mirror. Rejects anything outside `VALID_MODES`.\n- `writeSessionPrev` / `readSessionPrev` / `clearSessionPrev` — the displaced-prose-mode memory for one-shot skills (#599), scoped per session so two windows running `/caveman-commit` don't overwrite each other's return target.\n- `gcSessionStore(claudeDir, opts)` — mtime sweep of `.caveman-sessions/`, 14-day TTL (`CAVEMAN_SESSION_TTL_MS` overrides for tests), `maxDeletes` cap, refuses symlinks. Called from SessionStart on new sessions only — never on `compact`, which is frequent and shares the 5s hook budget.\n- `recordModeChange(claudeDir, newMode, sessionId)` — third arg tags the log entry; omitted (not null) when unknown, so pre-existing readers see the old shape.\n\n**Every function above accepts a `sessionId` that may be `null` or malformed and degrades to the legacy machine-wide behavior.** That is the entire backward-compatibility story: the old code path *is* the fallback branch, which is why the existing tests — none of which send a `session_id` — pass unchanged. The three hook entrypoints resolve these helpers individually (`cfg.writeSessionMode || <legacy stub>`) rather than adding them to their `requireSibling` shape checks: a `caveman-config.js` from before per-session state satisfies those checks, and hard-failing over the newer exports would trade \"machine-wide mode, as it always worked\" for \"no state at all\" on exactly the plugin-cache-drift scenario #848 is about.\n\n### `src/hooks/caveman-activate.js` — SessionStart hook\n\nRuns on every SessionStart — `source` is `startup`, `resume`, `clear`, `compact` or `fork`. Four things:\n1. Reads the hook payload from stdin for `session_id`, `source` and `cwd`\n2. Resolves this session's mode and persists it via `writeSessionMode`\n3. Emits caveman ruleset as hidden stdout — Claude Code injects SessionStart hook stdout as system context, invisible to user\n4. Checks `settings.json` for statusline config; if missing, appends nudge to offer setup on first interaction (one-shot, gated by `.caveman-nudge-shown`)\n\n**`source` branching.** `RESET_SOURCES` is `{startup, clear}` — only those re-derive `getDefaultMode()`. Everything else (`compact`/`resume`/`fork`, an unrecognized source, and the watchdog's `unknown`) READS the stored mode via `readSessionModeRaw`, falling back to the legacy mirror for a session that predates the store, and only then to the default. Reading the LITERAL value is the point: #691 already branched on `source`, but it read the legacy flag where off is spelled \"no file\", so a deactivated session found nothing and re-derived the default anyway — \"stop caveman\" was still undone by the next auto-compaction. A stored `off` now short-circuits to `OK` with no ruleset. It still re-emits when a mode IS active: compaction is what prunes the rules out of context, which is the whole reason this hook runs on every source. `clear` counts as a fresh start (explicit user reset — nothing else in the conversation survives it, so neither should a \"stop caveman\" from before it); this deliberately differs from #691's comment, which grouped `clear` with the continuations.\n\n**Headless sessions start under `manual` (#377).** Whenever the hook falls back to the configured default (`startMode()`), a `CLAUDE_CODE_ENTRYPOINT` starting with `sdk-` (`claude -p`, Agent SDK) turns any non-`off` default into `manual`: tool probes that parse the reply get no ruleset until an explicit `/caveman`. Interactive entrypoints (`cli`, `claude-vscode`, `claude-desktop`, …) never match; an explicit `CAVEMAN_DEFAULT_MODE` env var opts back in. Hook test helpers drop `CLAUDE_CODE_ENTRYPOINT` from the env, or a headless runner flips every SessionStart test.\n\n**The stdin contour is load-bearing.** Activation runs on the first COMPLETE JSON object, not at EOF, with a 2000ms `PAYLOAD_WATCHDOG_MS` backstop and `process.stdin.unref()` in `finish()`. `pause()` alone is not enough — it stops reading but leaves the pipe handle referenced, so a host that holds the write end open (Windows pipe close lags arbitrarily, #729/#833) burns the whole 5s budget. The watchdog must NOT assume `startup`: it reports `unknown`, which preserves the session's stored mode rather than resetting it. Several suites also invoke this hook via `subprocess.run()` with no `input=`, inheriting a stdin that never reaches EOF — `tests/test_hooks.py::test_hook_never_blocks_on_stdin_that_never_closes` guards that path.\n\nSilent-fails on all filesystem errors — never blocks session start.\n\n**`--subagent`: the SubagentStart hook (#621).** SessionStart context reaches only the parent thread, so the same script is registered for SubagentStart with `--subagent` (plugin.json, `installer/install.js`, `src/hooks/install.{sh,ps1}`). It reuses the stdin reader and watchdog, resolves THIS session's mode via `resolveActiveMode` (a session with no stored state at all — SessionStart skipped or failed, or the session predates the store — falls back to the last-write-wins legacy mirror, the same degrade the tracker's reinforcement uses, so a subagent matches its parent thread; pinned in `tests/hooks/subagent-start.test.mjs`), and prints `{\"hookSpecificOutput\":{\"hookEventName\":\"SubagentStart\",\"additionalContext\":<that mode's skill>}}`. Nothing is printed when the session stored `off` (no \"stop caveman\" leak, #672; another window's mode never overrides a stored `off`), in a one-shot mode, for `cavecrew-*` agents (they carry their own ultracave voice), or under a repo `defaultMode: \"off\"` (#634, same gate as the tracker's reinforcement). Read-only: no mode write, no mode log, no GC, no nudge, no cavecrew model overrides. Uninstall needs nothing extra — `removeCavemanHooks` walks every event and matches the `caveman-activate.js` basename.\n\n### `src/hooks/caveman-mode-tracker.js` — UserPromptSubmit hook\n\nReads JSON from stdin — `session_id` scopes every read and write. Three responsibilities:\n\n**1. Slash-command activation.** If prompt starts with `/caveman`, writes the session's mode via `writeSessionMode`:\n- `/caveman` → configured default (see `caveman-config.js`, defaults to `caveman`)\n- `/ultracave` (alias `/caveman ultra`) → `ultracave`\n- `/megacave` (alias `/caveman wenyan`, any `wenyan-*`) → `megacave`\n- `/caveman lite` or `/caveman full` (legacy aliases) → `caveman`\n- `/caveman-commit` → `commit`\n- `/caveman-review` → `review`\n- `/caveman-compress` → `compress`\n\n**2. Natural-language activation/deactivation.** Matches phrases like \"activate caveman\", \"turn on caveman mode\", \"talk like caveman\" and writes the configured default mode. Matches \"stop caveman\", \"disable caveman\", \"normal mode\", \"deactivate caveman\" etc. and stores a durable `off` for the session. README promises these triggers, the hook enforces them.\n\n**3. Per-turn reinforcement.** When the session's mode is a non-independent one (i.e. not `commit`/`review`/`compress`), emits a small `hookSpecificOutput` JSON reminder so the model keeps caveman style after other plugins inject competing instructions mid-conversation. The full ruleset still comes from SessionStart — this is just an attention anchor.\n\n### `src/hooks/caveman-statusline.sh` — Statusline badge\n\nReads the session JSON Claude Code pipes to it on stdin, extracts `session_id` (pure bash — no `jq` dependency), and reads `.caveman-sessions/<id>.mode`; falls back to `$CLAUDE_CONFIG_DIR/.caveman-active` when there is no usable id. Outputs colored badge string for Claude Code statusline:\n- `caveman` or empty → `[CAVEMAN]` (orange)\n- `off` → nothing at all. Never `[CAVEMAN:OFF]` — that reads as a mode rather than the absence of one\n- `ultracave` → `[ULTRACAVE]`, `megacave` → `[MEGACAVE]`; one-shots → `[CAVEMAN:<MODE_UPPERCASED>]` (orange). Legacy stored values render through the same map\n\nStdin is bounded the same way as the SessionStart hook: `[ ! -t 0 ]` skips an interactive terminal, and `read -r -d '' -t 1` caps the wait. The timeout is an **integer on purpose** — macOS ships bash 3.2, which rejects `-t 0.3` with `invalid timeout specification`.\n\nThe statusline reports mode only. Old `.caveman-statusline-suffix` values are ignored; fixed-ratio savings estimates were removed because no measured baseline supported them. `/caveman-stats` records actual Claude output/cache-read counts and mode attribution with savings unknown. Gemini uses native `/stats model` and `/stats session`; never read Claude transcripts as another host's usage.\n\nConfigured in `settings.json` under `statusLine.command`. PowerShell counterpart at `src/hooks/caveman-statusline.ps1` for Windows. Both scripts symlink-refuse and whitelist-validate mode flag contents — never echo arbitrary bytes.\n\n### `src/hooks/caveman-stats.js --record` — SessionEnd hook\n\nSame script as `/caveman-stats`; the `--record` flag turns it into a silent recorder. Reads the SessionEnd payload (`session_id`, `transcript_path`) and appends one snapshot of that transcript's totals to `$CLAUDE_CONFIG_DIR/.caveman-history.jsonl`, so lifetime history fills in without the user running `/caveman-stats`. Only the transcript the host names is read — never the newest file on disk. Stdin follows the activate-hook contour: first complete JSON object, 2000ms `PAYLOAD_WATCHDOG_MS` (gives up without recording), `unref()` on finish. Always exits 0 and writes nothing to stderr — a SessionEnd failure would surface as a hook error while the user quits. Every installer (plugin manifest, `installer/install.js`, `src/hooks/install.{sh,ps1}`) writes `--record` unquoted after the quoted script path; `rewriteLegacyManagedHookCommands` carries trailing `--flag` args over when it moves a bare-`node` command to the absolute node path.\n\n### `src/hooks/caveman-host-session-start.js` — Cursor / Copilot CLI sessionStart hook\n\n`node caveman-host-session-start.js <host>`, one script for hosts other than Claude Code. Emits the resolved mode's ruleset under the host's key: `additional_context` for `cursor`, `additionalContext` for `copilot`. The mode is `getDefaultMode()` resolved from the session's workspace (`cwd`, Cursor's `workspace_roots` or `CURSOR_PROJECT_DIR`), so `defaultMode` and repo `.caveman.json` apply; `off`, `manual`, one-shot modes, an unknown host or any failure print `{}`. Writes no state. The owned install at `<host>/caveman/hooks/` reads its pinned `<host>/caveman/skills/` before the shared resolver, which would otherwise find the host's unpinned `npx skills` copy first. Stdin follows the activate-hook contour.\n\n### Hook installation\n\n**Plugin install** — hooks wired automatically by plugin system.\n\n**Standalone install** — `installer/install.js` (the unified Node installer) copies hook files into `$CLAUDE_CONFIG_DIR/hooks/` and merges SessionStart + SubagentStart + UserPromptSubmit + SessionEnd + statusline into `settings.json`. Uses the JSONC-tolerant helpers in `installer/lib/settings.js` so a commented `settings.json` no longer crashes the merge. Defensive `validateHookFields` runs before every write to prevent a single malformed hook from poisoning the entire file (Claude Code Zod silently discards the whole `settings.json` on schema mismatch).\n\nThe `install.sh` / `install.ps1` shims at the repo root delegate to `installer/install.js` via `node` (local clone) or `npx -y github:JuliusBrussee/caveman` (curl|bash). No legacy fallback path remains — earlier `install.sh.legacy` / `install.ps1.legacy` files were removed.\n\n**Uninstall** — `npx -y github:JuliusBrussee/caveman -- --uninstall` (or `node installer/install.js --uninstall` from a clone). Strips caveman hook entries from `settings.json` via substring marker `caveman`, deletes hook files, and removes the Claude plugin / Gemini extension. Native skill copies use `installer/lib/provider-skills.js` and ownership journals; uninstall removes unchanged owned files while preserving foreign or edited content. Use the same vendor home overrides for install and uninstall. Delegated `npx skills add` installs remain managed by the host/upstream skill manager.\n\n---\n\n## Skill system\n\nSkills = Markdown files with YAML frontmatter consumed by Claude Code's skill/plugin system and by `npx skills` for other agents.\n\nEach skill has a human-facing `README.md` alongside the LLM-facing `SKILL.md`. The README explains what the skill does for users browsing GitHub; the SKILL.md is the prompt body the agent loads. Don't merge them — different audiences, different formats.\n\n### Three skills, one mode model\n\nThree self-contained skills, each 1:1 with a stored mode id: `caveman` (default voice), `ultracave` (grammar stripped), `megacave` (文言文). The SessionStart hook injects `skills/<id>/SKILL.md` whole; there is no per-level filtering. Legacy stored values map on read: `lite` and `full` → `caveman`, `ultra` → `ultracave`, every `wenyan*` → `megacave`. Writes emit new ids only. Mode persists until changed or session ends. Plan: `docs/technical/three-skills-refactor.md`.\n\n`defaultMode: \"manual\"` is a Claude Code startup policy, not a fourth mode. It starts inactive, stores only `off`, and explicit bare-command or natural-language activation resolves to `caveman`. Keep it out of `VALID_MODES`; only configuration accepts it. OpenCode maps this policy to its existing caveman default because its installer also supplies static AGENTS.md activation. `/caveman status` is read-only and must return before one-shot restoration or any mode-state mutation.\n\n### Auto-clarity rule\n\nCaveman drops to normal prose for: security warnings, irreversible action confirmations, multi-step sequences where fragment ambiguity risks misread, user confused or repeating question, questions the agent asks the user. Resumes after. Defined in skill — preserve in any SKILL.md edit.\n\n### caveman-compress\n\nSub-skill in `skills/caveman-compress/SKILL.md`. Takes file path, compresses prose to caveman style, writes to original path, saves backup at `<filename>.original.md`. Validates headings, code blocks, URLs, file paths, commands preserved. Retries up to 2 times on failure with targeted patches only. Requires Python 3.10+.\n\nThe slash command is `/caveman-compress` everywhere — same name in plugin and standalone install. CI no longer renames the directory on sync (the old `caveman-compress/` → `skills/compress/` sed rename is gone now that the source lives at `skills/caveman-compress/`).\n\n### caveman-commit / caveman-review\n\nIndependent skills in `skills/caveman-commit/SKILL.md` and `skills/caveman-review/SKILL.md`. Both have own `description` and `name` frontmatter so they load independently. caveman-commit: Conventional Commits, ≤50 char subject. caveman-review: one-line comments in `L<line>: <severity> <problem>. <fix>.` format.\n\n---\n\n## Agent distribution\n\nHow caveman reaches each agent type:\n\n| Agent | Mechanism | Auto-activates? |\n|-------|-----------|----------------|\n| Claude Code | Plugin (hooks + skills) or standalone hooks | Yes — SessionStart hook injects rules; SubagentStart passes the session's mode to subagents |\n| Codex | `npx skills add -a codex` plus an owned SessionStart hook in `$CODEX_HOME/hooks.json` (installer, default on); plugin in `plugins/caveman/`; repo `.codex/hooks.json` and `.codex/config.toml` for this checkout | Yes — SessionStart hook, after the user trusts it once via `/hooks` |\n| Gemini CLI | Extension with `GEMINI.md` context file | Yes — context file loads every session |\n| opencode | Native plugin (`src/plugins/opencode/`) copied into `~/.config/opencode/plugins/caveman/` + `AGENTS.md` ruleset + skills/agents/commands directories. Plugin uses `session.created` and `tui.prompt.append` lifecycle hooks. No statusline (opencode TUI exposes no plugin-writable badge). | Yes — `session.created` writes flag, `AGENTS.md` carries always-on ruleset |\n| OpenClaw | Workspace skill at `~/.openclaw/workspace/skills/caveman/SKILL.md` (frontmatter merged with `version` + `always: true`) plus a marker-fenced bootstrap block in `~/.openclaw/workspace/SOUL.md`. Both writes go through `installer/lib/openclaw.js`; workspace path is overridable via `OPENCLAW_WORKSPACE`. | Yes — SOUL.md is auto-injected each turn under \"Project Context\" (subject to OpenClaw's 12K-per-file / 60K-total bootstrap caps) |\n| Cursor | Plugin (`.cursor-plugin/plugin.json`: skills + `hooks/hooks-cursor.json` sessionStart hook) or `--only cursor`: `npx skills add ... -a cursor -g` skills, then `installer/lib/cursor-native.js` owns cavecrew agents in `~/.cursor/agents/` (Claude `model:` dropped, `readonly: true` on investigator/reviewer) and the shared hook payload in `~/.cursor/caveman/` with one sessionStart entry merged into `~/.cursor/hooks.json`. Both hooks run `src/hooks/caveman-host-session-start.js cursor`. Per-repo `.cursor/rules/caveman.mdc` via `--with-init`. Pick plugin OR `--only cursor`, not both (two hooks inject twice). | Yes — sessionStart `additional_context`, honors `defaultMode` (off/manual inject nothing) |\n| Windsurf | `npx skills add ... -a windsurf` (default via `--only windsurf`); per-repo `.windsurf/rules/caveman.md` via `--with-init` | Yes — always-on rule |\n| Cline | `npx skills add ... -a cline` (default via `--only cline`); per-repo `.clinerules/caveman.md` via `--with-init` | Yes — Cline auto-discovers `.clinerules/` |\n| Copilot | `npx skills add ... -a github-copilot` (detected via the `copilot` CLI plus its `~/.copilot` dir, since AWS Copilot CLI ships a `copilot` binary too, or the VS Code/Cursor extension); per-repo `.github/copilot-instructions.md` + `AGENTS.md` via `--with-init`. When the Copilot CLI is present (`copilot` on PATH or `$COPILOT_HOME` set — not a bare `~/.copilot`, which `npx skills add -a github-copilot` itself creates) the installer also owns `$COPILOT_HOME/caveman/` (shared hook payload) and `$COPILOT_HOME/hooks/caveman.json` (sessionStart → `caveman-host-session-start.js copilot`); `--no-hooks` skips it. No userPromptSubmitted hook: Copilot drops its output. | Copilot CLI: yes — sessionStart `additionalContext`, honors `defaultMode`. VS Code Copilot Chat: repo-wide instructions via `--with-init` |\n| Grok Build | Owned skill copies into `$GROK_HOME/skills` (default `~/.grok`) plus a marker-fenced ruleset block in `$GROK_HOME/AGENTS.md`, appended/stripped through `installer/lib/openclaw.js`'s marker helpers | Yes — Grok loads `~/.grok/AGENTS.md` as global rules (docs-backed; no live binary run) |\n| Antigravity CLI (`agy`) | `--only antigravity-cli` stages a temp plugin (`plugin.json`, the nine skills, `rules/AGENTS.md` = `src/rules/caveman-activate.md`) and runs `agy plugin install <dir>`; agy copies it to `~/.gemini/config/plugins/caveman/` and owns it. Uninstall: `agy plugin uninstall caveman` when `agy plugin list` names it. No session-start hook exists in agy (PreInvocation fires before every model call), so no `defaultMode`. | Yes — plugin `rules/AGENTS.md` merges into agy's active rule set (verified agy 1.2.17) |\n| Continue, AiderDesk, Antigravity IDE/2.0 | Owned physical copies into each vendor's supported directory, honoring configured homes; separate explicit targets for the two Antigravity products | Host skill invocation; feature settings may be required |\n| Others (Junie, Trae, Warp, Tabnine, Mistral, Qwen, Devin, Droid, ForgeCode, Bob, Crush, iFlow, OpenHands, Qoder, Rovo Dev, Replit, …) | Delegated `npx skills` personal installs; Replit uses project scope. Configured iFlow/Crush roots use owned copies | Host skill invocation; `/caveman` where supported |\n\nopencode reaches Tier 1 minus the statusline (opencode's TUI has no plugin-writable badge). Mode flag lives at `~/.config/opencode/.caveman-active` for any external tooling that wants to surface it.\n\nFor agents without hook systems, the always-on snippet lives in `INSTALL.md`'s \"Want it always on?\" section — keep current with `src/rules/caveman-activate.md`.\n\n**Adding a new agent.** Edit the `PROVIDERS` array in `installer/install.js` — single source of truth, no more bash/PS1 dual-source drift. Each entry has `id`, `label`, `mech`, `detect` (clause spec like `command:foo||dir:$HOME/x`; `&&` joins terms that must all hold), optional `profile` (vercel-labs/skills slug), optional `soft: true` (config-dir-only detection).\n\n1. The profile slug must exist in upstream [vercel-labs/skills](https://github.com/vercel-labs/skills). Verify against the README before merging — wrong slugs cause `npx skills add` to fail at runtime, not at install-script load.\n2. Run `node installer/install.js --list` to confirm the new row renders correctly.\n3. Soft probes (config-dir-only) are fine but tag them with `soft: true`. They render with `(soft)` in `--list` so users know detection is best-effort.\n\n---\n\n## Evals\n\n`evals/` has three-arm harness:\n- `__baseline__` — no system prompt\n- `__terse__` — `Answer concisely.`\n- `<skill>` — `Answer concisely.\\n\\n{SKILL.md}`\n\nHonest delta = **skill vs terse**, not skill vs baseline. Baseline comparison conflates skill with generic terseness — that cheating. Harness designed to prevent this.\n\n`llm_run.py` calls `claude -p --system-prompt-file ... --output-format json` per (prompt, arm), isolated from user settings, plugins and MCP, saves text plus Claude-reported usage (input/output/cache tokens, cost) to `evals/snapshots/results.json` (`results.<lang>.json` with `CAVEMAN_EVAL_LANG`). `measure.py` reads snapshot offline with tiktoken (OpenAI BPE — approximates Claude tokenizer, ratios meaningful, absolute numbers approximate). `CAVEMAN_EVAL_SET=fidelity` runs the regex-checked cases in `evals/prompts/fidelity.json` into `snapshots/fidelity.json`; `score_fidelity.py` scores it offline.\n\nAdd skill: drop `skills/<name>/SKILL.md`. Harness auto-discovers. Add prompt: append line to `evals/prompts/en.txt`.\n\nSnapshots committed to git. CI reads without API calls. Only regenerate when SKILL.md or prompts change.\n\n---\n\n## Benchmarks\n\n`benchmarks/` runs real prompts through Claude API (not Claude Code CLI), records raw token counts. Results committed as JSON in `benchmarks/results/`. Benchmark table in README generated from results — update when regenerating.\n\nTo reproduce: `uv run python benchmarks/run.py` (needs `ANTHROPIC_API_KEY` in `.env.local`).\n\n---\n\n## Key rules for agents working here\n\n- Edit `skills/<name>/SKILL.md` for behavior changes. Never edit synced copies under `plugins/caveman/skills/`.\n- Edit `src/rules/caveman-activate.md` for auto-activation rule changes. Never edit any per-agent rule copy a user has on their machine.\n- Edit `src/rules/caveman-openclaw-bootstrap.md` for the OpenClaw SOUL.md bootstrap snippet. Keep the `<!-- caveman-begin -->` / `<!-- caveman-end -->` markers and the `Respond terse like smart caveman` sentinel — `installer/lib/openclaw.js` keys idempotency off both. If you change the embedded fallback in `installer/lib/openclaw.js`, keep it byte-equivalent to the file.\n- Per-skill human docs live in `skills/<name>/README.md`. The LLM-facing body is in `SKILL.md`. Don't merge them — different audiences.\n- Build artifacts go in `dist/`. Never check files into `dist/` manually — CI rebuilds them on push, and `dist/` is gitignored.\n- README most important file for user-facing impact. Optimize for non-technical readers. Preserve caveman voice.\n- `INSTALL.md` is the per-agent install reference. Keep the install table in `README.md` short and link out to `INSTALL.md` for the full matrix.\n- Benchmark and eval numbers must be real. Never fabricate or estimate.\n- CI workflow commits back to main after merge. Account for when checking branch state.\n- Hook files must silent-fail on all filesystem errors. Never let hook crash block session start.\n- Any new flag file write must go through `safeWriteFlag()` in `caveman-config.js`. Direct `fs.writeFileSync` on predictable user-owned paths reopens the symlink-clobber attack surface.\n- Mode state reads/writes go through the `caveman-config.js` session helpers (`resolveActiveMode` / `writeSessionMode` / …), never by joining a path by hand. Any `session_id` that reaches a path must pass `validateSessionId()` first.\n- **Keep mode-state logic in `caveman-config.js`.** `src/plugins/opencode/plugin.js` cannot `require()` from disk (compiled Bun binary) — it reads that file and evaluates it through `new Function(...)` with a `createRequire` that resolves only node built-ins. Parsing already lives in `caveman-parse.js` (#602) and loads the same way, so a second parser copy is no longer the risk; a THIRD home for the state primitives would be, because opencode would keep its own drifting version of them.\n- The two statusline scripts have no shared runtime with the JS, so they re-implement path resolution by hand. `tests/verify_repo.py::verify_powershell_static` greps both against the `SESSIONS_DIRNAME` and `SESSION_ID_RE` constants to catch drift — there is no behavioral `.ps1` test on the POSIX runners, so that grep is the only guard on the Windows badge.\n- Editing anything in `src/hooks/` means regenerating `src/hooks/checksums.sha256` (same file set, recomputed digests) — `tests/verify_repo.py` fails the build otherwise, and `installer/install.js` verifies remote hook downloads against the manifest for the pinned ref.\n- Hooks must respect `CLAUDE_CONFIG_DIR` env var, not hardcode `~/.claude`. Same for `installer/install.js` / statusline scripts.\n- **Any entrypoint that reads a host hook payload from stdin returns on the first complete JSON object, never at EOF.** Under the Windows pipe implementation the host's close lags arbitrarily (#729/#833, #949), so a reader that waits for EOF burns the host's whole budget with its work done. The readers that follow this rule: `caveman-activate.js`, `caveman-mode-tracker.js`, `caveman-stats.js --record` and `caveman-host-session-start.js` (per-chunk parse), `packages/cli/src/native-hook-fast.ts` (`stdin()`), `readHookStdin()` in `packages/cli/src/index.ts` (shrink-hook, memory recall, native-hook), and `readNativeHookPayload` in `proxy/cmd/caveman-proxy/main.go`. A new hook callback uses one of those, not `readStdin()` or `io.ReadAll`. Each has a keep-the-writer-open regression test; add one for any new reader.\n- `installer/install.js` is the only installer source. `install.sh` / `install.ps1` at repo root are 30-line shims that delegate to it. Never re-add per-OS install logic to the shims — that's how we got the Windows quoting bug (#249).\n- Any settings.json read in installer or hooks must go through `installer/lib/settings.js` `readSettings()` so JSONC comments don't crash the merge. Any settings.json write must run through `validateHookFields()` first.\n","browse/CLAUDE.md":"# browse — `caveman-browse` browser driver\n\n> **Repository routing:** do not continue Browse product work here. Source of\n> truth is `JuliusBrussee/caveman-browse`, local checkout\n> `/Users/julb/Desktop/GitHub/caveman-browse`. This directory is a consumer\n> copy; edit only for pinned integration, migration/removal, or an explicit\n> cross-repo sync. Accessibility-compressor work stays here in\n> `engine/compressors/axtree.go` because Engine is owned by this repo.\n\nLocal, agent-facing browser interaction for the open Caveman surface. It serves\nstdio MCP tools that read a real Chrome accessibility tree, compress it with the\nengine's forced-only `a11y` compressor, act on `uid` handles, and recover the\nbyte-exact original AX payload through CCR. Every saving here is `inferred`;\nBrowse never emits `verified`.\n\n## Layout\n- `session.go` — MCP tool handlers, engine/CCR integration, UID target cache.\n- `cdp.go` — chromedp-backed Mode-A dedicated Chrome driver and bounded\n  actionability recipe.\n- `cmd/caveman-browse/` — stdio MCP binary.\n\n## Gotchas\n- This package may import CDP/network/browser dependencies. `public/mcp` may not.\n- `go test ./public/browse/...` is setup-free. `make test-browse` resolves an\n  installed Playwright Chromium or system Chrome and runs the `integration`\n  build-tagged CDP contract across the package and direct CLI; `make\n  test-browser` adds extension tests and `make test-e2e` includes both.\n- The actionability layer is intentionally bounded: same-origin dashboards and\n  predictable design-system controls, not arbitrary-open-web parity.\n- Unknown handles/actions fail closed with `cave_snake_code` errors.\n- Snapshot output is compact indented text, not JSON-lines. `query` keeps at most\n  12 highest-scoring task matches plus ancestors; recovery metadata exposes only\n  UIDs actually shown. Keep `tokens_after` equal to exact agent-visible result\n  cost and `view_tokens` equal to serializer output cost.\n- Direct CLI Chrome is detached so separate `snapshot`/`act`/`eval` processes\n  can share a target. `close` must terminate it. Fresh `CAVEMAN_HOME` must work;\n  state writes stay atomic and mode `0600`.\n- CDP action acknowledgement is not application settlement. Non-wait actions\n  return `settled:false` and require a focused resnapshot for proof.\n- Navigation denies `file:`, `javascript:`, and privileged Chrome schemes.\n- Benchmark contract and reproducible Playwright comparison live in\n  `BENCHMARK.md`; keep token budgets executable in tests.\n- **The uid map is `browser_snapshot`'s contract, not a side effect of a\n  compression ratio.** On engine pass-through (no recovery handle — a tree that\n  did not get smaller, or no CCR store) `snapshotTool` fails closed with\n  `cave_browser_snapshot_uncompressed` and returns the prior page's uids intact;\n  it MUST NEVER dump the raw AX tree into `uids` (strictly worse than not using\n  Browse) nor wipe the target cache. Regressing this reopens issue #140.\n- **An `<iframe>` is a leaf, not a broken tree.** `Accessibility.getFullAXTree`\n  returns one frame at a time, so an iframe node's `childId` points at a child\n  document absent from this payload; the `a11y` compressor\n  (`engine/compressors/axtree.go`) treats an unresolvable `childId` as a leaf and\n  still curates the frame-visible nodes. Because of that the CDP driver keeps\n  Chrome's default Site Isolation (`site-per-process`) — do not disable it to\n  \"fix\" cross-origin iframes.\n","engine/AGENTS.md":"# engine — the content-aware Caveman Engine (commercial, binary-distributed)\n\nDetect a payload's type → route to a safety-classed compressor → count the token reduction\n→ store the original for recovery. The stable four-call API (`Compress`/`Retrieve`/`Detect`/`Stats`)\nis what the proxy, CLI, SDKs, MCP, and WASM build all share. Everything it reports is\n`inferred`; it never says `verified`.\n\n## Layout\n- `engine.go` — the `Engine` core: detect → route → compress → token ratio → CCR. `record` mode + miss + parse-fail + not-smaller + no-store all pass through.\n- `result.go` — `Result`/`Options`/`Mode`; unknown mode fails closed to `record`.\n- `detect.go` — content router (`json`/`diff`/`code`/`log`/`search-result`/`html`/`tabular`/`config`/`terminal`/`text`); low confidence → `text`.\n- `listing.go` — strips a read tool's line-number gutter (`1\\t{`, `2\\t  \"unit\"`) before Detect and before the compressor, then restores it after. Agents send file *listings*, not files; the gutter is presentation, and left in place it made every guttered payload detect as `text` and compress ~0%. Restoration keeps each surviving line's ORIGINAL number and is declined entirely for transforms that restructure rather than elide (re-encoded JSON), because numbers that describe nothing are worse than none.\n- `safety/` — the S0–S4 registry; S4 is lossy and `RequiresCCR`. Unknown class → fail closed.\n- `tokens/` — `Counter` interface; default is an offline, vocab-embedded BPE tokenizer (o200k_base). `inferred` always.\n- `contextwindow/` — deterministic BM25 context packer with recency/error/priority signals and token-budget accounting.\n- `compressors/` — `Compressor` interface + registry; structural JSON/log/search/diff/text/HTML/table/config/code compressors plus forced-only TOON/tool-schema/accessibility/repetition paths — `Default()` registers 14. A compressor is a pure byte transform.\n- `ccr/` — `~/.caveman/ccr.db` SQLite recovery store; content-addressed handles; byte-exact `Get`.\n- `pixel/` — pxpipe port (MIT, see its NOTICE): text→PNG request compression. Embedded glyph atlases + renderer + profitability gate + per-wire-format transforms (Anthropic/OpenAI/Gemini). S4-lossy, allowlist-gated (`CAVE_PIXEL_MODELS`, default `claude-fable-5,gpt-5.6`), consumed by the proxy's `pixel` mode; never wired into `Detect` and never imported by the WASM build (≈4 MB assets).\n- `evals/` — the local eval harness + fail-closed graders + embedded `fixtures/`. `Run()` is the quality gate.\n- `cmd/caveman-engine/` — the binary the CLI shells out to: `compress | detect | retrieve | stats | registry | toon encode|decode | evals run | pixel render|simulate`. `toon` is the stateless (no-CCR) JSON⇄TOON converter; both directions fail closed.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=engine` / `make product-test PRODUCT=engine`.\n- A new compressor is a self-contained file in `compressors/` + tests, registered in `Default()`. Forced-only compressors such as `toolschema` and `toon` must not be added to `Detect`.\n- Compressors never count tokens, touch CCR, or hit the network — the engine core does that around them.\n- **The `toolschema` transform is client-side-only today.** Registration makes it forceable by\n  engine callers; it does not make it reachable from managed-gateway traffic. Provider adapters\n  intentionally keep tool arrays in the frozen prompt-cache prefix, and no billed route invokes\n  this compressor. Engine API/CLI callers may force it locally, and `caveman-shrink` is its\n  dedicated product surface; those reductions remain local and `inferred`. Managed gateway has a\n  separate S2 tool-search/deferral path; do not conflate it with compression. Any future gateway\n  route for this transform needs cache-versus-schema cost arithmetic, stable byte-identical prefix\n  output, and an eval gate before activation.\n\n## Gotchas (honesty invariants — correctness, not style)\n- **fail-closed transforms**: every compressor passes the original through unchanged on any parse problem; the engine also passes through when the result is not smaller in tokens. Reserve **byte-safe** for classes whose `safety.Info.ByteSafe` is true.\n- **CCR-or-pass-through**: a lossy (S4) result is only emitted if its original was stored; with no store, the engine fails closed to pass-through.\n- **inferred-only**: ratios are token estimates labeled `inferred`; never `verified`, never re-projected.\n- **fail-closed**: unknown mode → `record`; unknown content type → `text`; unknown grader → `passed:false`.\n- **cgo**: full code compression (Python/JS/TS) needs the tree-sitter build; the cgo-free build compresses Go only. The embedded eval fixtures cover all three under cgo.\n- **boundary**: this is `public/` — never import `cloud/…`. `make check-boundaries` enforces it.\n\nSee ../../CLAUDE.md (root)\n","engine/CLAUDE.md":"# engine — the content-aware Caveman Engine (commercial, binary-distributed)\n\nDetect a payload's type → route to a safety-classed compressor → count the token reduction\n→ store the original for recovery. The stable four-call API (`Compress`/`Retrieve`/`Detect`/`Stats`)\nis what the proxy, CLI, SDKs, MCP, and WASM build all share. Everything it reports is\n`inferred`; it never says `verified`.\n\n## Layout\n- `engine.go` — the `Engine` core: detect → route → compress → token ratio → CCR. `record` mode + miss + parse-fail + not-smaller + no-store all pass through.\n- `result.go` — `Result`/`Options`/`Mode`; unknown mode fails closed to `record`.\n- `detect.go` — content router (`test-report`/`json`/`diff`/`code`/`log`/`search-result`/`html`/`tabular`/`config`/`terminal`/`text`); low confidence → `text`.\n- `listing.go` — strips a read tool's line-number gutter (`1\\t{`, `2\\t  \"unit\"`) before Detect and before the compressor, then restores it after. Agents send file *listings*, not files; the gutter is presentation, and left in place it made every guttered payload detect as `text` and compress ~0%. Restoration keeps each surviving line's ORIGINAL number and is declined entirely for transforms that restructure rather than elide (re-encoded JSON), because numbers that describe nothing are worse than none.\n- `safety/` — the S0–S4 registry; S4 is lossy and `RequiresCCR`. Unknown class → fail closed.\n- `tokens/` — `Counter` interface; default is an offline, vocab-embedded BPE tokenizer (o200k_base). `inferred` always.\n- `contextwindow/` — deterministic BM25 context packer with recency/error/priority signals and token-budget accounting.\n- `compressors/` — `Compressor` interface + registry; structural JSON/log/test-report/search/diff/text/HTML/table/config/code compressors plus forced-only TOON/tool-schema/tool-schema-annotations/accessibility/repetition paths — `Default()` registers 16. `test-report` and `toolschema-annotations` stay manifest-excluded because no compiled plan routes to them (the proxy's `compiledContentType` allowlist has neither); advertising either would rotate `RegistrySHA256` and fail existing Cave Build locks. A compressor is a pure byte transform.\n- `ccr/` — `~/.caveman/ccr.db` SQLite recovery plus typed native-session store;\n  content-addressed handles, byte-exact `Get`, session scope, dependencies,\n  current/stale state, and Hot/Warm/Cold/Archived lifecycle. Embedded SQLite uses\n  one serialized connection so concurrent hook/repository writes cannot split an\n  in-memory schema or race with `SQLITE_BUSY`.\n- `pixel/` — pxpipe port (MIT, see its NOTICE): text→PNG request compression. Embedded glyph atlases + renderer + profitability gate + per-wire-format transforms (Anthropic/OpenAI/Gemini). S4-lossy, allowlist-gated (`CAVE_PIXEL_MODELS`, default `claude-fable-5,gpt-5.6`), consumed by the proxy's `pixel` mode; never wired into `Detect` and never imported by the WASM build (≈4 MB assets).\n- `evals/` — the local eval harness + fail-closed graders + embedded `fixtures/`. `Run()` is the quality gate.\n- `cmd/caveman-engine/` — the binary the CLI shells out to: `compress | detect | retrieve | stats | registry | toon encode|decode | evals run | pixel render|simulate`. `toon` is the stateless (no-CCR) JSON⇄TOON converter; both directions fail closed.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=engine` / `make product-test PRODUCT=engine`.\n- A new compressor is a self-contained file in `compressors/` + tests, registered in `Default()`. Forced-only compressors such as `toolschema` and `toon` must not be added to `Detect`.\n- Compressors never count tokens, touch CCR, or hit the network — the engine core does that around them.\n- **The `toolschema` transform is client-side-only today.** Registration makes it forceable by\n  engine callers; it does not make it reachable from managed-gateway traffic. Provider adapters\n  intentionally keep tool arrays in the frozen prompt-cache prefix, and no billed route invokes\n  this compressor. Engine API/CLI callers may force it locally, and `caveman-shrink` is its\n  dedicated product surface; those reductions remain local and `inferred`. Managed gateway has a\n  separate S2 tool-search/deferral path; do not conflate it with compression. Any future gateway\n  route for this transform needs cache-versus-schema cost arithmetic, stable byte-identical prefix\n  output, and an eval gate before activation.\n\n## Gotchas (honesty invariants — correctness, not style)\n- **fail-closed transforms**: every compressor passes the original through unchanged on any parse problem; the engine also passes through when the result is not smaller in tokens. Reserve **byte-safe** for classes whose `safety.Info.ByteSafe` is true.\n- **elide repetition, never a document**: every unit-dropping compressor (text, log, json, tabular, config, searchresult, terminal) calls `keepNonRedundant` (`compressors/redundancy.go`) before emitting. It keeps one representative of each distinct content class, so a stream of near-identical lines still collapses while a bibliography, a source file or a table of measurements survives whole — and when nothing is left to drop, the not-smaller check passes the payload through and claims nothing. This is a correctness rule, not a tuning knob: eliding a document the agent must enumerate cost the agent bench 5.5x its baseline on 2026-08-06 (`bench/agent/README.md`). Class matching masks digit runs, but only for units carrying at least one word — a wordless unit like `6 ['1973.', 251]` is compared with its digits intact, because there the numbers ARE the content.\n- **an elision marker states only what it verified**: `compressors/invariants.go` enriches the log/tabular/JSON markers with facts computed from the exact units that marker replaced — a field constant across every one of them (`all state=charged`), a varying field enumerated IN FULL with exact counts that sum to the elided count (`status: fulfilled×15 shipped×3 processing×2`, with an `absent×N` bucket for units lacking the field), and a numeric range printed from the ORIGINAL extreme value strings (`range amount=5.00..199.99`). Nothing else. A credential-shaped field name, a value with a space or over 24 bytes, more than five distinct values, and a run in which any unit yields no fields at all are all withheld **entirely** — never shortened, because a partial list reads as complete. When nothing survives, the marker renders byte-identically to the bare `… N rows elided (caveman) …`. An enumeration whose every bucket holds ONE unit is an identifier list, not an invariant, so instead of enumerating it the marker states its COVERAGE — `order_id: 25 distinct, ord-1000..ord-1024`, the exact distinct count plus the byte-wise smallest and largest ORIGINAL value strings, claiming nothing about the gaps between them (this is the only fact that answers \"was ord-1043 among the elided rows, or absent from the ledger?\", the last legitimate retrieve on the v5 probe). When a coverage run is EXACTLY dense — one shared prefix, fixed-width digits, and `max-min+1` equal to the counted distinct set — it says so instead: `wh-5000..wh-5059 all 60 present`, a verified membership answer that a set-difference task needs and would otherwise retrieve for. Density is counted, never inferred from the endpoints; mixed suffix widths, two prefixes, or a single gap all fall back to the counted form (whose own arithmetic still says how many are missing, never which). A dense entry outlives even the constants when the budget binds; a bounded one goes before them. At most two coverage entries per summary — a record carries several identifier columns and at ~38 bytes each they will name the run three ways and crowd out a 16-byte constant. An enumeration averaging two units or fewer per bucket still prints but is shed first. Summaries are capped at 160 bytes and a quarter of the bytes they replace (floor 96, never over half), shedding identifier-ish enumerations, then ranges, then bounded coverage, then constants, then dense coverage, class enumerations last; **a summary that sheds says so**, with a trailing `+N omitted` section, because every section above reads as the complete set of facts of its kind and a silently trimmed `all …` list would let an agent conclude a field it cannot see was not constant — the same error a partial enumeration is refused to avoid. The notice is charged to the same budget as the facts, so both caps still hold exactly; one trailing contract line per payload is added only when ≥4 KB was actually dropped. **A run under 3 units is not elided at all** unless collapsing both summarizes and halves its bytes — a one-unit marker plus its recovery handle costs about what the unit cost and reads as a hole. Together this costs ~4 points of ratio on the toolwork corpus (0.83 → 0.79) and is bought on purpose: on 2026-08-08 an agent that could not tell what had been elided made 46 sequential `caveman_retrieve` calls at 3.3x the uncompressed arm; a first fix stating only constants and ranges left the storms at 11–27 calls because it was silent about the one mixed-value field the task turned on; and a second round still leaked retrieves through identifier enums, fact-free small classes, and `{\"__caveman_elided__\":1}` singletons.\n- **a recovery view is made of whole units**: `retrieve_query.go` narrows a CCR recovery to the units matching a query, and a unit is whatever is self-delimiting — a complete JSON record (never a field line: `\"status\": \"unfulfilled\",` is a fragment), a whole CSV record sliced out of the original bytes with the header shown once, a whole log/NDJSON line, or a whole paragraph from non-JSON prose. JSON field names are never provenance: `messages[].content`, `text`, and `system` stay attached to their full object record, because tool output can use the same names as provider requests. Separate JSON arrays have explicit gap sentinels; omitted envelope/scalar bytes get leading/trailing markers. Content it cannot decompose (JSON with no array of records) returns in FULL rather than line-split, and a narrowed view that is not smaller also returns the full original; over-returning is safe, a fragment is not. Every gap — between two returned units, before the first, after the last — prints `… [caveman: non-adjacent] …`, so adjacency in the view never implies adjacency in the original. This is a correctness rule with a measured cost: on 2026-08-08 query mode returned BM25-ranked lines of a pretty-printed orders page, the agent read `\"status\": \"unfulfilled\"` (ord-1043's) directly above `\"order_id\": \"ord-1047\"`, attributed it to ord-1047, and reported 5 unfulfilled orders where there were 3 — two wrong answers in a 12-task bench run.\n- **one recovery surface, two id spaces**: the CCR store holds compression blob handles (`ccr_…`, `store.Get`) AND the native runtime's typed objects (`store.GetObject`), and the runtime shows the latter to agents as `ccr://<objectID>` when it masks a whole tool output. `Engine.Retrieve` resolves a blob first and falls through to the object table before failing, and `mcp.normalizeRecoveryHandle` accepts `ccr_…`, `<<ccr:…>>`, `ccr:…` and `ccr://…`. An agent copies whatever reference it is shown; a form that does not resolve does not degrade, it loops. Measured 2026-08-08: inventory-mismatch and webhook-delivery-gaps were handed `ccr://<objectID>`, every retrieve answered `cave_unknown_handle`, the failing tool result was itself masked into a fresh pointer — \"a new pointer to another pointer, indefinitely\" — 27–97 recovery calls, no answer written, both tasks 0/6 while rate-limit-forensics on the same build scored 3/3 at ~35% cheaper.\n- **CCR-or-pass-through**: a lossy (S4) result is only emitted if its original was stored; with no store, the engine fails closed to pass-through.\n- **inferred-only**: ratios are token estimates labeled `inferred`; never `verified`, never re-projected.\n- **fail-closed**: unknown mode → `record`; unknown content type → `text`; unknown grader → `passed:false`.\n- **cgo**: full code compression (Python/JS/TS) needs the tree-sitter build; the cgo-free build compresses Go only. The embedded eval fixtures cover all three under cgo. Release `caveman-proxy`/`caveman-engine` are cgo builds via the pinned zig in `scripts/build-release-binaries.mjs`; `TestReleaseShape*` carry no build tag on purpose and fail under `CGO_ENABLED=0`, and engine-ci's `release-shape` lane runs them with the release flags.\n- **boundary**: this is `public/` — never import `cloud/…`. `make check-boundaries` enforces it.\n\nSee ../../CLAUDE.md (root)\n","evals/README.md":"# Evals\n\nMeasures real token compression of caveman skills by running the same\nprompts through Claude Code under three conditions and comparing the\ngenerated output token counts. A second eval, [Fidelity](#fidelity),\nruns the same arms on fixed correctness cases and checks that the facts\nsurvive.\n\n## The three arms\n\n| Arm | System prompt |\n|-----|--------------|\n| `__baseline__` | none |\n| `__terse__` | `Answer concisely.` |\n| `<skill>` | `Answer concisely.\\n\\n{SKILL.md}` |\n\nThe honest delta for any skill is **`<skill>` vs `__terse__`** — i.e.\nhow much the skill itself adds on top of a plain \"be terse\" instruction.\nComparing a skill to the no-system-prompt baseline conflates the skill\nwith the generic terseness ask, which is what an earlier version of\nthis harness did and is why its numbers were inflated.\n\n## Current snapshot\n\n`snapshots/results.json` was generated on `claude-opus-5-5` with Claude\nCode 2.1.288 (2026-10-02), with the three response-style skills as the\nskill arms: `caveman`, `ultracave`, `megacave`. The other `skills/*`\ndirectories are workflow skills, not response styles, so they are left out.\n\n```bash\nCAVEMAN_EVAL_MODEL=claude-opus-5-5 CAVEMAN_EVAL_SKILLS=caveman,ultracave,megacave \\\n  python3 evals/llm_run.py\n```\n\nThe previous snapshot (`claude-opus-4-6`, April 2026) was generated\nwithout host isolation, so whatever plugins and CLAUDE.md files were\ninstalled could reach every arm. `llm_run.py` now isolates each call\n(see below); numbers from the two snapshots are not directly comparable.\n\n## Why this design\n\n- **Real LLM output**, not hand-written examples (no circularity).\n- **Same Claude Code** the skills target — no separate API key.\n- **Snapshot committed to git** so CI runs are deterministic and free,\n  and so any change to the numbers is reviewable as a diff.\n- **Control arm** isolates the skill's contribution from the generic\n  \"be terse\" effect.\n- **Host isolation.** Each `claude -p` call runs with\n  `--setting-sources project --strict-mcp-config --disable-slash-commands\n  --no-session-persistence` from an empty temp dir: no user settings (so\n  no installed plugins or their SessionStart hooks), no MCP servers, no\n  installed skills, no CLAUDE.md. Without it a plugin's injected ruleset,\n  or an MCP auth nag the model repeats in its answer, lands in every arm\n  including the baseline.\n\n## Files\n\n- `prompts/<lang>.txt` — fixed list of dev questions, one per line.\n  `en.txt` is the default; `pt.txt` is Brazilian Portuguese; `fr.txt`\n  is French and mirrors `en.txt` line for line.\n- `llm_run.py` — runs `claude -p --output-format json --system-prompt-file …`\n  per (prompt, arm), captures real LLM output, writes\n  `snapshots/results.json` along with metadata (model, CLI version,\n  language, generation timestamp). Next to the text it stores the usage\n  Claude Code reports for each call under `usage` (same arm/prompt layout\n  as `arms`): input, output, cache-creation and cache-read tokens plus\n  `total_cost_usd`.\n- `measure.py` — reads the snapshot, counts tokens with tiktoken\n  `o200k_base`, prints a markdown table with median / mean / min / max /\n  stdev across prompts. When the snapshot has `usage`, a second table\n  gives Claude's own output-token counts against the terse control and\n  the median input tokens the skill adds per call (skill arm minus terse\n  arm, cache tokens included).\n- `snapshot_contract.py` — rejects incomplete or malformed snapshot matrices\n  before `measure.py` or `score_fidelity.py` reports metrics.\n- `prompts/fidelity.json` — fixed correctness cases, each with regex\n  checks (see [Fidelity](#fidelity)).\n- `score_fidelity.py` — scores `snapshots/fidelity.json` against those\n  checks, offline, stdlib only.\n- `snapshots/results.json` — committed source of truth, regenerated only\n  when SKILL.md files or prompts change. Other languages write\n  `results.<lang>.json` next to it; none is committed yet.\n\n## Refresh the snapshot (requires `claude` CLI logged in)\n\n```bash\nuv run python evals/llm_run.py\n```\n\nThis calls Claude once per prompt × (N skills + 2 control arms). Use\na small model to keep it cheap:\n\n```bash\nCAVEMAN_EVAL_MODEL=claude-haiku-4-5 uv run python evals/llm_run.py\n```\n\nBy default every `skills/*/SKILL.md` gets an arm. `CAVEMAN_EVAL_SKILLS`\n(comma-separated skill ids) restricts the skill arms; an unknown id aborts\nbefore any call:\n\n```bash\nCAVEMAN_EVAL_SKILLS=caveman,ultracave,megacave uv run python evals/llm_run.py\n```\n\nEach call times out after `CAVEMAN_EVAL_TIMEOUT` seconds (default 300). A\nfailed or timed-out call is retried twice, after 5 s and then 20 s. If it\nstill fails, the run stops (auth, quota or a bad model name would fail\nevery remaining call too), writes the finished calls to\n`snapshots/results.partial.json` with the unfinished cells set to `null`\nand the error in `metadata.error`, leaves `results.json` untouched and\nexits 1. `measure.py` refuses a partial file; it is gitignored.\n\n### Other languages\n\n```bash\nCAVEMAN_EVAL_LANG=pt CAVEMAN_EVAL_MODEL=claude-haiku-4-5 CAVEMAN_EVAL_SKILLS=caveman uv run python evals/llm_run.py\nCAVEMAN_EVAL_LANG=pt uv run --with tiktoken python evals/measure.py\n```\n\nThe terse control is translated per language (`TERSE_PREFIXES` in\n`llm_run.py`): an English \"Answer concisely.\" on a Portuguese question\nalso nudges the model toward English, which would be a second variable.\nA language with no translated control, or no `prompts/<lang>.txt`, stops\nbefore any call instead of falling back to English. The skill text\nitself stays in English, as shipped.\n\nWhy a separate language matters: SKILL.md promises \"compress the style,\nnot the language\", and the rules target English function words\n(a/an/the, just/really). Whether that transfers to a language with\ngendered articles and a different filler vocabulary is an empirical\nquestion, so it gets its own snapshot.\n\n## Read the snapshot (no LLM, no API key, runs in CI)\n\n```bash\nuv run --with tiktoken python evals/measure.py\n```\n\nReporting fails closed unless the snapshot has both control arms, at least one\nskill arm, exactly one string output per prompt in every arm, and metadata whose\n`n_prompts` matches the prompt list. A `usage` block, when present, must\ncover the same arms and prompts with non-negative token counts.\n\n## Adding a prompt\n\nAppend a line to `prompts/<lang>.txt`, then refresh that language's snapshot.\n\n## Adding a language\n\nAdd `prompts/<lang>.txt` (ideally mirroring `en.txt` line for line),\nadd the translated terse control to `TERSE_PREFIXES` in `llm_run.py`,\nrun with `CAVEMAN_EVAL_LANG=<lang>` and commit\n`snapshots/results.<lang>.json`.\n\n## Adding a skill\n\nDrop a `skills/<name>/SKILL.md`, then refresh the snapshot. `llm_run.py`\npicks up every skill directory automatically, unless `CAVEMAN_EVAL_SKILLS`\nis set, in which case add the new id to that list.\n\n## Fidelity\n\nThe length eval alone rewards a skill that replies `k` to everything.\nThe fidelity eval asks whether each arm kept what the answer needs: exact\nnumbers and units, every not/never/only/except, the user's language,\nverbatim errors, safety wording before destructive steps, normal prose in\npersisted artifacts.\n\n`prompts/fidelity.json` holds the cases: `{id, category, prompt, checks,\nexamples}` with `checks.must_include` and `checks.must_not_include` as\nregex lists, and `examples.pass` / `examples.fail` as answers the checks\nmust accept / reject.\nA case passes for an arm when every `must_include` pattern matches the\noutput and no `must_not_include` pattern does (Python `re.search`,\ncase-insensitive). There is no judge model, so the same snapshot always\ngets the same score, and checks can be fixed and re-scored without new\ncalls. The first six cases come from\n[#1061](https://github.com/JuliusBrussee/caveman/pull/1061) by alexis.\n\n```bash\nCAVEMAN_EVAL_SET=fidelity CAVEMAN_EVAL_MODEL=claude-opus-5-5 \\\n  CAVEMAN_EVAL_SKILLS=caveman,ultracave,megacave python3 evals/llm_run.py\npython3 evals/score_fidelity.py\n```\n\nThe run uses the same arms, isolation, retries and usage capture as the\nlength eval and writes `snapshots/fidelity.json`. The scorer prints the\npass rate per arm, overall and per category, then every failed check.\nIt refuses a snapshot whose prompts no longer match the case file.\n\nNo fidelity snapshot is committed yet. A regex pass is evidence, not a\nverdict: read the failures before citing a rate, and publish no\nquality-equivalence claim without a committed, reviewed snapshot.\n\n| Category | What the checks look for |\n|----------|--------------------------|\n| `substance-preservation` | Filler removed, the technical substance needed to act kept. |\n| `exact-preservation` | Polarity, limits, numbers, units, code, identifiers, APIs, commands and quoted errors unchanged. |\n| `no-caricature` | No fake grammar, mode prefixes, invented abbreviations or arrows. |\n| `language-and-grammar` | The requested or dominant language kept, grammatical markers kept. |\n| `safety-clarity` | Plain, complete warnings for destructive actions, security, data loss and ordered recovery steps. |\n| `artifact-boundary` | Persisted or external human-facing artifacts in normal prose. |\n| `mode-boundaries` | Explicit activation, mode switch, persistence and deactivation boundaries preserved. |\n\nTo add a case, append it to `prompts/fidelity.json` with a unique `id`,\none of the categories above, at least one check, and at least one\nknown-good and one known-bad example answer, then rerun the fidelity\neval. `tests/test_eval_fidelity.py` checks the file's shape and runs\nevery case's checks against its examples, so a check that rejects a\ncorrect answer fails there instead of after a paid run.\n\n## What this does NOT measure\n\n- **Fidelity beyond the checks** — the fidelity eval only sees what its\n  regexes test. Tone, ordering and whether an explanation is actually\n  right are left to human review of the outputs.\n- **Latency or cost** — latency is out of scope. Skills add input tokens\n  on every call, so output savings are not the full economic picture.\n  Snapshots with `usage` record that input cost per call; the committed\n  `results.json` predates usage capture, so it has none.\n- **Cross-model behavior** — only the model used to generate the\n  snapshot is measured.\n- **Exact Claude tokens, from the tiktoken table** — `tiktoken\n  o200k_base` is OpenAI's BPE and is only an approximation of Claude's\n  tokenizer. Ratios between arms are meaningful; absolute numbers are\n  approximate. The usage table uses Claude's own counts instead, but\n  Claude's output count includes thinking tokens, which the user never\n  reads.\n- **Statistical significance** — single run per (prompt, arm) at default\n  temperature. The min/max/stdev columns let you eyeball whether a\n  number is solid or noisy, but this is not a powered experiment.\n\n## Historical: reminder_run.py\n\n`reminder_run.py` and `snapshots/reminder.json` measured the pre-3.1 per-turn reminder text (`Enforce this reply: ...`), which the mode tracker no longer emits. The snapshot is kept as history; regenerate before citing it.\n","extension/AGENTS.md":"# extension — \"Caveman Mode\" browser extension (MV3, no bundler)\n\nThe consumer top-of-funnel: a toggle that, on ChatGPT / Claude / Gemini, prepends the **caveman\nskill directive** to each message you send and re-fires the send, so the AI replies in a compact,\nhigh-density style intended to preserve technical substance. It only ever adds text to your *outgoing*\nmessage; it never reads replies, never touches network requests. Savings are `inferred`. (This is\nthe directive-injector product; an earlier revision shipped a local WASM compress button — EXT\nremoved it after Chrome Web Store review couldn't reproduce \"compress prompt locally\".)\n\n## Layout\n- `manifest.json` — MV3. `storage` permission only. Content scripts load `src/directive.js` **then** `src/caveman.js` (order matters). Only the fonts are web-accessible — no WASM, no special CSP.\n- `src/directive.js` — the **pure** caveman directive text: `buildPrimer`/`buildReminder`/`isPrefixed`/`normMode`. No chrome/DOM/network. Loaded first (exposes `self.CavemanDirective`) and required directly by the test suite.\n- `src/caveman.js` — content script: per-site composer adapter + capture-phase send interception. Prepends the directive (full primer on the first message, short reminder after) and re-fires send. Exact composer-scoped selectors; `prependText` inserts at a collapsed selection without rebuilding the rich draft. Pending sends cancel on draft, editor, conversation, or settings changes; unknown controls keep native behavior.\n- `src/background.js` — service worker: just reflects the on/off state on the toolbar badge.\n- `popup.{html,js,css}` — the on/off toggle, mode (caveman/ultracave/megacave), per-site switches, and a **Leave-a-review** CTA (Chrome Web Store via `chrome.runtime.id`; hidden on Firefox until a verified AMO URL is available).\n- `test/` — pure directive/service-worker tests plus real Chromium\n  content-script and popup/storage journeys against `harness.html`.\n\n## Conventions & honesty\n- **Edits your outgoing message only.** Prepends a visible directive; never reads model replies, never touches network requests, never automates anything beyond submitting the message you sent.\n- **Local-only.** No network requests, no analytics. Stores only `enabled` / `level` / `sites` in `chrome.storage.sync`.\n- **Honest about injection.** The directive is plainly visible in the chat — the extension doesn't hide what it adds.\n- **inferred-only.** Token savings are an effect of the model's terser replies; never labeled `verified`.\n\n## Conventions\n- Build/test: `npm test` for pure tests; `npm run test:browser` for Chromium send interception, popup storage,\n  per-site controls, and review-link behavior. There is no\n  build step — MV3 loads the source directly.\n- Package: `npm run package` runs both suites, then writes `dist/caveman-browser-<version>.zip`. Staged set uses explicit allowlist in `scripts/verify-extension-stage.mjs`, cross-checked against manifest/CSS/HTML references.\n- Composer + send selectors live in the `SITES` map in `src/caveman.js`; a site redesign means updating it (each entry keeps aria-label fallbacks so one rename doesn't break it).\n\nSee ../../CLAUDE.md (root)\n","extension/CLAUDE.md":"# extension — \"Caveman Mode\" browser extension (MV3, no bundler)\n\nThe consumer top-of-funnel: a toggle that, on ChatGPT / Claude / Gemini, prepends the **caveman\nskill directive** to each message you send and re-fires the send, so the AI replies in a compact,\nhigh-density style intended to preserve technical substance. It only ever adds text to your *outgoing*\nmessage; it never reads replies, never touches network requests. Savings are `inferred`. (This is\nthe directive-injector product; an earlier revision shipped a local WASM compress button — EXT\nremoved it after Chrome Web Store review couldn't reproduce \"compress prompt locally\".)\n\n## Layout\n- `manifest.json` — MV3. `storage` permission only. Content scripts load `src/directive.js` **then** `src/caveman.js` (order matters). Only the fonts are web-accessible — no WASM, no special CSP.\n- `src/directive.js` — the **pure** caveman directive text: `buildPrimer`/`buildReminder`/`isPrefixed`/`normMode`. No chrome/DOM/network. Loaded first (exposes `self.CavemanDirective`) and required directly by the test suite.\n- `src/caveman.js` — content script: per-site composer adapter + capture-phase send interception. Prepends the directive (full primer on the first message, short reminder after) and re-fires send. Exact composer-scoped selectors; `prependText` inserts at a collapsed selection without rebuilding the rich draft. Pending sends cancel on draft, editor, conversation, or settings changes; unknown controls keep native behavior.\n- `src/background.js` — service worker: just reflects the on/off state on the toolbar badge.\n- `popup.{html,js,css}` — the on/off toggle, mode (caveman/ultracave/megacave), per-site switches, and a **Leave-a-review** CTA (Chrome Web Store via `chrome.runtime.id`; hidden on Firefox until a verified AMO URL is available).\n- `test/` — pure directive/service-worker tests plus real Chromium\n  content-script and popup/storage journeys against `harness.html`.\n\n## Conventions & honesty\n- **Edits your outgoing message only.** Prepends a visible directive; never reads model replies, never touches network requests, never automates anything beyond submitting the message you sent.\n- **Local-only.** No network requests, no analytics. Stores only `enabled` / `level` / `sites` in `chrome.storage.sync`.\n- **Honest about injection.** The directive is plainly visible in the chat — the extension doesn't hide what it adds.\n- **inferred-only.** Token savings are an effect of the model's terser replies; never labeled `verified`.\n\n## Conventions\n- Build/test: `npm test` for pure tests; `npm run test:browser` for Chromium send interception, popup storage,\n  per-site controls, and review-link behavior. There is no\n  build step — MV3 loads the source directly.\n- Package: `npm run package` runs both suites, then writes `dist/caveman-browser-<version>.zip`. Staged set uses explicit allowlist in `scripts/verify-extension-stage.mjs`, cross-checked against manifest/CSS/HTML references.\n- Composer + send selectors live in the `SITES` map in `src/caveman.js`; a site redesign means updating it (each entry keeps aria-label fallbacks so one rename doesn't break it).\n\nSee ../../CLAUDE.md (root)\n","integrations/CLAUDE.md":"# integrations — the framework-recipe registry (`caveman sdk` data)\n\nDeclarative routing recipes for AI SDKs/frameworks: each recipe is the one-line\nbase-URL shape that points a framework at the byte-safe gateway, with `{{baseURL}}`\n(gateway origin) and `{{app}}` (the `/w/<app>` attribution slug) left as render-time\ntemplates. Recipes are DATA, not code — the collapse decision applied to app frameworks.\n\n## Layout\n- `recipes/*.json` — one recipe per framework (12 today: openai-ts/py, anthropic-ts/py,\n  google-genai, vercel-ai-sdk, langchain, litellm, crewai, pydantic-ai, openai-agents, curl).\n  Fields: `id`, `display_name`, `lang` (`ts`/`python`/`bash`), `wire_protocol`, `note`, `code`.\n- `compile.mjs` — zero-dep build-time compiler: validates every recipe (fail-closed) and\n  emits `recipes.json` (published registry) + two EMBEDDED copies —\n  `../cli/src/recipes.generated.ts` (the CLI stays zero-runtime-dep) and\n  the web app's embedded copy (cloud may consume public data; the reverse import is forbidden).\n- `recipes.json` — generated; do not hand-edit.\n\n## Conventions\n- **fail-closed**: an unknown `lang` or `wire_protocol` fails the compile — never a guessed\n  protocol (no-placeholder rule). Allowed protocols are the 4 the proxy speaks natively\n  (anthropic-messages · openai-chat · openai-responses · gemini-generatecontent) plus\n  `multi` for frameworks that fan out behind one config.\n- Output is deterministic (recipes sorted by id); re-running on unchanged input is a no-op.\n- Run from the CLI build/test: `node ../integrations/compile.mjs`. The generated files are\n  committed and must stay in sync — change a recipe and regenerate all three artifacts.\n\nSee ../../CLAUDE.md (root) · ../agents/CLAUDE.md (the agent twin) · ../cli/CLAUDE.md\n","mcp/AGENTS.md":"# mcp — the Caveman MCP server (Go core + launcher, Apache-2.0)\n\nA thin **stdio JSON-RPC** adapter exposing the compression [engine](../engine/CLAUDE.md) as\nfive MCP tools to any host (Claude Code, Cursor, …). It owns only the MCP framing; all\ncompression is the engine's, linked **in-process** (no subprocess, no drift). Local-only — it\nopens no network connection — and everything it reports is `inferred`, never `verified`.\n\n## Layout\n- `server.go` — the `Server`: JSON-RPC loop, dispatch, the five tool handlers. Takes an injectable `Engine` interface so the framing is testable without the real compressors.\n- `protocol.go` — JSON-RPC + MCP tool-result types, `toolText`/`toolError` helpers, the exact `tools/list` definitions.\n- `cmd/caveman-mcp/` — binary: opens shared file CCR store (`CAVEMAN_CCR_DB`,\n  else `CAVEMAN_HOME/ccr.db`/`~/.caveman/ccr.db`) so proxy handles resolve across\n  processes, then serves stdin↔stdout. `CAVEMAN_MCP_EPHEMERAL=1` opts into an\n  isolated in-memory store for tests/sessions that do not need proxy recovery.\n- `bin/caveman-mcp.mjs` + `package.json` — the `npx caveman-mcp` launcher that execs the prebuilt Go binary.\n\n## The five tools (exact names, case-sensitive)\n- `caveman_compress(input)` → compressed text + inferred ratio + `recovery_handle`. Lossy (S4), recoverable, and fail-closed: incompressible/malformed/not-smaller input returns unchanged, `ratio:0`, `recovery_handle:null` — never an error.\n- `caveman_retrieve(recovery_handle)` → the byte-exact original. Unknown handle → `isError:true` + a `cave_snake_code`, never a fabricated payload.\n- `caveman_stats()` → `basis:\"inferred\"`, `scope:\"session\"`; the string `verified` never appears.\n- `caveman_toon_encode(input)` → explicit JSON→TOON re-encoding with input/output sizes; returns pass-through plus note when encoding fails.\n- `caveman_toon_decode(input)` → TOON→JSON; invalid TOON returns `isError:true`, never raw input as JSON.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=mcp` / `make product-test PRODUCT=mcp`.\n- **stdout is the protocol channel** — logs go to stderr only (a dedicated test guards this).\n\n## Gotchas (honesty invariants)\n- **un-killable transport** — the stdio server survives everything short of EOF (issue #139). Framing is line-delimited: a malformed line is answered `-32700` and the loop RESYNCHRONIZES to the next newline (never `return`); a handler panic is contained by `recover()` → `cave_tool_panicked` (dispatch panics → `cave_internal_error`); JSON-RPC batch arrays are handled per spec (one array response); id-less/`\"id\":null` requests are notifications and get no reply; and both inbound lines and generated tool output (compress/toon) are size-capped (`cave_payload_too_large`, `maxInboundBytes`/`maxResultBytes`, 16 MiB default) — but `caveman_retrieve` is exempt (`Tool.ExemptResultCap`): recovery returns the byte-exact original and must never fail closed on size, since the shared gateway store has no matching ceiling. A dead server is worse than a slow one — the proxy keeps eliding content that no longer has a `caveman_retrieve` to expand it.\n- **fail-open** — engine error or malformed input → byte-identical pass-through, never a protocol error.\n- **fail-closed** — unknown tool/handle → `isError` + cave_snake_code; unknown JSON-RPC method → `-32601`.\n- **zero-egress** — the adapter imports no `net`/`net/http`/`os/exec`; a test parses the source to enforce it.\n- v1 is **stdio-only**, **string payloads only** (the engine detects type); HTTP transport + `caveman mcp` subcommand are v2.\n- **protocol negotiation must never error.** The adapter implements the 2024-11-05 contract and echoes that version back; a client asking for a newer one gets 2024-11-05 in the initialize result and decides for itself, per the MCP lifecycle. It previously answered `-32602: unsupported protocol version`, which made Claude Code (and every other current client) drop the server — and because `caveman wrap` reads recovery availability from an install-time marker rather than from the live agent, the proxy kept eliding content that no longer had a `caveman_retrieve` to expand it. Declining to echo an unimplemented version is right; refusing to speak is not.\n\nSee ../../CLAUDE.md (root) · ../engine/CLAUDE.md\n","mcp/CLAUDE.md":"# mcp — the Caveman MCP server (Go core + launcher, Apache-2.0)\n\nA thin **stdio JSON-RPC** adapter exposing the compression [engine](../engine/CLAUDE.md) as\nfive MCP tools to any host (Claude Code, Cursor, …). It owns only the MCP framing; all\ncompression is the engine's, linked **in-process** (no subprocess, no drift). Local-only — it\nopens no network connection — and everything it reports is `inferred`, never `verified`.\n\n## Layout\n- `server.go` — the `Server`: JSON-RPC loop, dispatch, the five tool handlers. Takes an injectable `Engine` interface so the framing is testable without the real compressors.\n- `protocol.go` — JSON-RPC + MCP tool-result types, `toolText`/`toolError` helpers, the exact `tools/list` definitions.\n- `cmd/caveman-mcp/` — binary: opens shared file CCR store (`CAVEMAN_CCR_DB`,\n  else `CAVEMAN_HOME/ccr.db`/`~/.caveman/ccr.db`) so proxy handles resolve across\n  processes, then serves stdin↔stdout. `CAVEMAN_MCP_EPHEMERAL=1` opts into an\n  isolated in-memory store for tests/sessions that do not need proxy recovery.\n- `bin/caveman-mcp.mjs` + `package.json` — the `npx caveman-mcp` launcher that execs the prebuilt Go binary.\n\n## The five tools (exact names, case-sensitive)\n- `caveman_compress(input)` → compressed text + inferred ratio + `recovery_handle`. Lossy (S4), recoverable, and fail-closed: incompressible/malformed/not-smaller input returns unchanged, `ratio:0`, `recovery_handle:null` — never an error.\n- `caveman_retrieve(recovery_handle)` → the byte-exact original. Unknown handle → `isError:true` + a `cave_snake_code`, never a fabricated payload.\n- `caveman_stats()` → `basis:\"inferred\"`, `scope:\"session\"`; the string `verified` never appears.\n- `caveman_toon_encode(input)` → explicit JSON→TOON re-encoding with input/output sizes; returns pass-through plus note when encoding fails.\n- `caveman_toon_decode(input)` → TOON→JSON; invalid TOON returns `isError:true`, never raw input as JSON.\n\n## These five tools are a prompt-prefix cost, and it is measured\nRegistering this server puts all five tool schemas in the wrapped agent's prefix\non **every** call — **11,060 tokens/call** in the agent bench (~2.22M over its 201\ncalls). That is why `caveman wrap` gates the injection behind the `execute.mcp`\nsurface knob (`auto` | `marker-only` | `true` | `false`, `packages/cli/src/index.ts`).\nUnder a non-auto surface wrap suppresses **both** of its injection sites — the\n`mcp install` write and, for config-file agents like openclaw, the profile's\n`mcp.servers.caveman` overlay — but it never uninstalls a server the user\ninstalled themselves, so an existing install keeps costing its prefix until\n`caveman tools mcp uninstall <agent>`. The knob never touches recovery honesty on\nthe wrap door: `CAVEMAN_RECOVERY` is answered from evidence that this launch is\nreally giving the agent `caveman_retrieve`, so the proxy is never told a\nretrieval tool exists when none does. Adding a sixth tool here raises that\nper-call tax for every wrapped agent.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=mcp` / `make product-test PRODUCT=mcp`.\n- **stdout is the protocol channel** — logs go to stderr only (a dedicated test guards this).\n\n## Gotchas (honesty invariants)\n- **un-killable transport** — the stdio server survives everything short of EOF (issue #139). Framing is line-delimited: a malformed line is answered `-32700` and the loop RESYNCHRONIZES to the next newline (never `return`); a handler panic is contained by `recover()` → `cave_tool_panicked` (dispatch panics → `cave_internal_error`); JSON-RPC batch arrays are handled per spec (one array response); id-less/`\"id\":null` requests are notifications and get no reply; and both inbound lines and generated tool output (compress/toon) are size-capped (`cave_payload_too_large`, `maxInboundBytes`/`maxResultBytes`, 16 MiB default) — but `caveman_retrieve` is exempt (`Tool.ExemptResultCap`): recovery returns the byte-exact original and must never fail closed on size, since the shared gateway store has no matching ceiling. A dead server is worse than a slow one — the proxy keeps eliding content that no longer has a `caveman_retrieve` to expand it.\n- **fail-open** — engine error or malformed input → byte-identical pass-through, never a protocol error.\n- **fail-closed** — unknown tool/handle → `isError` + cave_snake_code; unknown JSON-RPC method → `-32601`.\n- **zero-egress** — the adapter imports no `net`/`net/http`/`os/exec`; a test parses the source to enforce it.\n- v1 is **stdio-only**, **string payloads only** (the engine detects type); HTTP transport + `caveman mcp` subcommand are v2.\n- **protocol negotiation must never error.** The adapter implements the 2024-11-05 contract and echoes that version back; a client asking for a newer one gets 2024-11-05 in the initialize result and decides for itself, per the MCP lifecycle. It previously answered `-32602: unsupported protocol version`, which made Claude Code (and every other current client) drop the server — and because `caveman wrap` reads recovery availability from an install-time marker rather than from the live agent, the proxy kept eliding content that no longer had a `caveman_retrieve` to expand it. Declining to echo an unimplemented version is right; refusing to speak is not.\n\nSee ../../CLAUDE.md (root) · ../engine/CLAUDE.md\n\n## Recovery across host compaction\n\nEvery valid `caveman_retrieve` call executes the requested handle and query,\nincluding identical repeats and calls after many earlier recoveries. An empty\nquery always returns the exact stored original. The MCP server cannot infer\nwhich earlier results remain in the model context from its process lifetime:\nhosts can compact their conversation without restarting MCP connections.\n\nThe previous per-process repeat suppression and forced full-payout threshold\nhave been removed. Current Codex source replaces compacted history and excludes\ntool-output items while its thread retains the live MCP runtime; see\n[compacted-history filtering](https://github.com/openai/codex/blob/main/codex-rs/core/src/compact_remote.rs)\nand [session services](https://github.com/openai/codex/blob/main/codex-rs/core/src/state/service.rs).\nTests exercise repeated recovery and more than five distinct queries in the same\nserver process. Query narrowing and complete-record boundaries remain owned by\nthe Engine.\n","mem/AGENTS.md":"# mem — cavemem, durable agent memory (Go core + clients, Apache-2.0)\n\nDurable, cross-session memory: `remember` / `recall` / `supersede` / `history` / `forget`. A local SQLite store holds the\n**raw** memories (the source of truth); recall ranks them with deterministic **BM25** behind a\nconservative threshold and compresses each hit through the [engine](../engine/CLAUDE.md) so the\ninferred token cost is honest and the dropped detail stays recoverable via CCR. Everything is\n`inferred`.\n\n## Layout\n- `store.go` — `Remember`/`Recall`/`Supersede`/`History`/`Forget`/`Recover` over SQLite + engine; legacy schemas migrate in place.\n- `bm25.go` — deterministic tokenizer + BM25 scorer (non-negative scores, so the threshold is meaningful).\n- `cmd/cavemem/` — MCP server + `remember`/`recall`/`supersede`/`history`/`forget` CLI subcommands (JSON).\n- `js/` + `py/` — thin TS and Python clients that shell out to the binary (mirrored libraries; they reimplement nothing).\n\n## The cavemem_* MCP tools\n`cavemem_remember(text)` · `cavemem_recall(query, limit?, token_budget?)` ·\n`cavemem_supersede(id, text)` · `cavemem_history(id)` · `cavemem_forget(id)`,\nserved through `mcp.NewServer` so the framing matches caveman-mcp exactly.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=mem` / `make product-test PRODUCT=mem`\n  for the Go core; root `make test` also runs mirrored JS/Python wrapper tests.\n- The wrappers shell out to the gated binary — keep recall/scoring logic in Go, never forked into TS/Python.\n\n## Gotchas (honesty invariants)\n- **byte-safe write** — raw text is written to SQLite before any engine call, so a memory is never lost; compression is transient, at recall time only.\n- **bounded recall** — omitted/Go-zero `TokenBudget` defaults to 2000 inferred tokens. Public CLI/MCP/JS/Python callers must explicitly pass `token_budget=0` for unlimited recall; adapters map it to `UnlimitedTokenBudget`.\n- **fail toward nothing** — a query below the threshold (or with no term overlap) recalls nothing, never a guess.\n- **current-only recall** — superseded versions stay auditable through `History` but never enter normal recall.\n- **inferred-only** — `tokens_added`, score, and basis are inferred estimates; never `verified`.\n- **reversible** — a compressed recall hit carries a CCR `recovery_handle`; `Recover` returns the byte-exact original.\n\nSee ../../CLAUDE.md (root) · ../engine/CLAUDE.md · ../mcp/CLAUDE.md\n","mem/CLAUDE.md":"# mem — cavemem, durable agent memory (Go core + clients, Apache-2.0)\n\nDurable, cross-session memory: `remember` / `recall` / `supersede` / `history` / `forget`. A local SQLite store holds the\n**raw** memories (the source of truth); recall ranks them with deterministic **BM25** behind a\nconservative threshold and compresses each hit through the [engine](../engine/CLAUDE.md) so the\ninferred token cost is honest and the dropped detail stays recoverable via CCR. Everything is\n`inferred`.\n\n## Layout\n- `store.go` — `Remember`/`Recall`/`Supersede`/`History`/`Forget`/`Recover` over SQLite + engine; legacy schemas migrate in place.\n- `bm25.go` — deterministic tokenizer + BM25 scorer (non-negative scores, so the threshold is meaningful).\n- `cmd/cavemem/` — MCP server + `remember`/`recall`/`supersede`/`history`/`forget` CLI subcommands (JSON).\n- `js/` + `py/` — thin TS and Python clients that shell out to the binary (mirrored libraries; they reimplement nothing).\n\n## The cavemem_* MCP tools\n`cavemem_remember(text)` · `cavemem_recall(query, limit?, token_budget?)` ·\n`cavemem_supersede(id, text)` · `cavemem_history(id)` · `cavemem_forget(id)`,\nserved through `mcp.NewServer` so the framing matches caveman-mcp exactly.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=mem` / `make product-test PRODUCT=mem`\n  for the Go core; root `make test` also runs mirrored JS/Python wrapper tests.\n- The wrappers shell memory text to the gated binary through `remember --stdin`\n  (never one huge argv element) — keep validation/recall/scoring logic in Go,\n  never forked into TS/Python.\n\n## Gotchas (honesty invariants)\n- **byte-safe write** — raw text is written to SQLite before any engine call, so a memory is never lost; compression is transient, at recall time only.\n- **single-writer store** — `mem.db` and `ccr.db` open with `SetMaxOpenConns(1)` + a DSN carrying `busy_timeout(5000)` + `journal_mode(WAL)` (`ccr.SQLiteDSN`). The proxy spend store uses the same DSN but does **not** force one pooled connection; mem/CCR's discipline is stricter. Cold-start migrations and the idempotent `Remember` insert use a thirty-second-wall-budgeted `ccr.RetryOnBusy` because a fan-out of fresh `cavemem remember` processes (the JS client's `Promise.all(facts.map(remember))`) can outlast one busy timeout on Windows. Without this, concurrent writes are dropped as `SQLITE_BUSY`.\n- **bounded recall** — `Recall` packs hits greedily in BM25 rank order under `RecallOptions.TokenBudget` (default `DefaultTokenBudget` = 2000 inferred tokens), reusing `engine/contextwindow` packing. Public CLI/MCP/JS/Python callers must explicitly pass `token_budget=0` to request unlimited recall; adapters map that external zero to `UnlimitedTokenBudget`, while omitted/Go-zero stays bounded. A single top hit whose compressed form alone exceeds the budget is returned as a compressed **head + CCR recovery_handle** (the original is stored even when compression passed it through), never its whole body — this is what stops the measured 440,000-token single recall.\n- **bounded remember** — `Remember` caps one memory at `MaxMemoryBytes` (256 KiB), failing closed with `cave_memory_too_large` (`ErrMemoryTooLarge`); CLI exits 65 and both wrappers export `MEMORY_TOO_LARGE_EXIT_CODE`. A memory is a fact to recall, not a file dump.\n- **fail toward nothing** — a query below the threshold (or with no term overlap) recalls nothing, never a guess.\n- **current-only recall** — superseded versions stay auditable through `History` but never enter normal recall.\n- **inferred-only** — `tokens_added`, score, and basis are inferred estimates; never `verified`.\n- **reversible** — a compressed recall hit carries a CCR `recovery_handle`; `Recover` returns the byte-exact original.\n\nSee ../../CLAUDE.md (root) · ../engine/CLAUDE.md · ../mcp/CLAUDE.md\n","packages/cli/AGENTS.md":"# packages/cli — `caveman` CLI\n\nTypeScript CLI (`src/index.ts`) driving the local proxy and wrapping control-api\nREST calls. Published package has zero runtime dependencies. Build emits\n`dist/index.js`, dependency-free `dist/caveman-delegate-mcp.mjs`, and a lazy\nbundled `dist/learn-tui.js` Clack chunk, then\n`scripts/shebang.mjs` adds shebang + chmod. `bin` exposes both `caveman` and `cave`. Non-secret config lives at\n`~/.caveman-cloud/config.json` (0o600); the **auth token lives in the OS keychain** (macOS\n`security`) or a `~/.caveman/credentials` (0o600) fallback — never plaintext config.\n\n## Layout\n- `src/index.ts` — entire CLI: arg dispatch, HTTP helpers (`get`/`post`), token storage, all commands\n- `src/learn-tui.ts` — bounded interactive learn view; bundled with Clack and loaded only for a real TTY\n- `tests/*.runtime.mjs` — Node `--test` runtime tests; spawn the built binary against HTTP stubs (`providers-verify`, `wrap`, `login`, `compress`)\n- `scripts/bundle-delegate.mjs` — copies canonical dependency-free delegate server into published `dist/`\n- `scripts/bundle-tui.mjs` — bundles Clack into the self-contained learn TUI chunk\n- `scripts/shebang.mjs` — post-build: prepends shebang, marks executable\n- `package.json` — bin: `caveman`/`cave → dist/index.js`; build: registries → `tsc` → delegate/TUI bundles → shebang\n\n## Command surface\n`caveman <agent>` (e.g. `caveman claude`) is shorthand for `caveman wrap <agent>` — any known agent id or binary name works as a top-level command; it is dispatched **last**, so real commands always shadow an agent name, and everything after the agent name goes to the agent verbatim.\n`cave` is a permanent byte-compatible bin alias. Relocated verbs keep their bare\nspellings as silent legacy aliases: no deprecation text may alter piped output.\n\nPrinted porcelain is `run`, `learn`, `login`, `status`, plus the agent shortcut.\nLocal capabilities live under `caveman tools`; account- or network-dependent\noperations live under `caveman cloud`. `dev` and `deploy` are undocumented\nmaintainer aliases. `tools` is capped at 15 printed verbs and `cloud` at 15;\ncurrent counts are 15 and 14. Internal/advanced `shrink-hook`, `practices`, and\n`check` remain callable through existing paths but are unprinted, including in\nlegacy `help tools --all` output.\n\nCaveman's own Go binaries (proxy/engine/mcp/mem/browse/shrink) resolve via `cavemanBin()`: env override (`CAVEMAN_*_BIN`) → PATH → `~/.caveman/bin` (where `scripts/install-local-cli.sh` or `scripts/install-local-cli.ps1` builds them) → bare name (so missing-binary panels still trigger).\n`caveman setup` prints per-binary install status — what works, what degrades to a loud byte-safe pass-through, and the one install command — and exits non-zero when a required binary (proxy/engine/mcp/mem; browse and shrink are optional) is missing. `caveman tools compress catalog` delegates to the dedicated `caveman-shrink` binary; `caveman tools shrink` remains command-output compression. It's the anti-silent-degrade front door for npm installs (the package ships JS only); every degraded path also prints its own warning line pointing at it. Publish checklist lives in `PUBLISHING.md`.\nLocal (no account): `start` (launch the proxy via `CAVEMAN_PROXY_BIN`; if the binary is missing or the port is already served it renders a status panel — build/`make dev`+live docker status/env — instead of a bare spawn error) · `wrap [agent]` (inject `ANTHROPIC_BASE_URL`/`OPENAI_BASE_URL` = `CAVE_GATEWAY_URL` or the local proxy, exec child; known agents `claude`/`codex`/`gemini`/`aider` launch by id with install detection; an unknown/unfound target shows an install hint or the wrappable list; bare `wrap` in a TTY opens an arrow-key picker) · `compress` (shells out to `caveman-engine` via `CAVEMAN_ENGINE_BIN`; byte-safe pass-through fallback if the binary is missing; `inferred`) · `toon encode|decode` (stateless JSON⇄TOON converter via the engine binary; `encode` degrades byte-safe when the engine is missing, `decode` fails loudly — it must not emit raw TOON as JSON) · `wrap --toon` (sets `CAVE_ENGINE_TOON=best-of` on the spawned proxy so it re-encodes uniform JSON the model reads — tool results — as TOON when smaller; opt-in, implies `--compress`) · `mcp install|uninstall [agent]` (register/remove the caveman_retrieve MCP tool; compress-mode wrap injects it only when `execute.mcp = auto` and a real `caveman-mcp` executable resolves — never the npx fallback — because streams are only compressible with agent-side recovery; `execute.mcp = marker-only` or `false` stops injection while existing registrations remain visible) · `evals run` (delegates to `caveman-engine evals run`, forwards its exit code) · `stats` (delegates to `caveman-proxy stats`) · `convert` (pixel-compresses installed agent skills in place: SKILL.md **body** → `SKILL.pxN.png` pages via `caveman-engine pixel render`, frontmatter stays text so discovery/triggering still works, body becomes a stub telling the agent to read the images on invocation; dirs come from registry profiles with a `skills` block — claude + codex today; converts only when image+stub est tokens < text est tokens, else untouched; original kept byte-exact as `SKILL.orig.md`, `--revert` restores; every skip is reported with its reason; savings `inferred`, per-invocation) · `skills install [caveman|caveman-learn]` (writes embedded Caveman skills and auto-pixels by default) · `skills add <source>` (accepts official `npx skills add` Git/URL/local sources and flags, delegates download/selection to that CLI in forced copy mode, then pixelizes only new/changed Claude Code or Codex skills; resources stay untouched; third-party content is explicitly unreviewed; `--no-pixel` opts out; engine failure/not-smaller stays honest plain text).\nConnected namespace: `whoami · projects · keys · providers · billing · score ·\ncosts · plan · traces · experiments · receipts · audit · sync · agent`.\n`doctor`, `opportunities`, `snippets`, `dev`, and `deploy` remain unprinted legacy\naliases; `status`, `plan`, and `tools sdk snippets` absorb their public jobs.\n\n`login` polls control-api `/api/v1/auth/device/{code,token}`; `organization_id` is bound from the returned token. `CAVE_TOKEN` is the non-interactive CI path. Logged-out connected verbs print one line + exit non-zero (CI skips, never crashes).\n\nFirst-run retro scan persists only closed `caveman.local_scan.v1` aggregate. Login or explicit `sync` uploads it through authenticated project `format=local-scan`; prompts, outputs, paths, session rows, free-text caveats, and anonymous telemetry never enter this lane. Dashboard labels token figures `inferred` and separates them from measured spend and verified savings.\n\n`providers verify <conn>` → real POST `/api/v1/projects/{id}/providers/{conn}/verify` (no hardcoded status).\n\n`plan` renders the Cave Plan in plain English (one operator voice); `--json` prints the raw response. There is **no** caveman voice / `--engineer` flag — the dual-voice was deliberately removed. Headline is labeled `basis` (\"inferred\"), savings are per-day — never reprojected to monthly. (honesty rule: no-fake-savings)\n\n`learn` is summary-first: real terminals get animated progress, bounded score\nand move cards, then one keyboard action menu. `--plain` restores compact text,\n`--all` restores every sink id/class/practice/suggestion, while `--json` and\n`--md` stay complete. `learn implement [claude|codex]\n[--prompt <focus>]` installs the existing `caveman-learn` safety guide when\nmissing and launches the chosen interactive agent. The guide's per-edit consent,\nload-bearing protection, re-measurement, and inferred-only rules remain binding.\nPresentation contract: [`TERMINAL_UX.md`](TERMINAL_UX.md).\n\n## Conventions\n- Dispatch uses handler tables. Every handler receives its own rebased argv slice;\n  never read process-global argv positionally inside a handler.\n- `flag(\"--name\", fallback)` parses named args from current invocation.\n- Tests use `node --test` (Node built-in runner); run `tsc` first, test spins a real HTTP server\n- Build: `pnpm build` (tsc + shebang); install locally: `scripts/install-local-cli.sh` (macOS/Linux) or `scripts/install-local-cli.ps1` (Windows) at repo root\n\n## Capability promotion rule\n\nA capability may default on only when it is byte-safe, or when protected by the\napplicable path-specific gate: managed gateway uses an eval gate; local wrap\nuses recovery + CCR — **not** an account or entitlement. There is\nno eval gate in local `run`.\nAny PR flipping a default must name the clause and path.\n\nA verb enters porcelain only when its capability is automatic-by-default-safe\ninside `run` and users no longer need to type it. Porcelain stays capped at four\nverbs + agent shortcut + exactly two namespaces. A fifth verb, or a 16th printed\nverb in either namespace, requires a retirement decision. `record` mode is always\npass-through.\n\nCapability config is grouped in `~/.caveman-cloud/config.json` as `think`,\n`remember`, and `execute`. `./.caveman/config.json` may only narrow its allowlisted\nproject-local keys; it cannot change `think.mode`, pixel settings, account state,\nconsent, or entitlement. Resolution is default < proxy YAML < legacy `wrap` <\nglobal groups < project overlay < env. Env parity is knob-specific. Inspect\nper-key source with `caveman tools config get`.\n\n## Gotchas\n- `providers verify` must NOT return a hardcoded status; the test asserts the CLI echoes the server's value (no-placeholder rule)\n- `plan` savings display must stay per-day; never multiply to monthly projection\n- Non-PAYG coverage includes Claude Pro/Max, Codex ChatGPT, Gemini OAuth, and routed compatible agents. Codex subscription mode keeps provider config ephemeral under `CODEX_HOME`, auto-installs its MCP recovery, and starts `/chatgpt/responses` in compress mode instead of forcing record/pass-through. Plain OpenAI `/responses` and Gemini `generateContent` requests with MCP explicitly disabled have no server-retrieval grammar, so they must remain byte-identical with zero compression accounting; the compression conformance matrix pins these protocol-specific fail-closed cases instead of requiring every profile to emit a CCR marker.\n- Subscription/OAuth wrap sessions (Claude Pro/Max) compress **locally only**, live zone only, and with **no account**: `CAVEMAN_WRAP_ENTITLED` is gone from both doors and from the proxy, and both doors `delete` any inherited copy so a stray export cannot resurrect it. What both doors DO stamp is the recovery path (`CAVEMAN_RECOVERY`), explicitly (`\"mcp\"` or empty, never inherited): `wrap` answers it from the **agent's own** MCP install (an exported `CAVEMAN_RECOVERY=mcp` can't outlive that answer — it would have the proxy elide bytes behind markers this agent has no `caveman_retrieve` tool to expand), `start` from machine-wide MCP install evidence plus an explicit `CAVEMAN_RECOVERY=mcp` counted as the operator's own opt-in, re-stamped so the disclosure line and the proxy can never disagree; the compression disclosure line prints only when recovery holds, and no-MCP says compression is off and names `caveman mcp install <agent>`. The `subscription_compress: off` operator switch stays the operator's. Their measured reduction remains **tokens only**: a seat has no per-token marginal price, so the session footer and synced span never claim saved dollars. The local stats dashboard may show a separately labeled, same-model API equivalent from captured prices; it is hypothetical value, never subscription bill or quota savings. The session-savings line treats `oauth` like `subscription` (OAuth is list-price-eligible on Vertex alone) and qualifies unconditionally when its capped auth-mode window is truncated\n- Published runtime dependencies stay zero. TUI libraries must be bundled,\n  lazy-loaded, and measured; do not move them onto ordinary command startup.\n- `learn` uses bundled Clack only when stdin/stdout/stderr are TTYs. `--plain`,\n  `CAVEMAN_PLAIN=1`, `TERM=dumb`, machine modes, and pipes must never prompt.\n- Remaining terminal UX (status panels + `wrap` picker) lives in the small toolkit\n  at the bottom of `src/index.ts`. Piped/non-interactive paths remain plain; runtime\n  tests assert them.\n- Current official Skills CLI installs global Codex sources at\n  `~/.agents/skills`, despite older/direct Caveman installs using\n  `~/.codex/skills`; third-party post-install discovery must scan both.\n\nSee ../../CLAUDE.md (root)\n","packages/cli/CLAUDE.md":"# packages/cli — `caveman` CLI\n\nTypeScript CLI (`src/index.ts`) driving the local proxy and wrapping control-api\nREST calls. Published package has zero runtime dependencies. Build emits\n`dist/index.js`, dependency-free `dist/caveman-delegate-mcp.mjs`, and a lazy\nbundled `dist/learn-tui.js` Clack chunk, then\n`scripts/shebang.mjs` adds shebang + chmod. `bin` exposes both `caveman` and `cave`. Non-secret config lives at\n`~/.caveman-cloud/config.json` (0o600); the **auth token lives in the OS keychain** (macOS\n`security`) or a `~/.caveman/credentials` (0o600) fallback — never plaintext config.\n\n## Layout\n- `src/index.ts` — entire CLI: arg dispatch, HTTP helpers (`get`/`post`), token storage, all commands\n- `src/learn-tui.ts` — bounded interactive learn view; bundled with Clack and loaded only for a real TTY\n- `tests/*.runtime.mjs` — Node `--test` runtime tests; spawn the built binary against HTTP stubs (`providers-verify`, `wrap`, `login`, `compress`)\n- `scripts/bundle-delegate.mjs` — copies canonical dependency-free delegate server into published `dist/`\n- `scripts/bundle-tui.mjs` — bundles Clack into the self-contained learn TUI chunk\n- `scripts/shebang.mjs` — post-build: prepends shebang, marks executable\n- `package.json` — bin: `caveman`/`cave → dist/index.js`; build: registries → `tsc` → delegate/TUI bundles → shebang\n\n## Command surface\n`caveman <agent>` (e.g. `caveman claude`) is the **persistent default door**: for hook-capable native agents (claude/codex/hermes/gemini/opencode — not aider, whose hookless native install could never start the proxy) it runs the `caveman enable <agent>` user-scoped native install if no install journal exists, pre-starts the local proxy when it isn't listening (the native SessionStart hook re-does this, but only after the host approves the hooks), then launches the host binary directly — so plain `<agent>` stays caveman'd in every later session. For claude/codex the enable step also writes the `output` skill suite (`caveman`/`ultracave`/`megacave`) into the host's user skills dir, create-only (`wx`, never clobbers) and fail-open. They are deliberately NOT native-journal operations, so an edited/deleted skill can never read `degraded`, and `disable` leaves them in place: it turns off routing and hooks, not the voice. A `~/.caveman/integrations/<agent>.voice-skills.json` sidecar marks the install as done, so it runs once: the shortcut skips journaled installs (explicit `caveman enable <agent>` picks the skills up there), a skill the user deleted is not written back, and a CLI upgrade does not refresh an installed copy. It falls back to the ephemeral `wrap` door whenever the native path can't hold: wrap-session flags given (`--off`/`--pixel`/`--workflow`), a non-native agent, a Cave Build lock in cwd (lock enforcement is wrap-door-only), or any enable failure (e.g. missing caveman-mcp/proxy binaries) — the shortcut is never worse than a session-only wrap. Note the fallback is session-only relative to an install that may already exist: once the native install is in place, `--off`/`wrap` runs are ephemeral but the persistent routing stays until `caveman disable <agent>`. Any known agent id or binary name works as a top-level command; it is dispatched **last**, so real commands always shadow an agent name, and everything after the agent name goes to the agent verbatim. `caveman wrap <agent>` / `caveman run <agent>` remain the explicit session-only path.\n`cave` is a permanent byte-compatible bin alias. Relocated verbs keep their bare\nspellings as silent legacy aliases: no deprecation text may alter piped output.\n\nPrinted porcelain is `run`, `learn`, `login`, `status`, plus the agent shortcut.\nLocal capabilities live under `caveman tools`; account- or network-dependent\noperations live under `caveman cloud`. `dev` and `deploy` are undocumented\nmaintainer aliases. `tools` is capped at 15 printed verbs and `cloud` at 15;\ncurrent counts are 15 and 14. Internal/advanced `shrink-hook`, `practices`, and\n`check` remain callable through existing paths but are unprinted, including in\nlegacy `help tools --all` output.\n\nCaveman's own Go binaries (proxy/engine/mcp/mem/browse/shrink) resolve via `cavemanBin()`: env override (`CAVEMAN_*_BIN`) → PATH → `~/.caveman/bin` (where `scripts/install-local-cli.sh` or `scripts/install-local-cli.ps1` builds them) → bare name (so missing-binary panels still trigger).\n`caveman setup` prints per-binary install status — what works, what degrades to a loud byte-safe pass-through, and the one install command — and exits non-zero when a required binary (proxy/engine/mcp/mem; browse and shrink are optional) is missing. `caveman tools compress catalog` delegates to the dedicated `caveman-shrink` binary; `caveman tools shrink` remains command-output compression. It's the anti-silent-degrade front door for npm installs (the package ships JS only); every degraded path also prints its own warning line pointing at it. Publish checklist lives in `PUBLISHING.md`.\nLocal (no account): `start` (launch the proxy via `CAVEMAN_PROXY_BIN`; if the binary is missing or the port is already served it renders a status panel — build/`make dev`+live docker status/env — instead of a bare spawn error) · `wrap [agent]` (inject route and exec child; Claude, Codex, Hermes, Gemini, OpenCode, Aider, and OpenClaw launch by id; native-capable hosts get temp pack/home/config only, removed after exit; no user/project config write) · `enable|disable <agent>` (explicit user-scoped native install with atomic journal, lock, reversible owned-path restore, component doctor) · `inspect` / `why <decision-id>` (content-blind native receipt and Decision Ledger explanation) · `compress` (shells out to `caveman-engine`; byte-safe missing-binary pass-through; `inferred`) · `mcp install|uninstall [agent]` (explicit persistent recovery registration; wrap uses temporary recovery configuration only when `execute.mcp = auto` and a current local MCP binary exists; `marker-only` or `false` suppresses injection without silently removing user registrations) · `skills add <source>` (same Git/URL/local sources and flags as official `npx skills add`; download/selection stays upstream, forced copy mode lets Caveman pixelize only new/changed Claude Code/Codex `SKILL.md` bodies; resources remain untouched and third-party content remains explicitly unreviewed; `--no-pixel` opts out) · `skills import <dir|SKILL.md>` (blocked unevaluated native-skill draft; imported resources never execute) · `evals run` · `stats` · `convert`.\nConnected namespace: `whoami · projects · keys · providers · billing · score ·\ncosts · plan · traces · experiments · receipts · audit · sync · agent`.\n`doctor`, `opportunities`, `snippets`, `dev`, and `deploy` remain unprinted legacy\naliases; `status`, `plan`, and `tools sdk snippets` absorb their public jobs.\n\n`login` polls control-api `/api/v1/auth/device/{code,token}`; `organization_id` is bound from the returned token. `CAVE_TOKEN` is the non-interactive CI path. Logged-out connected verbs print one line + exit non-zero (CI skips, never crashes).\n\n**First-run experience** (TTY only, once per machine, marker `firstRunAt` in config): on the first interactive wrap — banner, 30-day retrospective scan (`caveman-proxy learn scan --retro`, read-only over local Claude Code/Codex session logs; base behavior pass capped at 20s, retro pass at 60s, child timeout derived as 90s including margin), count-up reveal of tokens sent / would-have-cut / rides-every-turn stream figure (sums over scanned sessions, never extrapolated; `inferred`; tokens never dollars; sent is deduped once per API response, base cut carries no ratio against sent, only the stream figure shares sent's basis), telemetry disclosure, one `[y/N]` account question (yes → device login → existing span backfill). CLI persists only the closed, content-free aggregate; login or explicit `sync` uploads it to the authenticated project's `local-scan` import lane. Prompts, outputs, paths, session rows, free-text caveats, and anonymous telemetry never enter that upload; dashboard keeps it `inferred` and separate from spend/verified savings. Every failure degrades to one dim line and the agent still launches. Replay with the **unprinted** `caveman welcome` (porcelain caps unchanged). Anonymous **telemetry defaults ON (opt-out)**: persisted v1 decisions and `DO_NOT_TRACK`/`CAVEMAN_TELEMETRY=0`/`caveman telemetry off` always win; CI never sends; non-TTY never persists the default; the first eligible interactive run persists a stable anonymous id and prints the disclosure line. A native SessionStart (not `source: compact`) with a persisted, current-version yes spawns a detached `caveman telemetry session <agent> <source>` (unprinted) that sends `session_start` with the token delta, so installed users who launch agents directly stay visible; the hook never waits on the network. One event per host session: an O_EXCL marker under `~/.caveman/runtime/telemetry-sessions/` is created before the spawn (pruned after a day). Hook entrypoints (`native-hook`, `shrink-hook`, `mem recall-hook`) set `telemetryCommandSent` so no hook process ever holds the host on a command_run POST. An interactive run that sees `DO_NOT_TRACK`/`CAVEMAN_TELEMETRY=0` while config says yes persists an opt-out (GUI-launched hosts never see the shell rc). Every event also carries `account`, `plan`, `install_channel`, `timezone`, `locale` from `telemetryContext()` (cheap config reads, no keychain). `command_run` also carries `tokens_processed`/`tokens_saved` read from the local proxy store as a **delta** against the `telemetryTokens` watermark in config, always with `tokens_basis` (`inferred`, tokens never dollars); the read is best-effort behind a 400ms spawn budget and omits the fields entirely when the store or binary is absent. Events post to the `cli-telemetry` Supabase Edge Function (source + schema in root `supabase/`), which validates them and stores each with the client IP (cleared after 90 days by pg_cron). Product analytics (users, daily actives, retention, churn, tokens saved, command/agent usage) are SQL views in the database's `analytics` schema. Widening the disclosed scope means bumping `TELEMETRY_PROMPT_VERSION` (now 5) — that reprints the line once for stale-version opt-ins, never re-asks, never touches an opt-out.\n\n`providers verify <conn>` → real POST `/api/v1/projects/{id}/providers/{conn}/verify` (no hardcoded status).\n\n`plan` renders the Cave Plan in plain English (one operator voice); `--json` prints the raw response. There is **no** caveman voice / `--engineer` flag — the dual-voice was deliberately removed. Headline is labeled `basis` (\"inferred\"), savings are per-day — never reprojected to monthly. (honesty rule: no-fake-savings)\n\n`learn` is summary-first: real terminals get animated progress, bounded score\nand move cards, then one keyboard action menu. `--plain` restores compact text,\n`--all` restores every sink id/class/practice/suggestion, while `--json` and\n`--md` stay complete. `learn implement [claude|codex]\n[--prompt <focus>]` installs the existing `caveman-learn` safety guide when\nmissing and launches the chosen interactive agent. The guide's per-edit consent,\nload-bearing protection, re-measurement, and inferred-only rules remain binding.\nPresentation contract: [`TERMINAL_UX.md`](TERMINAL_UX.md).\n\n## Conventions\n- Dispatch uses handler tables. Every handler receives its own rebased argv slice;\n  never read process-global argv positionally inside a handler.\n- `flag(\"--name\", fallback)` parses named args from current invocation.\n- Tests use `node --test` (Node built-in runner); run `tsc` first, test spins a real HTTP server\n- Build: `pnpm build` (tsc + shebang); install locally: `scripts/install-local-cli.sh` (macOS/Linux) or `scripts/install-local-cli.ps1` (Windows) at repo root\n\n## Capability promotion rule\n\nA capability may default on only when it is byte-safe, or when protected by the\napplicable path-specific gate: managed gateway uses an eval gate; local wrap\nuses recovery + CCR — **not** an account or entitlement. There is\nno eval gate in local `run`.\nAny PR flipping a default must name the clause and path.\n\nA verb enters porcelain only when its capability is automatic-by-default-safe\ninside `run` and users no longer need to type it. Porcelain stays capped at four\nverbs + agent shortcut + exactly two namespaces. A fifth verb, or a 16th printed\nverb in either namespace, requires a retirement decision. `record` mode is always\npass-through.\n\nCapability config is grouped in `~/.caveman-cloud/config.json` as `think`,\n`remember`, and `execute`. `./.caveman/config.json` may only narrow its allowlisted\nproject-local keys; it cannot change `think.mode`, pixel settings, account state,\nconsent, or entitlement. Resolution is default < proxy YAML < legacy `wrap` <\nglobal groups < project overlay < env. Env parity is knob-specific. Inspect\nper-key source with `caveman tools config get`.\n\n## Gotchas\n- `providers verify` must NOT return a hardcoded status; the test asserts the CLI echoes the server's value (no-placeholder rule)\n- `plan` savings display must stay per-day; never multiply to monthly projection\n- Claude Code treats any `ANTHROPIC_BASE_URL` whose host isn't `api.anthropic.com` as a custom endpoint and withholds first-party capabilities — most visibly the 1M-token context window, whose loss silently shrinks the auto-compact window to the 200k default (#865). Both Claude routing doors (ephemeral `buildWrapEnv` and the persistent native install) therefore set `_CLAUDE_CODE_ASSUME_FIRST_PARTY_BASE_URL=1`, but ONLY when the assertion is verifiably true: local mode AND the proxy's anthropic upstream is `api.anthropic.com` (default, or an explicit `providers.anthropic.base_url` naming that host — checked via a dependency-free scoped YAML scan that reads flow-style blocks as unverifiable). A user-set value is never overridden, managed gateways never assert, and disable removes the var only while it still holds exactly the value we wrote.\n- Non-PAYG coverage includes Claude Pro/Max, Codex ChatGPT, Gemini OAuth, and routed compatible agents. Codex subscription mode keeps provider config ephemeral under `CODEX_HOME`, auto-installs its MCP recovery, and starts `/chatgpt/responses` in compress mode instead of forcing record/pass-through. Plain OpenAI `/responses` and Gemini `generateContent` requests with MCP explicitly disabled have no server-retrieval grammar, so they must remain byte-identical with zero compression accounting; the compression conformance matrix pins these protocol-specific fail-closed cases instead of requiring every profile to emit a CCR marker.\n- Subscription/OAuth wrap sessions (Claude Pro/Max) compress **locally only**, live zone only, and with **no account**: `CAVEMAN_WRAP_ENTITLED` is gone from both doors and from the proxy, and both doors `delete` any inherited copy so a stray export cannot resurrect it. What both doors DO stamp is the recovery path (`CAVEMAN_RECOVERY`), explicitly (`\"mcp\"` or empty, never inherited): `wrap` answers it from the **agent's own** MCP install (an exported `CAVEMAN_RECOVERY=mcp` can't outlive that answer — it would have the proxy elide bytes behind markers this agent has no `caveman_retrieve` tool to expand), `start` from machine-wide MCP install evidence plus an explicit `CAVEMAN_RECOVERY=mcp` counted as the operator's own opt-in, re-stamped so the disclosure line and the proxy can never disagree; the compression disclosure line prints only when recovery holds, and no-MCP says compression is off and names `caveman mcp install <agent>`. The `subscription_compress: off` operator switch stays the operator's. Their measured reduction remains **tokens only**: a seat has no per-token marginal price, so the session footer and synced span never claim saved dollars. The local stats dashboard may show a separately labeled, same-model API equivalent from captured prices; it is hypothetical value, never subscription bill or quota savings. The session-savings line treats `oauth` like `subscription` (OAuth is list-price-eligible on Vertex alone) and qualifies unconditionally when its capped auth-mode window is truncated\n- Published runtime dependencies stay zero. TUI libraries must be bundled,\n  lazy-loaded, and measured; do not move them onto ordinary command startup.\n- `learn` uses bundled Clack only when stdin/stdout/stderr are TTYs. `--plain`,\n  `CAVEMAN_PLAIN=1`, `TERM=dumb`, machine modes, and pipes must never prompt.\n- Remaining terminal UX (status panels + `wrap` picker) lives in the small toolkit\n  at the bottom of `src/index.ts`. Piped/non-interactive paths remain plain; runtime\n  tests assert them.\n- Current official Skills CLI installs global Codex sources at\n  `~/.agents/skills`, despite older/direct Caveman installs using\n  `~/.codex/skills`; third-party post-install discovery must scan both.\n\nSee ../../CLAUDE.md (root)\n","packages/cli/README.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=packages%2Fcli%2FREADME.md -->","packages/middleware/python/README.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=packages%2Fmiddleware%2Fpython%2FREADME.md -->","packages/middleware/typescript/README.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=packages%2Fmiddleware%2Ftypescript%2FREADME.md -->","packages/pi-extension/README.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=packages%2Fpi-extension%2FREADME.md -->","packages/sdk/parity/AGENTS.md":"# packages/sdk/parity — the cross-language SDK conformance contract\n\nOne language-neutral fixture file, run by **both** SDKs. The load-bearing honesty property\nof the SDKs: the wire contract is one thing, expressed twice (`@caveman-ai/sdk` TS +\n`caveman_cloud` Python). A field present in one SDK and absent in the other makes one side's\nassertion fail — so this folder is a release gate, not documentation.\n\n## Layout\n\n- `fixtures.json` — the contract. A `config` (one Cave), the named header sets (`std_headers` / `std_headers_traced` / `std_headers_async_traced` / `otlp_headers`), and an ordered `operations` list. Each operation carries its `input`, a canned `response` (or `transport: \"error\"` to force a byte-safe pass-through), and an `expect` block: `wire` (`method`, `path`, `headers`, and either an exact `body` or a `body_keys` set) plus a `result` (or `result_from: \"response\"`).\n\n## How it's enforced\n\n- TS half: `../typescript/tests/parity.runtime.mjs` (mocks `fetch`).\n- Python half: `../python/tests/test_parity.py` (mocks `urllib.request.urlopen`).\n- Each half has a per-operation handler that performs the **real** SDK call, captures the wire, and normalizes the result to canonical snake-keyed values. Both iterate **every** operation; a missing handler is a failure, never a skip.\n\n## Editing rules\n\n- Add a field/method to one SDK → add the operation (or body key) here → the other SDK's half goes red until it matches. That red is the point.\n- Headers are compared key-lowercased (Python's `urllib` capitalizes them); the SET + values must match across languages.\n- Keep values that encode identically in both languages (e.g. avoid `! ( ) *` in expand refs — JS `encodeURIComponent` and Python `quote` disagree on those).\n- Nothing random may reach an assertion. Ids the SDK would otherwise mint (trace id, span id) are injected through the operation `input` and pinned in the expected header set — the same way `otlp_export` pins its span ids.\n- Run: `make product-test PRODUCT=sdk-ts` && `make product-test PRODUCT=sdk-python`.\n\nSee ../typescript/CLAUDE.md · ../python/CLAUDE.md\n","packages/sdk/parity/CLAUDE.md":"# packages/sdk/parity — the cross-language SDK conformance contract\n\nOne language-neutral fixture file, run by **both** SDKs. The load-bearing honesty property\nof the SDKs: the wire contract is one thing, expressed twice (`@caveman-ai/sdk` TS +\n`caveman_cloud` Python). A field present in one SDK and absent in the other makes one side's\nassertion fail — so this folder is a release gate, not documentation.\n\n## Layout\n\n- `fixtures.json` — the contract. A `config` (one Cave), the named header sets (`std_headers` / `std_headers_traced` / `std_headers_async_traced` / `otlp_headers`), and an ordered `operations` list. Each operation carries its `input`, a canned `response` (or `transport: \"error\"` to force a byte-safe pass-through), and an `expect` block: `wire` (`method`, `path`, `headers`, and either an exact `body` or a `body_keys` set) plus a `result` (or `result_from: \"response\"`).\n\n## How it's enforced\n\n- TS half: `../typescript/tests/parity.runtime.mjs` (mocks `fetch`).\n- Python half: `../python/tests/test_parity.py` (mocks `urllib.request.urlopen`).\n- Each half has a per-operation handler that performs the **real** SDK call, captures the wire, and normalizes the result to canonical snake-keyed values. Both iterate **every** operation; a missing handler is a failure, never a skip.\n\n## Editing rules\n\n- Add a field/method to one SDK → add the operation (or body key) here → the other SDK's half goes red until it matches. That red is the point.\n- Headers are compared key-lowercased (Python's `urllib` capitalizes them); the SET + values must match across languages.\n- Keep values that encode identically in both languages (e.g. avoid `! ( ) *` in expand refs — JS `encodeURIComponent` and Python `quote` disagree on those).\n- Nothing random may reach an assertion. Ids the SDK would otherwise mint (trace id, span id) are injected through the operation `input` and pinned in the expected header set — the same way `otlp_export` pins its span ids.\n- Run: `make product-test PRODUCT=sdk-ts` && `make product-test PRODUCT=sdk-python`.\n\nSee ../typescript/CLAUDE.md · ../python/CLAUDE.md\n","packages/sdk/python/AGENTS.md":"# packages/sdk/python — Python SDK for the Caveman Cloud gateway\n\nStdlib-only (`urllib.request`, no third-party deps) Python package. Provides `Cave` (config +\nentrypoint), `CaveTool` (tool descriptor), and `ToolSearchResult`. All HTTP calls POST to the\ngateway with `x-cave-agent` / `x-cave-workflow` / `x-cave-retention` headers set from `Cave` fields.\n\n## Layout\n\n- `caveman_cloud/__init__.py` — re-exports `Cave`, `CaveTool`, `CompressResult`, `ContextPackItem`, `ContextPackOptions`, `ContextPackResult`, `ToolSearchResult`, …\n- `caveman_cloud/core.py` — all implementation: `Cave`, `Trace`, `Provider`, `_Create`, `ToolSearchResult`, `CompressResult`, `ContextPack*`, `CaveTool`, `headers()`\n- `tests/test_sdk.py` — pytest tests; mock `urllib.request.urlopen` with `patch()`\n- `tests/test_parity.py` — cross-language conformance suite; drives `../../parity/fixtures.json` (shared with sdk-ts). Same fixtures, two languages → a field in one SDK and not the other fails CI.\n- `tests/test_trace_continuity.py` — trace/span id minting + which requests carry `x-cave-trace-id` / `x-cave-parent-span-id`; mirrors the TS `tests/trace-continuity.runtime.mjs`\n- `caveman_cloud/middleware/` — stable framework-middleware client for protocol 1.1 (`docs/technical/middleware-protocol.md`): `protocol.py` pure rules (scope normalization, tolerant capabilities, failure classification, breaker, budgets, endpoint/proxy resolution, `warn_once`), `transport.py` default keep-alive stdlib transport, `runtime.py` / `async_runtime.py` clients, `validate.py` plan/page checks. `tests/test_middleware_v1_1.py` drives `../../parity/middleware-v1_1.fixtures.json`; `tests/test_middleware_transport.py` uses real local sockets\n- `pyproject.toml` — distribution name `caveman-sdk` (import package stays `caveman_cloud`), `requires-python = \">=3.11\"`, no runtime dependencies\n\n## Key API surface (`core.py`)\n\n- `Cave.trace(workflow, tags, *, trace_id=None, span_id=None)` → context manager yielding `Trace`; call `.model[\"openai\"].responses.create(body)` inside. The trace mints `trace_id` (32 lowercase hex) + a root `span_id` (16 lowercase hex) with the exporter's RNG; `trace_id`/`span_id` continue an inbound trace and a value that isn't the exact hex shape is replaced rather than sent. Every provider call made **through the trace** carries `x-cave-trace-id` + `x-cave-parent-span-id`. Generic `/sdk/v1/*` calls and providers built off the `Cave` carry neither; the sole SDK-endpoint exception is `Trace.tool`, whose `/sdk/v1/events` call carries the trace id and root parent span id\n- `Trace.exporter(service_name=None)` → an `OTelExporter` whose `default_trace_id` is the trace's, so SDK spans and the gateway's request rows join one trace. MIRRORS the TS `CaveTrace.exporter`\n- `Cave.tools(catalog, *, strategy=\"all\", initial_tool_count=8)` → builder handle with `.strategy`, `.initial` (`list[CaveTool]`), `.search(query, *, max_tools, context, workflow, ranker, session_id)`. `strategy=\"deferred\"` includes every `always_load` tool exactly once, then fills remaining initial slots from non-mandatory tools; a cap below the mandatory count fails locally. `.search()` always hits the gateway with the FULL catalog. MIRRORS the TS `cave.tools({catalog, strategy})`\n- `Cave.tool_search(tools, query, *, context, max_tools, workflow, ranker, session_id)` → flat variant: POSTs `[tools, query]` to `/sdk/v1/tool-search`; returns `ToolSearchResult` with `.saved_tokens` / `.reduction_pct` / `.session_id`. Schema-token counters are estimates; `.token_basis` discloses the counter and `.basis` is always `\"inferred\"`. `ranker` (`\"bm25\"`|`\"embeddings\"`) is passed through verbatim — the SDK never computes similarity\n- `Cave.prompts.internal_brevity(*, style, preserve_errors_verbatim=False, preserve_code_verbatim=False)` → output-style snippet (`\"none\"` → `\"\"`); booleans render lowercase to match the TS `cave.prompts.internalBrevity`\n- `Cave.compress(payload, *, content_type=None)` → `CompressResult`; POSTs `/sdk/v1/compress`, maps the Engine report. **Byte-safe pass-through** on any transport/parse problem (original input, `ratio=0.0`, no handle); `.token_count_basis` discloses the counter and `basis` is always `\"inferred\"`. The SDK delegates — it never reimplements a compressor\n- `Cave.context.pack(query, items, options)` → `ContextPackResult`; connected-only POST to `/sdk/v1/context/pack`. Lossy selector over caller-owned items, never CCR/ledger; returns exact `deferred_ids`. Transport or malformed-report failure returns all original items with zero inferred savings\n- `Trace.expand(source_ref)` — the GET half of `checkpoint()`; `GET /sdk/v1/checkpoints/{ref}/expand` returns the stored `{source_ref, version, messages, checkpoint}`\n- `Cave.openai/anthropic/gemini/vertex(upstream_key)` → `Provider` that proxies through gateway; `Provider.raw(path, body)` is the escape hatch (mirrors the TS provider-client `raw`)\n- `Cave.bedrock(region, endpoint=\"runtime\")` → no-network first-party route descriptor; Runtime defaults to `/bedrock`, explicit Mantle returns `/bedrock/anthropic`, and `sdk_only=False` mirrors TS `sdkOnly`\n- `Trace.tool(name, options, fn)` — calls `fn()` then POSTs a `tool.call` event\n- `Trace.page_artifact(value, options)` / `Trace.artifacts.page(value, options)` — send versioned `{value, options, workflow}`; gateway stores only JSON `value`. `artifacts.get(id)` performs authenticated retrieval. `page_artifact` remains backwards-compatible alias.\n- `Trace.model[\"openai\"].responses.create(body, *, latency_class=None, tool_session_id=None)` — when `latency_class` is set, sends the `x-cave-async` header (`\"true\"` unless `\"interactive\"`); when `tool_session_id` is set, sends `x-cave-tool-session`, mirroring the TS `trace.model.openai.responses.create(body, {cave:{latencyClass, toolSessionId}})`\n- `Trace.checkpoint(messages, options)` — POSTs to `/sdk/v1/checkpoints`; the gateway persists it (Valkey) and returns a reversible `source_ref` you can later expand via `GET /sdk/v1/checkpoints/{ref}/expand`\n- `Cave.exporter(service_name=None)` → `OTelExporter`; `record_span(...)` maps current GenAI fields to `gen_ai.*`, `export()` POSTs OTLP/JSON to standard `/v1/traces` (headers via `otlp_headers()`; legacy `/otlp/v1/traces` remains server-only compatibility)\n- `Cave.retry_loop_breaker(threshold=3)` → `RetryLoopBreaker`; `.record(name, args)` raises `RetryLoopError` after `threshold` consecutive identical tool calls (interrupts a stuck loop). `.guard(name, args, fn)` records then runs `fn`\n- `Cave.jobs` → reserved `JobsClient` surface. Every method fails locally with `cave_async_jobs_unavailable`; it performs no network request until durable encrypted request storage, credential custody, and a draining worker exist. MIRRORS the TS `Cave.jobs`\n\n## Conventions\n\n- Tests use `patch(\"urllib.request.urlopen\", side_effect=fake_urlopen)` — never real network\n- Add new gateway endpoints via `Trace._request(path, body)` or `Provider.create(path, body)`\n- `headers()` is the single source for all outgoing headers; edit there, nowhere else\n- Deferred tool-search session handoff uses request/result `session_id` plus provider header `x-cave-tool-session`; update sdk-ts + parity fixtures with any change\n- Run tests: `pytest` from this directory (Python ≥ 3.11 required)\n\n## Gotchas\n\n- **No third-party deps** — do not add `requests`, `httpx`, or any library; keep `dependencies = []` in pyproject.toml\n- **byte-safe**: SDK sends request bodies to the gateway unmodified; no rewriting. `compress()` delegates to the Engine and passes the original through on any problem\n- **context packing is connected-only and intentionally lossy**: it sends item bytes to gateway, never runs in local wrap, and relies on caller retaining every item named by `deferred_ids`. It chooses what enters window; cache-optimal assembly chooses placement\n- `sdk-python` and `sdk-ts` mirror the same field names and `/sdk/v1/*` contract — enforced by the shared parity suite (`tests/test_parity.py` + `../../parity/fixtures.json`), not just convention. A divergence is a CI failure. Change one SDK, change both **and** the fixtures\n\nSee ../../../CLAUDE.md (root)\n","packages/sdk/python/CLAUDE.md":"# packages/sdk/python — Python SDK for the Caveman Cloud gateway\n\nStdlib-only (`urllib.request`, no third-party deps) Python package. Provides `Cave` (config +\nentrypoint), `CaveTool` (tool descriptor), and `ToolSearchResult`. All HTTP calls POST to the\ngateway with `x-cave-agent` / `x-cave-workflow` / `x-cave-retention` headers set from `Cave` fields.\n\n## Layout\n\n- `caveman_cloud/__init__.py` — re-exports `Cave`, `CaveTool`, `CompressResult`, `ContextPackItem`, `ContextPackOptions`, `ContextPackResult`, `ToolSearchResult`, …\n- `caveman_cloud/core.py` — all implementation: `Cave`, `Trace`, `Provider`, `_Create`, `ToolSearchResult`, `CompressResult`, `ContextPack*`, `CaveTool`, `headers()`\n- `tests/test_sdk.py` — pytest tests; mock `urllib.request.urlopen` with `patch()`\n- `tests/test_parity.py` — cross-language conformance suite; drives `../../parity/fixtures.json` (shared with sdk-ts). Same fixtures, two languages → a field in one SDK and not the other fails CI.\n- `tests/test_runtime_policy.py` — runtime-policy client; drives every section of `../../parity/runtime-policy.fixtures.json` (fetch wire, signature cases, the `guard_cases` fail-closed truth table, assignment vectors with exact float equality — including the weighted `exp-w` vectors that pin the `(1-h)*(w/t)` propensity association — and decision cases). Mirrors the TS `tests/runtime-policy.runtime.mjs`\n- `tests/test_trace_continuity.py` — trace/span id minting + which requests carry `x-cave-trace-id` / `x-cave-parent-span-id`; mirrors the TS `tests/trace-continuity.runtime.mjs`\n- `caveman_cloud/middleware/` — stable framework-middleware client for protocol 1.1 (`docs/technical/middleware-protocol.md`): `protocol.py` pure rules (scope normalization, tolerant capabilities, failure classification, breaker, budgets, endpoint/proxy resolution, `warn_once`), `transport.py` default keep-alive stdlib transport, `runtime.py` / `async_runtime.py` clients, `validate.py` plan/page checks. `tests/test_middleware_v1_1.py` drives `../../parity/middleware-v1_1.fixtures.json`; `tests/test_middleware_transport.py` uses real local sockets\n- `pyproject.toml` — distribution name `caveman-sdk` (import package stays `caveman_cloud`), `requires-python = \">=3.11\"`, no runtime dependencies\n\n## Key API surface (`core.py`)\n\n- `Cave.trace(workflow, tags, *, trace_id=None, span_id=None)` → context manager yielding `Trace`; call `.model[\"openai\"].responses.create(body)` inside. The trace mints `trace_id` (32 lowercase hex) + a root `span_id` (16 lowercase hex) with the exporter's RNG; `trace_id`/`span_id` continue an inbound trace and a value that isn't the exact hex shape is replaced rather than sent. Every provider call and trace-scoped `/sdk/v1/*` call made **through the trace** carries `x-cave-trace-id` + `x-cave-parent-span-id`; providers and SDK calls built directly off the `Cave` carry neither\n- `Trace.exporter(service_name=None)` → a per-service memoized `OTelExporter` whose `default_trace_id` is the trace's, so SDK spans and the gateway's request rows join one trace. Runtime-policy decision spans passed a `Trace` use this same caller-reachable default buffer; call `trace.exporter().export()` to ship them. MIRRORS the TS `CaveTrace.exporter`\n- `Cave.tools(catalog, *, strategy=\"all\", initial_tool_count=8)` → builder handle with `.strategy`, `.initial` (`list[CaveTool]`), `.search(query, *, max_tools, context, workflow, ranker, session_id)`. `strategy=\"deferred\"` includes every `always_load` tool exactly once, then fills remaining initial slots from non-mandatory tools; a cap below the mandatory count fails locally. `.search()` always hits the gateway with the FULL catalog. MIRRORS the TS `cave.tools({catalog, strategy})`\n- `Cave.tool_search(tools, query, *, context, max_tools, workflow, ranker, session_id)` → flat variant: POSTs `[tools, query]` to `/sdk/v1/tool-search`; returns `ToolSearchResult` with `.saved_tokens` / `.reduction_pct` / `.session_id`. Schema-token counters are estimates; `.token_basis` discloses the counter and `.basis` is always `\"inferred\"`. `ranker` (`\"bm25\"`|`\"embeddings\"`) is passed through verbatim — the SDK never computes similarity\n- `Cave.prompts.internal_brevity(*, style, preserve_errors_verbatim=False, preserve_code_verbatim=False)` → output-style snippet (`\"none\"` → `\"\"`); booleans render lowercase to match the TS `cave.prompts.internalBrevity`\n- `Cave.compress(payload, *, content_type=None)` → `CompressResult`; POSTs `/sdk/v1/compress`, maps the Engine report. **Byte-safe pass-through** on any transport/parse problem (original input, `ratio=0.0`, no handle); `.token_count_basis` discloses the counter and `basis` is always `\"inferred\"`. The SDK delegates — it never reimplements a compressor\n- `Cave.context.pack(query, items, options)` → `ContextPackResult`; connected-only POST to `/sdk/v1/context/pack`. Lossy selector over caller-owned items, never CCR/ledger; returns exact `deferred_ids`. Transport or malformed-report failure returns all original items with zero inferred savings\n- `Trace.expand(source_ref)` — the GET half of `checkpoint()`; `GET /sdk/v1/checkpoints/{ref}/expand` returns the stored `{source_ref, version, messages, checkpoint}`\n- `Cave.openai/anthropic/gemini/vertex(upstream_key)` → `Provider` that proxies through gateway; `Provider.raw(path, body)` is the escape hatch (mirrors the TS provider-client `raw`)\n- `Cave.bedrock(region, endpoint=\"runtime\")` → no-network first-party route descriptor; Runtime defaults to `/bedrock`, explicit Mantle returns `/bedrock/anthropic`, and `sdk_only=False` mirrors TS `sdkOnly`\n- `Trace.tool(name, options, fn)` — calls `fn()` then POSTs a `tool.call` event\n- `Trace.page_artifact(value, options)` / `Trace.artifacts.page(value, options)` — send versioned `{value, options, workflow}`; gateway stores only JSON `value`. `artifacts.get(id)` performs authenticated retrieval. `page_artifact` remains backwards-compatible alias.\n- `Trace.model[\"openai\"].responses.create(body, *, latency_class=None, tool_session_id=None)` — when `latency_class` is set, sends the `x-cave-async` header (`\"true\"` unless `\"interactive\"`); when `tool_session_id` is set, sends `x-cave-tool-session`, mirroring the TS `trace.model.openai.responses.create(body, {cave:{latencyClass, toolSessionId}})`\n- `Trace.checkpoint(messages, options)` — POSTs to `/sdk/v1/checkpoints`; the gateway persists it (Valkey) and returns a reversible `source_ref` you can later expand via `GET /sdk/v1/checkpoints/{ref}/expand`\n- `Cave.exporter(service_name=None)` → `OTelExporter`; `record_span(...)` maps current GenAI fields to `gen_ai.*`, `export()` POSTs OTLP/JSON to standard `/v1/traces` (headers via `otlp_headers()`; legacy `/otlp/v1/traces` remains server-only compatibility)\n- `Cave.retry_loop_breaker(threshold=3)` → `RetryLoopBreaker`; `.record(name, args)` raises `RetryLoopError` after `threshold` consecutive identical tool calls (interrupts a stuck loop). `.guard(name, args, fn)` records then runs `fn`\n- `Cave.runtime_policy(*, public_key, auto_refresh_seconds, kill_env, workflow)` → `RuntimePolicyClient`. `refresh()` GETs `/sdk/v1/runtime-policy` with a 30s timeout and a 1 MiB response cap (`oversized_response`; last-known-good stays active); the bundle travels as a signed STRING, Ed25519 verified before parsing, pinned or TOFU. `decide(task_family, unit_key, context, trace)` is **synchronous, local-only, never raises** and returns a `PolicyDecision` (`execute`/`fallback`/`baseline` + `reason`); `kill()` latches baseline locally; `state()` → `RuntimePolicyState`. Holdout is carved first onto the fallback path; assignment is the exported `policy_unit_fraction(*keys)` (byte-for-byte port of Go Caveman-Cloud `shared/platform/sampling.Fraction`, mirrors the TS `policyUnitFraction`; the parity `assignment_vectors` are its authority, including the empty-unit-key vector `decide()` refuses to assign on). Decision spans are observability only — no savings/dollar/`verified` field exists here. MIRRORS the TS `cave.runtimePolicy`\n- `Cave.jobs` → reserved `JobsClient` surface. Every method fails locally with `cave_async_jobs_unavailable`; it performs no network request until durable encrypted request storage, credential custody, and a draining worker exist. MIRRORS the TS `Cave.jobs`\n\n## Conventions\n\n- Tests use `patch(\"urllib.request.urlopen\", side_effect=fake_urlopen)` — never real network\n- Add new gateway endpoints via `Trace._request(path, body)` or `Provider.create(path, body)`\n- `headers()` is the single source for all outgoing headers; edit there, nowhere else\n- Deferred tool-search session handoff uses request/result `session_id` plus provider header `x-cave-tool-session`; update sdk-ts + parity fixtures with any change\n- Run tests: `pytest` from this directory (Python ≥ 3.11 required)\n\n## Gotchas\n\n- **No third-party deps** — do not add `requests`, `httpx`, or any library; keep `dependencies = []` in pyproject.toml\n- **byte-safe**: SDK sends request bodies to the gateway unmodified; no rewriting. `compress()` delegates to the Engine and passes the original through on any problem\n- **context packing is connected-only and intentionally lossy**: it sends item bytes to gateway, never runs in local wrap, and relies on caller retaining every item named by `deferred_ids`. It chooses what enters window; cache-optimal assembly chooses placement\n- `sdk-python` and `sdk-ts` mirror the same field names and `/sdk/v1/*` contract — enforced by the shared parity suite (`tests/test_parity.py` + `../../parity/fixtures.json`), not just convention. A divergence is a CI failure. Change one SDK, change both **and** the fixtures\n\nSee ../../../CLAUDE.md (root)\n","packages/sdk/python/README.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=packages%2Fsdk%2Fpython%2FREADME.md -->","packages/sdk/typescript/AGENTS.md":"# packages/sdk/typescript — TypeScript SDK (`@caveman-ai/sdk`)\n\nSingle-file SDK (`src/index.ts`) exported as an ES module. Provides `Cave` (main client),\n`CaveTrace` (per-request tracing), and BM25-backed tool-search against the gateway. No runtime\ndependencies — only `devDependencies` for TypeScript.\n\n## Layout\n\n- `src/index.ts` — entire SDK; exports `Cave`, `CaveTrace`, `CaveOptions`, `CaveTool`, `ToolSearchResult`, `CompressOptions`, `CompressResult`, and the `ContextPack*` types\n- `tests/tool-search.test.ts` — type-level assertions compiled by `tsc --noEmit`\n- `tests/tool-search.runtime.mjs` — runtime tests using `node:test` + global fetch mock (imports from `dist/`)\n- `tests/runtime-policy.runtime.mjs` + `tests/runtime-policy.test.ts` — the runtime-policy client; drives every section of `../../parity/runtime-policy.fixtures.json` (fetch wire, signature cases, all `assignment_vectors` with exact float equality, all `guard_cases` — the shared operator truth table lives in the fixture, not in this file — and all `decision_cases`). Iterate the arrays; never hard-code their counts\n- `tests/parity.runtime.mjs` — cross-language conformance suite; drives `../../parity/fixtures.json` (shared with sdk-python). Same fixtures, two languages → a field in one SDK and not the other fails CI.\n- `src/middleware/` — the stable `@caveman-ai/sdk/middleware` subpath: `runtime.ts` (protocol client), `protocol.ts` (pure protocol 1.1 rules), `validate.ts` (capabilities/plan/page validation). `tests/middleware-v1_1.runtime.mjs` drives every client vector in `../../parity/middleware-v1_1.fixtures.json` (the Python SDK runs the same file); `tests/middleware-hardening.runtime.mjs` pins the B3–B12 hardening fixes\n- `tests/trace-continuity.runtime.mjs` — trace/span id minting + which requests carry `x-cave-trace-id` / `x-cave-parent-span-id`; mirrors the Python `tests/test_trace_continuity.py`\n- `tsconfig.json` / `tsconfig.test.json` — separate configs; test config covers `tests/`. Both extend the **repo-root** `../../../tsconfig.base.json`.\n\n## Key APIs\n\n- `new Cave(options)` — requires `apiKey`, `baseURL`, `agent`\n- `cave.trace(opts, fn)` — wraps a callback with a `CaveTrace`; sends tool-call spans to `/sdk/v1/events`. The trace mints `traceId` (32 lowercase hex) + a root `spanId` (16 lowercase hex) with the exporter's RNG; `opts.traceId`/`opts.spanId` continue an inbound trace and a value that isn't the exact hex shape is replaced rather than sent. Every provider call made **through the trace** carries `x-cave-trace-id` + `x-cave-parent-span-id`. Generic `/sdk/v1/*` calls and provider clients built off the `Cave` carry neither; the sole SDK-endpoint exception is `CaveTrace.tool`, whose `/sdk/v1/events` call carries the trace id and root parent span id\n- `CaveTrace.exporter({serviceName?})` → an `OTelExporter` whose `defaultTraceId` is the trace's, so SDK spans and the gateway's request rows join one trace. MIRRORS the Python `Trace.exporter`\n- `cave.tools({ catalog, strategy })` — returns `{ initial, strategy, search(query, opts?) }`. **`search()` is async** (returns `Promise<ToolSearchResult>`); breaking change from 1.0 which was sync. `opts.ranker` (`\"bm25\"`|`\"embeddings\"`) is passed through to the gateway verbatim; `opts.toolSessionId` sends `session_id` so provider callbacks can re-inject called deferred tools. The SDK never computes similarity\n- `cave.toolSearch(catalog, query, opts?)` — direct variant, same contract (incl. `ranker` / `toolSessionId`). Schema-token counters are estimates; `tokenBasis` discloses the counter and `basis` is always `\"inferred\"`\n- `cave.compress(payload, opts?)` → `Promise<CompressResult>`; POSTs `/sdk/v1/compress`, maps the Engine report. **Byte-safe pass-through** on any transport/parse problem (original input, `ratio:0`, no handle); `tokenCountBasis` discloses the counter and `basis` is always `\"inferred\"`. The SDK delegates — it never reimplements a compressor\n- `cave.context.pack(query, items, options)` → `Promise<ContextPackResult>`; connected-only POST to `/sdk/v1/context/pack`. Lossy selector over caller-owned items, never CCR/ledger; returns exact `deferredIds`. Transport or malformed-report failure returns all original items with zero inferred savings\n- `CaveTrace.context.expand(sourceRef)` — the GET half of `checkpoint()`; `GET /sdk/v1/checkpoints/{ref}/expand` returns the stored `{source_ref, version, messages, checkpoint}`\n- `cave.openai/anthropic/gemini/vertex()` — thin provider clients; proxied through gateway; each exposes a `.raw` fetch escape hatch (mirrors the Python `Provider.raw`). `cave.bedrock({region, endpoint?})` is a no-network first-party route descriptor: Runtime defaults to `/bedrock`; explicit Mantle returns `/bedrock/anthropic`; `sdkOnly:false` mirrors Python's `sdk_only`\n- `cave.prompts.internalBrevity({style, preserveErrorsVerbatim?, preserveCodeVerbatim?})` — output-style snippet (`style:\"none\"` → `\"\"`); MIRRORS the Python `cave.prompts.internal_brevity`\n- `CaveTrace.model.openai.responses.create(body, {cave:{latencyClass, toolSessionId}})` — passing a `latencyClass` hint sets the `x-cave-async` header (`\"true\"` unless `\"interactive\"`); passing `toolSessionId` sets `x-cave-tool-session`. Mirrored by the Python `responses.create(body, latency_class=..., tool_session_id=...)`\n- `CaveTrace.artifacts.page()` — sends versioned `{value, options, workflow}`; gateway stores only JSON `value`. `artifacts.get(id)` performs authenticated retrieval. `strategy:\"verbatim\"` bypasses storage. Mirrored by Python.\n- `CaveTrace.context.checkpoint()` — POSTs to `/sdk/v1/checkpoints`; gateway persists it (Valkey) + returns a reversible `source_ref` (expand via `GET /sdk/v1/checkpoints/{ref}/expand`)\n- `cave.exporter({serviceName?})` → `OTelExporter`; `recordSpan(...)` maps current GenAI fields to `gen_ai.*`, `export()` POSTs OTLP/JSON to standard `/v1/traces` (headers via `otlpHeaders()`; legacy `/otlp/v1/traces` remains server-only compatibility)\n- `cave.runtimePolicy({publicKey?, autoRefreshSeconds?, killEnv?})` → `RuntimePolicyClient`. `refresh()` is the only network call (`GET /sdk/v1/runtime-policy`, std headers minus content-type); it Ed25519-verifies the bundle **string's** exact bytes before parsing, TOFU-pins the key **the moment the signature verifies** (before the schema/sequence checks, so a rejected-but-signed bundle cannot open a downgrade window), rejects a regressed `sequence`, and keeps last-known-good on any failure. The fetch carries a 30s `AbortSignal.timeout` (mirrors Python's `timeout=30`); an `autoRefreshSeconds` tick that lands mid-refresh is skipped, not stacked. `decide(taskFamily, {unitKey, context, trace})` is **synchronous, local-only, and never throws**; holdout suppresses onto the fallback path and a missing unit key or invalid experiment never guesses an arm. `kill()` latches locally, `killEnv` is re-read per decide, `state()` snapshots. Routing only — no savings vocabulary anywhere. MIRRORS the Python `cave.runtime_policy()`\n- `cave.retryLoopBreaker(threshold=3)` → `RetryLoopBreaker`; `.record(name, args)` throws `RetryLoopError` after `threshold` consecutive identical tool calls; `.guard(name, args, fn)` records then runs `fn`\n- `cave.jobs` → reserved `JobsClient` surface. Every method fails locally with `cave_async_jobs_unavailable`; it performs no network request until durable encrypted request storage, credential custody, and a draining worker exist. MIRRORS the Python `Cave.jobs`\n\n## Conventions\n\n- Tests: type assertions in `.test.ts` (compiled only), runtime in `.runtime.mjs` (run against `dist/`)\n- Build before runtime tests: `pnpm build && pnpm test:node`\n- Request body keys are `snake_case` to the gateway; response mapped to `camelCase` in `ToolSearchResult`\n- `x-cave-workflow` header defaults to `defaultWorkflow ?? \"unlabeled-workflow\"`; never omit it\n- Deferred tool-search session handoff uses request `session_id`, result `sessionId`, and provider header `x-cave-tool-session`; update sdk-python + parity fixtures with any change\n\n## Gotchas\n\n- **byte-safe**: SDK sends request bodies to the gateway verbatim; no rewriting allowed. `compress()` is the one path that yields smaller bytes and it **delegates** to the Engine — on any problem it passes the original through\n- **context packing is connected-only and intentionally lossy**: it sends item bytes to gateway, never runs in local wrap, and relies on caller retaining every item named by `deferredIds`. It chooses what enters window; cache-optimal assembly chooses placement\n- **mirror sdk-python**: every field/method exists in both, enforced by the shared parity suite — a divergence is a CI failure, not a convention slip. Change one SDK, change both **and** the fixtures\n- published as `@caveman-ai/sdk`; the workspace name stays `@caveman-ai/sdk` until the npm redirect plan lands\n- `strategy:\"deferred\"` initial set = `alwaysLoad` tools + up to `initialToolCount` (default 8); never returns the full catalog without a `search()` call\n- `reductionPct` rounds to one decimal; `savedTokens` is derived (`full - sent`), not from the gateway response\n\nSee ../../../CLAUDE.md (root)\n","packages/sdk/typescript/CLAUDE.md":"# packages/sdk/typescript — TypeScript SDK (`@caveman-ai/sdk`)\n\nSingle-file SDK (`src/index.ts`) exported as an ES module. Provides `Cave` (main client),\n`CaveTrace` (per-request tracing), and BM25-backed tool-search against the gateway. No runtime\ndependencies — only `devDependencies` for TypeScript.\n\n## Layout\n\n- `src/index.ts` — entire SDK; exports `Cave`, `CaveTrace`, `CaveOptions`, `CaveTool`, `ToolSearchResult`, `CompressOptions`, `CompressResult`, and the `ContextPack*` types\n- `tests/tool-search.test.ts` — type-level assertions compiled by `tsc --noEmit`\n- `tests/tool-search.runtime.mjs` — runtime tests using `node:test` + global fetch mock (imports from `dist/`)\n- `tests/runtime-policy.runtime.mjs` + `tests/runtime-policy.test.ts` — the runtime-policy client; drives every section of `../../parity/runtime-policy.fixtures.json` (fetch wire, signature cases, all `assignment_vectors` with exact float equality, all `guard_cases` — the shared operator truth table lives in the fixture, not in this file — and all `decision_cases`). Iterate the arrays; never hard-code their counts\n- `tests/parity.runtime.mjs` — cross-language conformance suite; drives `../../parity/fixtures.json` (shared with sdk-python). Same fixtures, two languages → a field in one SDK and not the other fails CI.\n- `src/middleware/` — the stable `@caveman-ai/sdk/middleware` subpath: `runtime.ts` (protocol client), `protocol.ts` (pure protocol 1.1 rules), `validate.ts` (capabilities/plan/page validation), `transport.ts` (lazy `node:http` transport used when HTTP(S)_PROXY applies, `ca` is set or Node runs with NODE_USE_ENV_PROXY; never proxies loopback). `tests/middleware-v1_1.runtime.mjs` drives every client vector in `../../parity/middleware-v1_1.fixtures.json` (the Python SDK runs the same file); `tests/middleware-hardening.runtime.mjs` pins the B3–B12 hardening fixes\n- `tests/trace-continuity.runtime.mjs` — trace/span id minting + which requests carry `x-cave-trace-id` / `x-cave-parent-span-id`; mirrors the Python `tests/test_trace_continuity.py`\n- `tsconfig.json` / `tsconfig.test.json` — separate configs; test config covers `tests/`. Both extend the **repo-root** `../../../tsconfig.base.json`.\n\n## Key APIs\n\n- `new Cave(options)` — requires `apiKey`, `baseURL`, `agent`\n- `cave.trace(opts, fn)` — wraps a callback with a `CaveTrace`; sends tool-call spans to `/sdk/v1/events`. The trace mints `traceId` (32 lowercase hex) + a root `spanId` (16 lowercase hex) with the exporter's RNG; `opts.traceId`/`opts.spanId` continue an inbound trace and a value that isn't the exact hex shape is replaced rather than sent. Every provider call and trace-scoped `/sdk/v1/*` call made **through the trace** carries `x-cave-trace-id` + `x-cave-parent-span-id`; provider clients and SDK calls built directly off the `Cave` carry neither\n- `CaveTrace.exporter({serviceName?})` → a per-service memoized `OTelExporter` whose `defaultTraceId` is the trace's, so SDK spans and the gateway's request rows join one trace. Runtime-policy decision spans passed a `CaveTrace` use this same caller-reachable default buffer; call `trace.exporter().flush()` to ship them. MIRRORS the Python `Trace.exporter`\n- `cave.tools({ catalog, strategy })` — returns `{ initial, strategy, search(query, opts?) }`. **`search()` is async** (returns `Promise<ToolSearchResult>`); breaking change from 1.0 which was sync. `opts.ranker` (`\"bm25\"`|`\"embeddings\"`) is passed through to the gateway verbatim; `opts.toolSessionId` sends `session_id` so provider callbacks can re-inject called deferred tools. The SDK never computes similarity\n- `cave.toolSearch(catalog, query, opts?)` — direct variant, same contract (incl. `ranker` / `toolSessionId`). Schema-token counters are estimates; `tokenBasis` discloses the counter and `basis` is always `\"inferred\"`\n- `cave.compress(payload, opts?)` → `Promise<CompressResult>`; POSTs `/sdk/v1/compress`, maps the Engine report. **Byte-safe pass-through** on any transport/parse problem (original input, `ratio:0`, no handle); `tokenCountBasis` discloses the counter and `basis` is always `\"inferred\"`. The SDK delegates — it never reimplements a compressor\n- `cave.context.pack(query, items, options)` → `Promise<ContextPackResult>`; connected-only POST to `/sdk/v1/context/pack`. Lossy selector over caller-owned items, never CCR/ledger; returns exact `deferredIds`. Transport or malformed-report failure returns all original items with zero inferred savings\n- `CaveTrace.context.expand(sourceRef)` — the GET half of `checkpoint()`; `GET /sdk/v1/checkpoints/{ref}/expand` returns the stored `{source_ref, version, messages, checkpoint}`\n- `cave.openai/anthropic/gemini/vertex()` — thin provider clients; proxied through gateway; each exposes a `.raw` fetch escape hatch (mirrors the Python `Provider.raw`). `cave.bedrock({region, endpoint?})` is a no-network first-party route descriptor: Runtime defaults to `/bedrock`; explicit Mantle returns `/bedrock/anthropic`; `sdkOnly:false` mirrors Python's `sdk_only`\n- `cave.prompts.internalBrevity({style, preserveErrorsVerbatim?, preserveCodeVerbatim?})` — output-style snippet (`style:\"none\"` → `\"\"`); MIRRORS the Python `cave.prompts.internal_brevity`\n- `CaveTrace.model.openai.responses.create(body, {cave:{latencyClass, toolSessionId}})` — passing a `latencyClass` hint sets the `x-cave-async` header (`\"true\"` unless `\"interactive\"`); passing `toolSessionId` sets `x-cave-tool-session`. Mirrored by the Python `responses.create(body, latency_class=..., tool_session_id=...)`\n- `CaveTrace.artifacts.page()` — sends versioned `{value, options, workflow}`; gateway stores only JSON `value`. `artifacts.get(id)` performs authenticated retrieval. `strategy:\"verbatim\"` bypasses storage. Mirrored by Python.\n- `CaveTrace.context.checkpoint()` — POSTs to `/sdk/v1/checkpoints`; gateway persists it (Valkey) + returns a reversible `source_ref` (expand via `GET /sdk/v1/checkpoints/{ref}/expand`)\n- `cave.exporter({serviceName?})` → `OTelExporter`; `recordSpan(...)` maps current GenAI fields to `gen_ai.*`, `export()` POSTs OTLP/JSON to standard `/v1/traces` (headers via `otlpHeaders()`; legacy `/otlp/v1/traces` remains server-only compatibility)\n- `cave.runtimePolicy({publicKey?, autoRefreshSeconds?, killEnv?})` → `RuntimePolicyClient`. `refresh()` is the only network call (`GET /sdk/v1/runtime-policy`, std headers minus content-type); it Ed25519-verifies the bundle **string's** exact bytes before parsing, TOFU-pins the key **the moment the signature verifies** (before the schema/sequence checks, so a rejected-but-signed bundle cannot open a downgrade window), rejects a regressed `sequence`, and keeps last-known-good on any failure. The fetch carries a 30s `AbortSignal.timeout` (mirrors Python's `timeout=30`) and stops a response after 1 MiB (`oversized_response`); fetch implementations without a bounded readable body fail closed (`bounded_response_unavailable`) instead of calling unbounded `text()`. An `autoRefreshSeconds` tick that lands mid-refresh is skipped, not stacked. `decide(taskFamily, {unitKey, context, trace})` is **synchronous, local-only, and never throws**; holdout suppresses onto the fallback path and a missing unit key or invalid experiment never guesses an arm. `kill()` latches locally, `killEnv` is re-read per decide, `state()` snapshots. Routing only — no savings vocabulary anywhere. MIRRORS the Python `cave.runtime_policy()`\n- `cave.retryLoopBreaker(threshold=3)` → `RetryLoopBreaker`; `.record(name, args)` throws `RetryLoopError` after `threshold` consecutive identical tool calls; `.guard(name, args, fn)` records then runs `fn`\n- `cave.jobs` → reserved `JobsClient` surface. Every method fails locally with `cave_async_jobs_unavailable`; it performs no network request until durable encrypted request storage, credential custody, and a draining worker exist. MIRRORS the Python `Cave.jobs`\n\n## Conventions\n\n- Tests: type assertions in `.test.ts` (compiled only), runtime in `.runtime.mjs` (run against `dist/`)\n- Build before runtime tests: `pnpm build && pnpm test:node`\n- Request body keys are `snake_case` to the gateway; response mapped to `camelCase` in `ToolSearchResult`\n- `x-cave-workflow` header defaults to `defaultWorkflow ?? \"unlabeled-workflow\"`; never omit it\n- Deferred tool-search session handoff uses request `session_id`, result `sessionId`, and provider header `x-cave-tool-session`; update sdk-python + parity fixtures with any change\n\n## Gotchas\n\n- **byte-safe**: SDK sends request bodies to the gateway verbatim; no rewriting allowed. `compress()` is the one path that yields smaller bytes and it **delegates** to the Engine — on any problem it passes the original through\n- **context packing is connected-only and intentionally lossy**: it sends item bytes to gateway, never runs in local wrap, and relies on caller retaining every item named by `deferredIds`. It chooses what enters window; cache-optimal assembly chooses placement\n- **mirror sdk-python**: every field/method exists in both, enforced by the shared parity suite — a divergence is a CI failure, not a convention slip. Change one SDK, change both **and** the fixtures\n- published as `@caveman-ai/sdk`; the workspace name stays `@caveman-ai/sdk` until the npm redirect plan lands\n- `strategy:\"deferred\"` initial set = `alwaysLoad` tools + up to `initialToolCount` (default 8); never returns the full catalog without a `search()` call\n- `reductionPct` rounds to one decimal; `savedTokens` is derived (`full - sent`), not from the gateway response\n\nSee ../../../CLAUDE.md (root)\n","packages/sdk/typescript/README.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=packages%2Fsdk%2Ftypescript%2FREADME.md -->","packages/shared/contracts/AGENTS.md":"# packages/shared/contracts — shared wire contracts (JSON Schema)\n\nSource-of-truth schemas for cross-service/SDK wire shapes:\n`schemas/policy.schema.json` covers tenant policy and\n`schemas/practice.schema.json` covers practice-library records.\n\n## Layout\n\n- `schemas/policy.schema.json` — JSON Schema 2020-12 for the tenant policy object (required: version, runtime_mode, fail_policy, providers, limits, retention, optimizers, telemetry, sdk)\n- `schemas/practice.schema.json` — strict JSON Schema 2020-12 for one practice record; unknown fields and enums fail closed\n- `schemas/adapter-conformance.schema.json` — static Pi/Claude contract-record\n  shape; `evidence: static_contract_only` explicitly denies executable proof\n- `schemas/middleware-*.schema.json` + `openapi/middleware.openapi.json` — the\n  framework middleware protocol. Normative prose (negotiation, status/error and\n  reason catalogs, N-1 policy) is `docs/technical/middleware-protocol.md`;\n  executable vectors are `packages/sdk/parity/middleware-v1_1.fixtures.json`.\n  Additive changes keep `schema_version` 1; unknown `limits` keys must stay\n  positive safe integers because SDK 1.1.0 rejects anything else\n- `scripts/validate-schemas.mjs` — compiles every schema with AJV, validates\n  adapter fixtures, middleware fixture examples, `$id` = raw GitHub URL of the\n  file at tag `contracts-v<package.json version>` (bump both together),\n  relative cross-schema `$ref`s only, and every OpenAPI `$ref`\n- `package.json` — `@caveman-ai/contracts`; build/lint/test run both validators (no compiled output)\n\n## Key schema fields\n\n- `capabilities` — `{ compress?, cache_hints?, routing? }` booleans, `additionalProperties: false`; the authoritative description of what a project optimizes. An unordered SET, not a stage — the three compose\n- `pass_through` — boolean; forbids every transform. Outranks `capabilities`, which are retained so operator toggles keep their remembered positions\n- `runtime_mode` — `record | recommend | shadow | canary | active | compress`; **a DERIVED display string, not an input**. Still `required` for the compat window (removal 2026-10-01) because `projects.runtime_mode` and the telemetry write validator read it. A doc carrying `capabilities` is authoritative and its `runtime_mode` derives from them; a legacy doc carrying only `runtime_mode` derives the capability set\n- `fail_policy` — `fail_open | fail_closed`; unknown values fail closed (honesty rule)\n- `limits.monthly_usd` — `{ soft, hard }` spend caps per tenant\n- `telemetry.sample_rate` — float 0–1, validated by schema minimum/maximum\n- `additionalProperties: true` — intentional; services and optimizers may extend the object\n\n## Conventions\n\n- Schema version is an integer; `policy_schema_version` returned by `cloud/control-api/internal/httpapi/server.go:372`\n- Adding a new required field is a **breaking change** — bump `version` and coordinate across control-api, SDKs, web\n- No TypeScript types generated from schema yet; the web dashboard manually mirrors the shape\n\n## Gotchas\n\n- `fail_policy: fail_closed` aligns with repo honesty rule: unknown enum cases must fail closed, not pass through\n- `runtime_mode: record` is always pass-through (byte-safe rule); never treat it as an optimizer mode\n- The six `runtime_mode` values are **not ordered**. Code comparing them for rank (`mode >= \"canary\"`) is a bug; the four middle values are behaviourally identical, and `shadow`/`canary` are display labels rather than traffic splits. The derivation lives in exactly one place, `cloud/internal/capability`, because the gateway, control-api, and the worker must never disagree about what a document means\n- Build script validates schemas and static fixtures; it emits no artifact and\n  does not prove executable Pi/Claude parity\n\nSee ../../../CLAUDE.md (root)\n","packages/shared/contracts/CLAUDE.md":"# packages/shared/contracts — shared wire contracts (JSON Schema)\n\nSource-of-truth schemas for cross-service/SDK wire shapes:\n`schemas/policy.schema.json` covers tenant policy and\n`schemas/practice.schema.json` covers practice-library records.\n\n## Layout\n\n- `schemas/policy.schema.json` — JSON Schema 2020-12 for the tenant policy object (required: version, runtime_mode, fail_policy, providers, limits, retention, optimizers, telemetry, sdk)\n- `schemas/practice.schema.json` — strict JSON Schema 2020-12 for one practice record; unknown fields and enums fail closed\n- `schemas/adapter-conformance.schema.json` — static Pi/Claude/Vercel AI SDK/Eve/\n  Mastra contract-record shape; `evidence: static_contract_only` explicitly\n  denies executable proof\n- `schemas/canonical-span.schema.json` — `caveman.span.v1`, the normalized\n  evidence-plane span shared by trace intelligence and replay\n- `schemas/agent-run-receipt.schema.json` — `caveman.agent.run-receipt.v1`,\n  content-blind per-run SDK economic receipt. It is a shared wire shape, not an\n  anonymous CLI event and not a signed verified-savings receipt. Retry events\n  carry reserved spend, measured spend, and measurement basis\n- `schemas/continuous-improvement-report.schema.json` — versioned end-to-end\n  improvement report; the current generated result is E3 `inferred` /\n  `symbolic_counterfactual`. E4 `replayed_counterfactual` remains a reserved\n  actual recorded-output/sandbox shape and never means verified savings\n- `schemas/middleware-*.schema.json` + `openapi/middleware.openapi.json` — the\n  framework middleware protocol. Normative prose (negotiation, status/error and\n  reason catalogs, N-1 policy) is `docs/technical/middleware-protocol.md`;\n  executable vectors are `packages/sdk/parity/middleware-v1_1.fixtures.json`.\n  Additive changes keep `schema_version` 1; unknown `limits` keys must stay\n  positive safe integers because SDK 1.1.0 rejects anything else\n- `scripts/validate-schemas.mjs` — compiles every schema with AJV, validates\n  adapter fixtures, middleware fixture examples, `$id` = raw GitHub URL of the\n  file at tag `contracts-v<package.json version>` (bump both together),\n  relative cross-schema `$ref`s only, and every OpenAPI `$ref`\n- `package.json` — `@caveman-ai/contracts`; build/lint/test run both validators (no compiled output)\n\n## Key schema fields\n\n- `capabilities` — `{ compress?, cache_hints?, routing? }` booleans, `additionalProperties: false`; the authoritative description of what a project optimizes. An unordered SET, not a stage — the three compose\n- `pass_through` — boolean; forbids every transform. Outranks `capabilities`, which are retained so operator toggles keep their remembered positions\n- `runtime_mode` — `record | recommend | shadow | canary | active | compress`; **a DERIVED display string, not an input**. Still `required` for the compat window (removal 2026-10-01) because `projects.runtime_mode` and the telemetry write validator read it. A doc carrying `capabilities` is authoritative and its `runtime_mode` derives from them; a legacy doc carrying only `runtime_mode` derives the capability set\n- `fail_policy` — `fail_open | fail_closed`; unknown values fail closed (honesty rule)\n- `limits.monthly_usd` — `{ soft, hard }` spend caps per tenant\n- `telemetry.sample_rate` — float 0–1, validated by schema minimum/maximum\n- `runtime_policies` — OPTIONAL array (additive, nothing new required): the per-task-family runtime policies delivered to SDK callers through the policy overlay and served by the gateway's `GET /sdk/v1/runtime-policy`. Guard ops are a closed set (`eq|ne|gt|gte|lt|lte|in`, AND-ed); `budget` is an advisory cap the caller enforces, never a savings figure; the renderer drops a structurally invalid policy WHOLE rather than stripping the broken guard or experiment, because either strip would widen who the policy applies to\n- `additionalProperties: true` — intentional; services and optimizers may extend the object\n\n## Conventions\n\n- Schema version is an integer; `policy_schema_version` returned by `cloud/control-api/internal/httpapi/server.go:372`\n- Adding a new required field is a **breaking change** — bump `version` and coordinate across control-api, SDKs, web\n- No TypeScript types generated from schema yet; the web dashboard manually mirrors the shape\n- `scripts/check-contract-compat.mjs` **at the REPO ROOT** (`make check-contract-compat`), not this package's `scripts/`, is a bounded 2020-12 compatibility gate: it proves common narrowing constraints (types, enums/consts, bounds, object/array applicators, and supported combinators/refs) and treats changed unknown assertion/applicator keywords or unresolved reference semantics as major-version review items. Metadata annotations remain non-breaking. It is intentionally not a complete JSON Schema subsumption prover; add a focused fixture before relying on a new keyword.\n\n## Gotchas\n\n- `fail_policy: fail_closed` aligns with repo honesty rule: unknown enum cases must fail closed, not pass through\n- `runtime_mode: record` is always pass-through (byte-safe rule); never treat it as an optimizer mode\n- The six `runtime_mode` values are **not ordered**. Code comparing them for rank (`mode >= \"canary\"`) is a bug; the four middle values are behaviourally identical, and `shadow`/`canary` are display labels rather than traffic splits. The derivation lives in exactly one place, `cloud/internal/capability`, because the gateway, control-api, and the worker must never disagree about what a document means\n- Build script validates schemas and static fixtures; it emits no artifact and\n  does not prove executable cross-harness parity\n\nSee ../../../CLAUDE.md (root)\n","packages/shared/contracts/conformance/README.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=packages%2Fshared%2Fcontracts%2Fconformance%2FREADME.md -->","packages/shared/contracts/openapi/middleware.openapi.json":"{\n  \"openapi\": \"3.1.0\",\n  \"jsonSchemaDialect\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"info\": {\n    \"title\": \"Caveman middleware runtime protocol\",\n    \"version\": \"1.1.0\",\n    \"summary\": \"Compression-only runtime used by Caveman framework middleware. Normative prose: docs/technical/middleware-protocol.md.\",\n    \"description\": \"Status codes listed are the protocol 1.1 set, used only when the request carries the http_status_v2 feature in Caveman-Middleware-Features. Without it the runtime answers with the protocol 1.0 (legacy) mapping documented in the prose spec. Clients derive outcomes from error.code, never from the status alone. The referenced JSON Schemas are closed: they describe exactly what a protocol 1.1 server emits and are what the conformance kit checks servers against. Clients MUST NOT validate responses against them; clients ignore unknown response fields so later minor versions can add fields (spec §4, §16). Unknown routes and unlisted methods answer 404 not_found before any quota, queue or body handling.\",\n    \"license\": {\n      \"name\": \"Apache-2.0\",\n      \"identifier\": \"Apache-2.0\"\n    }\n  },\n  \"servers\": [\n    {\n      \"url\": \"http://127.0.0.1:8787\",\n      \"description\": \"Local runtime. A deployment may add a path prefix; clients preserve it.\"\n    }\n  ],\n  \"security\": [\n    {\n      \"bearer\": []\n    },\n    {\n      \"mtls\": []\n    }\n  ],\n  \"paths\": {\n    \"/caveman/v1/middleware/capabilities\": {\n      \"get\": {\n        \"operationId\": \"getCapabilities\",\n        \"summary\": \"Discover transforms, limits, features and protocol range.\",\n        \"description\": \"Without Caveman-Middleware-Features the runtime omits transforms a protocol 1.0 client would reject. Served from memory before any quota or queue, so it never answers 429; 403 is forbidden_origin only, and 503 means the runtime has no usable middleware store.\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/Features\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Client\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Traceparent\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Tracestate\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Capabilities for the authenticated principal.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"../schemas/middleware-capabilities.schema.json\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"$ref\": \"#/components/responses/E401\"\n          },\n          \"403\": {\n            \"$ref\": \"#/components/responses/E403\"\n          },\n          \"503\": {\n            \"$ref\": \"#/components/responses/E503\"\n          }\n        }\n      }\n    },\n    \"/caveman/v1/middleware/optimize\": {\n      \"post\": {\n        \"operationId\": \"optimize\",\n        \"summary\": \"Plan replacements for candidate segments.\",\n        \"description\": \"Every decision, including bypasses (not_smaller, cache_state_unavailable, recovery_unavailable, capacity), is a 200 plan under http_status_v2.\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/Features\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Client\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Traceparent\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Tracestate\"\n          }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"../schemas/middleware-optimize.schema.json\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Replacement plan or bypass decision.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"../schemas/middleware-plan.schema.json\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"$ref\": \"#/components/responses/E400\"\n          },\n          \"401\": {\n            \"$ref\": \"#/components/responses/E401\"\n          },\n          \"403\": {\n            \"$ref\": \"#/components/responses/E403\"\n          },\n          \"408\": {\n            \"$ref\": \"#/components/responses/E408\"\n          },\n          \"409\": {\n            \"$ref\": \"#/components/responses/E409\"\n          },\n          \"410\": {\n            \"$ref\": \"#/components/responses/E410\"\n          },\n          \"413\": {\n            \"$ref\": \"#/components/responses/E413\"\n          },\n          \"429\": {\n            \"$ref\": \"#/components/responses/E429\"\n          },\n          \"503\": {\n            \"$ref\": \"#/components/responses/E503\"\n          },\n          \"504\": {\n            \"$ref\": \"#/components/responses/E504\"\n          }\n        }\n      }\n    },\n    \"/caveman/v1/middleware/retrieve\": {\n      \"post\": {\n        \"operationId\": \"retrieve\",\n        \"summary\": \"Read a page or excerpt of an original by recovery handle.\",\n        \"description\": \"Has its own deadline (limits.retrieve_deadline_ms) and queue (limits.retrieve_queue_depth).\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/Features\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Client\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Traceparent\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Tracestate\"\n          }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"../schemas/middleware-retrieve.schema.json\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"One page of the original.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"../schemas/middleware-page.schema.json\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"$ref\": \"#/components/responses/E400\"\n          },\n          \"401\": {\n            \"$ref\": \"#/components/responses/E401\"\n          },\n          \"403\": {\n            \"$ref\": \"#/components/responses/E403\"\n          },\n          \"404\": {\n            \"$ref\": \"#/components/responses/E404\"\n          },\n          \"408\": {\n            \"$ref\": \"#/components/responses/E408\"\n          },\n          \"410\": {\n            \"$ref\": \"#/components/responses/E410\"\n          },\n          \"413\": {\n            \"$ref\": \"#/components/responses/E413\"\n          },\n          \"429\": {\n            \"$ref\": \"#/components/responses/E429\"\n          },\n          \"503\": {\n            \"$ref\": \"#/components/responses/E503\"\n          },\n          \"504\": {\n            \"$ref\": \"#/components/responses/E504\"\n          }\n        }\n      }\n    },\n    \"/caveman/v1/middleware/receipts\": {\n      \"post\": {\n        \"operationId\": \"recordReceipt\",\n        \"summary\": \"Record a content-free call observation.\",\n        \"description\": \"Aggregated or retention-bounded; never creates rows that outlive retention_seconds. Deduplicated on (logical_call_id, attempt_id, event_kind): the first write wins, and a retry, even with a different body, gets the same 200. A scope revoked by sessions/delete answers 410 deleted.\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/Features\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Client\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Traceparent\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Tracestate\"\n          }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"../schemas/middleware-receipt.schema.json\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Observation accepted.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"../schemas/middleware-receipt-response.schema.json\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"$ref\": \"#/components/responses/E400\"\n          },\n          \"401\": {\n            \"$ref\": \"#/components/responses/E401\"\n          },\n          \"403\": {\n            \"$ref\": \"#/components/responses/E403\"\n          },\n          \"408\": {\n            \"$ref\": \"#/components/responses/E408\"\n          },\n          \"409\": {\n            \"$ref\": \"#/components/responses/E409\"\n          },\n          \"410\": {\n            \"$ref\": \"#/components/responses/E410\"\n          },\n          \"413\": {\n            \"$ref\": \"#/components/responses/E413\"\n          },\n          \"429\": {\n            \"$ref\": \"#/components/responses/E429\"\n          },\n          \"503\": {\n            \"$ref\": \"#/components/responses/E503\"\n          },\n          \"504\": {\n            \"$ref\": \"#/components/responses/E504\"\n          }\n        }\n      }\n    },\n    \"/caveman/v1/middleware/sessions/delete\": {\n      \"post\": {\n        \"operationId\": \"deleteSession\",\n        \"summary\": \"Revoke a scope and delete every original it owns.\",\n        \"description\": \"Idempotent. An unknown scope answers 200 with zero counts. Runs on the retrieve queue and limits.retrieve_deadline_ms. The revocation commits first, so later use of the scope is 410 deleted at once; the scope's content is then deleted in bounded batches. A delete that fails or runs out of time (including a Postgres write conflict) answers 503 runtime_unavailable or 504 deadline, never 200 and never 409; retrying resumes where it stopped, and the retry's counts cover only what it removed.\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/Features\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Client\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Traceparent\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Tracestate\"\n          }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"../schemas/middleware-session-delete.schema.json\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Scope revoked.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"../schemas/middleware-session-delete-response.schema.json\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"$ref\": \"#/components/responses/E400\"\n          },\n          \"401\": {\n            \"$ref\": \"#/components/responses/E401\"\n          },\n          \"403\": {\n            \"$ref\": \"#/components/responses/E403\"\n          },\n          \"408\": {\n            \"$ref\": \"#/components/responses/E408\"\n          },\n          \"413\": {\n            \"$ref\": \"#/components/responses/E413\"\n          },\n          \"429\": {\n            \"$ref\": \"#/components/responses/E429\"\n          },\n          \"503\": {\n            \"$ref\": \"#/components/responses/E503\"\n          },\n          \"504\": {\n            \"$ref\": \"#/components/responses/E504\"\n          }\n        }\n      }\n    }\n  },\n  \"components\": {\n    \"securitySchemes\": {\n      \"bearer\": {\n        \"type\": \"http\",\n        \"scheme\": \"bearer\",\n        \"description\": \"Runtime credential (static token or OIDC JWT). Never a model provider API key.\"\n      },\n      \"mtls\": {\n        \"type\": \"mutualTLS\",\n        \"description\": \"Client certificate; the principal comes from the certificate SAN.\"\n      }\n    },\n    \"parameters\": {\n      \"Features\": {\n        \"name\": \"Caveman-Middleware-Features\",\n        \"in\": \"header\",\n        \"required\": false,\n        \"description\": \"Comma-separated client features (for example `http_status_v2, revision_tolerant`). Absent means a protocol 1.0 client.\",\n        \"schema\": {\n          \"type\": \"string\",\n          \"maxLength\": 1024\n        }\n      },\n      \"Client\": {\n        \"name\": \"Caveman-Middleware-Client\",\n        \"in\": \"header\",\n        \"required\": false,\n        \"description\": \"Space-separated product tokens, for example `caveman-sdk-typescript/1.2.0`. Informational only.\",\n        \"schema\": {\n          \"type\": \"string\",\n          \"maxLength\": 256\n        }\n      },\n      \"Traceparent\": {\n        \"name\": \"traceparent\",\n        \"in\": \"header\",\n        \"required\": false,\n        \"description\": \"W3C Trace Context.\",\n        \"schema\": {\n          \"type\": \"string\"\n        }\n      },\n      \"Tracestate\": {\n        \"name\": \"tracestate\",\n        \"in\": \"header\",\n        \"required\": false,\n        \"description\": \"W3C Trace Context.\",\n        \"schema\": {\n          \"type\": \"string\"\n        }\n      }\n    },\n    \"headers\": {\n      \"RetryAfter\": {\n        \"description\": \"Delay in whole seconds (>= 1).\",\n        \"schema\": {\n          \"type\": \"integer\",\n          \"minimum\": 1\n        }\n      }\n    },\n    \"responses\": {\n      \"E400\": {\n        \"description\": \"invalid_request, unsupported_version, unknown_capability or invalid_range (legacy clients get 413 payload_limit for an out-of-range retrieve limit).\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      },\n      \"E401\": {\n        \"description\": \"unauthorized: missing or unknown runtime credential.\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      },\n      \"E403\": {\n        \"description\": \"forbidden_origin (browser request) or forbidden_namespace (principal may not use scope.namespace).\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      },\n      \"E404\": {\n        \"description\": \"not_found: unknown or foreign recovery handle.\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      },\n      \"E408\": {\n        \"description\": \"request_timeout: the request body did not arrive before the deadline. Legacy clients: 413 payload_limit.\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      },\n      \"E409\": {\n        \"description\": \"epoch_changed (optimize only, protocol 1.0 clients only: the context manifest is not an extension of the stored one, or sequence went backwards) or identity_conflict (retryable: an optimize replayed under the same idempotency_key with a different body, or a concurrent-writer conflict such as a Postgres deadlock or serialization failure on optimize or receipts).\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      },\n      \"E410\": {\n        \"description\": \"deleted (scope revoked by sessions/delete; receipts included) or expired (scope past retention or max retention).\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      },\n      \"E413\": {\n        \"description\": \"payload_limit: the request body exceeds a size limit.\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      },\n      \"E429\": {\n        \"description\": \"capacity (queue saturated) or quota_exceeded (per-principal quota_requests_per_minute). Legacy clients: a queue wait past the deadline is 504 deadline, and quota_exceeded is 503 capacity.\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        },\n        \"headers\": {\n          \"Retry-After\": {\n            \"$ref\": \"#/components/headers/RetryAfter\"\n          }\n        }\n      },\n      \"E503\": {\n        \"description\": \"runtime_unavailable (storage or dependency outage, a failed sessions/delete, or no middleware store mounted). retrieve also uses recovery_unavailable. Legacy clients also get 503 for optimize decisions (not_smaller, cache_state_unavailable, recovery_unavailable, capacity) and for quota_exceeded (as capacity).\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        },\n        \"headers\": {\n          \"Retry-After\": {\n            \"$ref\": \"#/components/headers/RetryAfter\"\n          }\n        }\n      },\n      \"E504\": {\n        \"description\": \"deadline: the runtime could not finish before its deadline.\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n","packages/subagent-tax/README.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=packages%2Fsubagent-tax%2FREADME.md -->","plugins/caveman/.codex-plugin/plugin.json":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=plugins%2Fcaveman%2F.codex-plugin%2Fplugin.json -->","plugins/caveman/skills/cavecrew/SKILL.md":"---\nname: cavecrew\ndescription: >\n  When to delegate to `cavecrew-investigator` (locate code), `cavecrew-builder`\n  (1-2 file edit) or `cavecrew-reviewer` (diff review) instead of working inline\n  or using `Explore`. Their output is compressed, so main context lasts longer.\n---\n\nCavecrew = three subagent presets that answer in ultracave voice (tool runs: one status line in, one out; nothing between routine calls). Same job as Anthropic defaults (`Explore`, edit-style agents, reviewer); difference is the tool-result they return is compressed, so main context shrinks per delegation.\n\n## When to use cavecrew vs alternatives\n\n| Task | Use |\n|---|---|\n| \"Where is X defined / what calls Y / list uses of Z\" | `cavecrew-investigator` |\n| Same but you also want suggestions/architecture commentary | `Explore` (vanilla) |\n| Surgical edit, ≤2 files, scope obvious | `cavecrew-builder` |\n| New feature / 3+ files / cross-cutting refactor | Main thread, or `feature-dev:code-architect` if installed |\n| Review diff, branch, or file for bugs | `cavecrew-reviewer` |\n| Deep code review with rationale + alternatives | `Code Reviewer` (vanilla) if installed, else main thread |\n| One-line answer you already know | Main thread, no subagent |\n\nRule of thumb: **if you'd want the subagent's output in 1/3 the tokens, pick cavecrew. If you'd want prose, pick vanilla.**\n\n## Why this exists (the real win)\n\nSubagent tool results get injected into main context verbatim. A vanilla `Explore` that returns 2k tokens of prose costs 2k tokens of main-context budget every time. The same finding from `cavecrew-investigator` comes back as a compressed `path:line` table instead. Across 20 delegations in one session that's the difference between context exhaustion and finishing the task.\n\n## Output contracts\n\nWhat main thread can rely on per agent:\n\n**`cavecrew-investigator`**\n```\n<Header>:\n- path:line — `symbol` — short note\ntotals: <counts>.\n```\nOr `No match.` Always file-path-first, line-number-attached, backticked symbols. Safe to grep with `path:\\d+`.\n\n**`cavecrew-builder`**\n```\n<path:line-range> — <change ≤10 words>.\nverified: <re-read OK | mismatch @ path:line>.\n```\nOr one of: `too-big.` / `needs-confirm.` / `ambiguous.` / `regressed.` (terminal first token).\n\n**`cavecrew-reviewer`**\n```\npath:line: <emoji> <severity>: <problem>. <fix>.\ntotals: N🔴 N🟡 N🔵 N❓\n```\nOr `No issues.` Findings sorted file → line ascending.\n\n## Chaining patterns\n\n**Locate → fix → verify** (most common):\n1. `cavecrew-investigator` returns site list.\n2. Main thread picks 1-2 sites, hands paths to `cavecrew-builder`.\n3. `cavecrew-reviewer` audits the diff.\n\n**Parallel scout** (when investigation is broad):\nSpawn 2-3 `cavecrew-investigator` calls in one message (different angles: defs vs callers vs tests). Aggregate in main thread.\n\n**Single-shot edit** (when site is already known):\nSkip investigator. Hand exact path:line to `cavecrew-builder` directly.\n\n## What NOT to do\n\n- Don't use `cavecrew-builder` when you don't already know the file. Spawn investigator first or main thread will eat tokens passing context.\n- Don't chain `cavecrew-investigator → cavecrew-builder` for a 5-file refactor. Builder will return `too-big.` and you'll have wasted a turn.\n- Don't ask `cavecrew-reviewer` for \"general feedback\" — it returns findings only, no architecture opinions. Use `Code Reviewer` for that when installed.\n- Don't expect prose. Cavecrew output is structured, sometimes terse to the point of cryptic. If a human will read it directly, paraphrase.\n\n## Auto-clarity (inherited)\n\nSubagents drop caveman → normal English for security warnings, irreversible-action confirmations, and any output where fragment ambiguity could be misread. Resume caveman after.\n","plugins/caveman/skills/caveman-compress/SKILL.md":"---\nname: caveman-compress\ndescription: >\n  Compress a memory file such as CLAUDE.md or a todo list into caveman format\n  to save input tokens, keeping a readable backup. Sends the file's text to the\n  configured model provider. Trigger: /caveman-compress.\n---\n\n# Caveman Compress\n\n## Purpose\n\n> **Data leaves the machine.** Compression sends the file's full text to the model provider that `scripts/compress.py` is configured for: the `claude` CLI, the Anthropic API, or an OpenAI-compatible endpoint. Files whose names look like secrets (`.env`, `credentials`, `secrets`, `.netrc`, …) are refused before they are read. Don't compress a file the provider shouldn't see.\n\nCompress natural language files (CLAUDE.md, todos, preferences) into caveman-speak to reduce input tokens. Compressed version overwrites original. Human-readable backup saved as `<filename>.original.md`, but NOT beside the source file — it lives in an out-of-tree data dir (`$XDG_DATA_HOME/caveman-compress/backups/<parent-dir-name>/`, or `%LOCALAPPDATA%\\caveman-compress\\backups\\<parent-dir-name>\\` on Windows) so skill auto-loaders don't re-ingest it as a live file.\n\n## Trigger\n\n`/caveman-compress <filepath>` or when user asks to compress a memory file.\n\n**Requires an explicit filepath.** If the user pastes raw text instead of naming a file, do NOT improvise an inline compression — ask them for the filepath to compress, or offer to save the pasted text to a file first. Writing your own reasoning/commentary about the request into the compressed output (e.g. \"User passed text directly, not a filepath...\") is the exact failure this guards against — never let that leak into a saved file.\n\n## Process\n\n1. The compression scripts live in `scripts/` (adjacent to this SKILL.md). If the path is not immediately available, search for `scripts/__main__.py` next to this SKILL.md.\n\n2. From the directory containing this SKILL.md, run:\n\npython3 -m scripts <absolute_filepath>\n\n3. The CLI will:\n- detect file type (no tokens)\n- call Claude to compress\n- validate output (no tokens)\n- if errors: cherry-pick fix with Claude (targeted fixes only, no recompression)\n- retry up to 2 times\n- if still failing after 2 retries: report error to user, leave original file untouched\n\n4. Return result to user\n\n## Compression Rules\n\n### Remove\n- Articles: a, an, the\n- Filler: just, really, basically, actually, simply, essentially, generally\n- Pleasantries: \"sure\", \"certainly\", \"of course\", \"happy to\", \"I'd recommend\"\n- Hedging: \"it might be worth\", \"you could consider\", \"it would be good to\"\n- Redundant phrasing: \"in order to\" → \"to\", \"make sure to\" → \"ensure\", \"the reason is because\" → \"because\"\n- Connective fluff: \"however\", \"furthermore\", \"additionally\", \"in addition\"\n\n### Preserve EXACTLY (never modify)\n- Code blocks (fenced ``` and indented)\n- Inline code (`backtick content`)\n- URLs and links (full URLs, markdown links)\n- File paths (`/src/components/...`, `./config.yaml`)\n- Commands (`npm install`, `git commit`, `docker build`)\n- Technical terms (library names, API names, protocols, algorithms)\n- Proper nouns (project names, people, companies)\n- Dates, version numbers, numeric values\n- Environment variables (`$HOME`, `NODE_ENV`)\n\n### Preserve Structure\n- All markdown headings (keep exact heading text, compress body below)\n- Bullet point hierarchy (keep nesting level)\n- Numbered lists (keep numbering)\n- Tables (compress cell text, keep structure)\n- Frontmatter/YAML headers in markdown files\n\n### Compress\n- Use short synonyms: \"big\" not \"extensive\", \"fix\" not \"implement a solution for\", \"use\" not \"utilize\"\n- Fragments OK: \"Run tests before commit\" not \"You should always run tests before committing\"\n- Drop \"you should\", \"make sure to\", \"remember to\" — just state the action\n- Merge redundant bullets that say the same thing differently\n- Keep one example where multiple examples show the same pattern\n\nCRITICAL RULE:\nAnything inside ``` ... ``` must be copied EXACTLY.\nDo not:\n- remove comments\n- remove spacing\n- reorder lines\n- shorten commands\n- simplify anything\n\nInline code (`...`) must be preserved EXACTLY.\nDo not modify anything inside backticks.\n\nIf file contains code blocks:\n- Treat code blocks as read-only regions\n- Only compress text outside them\n- Do not merge sections around code\n\n## Pattern\n\nOriginal:\n> You should always make sure to run the test suite before pushing any changes to the main branch. This is important because it helps catch bugs early and prevents broken builds from being deployed to production.\n\nCompressed:\n> Run tests before push to main. Catch bugs early, prevent broken prod deploys.\n\nOriginal:\n> The application uses a microservices architecture with the following components. The API gateway handles all incoming requests and routes them to the appropriate service. The authentication service is responsible for managing user sessions and JWT tokens.\n\nCompressed:\n> Microservices architecture. API gateway route all requests to services. Auth service manage user sessions + JWT tokens.\n\n## Boundaries\n\n- ONLY compress natural language files (.md, .txt, .typ, .typst, .tex, extensionless)\n- NEVER modify: .py, .js, .ts, .json, .yaml, .yml, .toml, .env, .lock, .css, .html, .xml, .sql, .sh\n- If file has mixed content (prose + code), compress ONLY the prose sections\n- If unsure whether something is code or prose, leave it unchanged\n- Original file is backed up as FILE.original.md before overwriting — in the out-of-tree backup data dir (see Purpose), not beside the source file\n- Never compress FILE.original.md (skip it)\n","plugins/caveman/skills/caveman-stats/SKILL.md":"---\nname: caveman-stats\ndescription: >\n  Show recorded output and cache-read token usage and mode attribution for\n  the current Claude Code session, or locate the host's native usage report.\n  Trigger: /caveman-stats.\n---\n\nIn Claude Code, `src/hooks/caveman-mode-tracker.js` runs `src/hooks/caveman-stats.js` on `/caveman-stats` and supplies its report through `hookSpecificOutput.additionalContext`. Print that report verbatim inside a fenced code block, as the hook instructs. Do not calculate or add numbers yourself.\n\nIn Gemini CLI, direct the user to `/stats model` for current session token usage or `/stats session` for session statistics. Gemini custom commands are prompts; they cannot invoke the built-in command or read its live session metrics. Never read Claude Code transcripts as Gemini usage. In other hosts, use a native usage report if one is available; otherwise say that current session usage is unavailable. The Claude reader and its lifetime history apply only to Claude Code. Savings remain unknown in every host without a measured comparison.\n\nThe report shows recorded output and cache-read tokens, response counts, and mode attribution where available. Savings are unknown: the transcript has no measured comparison without Caveman. Do not infer saved tokens, percentages, dollars, rule overhead, or a net result from output counts or the current mode.\n\n`--all` and `--since 7d` aggregate the latest recorded output count per session. `--share` reports observed usage with savings unknown. Historical `est_saved_*` fields are ignored; their original history rows remain on disk. The statusline shows the active mode without the retired savings badge.\n\nOriginal/current memory-file pairs are reported by their measured byte sizes. Those file-size differences do not establish provider token or billing savings. See `docs/HONEST-NUMBERS.md`.\n","plugins/caveman/skills/caveman/SKILL.md":"---\nname: caveman\ndescription: >\n  Terse caveman voice: answer first, fluff gone, every technical fact kept.\n  Use for /caveman, \"caveman mode\", \"talk like caveman\", \"be brief\", \"less\n  tokens\". Stays on until \"stop caveman\" or \"normal mode\".\n---\n\n# caveman\n\nRespond terse like smart caveman. All technical substance stay. Only fluff die.\n\nCaveman is a voice, not broken grammar. Reader pays per token and reads in a terminal. Every word earns its place. Every fact survives.\n\n## Persistence\n\nEvery response, whole session, until user says \"stop caveman\" or \"normal mode\". Unsure if still on? It is. Confirm the switch-off in one line.\n\n`/caveman ultra` and `/caveman wenyan` are aliases: follow the `ultracave` or `megacave` skill instead of this one. `/caveman status` reports the mode and changes nothing. Relay the hook's `Caveman mode: <mode>` value when present. No hook value (host without hooks): report the mode you followed before this command, or `off` if caveman was turned off or never active, plus `(not tracked by this host)`. Example: `Caveman mode: caveman (not tracked by this host)`. Loading this skill to answer status is not activation. Never infer a mode from the configured default.\n\n## Why\n\n1. Every output token is billed and read. Filler costs twice.\n2. Code, commands, paths, numbers, errors are the payload. One changed character breaks them.\n3. Ceremony is expensive, grammar is cheap. \"Sure, I'd be happy to help\" is ten tokens. \"the\" is one.\n4. A dropped negation costs more than every token saved. Clarity beats compression.\n\n## Rules\n\n### 1. Answer first\n\nAnswer, then reason, then next step. Pattern: `[thing] [action] [reason]. [next step].`\n\nBad: \"Sure! I'd be happy to help. The issue you're experiencing is likely caused by...\"\nGood: \"Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:\"\n\n### 2. Kill ceremony\n\nNo greeting, hedging, pleasantries, recap, or closer. No \"Sure!\", \"Let me\", \"I'll now\", \"Hope this helps\". No just/really/basically/actually/simply.\n\n### 3. Short word\n\n\"fix\" not \"implement a solution for\". Standard acronyms fine (DB, API, HTTP). Invented abbreviations not (cfg, impl, fn): same tokens, harder read. No arrows.\n\n### 4. Articles optional, meaning never\n\nDrop a/an/the when the sentence still reads in one pass. Fragments fine. Never drop not/never/no/only/except. Numbers and units exact.\n\nBad: \"Migration drop column backup first.\"\nGood: \"Back up first. Then run migration: it drops the column.\"\n\n### 5. One idea per sentence\n\nASD-STE100 is the floor: 20 words max, active voice, imperative for instructions, one term per thing, pronoun only with an obvious referent. Compression and clarity conflict? Clarity wins.\n\n### 6. Payload verbatim\n\nCode blocks unchanged. Commands, paths, API names exact. Errors quoted exact, shortest decisive line only. Code change shown in chat: changed lines plus 1-2 lines of context, not the whole file. Whole file only if the user asks, the file is new, or most of it changes. Existing comments in files you edit are payload too: never delete or shorten one you were not asked to change.\n\n### 7. Tool runs: bounded status\n\nNo text between routine calls. One line before a multi-step run, one line per phase change, one line with the result at the end. Otherwise text before a call only to clarify, warn, or disambiguate.\n\n### 8. User's language\n\nCompress the style, not the language. An explicit reply-language instruction wins. Never switch because of quoted text. Technical terms and errors stay verbatim. Particles and case markers are grammar, not filler.\n\n### 9. Never perform caveman\n\nNo \"caveman mode on\", no \"me think\", no \"Caveman:\" prefix, no normal answer plus caveman copy. No decorative tables or emoji. Never add a word to sound caveman. Caveman phrasing not shorter than plain? Use plain.\n\n## When to break the rules\n\nPlain prose, then resume:\n\n1. Security warning.\n2. Irreversible action. Confirm in full sentences first.\n3. Step order a fragment could scramble.\n4. User confused or repeats the question.\n5. Anything persisted outside chat: code, comments, commits, docs, issues, PRs, tickets, memory files, third-party messages. `/caveman-compress` exempt.\n6. Harness asks for a status line or confirmation. Give it. Harness decides *when* you speak, caveman decides *how*.\n7. You ask the user a question or offer options. Full sentences, so the answer comes back right first time.\n\n## Pre-send check\n\n1. First sentence announces what you will do? Delete.\n2. Last sentence recaps or offers help? Delete.\n3. Every not/never/no/only present? Every code span, path, number, error verbatim?\n4. Any sentence with two readings? Make it a full sentence.\n","plugins/caveman/skills/caveman/agents/openai.yaml":"interface:\n  display_name: \"Caveman\"\n  short_description: \"Talk like caveman. Cut filler. Keep technical accuracy.\"\n  icon_small: \"./assets/caveman-small.svg\"\n  icon_large: \"./assets/caveman.svg\"\n  default_prompt: \"Use $caveman to answer briefly, cut filler, and preserve exact technical substance.\"\n","plugins/caveman/skills/megacave/SKILL.md":"---\nname: megacave\ndescription: >\n  Caveman in Classical Chinese: 文言文 register, far fewer characters, technical\n  terms verbatim. Invoke only with /megacave or /caveman wenyan. Stays on until\n  \"stop caveman\" or \"normal mode\".\ndisable-model-invocation: true\n---\n\n# megacave\n\n以文言答。技術之實皆存，唯贅言去之。\n\nMegacave is caveman in Classical Chinese. 文言文: subjects omitted where recoverable, verb before object, particles (之, 乃, 為, 其, 則, 故, 以) instead of connective phrases. Fewer characters on screen. Token savings are small and noisy; never claim them.\n\n## Persistence\n\nEvery response, whole session, until \"stop caveman\" or \"normal mode\". Unsure? Still on. `/caveman status` reports the mode and changes nothing: relay the hook's `Caveman mode: <mode>` value; with no hook, the mode you were in (or `off`) plus `(not tracked by this host)`.\n\n## Floor\n\nNever cut or translate: code, commands, paths, API names, error strings, in their original script. Negation (不, 非, 勿, 未, 毋). Numbers and units, Arabic numerals. One term per thing. A clause ambiguous in 文言 becomes 白話. Existing comments in files you edit: never delete or shorten unless asked.\n\n## Rules\n\n### 1. Classical register\n\n文言, not 白話. Verb before object. Subjects omitted where recoverable.\n\nBad: \"你的組件每次渲染都會重新渲染，因為你每次都創建了新的對象引用。請用 useMemo 包起來。\"\nGood: \"每繪新生對象參照，故重繪；以 `useMemo` 包之則免。\"\n\n### 2. Particles carry structure\n\nOne particle, one relation.\n\nBad: \"因為連接池會重複使用已經打開的連接，所以不需要每次請求都新建連接，這樣就省去了握手的開銷。\"\nGood: \"池蓄已開之連，不逐請而新開，省握手之費。\"\n\n### 3. Each fact once\n\nCode change: changed lines plus context, not the whole file.\n\nGood: \"新參照則重繪。`useMemo` 包之。\"\n\n### 4. Payload in original script\n\nBad: \"以記憶化鉤子包之。\"\nGood: \"以 `useMemo` 包之。\"\n\n### 5. Tool runs\n\nOne 文言 line in, one out. Nothing between calls unless the harness asks or a security or irreversible step is ahead.\n\n### 6. Never perform\n\nNo \"文言模式啟\", no modern answer plus classical copy. 文言 not shorter than 白話 for a sentence? Use 白話.\n\n## When to break the rules\n\n白話 or the user's language, full sentences, then resume: security warning. Irreversible action, confirm first. Any clause with two readings. User confused. Question to the user, with its options. Anything persisted outside chat (code, comments, commits, docs, issues, PRs, tickets, memory, third-party messages; `/caveman-compress` exempt). Harness asks for a status line.\n\n## Pre-send check\n\nIdentifiers verbatim, original script? Negations present? Two readings? 白話. Flavor character? Cut.\n","plugins/caveman/skills/ultracave/SKILL.md":"---\nname: ultracave\ndescription: >\n  Caveman at maximum compression: fragments, one word when one word is enough,\n  each fact once. Invoke only with /ultracave or /caveman ultra. Stays on until\n  \"stop caveman\" or \"normal mode\".\ndisable-model-invocation: true\n---\n\n# ultracave\n\nRespond terse like smart caveman. All technical substance stay. Only fluff die. Then cut again.\n\nUltracave is caveman with the grammar stripped. Payload only.\n\n## Persistence\n\nEvery response, whole session, until \"stop caveman\" or \"normal mode\". Unsure? Still on. `/caveman status` reports the mode and changes nothing: relay the hook's `Caveman mode: <mode>` value; with no hook, the mode you were in (or `off`) plus `(not tracked by this host)`.\n\n## Floor\n\nNever cut: code, commands, paths, API names, error strings (verbatim). not/never/no/only/except. Numbers and units. The user's language. One term per thing. No invented abbreviations, no arrows. Existing comments in files you edit: never delete or shorten unless asked.\n\n## Rules\n\n### 1. Fragments\n\nDrop articles, copulas, connectives when order stays clear.\n\nBad: \"The component re-renders because an inline object prop creates a new reference each render.\"\nGood: \"Inline object prop, new ref, re-render. `useMemo`.\"\n\n### 2. One word when one word is enough\n\nBad: \"Yes, that should work, though you'll want a null check first.\"\nGood: \"Yes. Null check first.\"\n\n### 3. Each fact once\n\nNo restating, no summary after a list. Code change: changed lines plus context, not the whole file.\n\nGood: \"Pool reuses open DB connections. No per-request handshake.\"\n\n### 4. Cut conjunctions only when order survives\n\nBad: \"Migrate table drop column backup first.\"\nGood: \"Back up first. Then migrate: drops column.\"\n\n### 5. Tool runs\n\nOne line in, one line out. Nothing between calls unless direction changes, confirmation needed, or a security or irreversible step is ahead.\n\n### 6. Never perform\n\nNo prefix, no announcement, no mangled verbs for flavor. Fragment not shorter than the sentence? Use the sentence.\n\n## When to break the rules\n\nPlain prose, then resume: security warning. Irreversible action, confirm first. Any fragment with two readings. User confused. Question to the user, with its options. Anything persisted outside chat (code, comments, commits, docs, issues, PRs, tickets, memory, third-party messages; `/caveman-compress` exempt). Harness asks for a status line.\n\n## Pre-send check\n\nTwo readings? Full sentence. Negations present? Payload verbatim? Flavor word? Cut.\n","proxy/AGENTS.md":"# proxy — the byte-safe standalone `caveman` proxy (commercial, binary-distributed)\n\nA base-URL-swap reverse proxy: match → authenticate → inspect → byte-safe transform → upstream\n→ meter. Single-operator, BYOK, **zero cloud dependencies**. It shares its provider adapters with\nthe managed gateway (the managed gateway imports them from here). `caveman start` launches the\n`caveman-proxy` binary.\n\n## Layout\n- `providers/` — the shared, public byte-safe adapter set: `Adapter` interface + `Base` embed + `UsageScanner`/`ParseUsageBytes` (`adapter.go`), and `anthropic`/`openai`/`gemini`/`azureopenai`/`bedrock`/`vertex`/`openaicompat`. `ResolveUpstreamURL` takes `providers.RouteContext` (no control-plane coupling). Anthropic + OpenAI carry both prefixed and bare routes (`/v1/messages`, `/v1/chat/completions`). `vertex` preserves caller OAuth bearer or Express-mode Google API-key credentials for Gemini + Claude on Vertex AI (no signing, no environment-key fallback, no custom usage parser). `MatchRoute` is POST-only; read-only model discovery (`GET /v1/models`, `/v1/models/{id}`) is a separate GET-only allowlist, `Base.MetadataRoutes`, declared by `openai`/`anthropic`/`openaicompat` and forwarded unchanged with no body, no transform and no spend row. Bare `/v1/models` belongs to both `openai` and `anthropic`, so they implement `MetadataRequestMatcher` and choose by wire protocol (`anthropic-version` or `x-api-key` present → anthropic; a Google key matches neither), never by registration order (#1187).\n- `internal/gateway/` — the request lifecycle (`server.go` + `proxy.go`) behind three injected seams: `Authenticator`, `CredentialResolver`, `TelemetrySink`. Ports the managed loop with the fail-open fix. `serve` sets `Config.AsyncRecord`: a row's request token count, sink write (batched) and Cloud observe run after the handler returns, in record order (`recorder.go`). A reader that must see the request it follows calls `Server.Flush` first (session receipts do); close the server before the sink.\n- `internal/config/` — `caveman.yaml` loader + BYOK env-key resolution; unknown mode fails closed to `record`.\n- `internal/store/` — `~/.caveman/caveman.db` SQLite spend store (`modernc.org/sqlite`, cgo-free); implements `TelemetrySink` and `BatchSink`. `RecordBatch` commits request rows with `synchronous=NORMAL` on its own connection (WAL): a power loss or OS crash can drop the last batches, a process crash or clean exit loses none; every other table keeps FULL.\n- `internal/standalone/` — wiring: static `Auth`, BYOK `Creds`, adapter set, and the always-on SSRF-guarded client.\n- `internal/identity/` — who calls the framework middleware routes (legacy token, token map, OIDC/JWT, mTLS) and the reloadable TLS listener config.\n- `cmd/caveman-proxy/` — binary: `serve` (default), `stats`, and content-blind\n  `agent-evidence --session --build --plan`. Evidence query returns only exact\n  provider usage, request hashes, declared context/plan identity, ordered\n  provider-prefix component hashes, actual transform IDs/counts, and CCR handle;\n  basis is always `inferred`, verified dollars always zero.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=proxy` / `make product-test PRODUCT=proxy`.\n- Tests inject a plain `*http.Client` to reach loopback stubs; the binary uses the SSRF-guarded client.\n- New provider/optimizer work goes in `providers/` (shared) — change it once, both proxies get it.\n\n## Gotchas (honesty invariants — correctness, not style)\n\n- **listener lifetime is not session lifetime**: `serve` never exits because a\n  wrapper, native session, heartbeat, or idle timer expires. Legacy\n  `CAVEMAN_NATIVE_IDLE_TIMEOUT` does not arm shutdown. The CLI never restarts a\n  shared listener to change mode/recovery; an incompatible new wrap runs direct.\n  Failed local startup also runs direct. Native session correlation entries may\n  age out without affecting API traffic.\n- **no default generation deadline**: `CAVE_GATEWAY_UPSTREAM_TIMEOUT_MS` defaults\n  to `0` (no total request deadline). A positive value is an explicit operator\n  cap and includes response streaming. Client cancellation still cancels upstream;\n  connection setup, inbound header/upload limits, and idle keep-alive socket\n  cleanup remain bounded separately. The replacement bound for an upstream that\n  connects and then goes silent is the transport's response-header deadline\n  (`CAVE_GATEWAY_RESPONSE_HEADER_TIMEOUT_MS`, default 900000, `0` disables) —\n  headers only, so it never truncates a live stream.\n- **response protocol controls streaming**: SSE/event-stream responses flush\n  headers and chunks even without a JSON `stream` flag. Encoded requests keep\n  `Content-Encoding` and bypass transforms. Interrupted response copies record\n  an error and abort HTTP framing; they never become a clean successful EOF.\n- **replay only when non-delivery is proven**: proxy transport retries are\n  limited to connection-setup failures — `*net.OpError` with `Op` `dial` or\n  `proxyconnect` (a proxy that is down fails as the latter, never as a dial). A failed upload/header read or truncated response\n  does not prove an inference was unprocessed; do not automatically replay it.\n  An explicit transformed-request 4xx still retries once with original bytes —\n  except a 429 that carries `Retry-After` (a rate limit; Anthropic sends its\n  `anthropic-ratelimit-*` headers on every response, so they prove nothing),\n  which is returned as-is; an accepted retry pins the conversation raw\n  (`internal/gateway/raw_pin.go`).\n  See `docs/technical/proxy-reliability.md` at the repository root.\n- **byte-safe**: `record` mode never transforms; on transform error the ORIGINAL bytes are forwarded (HTTP 200, fail-open) — never a 400. The one carve-out: bytes the provider already cached in replaced form are re-sent replaced, because forwarding the original there busts the prefix (see cache safety).\n- **request-wide opt-out**: `x-cave-transforms: caveman.pass-through.v1` suppresses every request transform path — compress, pixel, and provider-native — not only compiled plan routes. Tests cover all three modes.\n- **no-fake-savings**: standalone records `Basis: \"inferred\"` on every row; it never writes `verified` and never re-projects to a monthly figure.\n- **practice join**: local learn sinks carry additive `practice_id`; one\n  fail-closed mapping table owns sink→practice and unknown sinks keep `\"\"`.\n  The historical `subagent_overuse` sink is count-only and deliberately has no\n  practice id: spawn count cannot reactivate the retired\n  `context-exploration-offload` opportunity or prove any spawn unnecessary.\n- **local trial heuristics are not actuation evidence**: a model name never emits\n  the retired `model-right-sizing` id, and provider plus positive cost never\n  emits a cache move because neither proves stable-prefix eligibility. Legacy\n  rows for those identities are hidden at read time. Compression replay reports\n  one trial's local engine `estimated_engine_o200k` before/after shape with zero\n  dollars and low confidence; it is not provider-counted, a rate, an invoice,\n  causal/verified savings, or task-outcome evidence.\n- **Anthropic automatic caching is experimental observation only**:\n  `anthropic-automatic-prompt-cache` is a typed, default-off manual policy\n  experiment and may add only Anthropic's top-level 5-minute marker on direct\n  Messages API requests. Managed traffic additionally requires server-attested\n  official Anthropic origin; custom or provenance-unknown origins lose the flag\n  before the adapter. It is mutually\n  exclusive with the explicit `anthropic-cache-breakpoints` transform and any\n  caller `cache_control`; Bedrock and count-tokens requests stay byte-identical.\n  An applied marker records only its optimizer id plus actual provider usage and\n  cost. It has no practice, recipe, generic mode/candidate activation, ledger\n  tuple, inferred savings, or verified-savings path (cache-only and forged IDs\n  are excluded from the counted-baseline method too); evaluate it by manual\n  paired observation because shared provider cache state can contaminate an A/B.\n- **SSRF always on**: `standalone.StandaloneHTTPClient` guards every upstream dial (not gated on `CAVE_ENV`) using `ssrf.SelfHostedConfig` — NOT ManagedConfig, which ignores the allowlist and would make the escape hatch a silent no-op. `CAVE_SSRF_ALLOWLIST` opts loopback/private hosts back in (local model servers like Ollama; `localhost` as an entry covers 127.0.0.0/8 + ::1); metadata/link-local stay blocked in every mode. Provider traffic honours `HTTPS_PROXY`/`NO_PROXY` by default (#1001) — `upstream_proxy: env|off|<url>` / `CAVE_UPSTREAM_PROXY` — via `ssrf.Config.Proxy`, which validates IP-literal/localhost destinations before proxy selection and dials the (operator-configured) proxy address unguarded; `ssrf.NewHTTPClient` itself stays direct unless a caller sets `Proxy`, and a managed-mode Config carrying one is refused loudly (every request fails with `ssrf.ErrProxyInManagedMode`) rather than silently dialing direct. Adapters that pre-flight a resolved endpoint (bedrock, vertex) read the selector off the request context (`providers.WithUpstreamProxy`, published by `gateway.Handler` from the upstream transport) and use `ssrf.ValidateURLNoResolve` for proxied destinations: a proxy-only network has no outbound DNS, so resolving there failed the request before the proxy was ever consulted. `ca_bundle` / `CAVE_CA_BUNDLE` plus inherited `SSL_CERT_FILE`/`REQUESTS_CA_BUNDLE`/`NODE_EXTRA_CA_CERTS` append private roots for TLS inspection through `shared/platform/cabundle` (fail-closed parser shared with chhttp).\n- **inbound token is consumed, never forwarded**: `CAVEMAN_AUTH_TOKEN` gates non-loopback listens (a middleware token map, OIDC issuer or TLS client CA also makes one legal; provider routes still accept only the token, and without it `Auth.closed` refuses every provider request off loopback); `standalone.Auth` deletes the matching `x-cave-api-key`/`Authorization` header before `Creds.Resolve` so the shared token can never be forwarded to a provider or classified as a provider credential; a bearer that is not the token survives untouched (Claude OAuth, `/chatgpt/`). `Authenticate` runs BEFORE `matchAdapter` so the 401/404 split cannot be used to enumerate routes, and every 401 goes through `Server.rejectUnauthorized`, which counts `cave_proxy_unauthorized_total` and warns with the path and remote host only. Health and the no-op `/caveman/keepalive` beacon stay unauthenticated, `/metrics` too unless `CAVEMAN_METRICS_TOKEN` is set; the `X-Caveman-Instance` header on `/health/live` is published only on a loopback bind. A token on a LOOPBACK listener is honored too (the local `caveman wrap` path sends none, so `runServe` warns). Bedrock credentials then come from `shared/platform/awscreds` (env → web identity → container → IMDSv2), never from the request; a PARTIAL env pair fails closed instead of falling through to an ambient role, and the winning source is logged once.\n- **middleware identity is server-side and per route**: `middleware.Config.Identify` resolves an `identity.Principal` from the credential alone (bearer decides when present, else a verified client certificate); `ServeHTTP`'s `decode` checks `Principal.Allows(scope.Namespace)` on EVERY scoped route, so a new route that skips `decode` skips authorization. The legacy token stays `single_operator` with every namespace; JWT and certificate principals may never be named `single_operator`. Token and hash comparisons are constant-time over every entry. A token map or TLS file that fails to reload keeps the previous one. Tests: `identity/*_test.go` (negative JWT and mTLS cases), `middleware/identity_test.go`.\n- **Postgres middleware store keeps SQLite's single-writer semantics per authority**: `store.PostgresMiddleware` serializes writers of one authority with a transaction-scoped advisory lock taken by `Scope` (then re-read), `SaveScope`, `Revoke`, `PurgeBatch`, `Receipt` and `SaveOriginal`; the sweep is one-replica (try-lock) and skips busy authorities and row-locked scopes. Counters are statement-level transition-table triggers striped by txid; a per-row trigger goes quadratic on bulk statements. Lock keys include `current_schema()`. Run the store suite against both backends with `CAVEMAN_TEST_POSTGRES_URL`; `middleware_concurrency_test.go` fails without the locks.\n- **Auth scheme is preserved**: a key from an inbound `Authorization: Bearer` keeps `Scheme:\"bearer\"` on the `providers.Credential`; Anthropic and Gemini forward bearer credentials as bearer credentials (Claude/Gemini OAuth breaks if remapped to an API-key header). BYOK env keys and inbound `x-api-key` keep provider API-key mapping.\n  On a named compat mount the header follows the wire protocol of the path:\n  `/compat/<name>/v1/messages` gets `x-api-key` plus a default\n  `anthropic-version`, every other path gets Bearer. A real inbound Bearer stays\n  Bearer on every path. OpenCode Go rejects Bearer\n  on `/v1/messages` (401 `Missing API key.`, 2026-09-03). The gateway env\n  fallback puts the key in the header that the adapter emitted, so the upstream\n  never gets two credentials.\n- **fail-closed**: unknown route → 404; unknown mode → `record`.\n- **subscription AND oauth compression is NOT account-gated**: non-PAYG sessions from Claude Code, Codex ChatGPT, Gemini CLI, and other routed clients take live-zone compression with no Caveman account, entitlement, or seat. `CAVEMAN_WRAP_ENTITLED` and every `WrapEntitled` field are **deleted**, not merely ignored — do not reintroduce them. Exactly four conditions remain, all technical and all fail-closed (`liveZoneCompressionAllowed`): the operator `subscription_compress` switch (empty/`live_zone` allow, `off` and any unknown value close it), the adapter must implement schema-aware `PrefixStabilizer` zones, recovery must run through the agent's own MCP `caveman_retrieve`, and a durable prefix cache must be wired. The dedicated Codex `/chatgpt/responses` route uses the OpenAI Responses stabilizer while preserving OAuth and `ChatGPT-Account-ID` headers; recovery is proven by the `CAVEMAN_RECOVERY=mcp` stamp or the decoded request's own `mcp__caveman__caveman_retrieve` (top-level `tools` or Codex's `input[]` `additional_tools`, flat or under an MCP namespace); zstd bodies decode → transform → re-encode, anything else forwards the original wire bytes, and the `chatgpt_proxy` log carries `skip_reason`; transformed 4xx responses retry once with exact original bytes (never a rate-limit 429; an accepted retry pins the conversation raw). Legacy savings fields remain **tokens-only** — `compression_tokens_before/after` + `estimated_engine_o200k`, never booked compression dollars. New request-comparison and price-snapshot fields support separately labeled API equivalents in local stats; those values never enter subscription spend or legacy saved-dollar fields. LOCAL wrap only; managed gateway non-PAYG behavior is unchanged.\n- **cache safety is byte-stable replacement**: THE INVARIANT (#1105): for accepted requests P then R of one conversation, if R's client bytes over P's cached prefix (system, tools and messages through P's last `cache_control` marker; the whole prompt for implicit caches) equal P's, R's forwarded bytes over that range equal P's, and every request caches at least what its client asked for (a dropped breakpoint is a bust too). Two exceptions, both re-caching the range raw: a request the provider rejected transformed and accepted through the raw retry, and a compressed PAYG conversation without MCP recovery that streams (the server-side retrieve tool cannot ride a stream, so the conversation stays raw from there). A raw retry splits a range into a raw and a replaced form, and a later request follows the LONGEST lineage it extends, ties going raw: it is held to the raw request (raw pin) unless it repeats a longer prefix that went out replaced, like a sibling subagent started with the same task. Pins are taken only on a conversation's own bytes (past system and tools); pins and lineages live in the PrefixCache (`rawpin` and `lineage` scopes, pin rows never evicted) and in memory. `internal/gateway/cache_prefix_invariant_test.go` (Anthropic compress, PAYG, pixel and Codex Responses scenarios + `FuzzCachePrefixInvariant`) proves it; keep it green. A compressed live-zone turn becomes prefix on the next request, so the replacement is a pure function of segment content (deterministic compressor + content-hash CCR marker), memoised in the durable replacement cache, and the FIRST decision for a block wins — replaced or raw (`RawDecisionHandle`); a request whose write finds a row on record sends that row, not its own candidate. A frozen block with no decision (a `--resume` history, an evicted row) goes out raw and raw becomes its decision; a failed memo write keeps the block raw for the process (`rawMemory`, consulted only on a store miss and written through once the store takes it); new rows are remembered only once the request's splice holds. Raw decisions are global and durable, a known savings cost: a block declined once (frozen at first sight, a compressor decline, a CCR failure, a non-newest parallel tool output) stays raw in every conversation. Bounding them needs a TTL of at least the longest provider retention (24 h for OpenAI extended caching) plus time-aware store interfaces, or per-conversation rows keyed on a stable conversation identity with a scope rollover. Substitution is never gated: the derived epoch gate is gone, and an explicit framework epoch may veto only NEW compression. Pixel renders and the tool-schema strip are memoised the same way. New compression stays live-zone-only. The runtime tripwire (`prefix_monitor.go`, on the provider routes and `/chatgpt`) checks the same rule on live traffic and records `requests.cache_bust_cause`: `client` (DEBUG); `raw_retry`, `stream_switch`, and `lever_freeze` (the harm tripwire's one-time strip rollover), all WARN; `caveman` (ERROR `caveman changed bytes the provider already cached` or `caveman dropped a cache breakpoint the client set`, counted as `caveman_cache_bust_requests` and raised by `caveman status`) — any `caveman` row is a bug. A request is held only to anchors accepted before its own forwarding decision. Cross-turn stability covers **anthropic, openai, azureopenai/openaicompat, and gemini** through `ExtractStabilizable`; `bedrock` and `vertex` expose no compressible blocks and stay pass-through. Anthropic uses declared `cache_control`; OpenAI/Gemini use latest-user/latest-tool zones against implicit provider caches. Replacement cache is SQLite spend store and must retain `journal_mode(WAL)` + `busy_timeout`; a hit rewrites `last_used_at` only once the row is 10 minutes stale, and pixel rows have their own cap inside the table's.\n- **cache warming is predicted per stream, and replays only what Anthropic can answer at zero output** (`internal/gateway/cache_warm.go`, `cache_warm_table.go`): after a successful Anthropic Messages response on `api.anthropic.com` (never Bedrock, Vertex, Azure, a compat mount, a custom base URL, the pass-through header or an encoded body), the exact bytes and headers sent upstream (after compression, routing marks, heals and fail-open retries) are kept in memory only (LRU 1024 streams and 256 MiB, never stored, logged or captured) and may be replayed with `max_tokens: 0` and `stream: false` at `min(0.9*ttl, ttl-10s)` steps from the real request's start (wall time, so laptop sleep never sends a stale warm). Not replayable, so never warmed: `thinking.type: \"enabled\"`, `tool_choice` `tool`/`any`, `output_config.format`, no `cache_control`, a `ttl` other than `5m`/`1h`, mixed `5m` and `1h` markers, writes of another lifetime than the markers (one lifetime or no warm; the simulator classes by the same rule and none of the measured requests mixed them); the request scan reads top-level fields and cache_control markers only, no full parse. WHETHER to warm is optimal stopping, not a threshold: `cache_warm_table.json` holds the return-time distribution (30 s bins to 2 h) per stream class `main|subagent / tool|end / 5m|1h` (the class comes from the session key and whether the answer's `stop_reason` was `tool_use`, scanned off the plain bytes with a rolling tail; the proxy forwards the agent's `accept-encoding`, so an answer that comes back with a `Content-Encoding` is not classed: it is planned as whichever of the two classes warms less and its gap teaches neither), measured on 364,048 request transitions of real Claude Code sessions (2026-08-30 to 2026-10-08, `.blocks/cache-warm-sim.py`, aggregate probabilities only) and shrunk toward the parent class; the proxy learns each user's own gaps online (counts only, `$CAVEMAN_HOME/cache-warm-gaps.json`, 0600, written outside the warmer's lock, flushed by `Server.Close` at shutdown, symlink-refused, any implausible count drops the whole file; two proxies on one home are last-writer-wins) and blends them in with the defaults worth 100 observations. A stream evicted or still waiting at shutdown is counted as returning later than it was seen silent (spread over the later bins by the class's own distribution, \"never\" past 2 h): dropping it would keep only the returns and drift the table toward over-warming. At arm time the whole chain is planned by backward induction with the catalog price of the model sent (warm = read x prefix + input x uncached tail; avoided miss = (write_ttl - read) x prefix x the class's measured realized share, taken over the gaps that DID return: the window probability already leaves out the streams that never do), so the cumulative cost of every warm is counted before the first; unknown price, class or lifetime = no warm. Subscription traffic is valued at list prices as plan-usage equivalent. Stops: any real request of the stream (every model; it never waits for a warm in flight), any warm error, a warm reading `cache_read_input_tokens == 0`, the plan, 60 min, or the switch; the held request (body and credential headers) is released the moment its chain ends. A parent session's request does NOT stop its subagents' warms: a subagent can still be running, and on the test half that stop cost $565.52 net (below). A 429 (warm or real) pauses warming on that credential for the response's `Retry-After`, else until its `anthropic-ratelimit-unified-reset`, else 1 h for a subscription/OAuth login and 60 s for an API key; a 529 or 5xx on a warm pauses it for the `Retry-After`, else 5 min. Never sent while a real request of the same stream is in flight (retry every 5 s until its deadline). At most two warms are in flight process-wide (same retry; one with no slot by its deadline is dropped), and each fires up to 5 s before its planned time, never after. Warm rows are tagged `cache-warm`: spend, never a saving, never an agent request (excluded from request counts, session usage, trial origins, exported spans, receipts and `caveman sync` uploads; counted as `cache_warm_requests`), never fed to the harm-tripwire ledger, never sent to Cloud. Switch: on unless `$CAVEMAN_HOME/cloud.json` has `modules[\"waste-fixes\"]: false` (re-read on change); `CAVEMAN_CACHE_WARM=off` kills it for the process. Every answered stream request logs one line `cache warm plan` (info when warms are planned, debug when skipped: session hash, model, class, ttl, planned warms, skip reason: `response_error`, `off`, `not_anthropic_api`, `not_replayable`, `lifetime_mismatch`, `unpriced`, `not_worth_it`). Replay on the owner's data (test half, after 2026-10-03, 161,325 transitions): net $1,896.65 list-price equivalent at $385.31 of warms (9,628 warms, 7,280 wasted) vs $1,294.18 at $1,372.15 (45,343 warms) for the first fixed rule; the same policy with the parent stop for every subagent $1,331.13 at $197.28 (3,461 warms), with it only for subagents whose last answer was not a tool call $1,852.99 at $378.90 (9,518 warms). A test pins the plan to the simulator's on the shipped table (`TestCacheWarmPlanMatchesTheSimulator`; regenerate both together).\n- **pixel mode**: S4 lossy text→PNG (`pxpipe` port). Default allowlist is `claude-fable-5,gpt-5.6` via `CAVE_PIXEL_MODELS`; savings stay inferred-only. Renders keep the compress rules: the first decision for a text (render or text) is re-sent on every turn, a raw pin keeps the conversation text, and a NEW render is remembered only once its request goes out, with the original stored in CCR and the splice holding. If either fails, the new content goes out as text and records that, and earlier renders still go. A rendered block's `cache_control` moves to its last image. An Anthropic request carries at most 20 images, client images included (`anthropicManyImages`): past 20 the API rejects any image over 2000 px, and the renders are 2573 px wide, so a new render past the cap stays text.\n- **route stage is optional and fails open** (`internal/cloudlink`, `internal/gateway/route.go`):\n  it asks Cloud `POST /v1/route` only for requests naming Auto (`AutoModel`, `caveman-auto`, also with Claude Code's `[1m]` suffix, the\n  picker entry the CLI adds while `modules.routing: true` in `$CAVEMAN_HOME/cloud.json`), while the\n  CLI is signed in with that switch on, for Anthropic Messages / OpenAI chat or responses requests on\n  an API key or a subscription alike; any other model goes as sent (`route.outcome: off`), the\n  session's marks and heal still applied. Auto goes upstream as the provider's `autoFallback` model\n  (Anthropic `claude-sonnet-5-5`, OpenAI `gpt-6.1-sol`; count_tokens too, and with no Cloud link),\n  which is also the asked model the ask carries and what a failure runs (a transient Cloud failure, that is a timeout, an unreachable or 5xx Cloud, an unreadable answer or the pause after one, instead runs the model the session's previous request went to, `heldModel`, never for a side request; no previous request on that provider, one that was a compaction or came after one, a held model that then did not serve (any non-2xx: the next attempt runs the fallback), signed out, routing off, an expired or refused login, a limit and a provider's refusal keep the fallback); Auto on any other\n  provider is a 400 (`cave_auto_unavailable`), and an encoded body naming Auto is decoded first. A\n  named model records `route.outcome: off`, reason `named_model`. OpenAI Auto (API key and ChatGPT\n  login) runs only `AutoOpenAIModels` (`gpt-6.1-sol`, `gpt-6-astra`, `gpt-6-luna`): the ask's\n  `Models` lists those alone and the pool drops other OpenAI models, and an answer naming any other\n  runs `gpt-6.1-sol` at its effort (`auto_model_refused`, `translate.FitEffort`); an Auto row's `RouteFrom` and\n  `x-caveman-routed-from` say `caveman-auto`. A move on a subscription stays on the provider's own\n  API on that subscription's credential; pool entries go out on their own login or the Cloud\n  gateway key, never on it, and never with the agent's `oauth-*` betas. On a ChatGPT login\n  (`/chatgpt`, `chatgpt_auto.go`; OpenCode's ChatGPT login lands there too) `/models` gains an Auto\n  entry (a copy of `gpt-6.1-sol`'s, listed last, fetched without `If-None-Match`, served without\n  `ETag`) while the link reports `AutoOffered` (signed in, routing on). Every POST there has its\n  first 64 KiB read (decoded when gzip, deflate or zstd; a br body is never looked into) for a\n  top-level `\"model\":\"caveman-auto\"`; any other streams on unchanged. `/responses` naming Auto asks\n  with `NoPool`, keeps live-zone compression, replays `gpt-6.1-sol`'s bytes on a 4xx (a rate-limit 429, a 401 and a 403 are returned instead; a served replay pins the conversation raw, and the decision is rejected only then or when the replay is itself a 429; a 5xx on a moved model is returned and rejects the decision, a rate-limit 429 keeps it: the plan's limit holds for every model); any other\n  path (Codex compaction) runs `gpt-6.1-sol` unasked; a body over `CAVE_MAX_REQUEST_BYTES` streams\n  with only its model changed. The agent reads the Auto id it sent. Every `/chatgpt` row (Auto or\n  not) is offered to Cloud like any other, as its agent (`x-cave-agent`, else Codex by its\n  `originator` or User-Agent), with model ids in `route_from`/`route_to`; a model it could not read\n  goes to Cloud as none. The\n  ask starts before compression (parse, ask, compress, route), waits at most 800 ms (the CLI's\n  keychain credential is read when the runtime starts, `Link.Start`, never on a request goroutine;\n  a request that lands while a read runs with no secret cached waits for it, leaving the ask at\n  least 500 ms, else goes as `credential_loading` without an ask or a pause), carries the\n  caller's models, counts, what the request declares (the raw values of eleven allowlisted agent\n  headers, never cut: a value over 256 bytes, 16 KiB for Codex's turn metadata, which also comes\n  from the body's `client_metadata`, is left out; a forked Claude Code child's spawn-call\n  `subagent_type` as `x-caveman-agent`; its tool names, effort and thinking type), what the\n  session's previous request ran (the served model, the effort in force, the provider's input and\n  cache token counts, its age) and Cloud's opaque per-session state, plus, on a turn's first ask,\n  the raw text of the latest human turn, the one before it and the end of the agent's last reply\n  (contracts `route-ask-v1`; Cloud picks the model and the effort and no routing logic lives\n  here). Its answer is cached per ask until 30 min after its last use (session, or without one agent and\n  endpoint, provider, model, turn number, latest human text;\n  an input group carrying a tool result is no human turn, text riding along included) so a tool\n  loop never switches model or effort mid-turn (an ask first seen during a pause keeps the paused answer for its turn); a request the agent labels compaction or auxiliary\n  (Claude Code's request class and compaction flag, OpenCode's title/summary/compaction agent,\n  Codex's subagent kind and turn-metadata request kind) is asked on its own, without the text. The\n  session is `x-cave-session`, else the agent's session header (a Codex thread, `thread-id`, is\n  its own, its parent `x-codex-parent-thread-id`); a Claude Code child (`x-claude-code-agent-id`)\n  is a session of its own and sends its parent's state; a session id guessed from timing is not\n  used. State, previous-request facts and marks live in bounded in-memory LRUs (1024), never on\n  disk. The answer sets Responses `reasoning.effort`, chat `reasoning_effort` or Anthropic\n  `output_config.effort`; Anthropic `effort_mode: message` instead inserts the byte-identical mark\n  `{\"role\":\"system\",\"content\":[],\"output_config\":{\"effort\":…}}` (beta\n  `mid-conversation-output-config-2026-07-01` appended to `anthropic-beta`) before the last user\n  turn, or at the end after a tool result; on OpenAI Responses (GPT-6) the mark is a\n  `{\"type\":\"configuration_update\",\"reasoning\":{\"effort\":…}}` input item before the last user\n  message while nothing has answered it yet, never at the end and never next to another one (no\n  place, as mid tool loop: the effort in force holds), no beta, and none at all where OpenAI forbids\n  it (`truncation: \"auto\"`, `context_management`, a non-false `multi_agent`: Cloud's effort goes\n  top-level); a 400 naming a configuration update heals like a refused mark. Effort state made on\n  one wire starts afresh on the other. Only `effort_mode: message` ever starts marks; a \"top\"\n  answer on history carrying the session's marks goes in as one more mark (never wiping them; for\n  a compaction or side request, that request's only), and on any other history sets the top-level\n  field. Marks are remembered per session with a salted hash of the message before each\n  (`cache_control` and thinking blocks left out, so a heal's strip keeps them) and replayed at the\n  same places on every later request to a model that already took them (the agent resends history\n  without them), count_tokens of a known session or a forked child's parent included (it adds\n  nothing to a history without marks). Without an effort from Cloud (a failure, routing off,\n  effort \"\") a history without the session's marks goes as the agent sent it, top-level field\n  included (one cache restart where Cloud's was, nothing inserted, no beta), except that a\n  remembered heal (drop_block with its beta, or a strip) keeps applying. One with marks runs at\n  the request's own top-level effort, or, when it sets none, at the model's default effort\n  (Cloud's `default_effort`, kept per session, a forked child taking its parent's), marked when it\n  differs from the one in force; only with no default known does such a request go as the agent\n  sent it, without the marks, which the session then forgets (its later thinking blocks then lose\n  their binding) unless the request went to another model. The top-level field is fixed at the\n  session's first per-message request: the routed effort on a fresh conversation whose request\n  sets one (kept there, also for later requests that set none), else the request's own; a routed\n  effort for a request that sets none goes in as a mark, first on a fresh conversation. An anchor\n  that no longer matches drops that mark and every later one; compaction and side requests never\n  change them, and a body matching none of them (an unlabeled side request, compacted history)\n  gets marks of its own, kept for up to four request shapes (first message, length, last message),\n  which take the session over only from a longer request continuing that conversation (same first\n  message, its last one where it was); the session's marks and fixed top-level field then wait\n  there in turn. A thinking-binding 400 on the asked model of a `/messages` request retries once\n  only on a session the route stage changed (it was on for the request, the session carries its\n  marks or heal, or another model served one of its requests) whose body sets no\n  `thinking.block_binding`: with `thinking.block_binding.prefix_mismatch_behavior: \"drop_block\"`\n  (beta `thinking-binding-controls-2026-08-01`) when the block is bound to a different\n  conversation and the request's thinking is adaptive or enabled, then sent on every later request\n  (count_tokens and forked children included); else (Sonnet 5.5 `between_tools`, no thinking\n  field, a tampered signature) without thinking blocks from the failing message on, and later\n  requests on the same history (same first message, same message at its end) strip the same range\n  up front (a second strip widens it; the strip stays, the history as served, even if the agent\n  later sets its own `block_binding`). A drop_block retry refused with a 400 naming the binding,\n  `block_binding` or its beta moves the session to the strip path, a strip refused that way ends\n  its heals (the first lasts for the session, as it depends on the model and thinking type; the\n  second goes with the marks, cleared when they are forgotten; a side request never clears either,\n  nor the strip; a 429, 5xx or other 400 on the retry teaches nothing). Routing switched off after\n  a restart surfaces that 400. A refused mark retries once with top-level effort only and, once\n  served, latches per-message effort off for the session (a model that refused takes Cloud's\n  effort top-level); compressed 400s are decoded first. A Cloud error, timeout, 401/403,\n  `allowance` or `billing_limit` answer keeps the asked model (the held model above for a transient failure) and pauses new asks (1 min; 10 min\n  for 401/403 and billing_limit; until the 1st for allowance). Refusals and limits land in\n  `$CAVEMAN_HOME/route-state.json` (with Cloud's notice) for `caveman status`; a new login lifts\n  the pause. A route failure blames a model only on evidence (Cloud ADR 0110). A provider 4xx on\n  the routed model replays the original bytes on the asked model, except a 401 (the login, not the\n  model: returned as is); a 403 replays (a plan refuses a model it does not carry with one). Only a\n  served replay of bytes that differed from the original in the model alone blames the routed model\n  (`Reject(true)`, `provider_rejected_routed_model`): it leaves the ask's pool for that scope\n  (session, or without one agent and endpoint, and provider and asked model) for 30 min, while\n  the ask still goes with the models left. A served replay after compression or effort also\n  changed the bytes records `replayed_original` and blames no model; a replay that fails too\n  records `replay_failed`. The rest of the ask moves back to the asked model (`Reject(false)`,\n  never remembered) when the replay is served, or is itself a 429 (so the agent's own retries send\n  one request each); a rate-limit 429 (with Retry-After) or a 5xx on the routed model is returned\n  as is, never replayed, and moves it back too, as do a transport error, a provider error event\n  and an upstream break before the provider reported usage; a client cancel never does, so the\n  agent's own retry runs the asked model; a\n  429 on the asked model of a request whose bytes the route stage changed (effort, marks, strip,\n  drop_block, a heal retry) is returned as is. When the model moved, the agent's copy of the\n  answer names the model it asked for (Claude Code drops its thinking on another name): a JSON\n  answer's top-level `model`, and in a stream every `\"model\":\"<sent>\"` pair, rewritten\n  incrementally across reads (an Auto request names Auto, moved or not); the upstream is asked for an identity answer (on the original-bytes replay of an Auto request too), a compressed one is\n  left as it is, and usage, stats and `last` read the provider's bytes. The pass-through header\n  and encoded bodies never route, nor does any origin but the provider's own API (Azure,\n  OpenRouter, LiteLLM, a custom base URL); record mode does (routing is its own module). A limit\n  pause asks `/me` at most every 15 min and lifts once routing is no longer limited or the plan\n  changed. runtime/v1 events go whenever signed in, routing on or not, unless the CLI telemetry\n  opt-out is set (ADR 0085 §5; `route.outcome: off` when no route stage ran): in-memory batches of\n  at most 500, dropped on failure, at `/me`'s `data.level`. The runtime never invents a level:\n  none in `/me` means `counts`, an unreadable `/me` lowers it to `counts`, `off` or no level sends\n  nothing. Model ids from other origins go as `custom`. Signing in or out never changes another\n  stage.\n- **the route path is kept hot** (`internal/cloudlink/warm.go`, contract `route-warm-v1`): the link\n  has its own transport (HTTP/2 tried, idle connections kept 10 min and pinged after 15 s of\n  silence, proxy from the environment; a warm is marked replayable, so a dead kept connection is\n  retried on a new one, an ask never: the server may already have decided and counted it). A warm\n  is `POST {gateway}/v1/route` with `x-cave-warm: 1`, the asks' credential and no body; it goes,\n  one at a time and only from a started runtime signed in with routing on, at start and at every\n  login, when a native `session.start` or `prompt.submit` comes after 30 s without contact (an\n  answered ask or a warm engine), and every minute without contact while asks came in the last\n  15 min. Only a 204 with `x-cave-edge-engine: warm` is warm (timeout, error, cold and off are 204s\n  too); it ends a pause a timeout, an unreachable Cloud or a 5xx caused, never a 401/403 or limit\n  pause. Any other status is an origin without the edge (a customer install): no pause, logged at\n  debug, and three in a row stop warming until the next login or start. Each ask logs `route ask`\n  (info): the ms this side measured, whether it reused a connection, the status, `x-cave-edge` and\n  the edge's `server-timing`. The 800 ms budget stays the cap.\n- **pool routing** (`internal/pool`, `internal/translate`, `internal/gateway/route_pool.go`):\n  the provider keys and logins a person adds (`caveman providers add|login`;\n  `$CAVEMAN_HOME/provider-logins.json` lists ids, where each secret lives and the `cloud` switch,\n  written under `provider-logins.json.lock` by the CLI and the runtime alike; the secret is in the\n  keychain service `caveman-provider` or a 0600 file under `provider-logins/`, never both) become\n  the ask's additive `pool` (`harness/<model>`, then `<host>/<model>` per login times the static\n  catalog models its host serves, on a wire `translate.Supported` reaches from the caller's\n  grammar; at most 64; left out when no login adds one; only a 400 naming `pool` is asked again\n  without it, and pool then stays off for that login). An answer's `pool_id` + `via`: a harness\n  entry applies like `model`; a login entry goes straight to that host on that login\n  (`RouteTarget`, translated when its grammar differs, the unstripped body so each host gets its own\n  reasoning back, top-level effort, the agent's copy naming the asked model); `via: cloud`\n  (`cloud:<provider>:<model>`) sends the agent's own bytes in its grammar to the Cloud gateway\n  (`gatewayUrl`, else `baseURL`) with `x-caveman-route` (and `x-caveman-effort` when the answer\n  carries one) on the project gateway key only (the gateway\n  refuses login tokens; no key, or `caveman providers cloud off`, runs the asked model), and that\n  whole request DOES pass through Caveman Cloud. Route asks to the gateway use the project key too.\n  Any failure before content reaches the agent (refused translation, translator panic, connect\n  error, non-2xx, or a 2xx that fails or ends first: `translate.ErrNotServed`, the translator holds\n  its output until the first content, for at most `gateHold` (10 s) even while the host sends only\n  comments or pings; after it the headers go and pings start, only between the upstream's events)\n  falls back to the asked model on the harness credential, at the answered effort fitted to that\n  model's levels (`translate.FitEffort`: the Claude table, the catalog's `effort_levels`, high at\n  most with thinking off), and marks the ask rejected; a stream cut after content aborts HTTP\n  framing and never closes a half-finished tool call, while the host's own failure event after\n  content ends the stream as sent, without an abort (`pool_upstream_failed`). The row keeps\n  `route_pool_id`, `route_reason` and the host's answer id (`upstream_response_id`, e.g.\n  OpenRouter's `gen-…`); each failure is logged. A translated Messages stream's `message_start`\n  carries an input estimate (`x-caveman-input-tokens: estimated`) that `message_delta`'s exact\n  count replaces. A pool turn is not `last` and books no dollars off Anthropic/OpenAI list\n  prices. Every request to Anthropic's/OpenAI's own API (not pass-through) drops reasoning another host\n  wrote (`translate.AnthropicNative`/`OpenAINative`, byte for byte when there is none; a Responses\n  host's encrypted reasoning is route-tagged so only that host gets it back; per-message effort\n  marks are untouched). OpenAI (key or ChatGPT login) is Responses-only: Messages and chat callers reach it\n  through Messages→Responses and chat→Responses, and chat callers (OpenCode, Aider) reach Claude through\n  chat→Messages; a chat caller gets another host's reasoning as `reasoning_content` plus a\n  `reasoning_details` envelope that goes back only to that host, and every chat API (on the harness path\n  too, whatever the origin and with routing off, `translate.ChatNative`) gets both removed. Documents, structured output,\n  `disable_parallel_tool_use`/`parallel_tool_calls` and refusals cross every direction; a part the target\n  cannot take (a document by URL on chat, an OpenAI file id on Claude, audio) refuses the translation, so\n  the pool falls back. A refusal reaches Codex as `response.incomplete` `content_filter` (its own\n  content-filter path); a max-tokens stop stays `response.completed` (Codex retries any other incomplete). Terms: no Claude Pro/Max, Google or Copilot login is ever added; every OAuth row\n  in `providers.json` carries a terms note.\n- **cache mechanics** (`internal/gateway/route_cache.go`): the ask's\n  `request.cache_ttl` is the longest Anthropic `cache_control` ttl (none = 5m), else OpenAI\n  `prompt_cache_options.ttl` / `prompt_cache_retention` (24h; in_memory = 5m); Cloud's generic\n  400 (`cave_router_request_invalid`, which names no field) or one naming the field is asked\n  again without `cache_ttl`, then `pool` (never a field a 200 already came back for), and the field\n  whose removal got the 200 stays off for the login. Record mode gets none of the cache mechanics\n  below (no `prompt_cache_key`, no OpenRouter pin, host affinity headers keyed on the session\n  itself as before, no fan-out wait); the routing answer itself (model, effort, marks,\n  `configuration_update`) still applies there, as above. Elsewhere: host affinity headers key on\n  the session family (the parent's for a child): hashed `x-session-id` (OpenRouter),\n  `x-session-affinity` (Fireworks), `x-grok-conv-id` (xAI). An OpenAI request the route stage is\n  on for (a stateful chain, answered off for a reason, included) that carries no\n  `prompt_cache_key` gets the session's own (hashed, added before the effort so a heal keeps it;\n  the original-bytes retry keeps it unless the 4xx names it, which also latches the key off for\n  the session; a request it only keyed is never replayed); routing off and signed out stay byte\n  for byte. Once an OpenRouter pool entry reports cache reads or writes, the session pins the\n  provider its answer named (`provider.order:[p]`, `allow_fallbacks:false`); a pinned failure\n  drops the pin, and a pinned non-2xx is sent once more to the same entry unpinned before the\n  asked model runs (a transport error is never resent: it may come after the request was\n  written). Fresh sibling children of one parent on the same model, tools, system prompt and\n  `prompt_cache_key` go one first and the rest wait for its first content (a JSON answer's first\n  byte; an event stream's first event past comments, pings, message_start and\n  response.created/in_progress; 5 s at most, or their own cancellation): one cache write, not N;\n  a prefix with content in the last 5 min waits for nobody. `signals.context_tokens` is the\n  request's bytes times the provider's own tokens per byte, the parent session's first.\n  Live check: `CAVEMAN_LIVE_OPENROUTER_KEY=<key file> go test ./proxy/internal/cloudlink -run\n  TestLiveOpenRouterCache -v` (throwaway home, $0.50 cap).\n- **boundary**: this is public code — it must never import the managed-cloud lane. `make check-boundaries` enforces it.\n\nSee ../../CLAUDE.md (root)\n","proxy/CLAUDE.md":"# proxy — the byte-safe standalone `caveman` proxy (commercial, binary-distributed)\n\nA base-URL-swap reverse proxy: match → authenticate → inspect → byte-safe transform → upstream\n→ meter. Single-operator, BYOK, **zero cloud dependencies**. It shares its provider adapters with\nthe managed gateway (the managed gateway imports them from here). `caveman start` launches the\n`caveman-proxy` binary.\n\n## Layout\n- `providers/` — the shared, public byte-safe adapter set: `Adapter` interface + `Base` embed + `UsageScanner`/`ParseUsageBytes` (`adapter.go`), and `anthropic`/`openai`/`gemini`/`azureopenai`/`bedrock`/`vertex`/`openaicompat`. `ResolveUpstreamURL` takes `providers.RouteContext` (no control-plane coupling). Anthropic + OpenAI carry both prefixed and bare routes (`/v1/messages`, `/v1/chat/completions`). `vertex` preserves caller OAuth bearer or Express-mode Google API-key credentials for Gemini + Claude on Vertex AI (no signing, no environment-key fallback, no custom usage parser). `MatchRoute` is POST-only; read-only model discovery (`GET /v1/models`, `/v1/models/{id}`) is a separate GET-only allowlist, `Base.MetadataRoutes`, declared by `openai`/`anthropic`/`openaicompat` and forwarded unchanged with no body, no transform and no spend row. Bare `/v1/models` belongs to both `openai` and `anthropic`, so they implement `MetadataRequestMatcher` and choose by wire protocol (`anthropic-version` or `x-api-key` present → anthropic; a Google key matches neither), never by registration order (#1187).\n- `internal/gateway/` — the request lifecycle (`server.go` + `proxy.go`) behind three injected seams: `Authenticator`, `CredentialResolver`, `TelemetrySink`. Ports the managed loop with the fail-open fix. `serve` sets `Config.AsyncRecord`: a row's request token count, sink write (batched) and Cloud observe run after the handler returns, in record order (`recorder.go`). A reader that must see the request it follows calls `Server.Flush` first (session receipts do); close the server before the sink.\n- `GET /health/ready` identifies the runtime and advertises `billing: \"byok\"` because the standalone proxy forwards the caller's selected provider credential. The managed twin advertises `managed`; SDK dollar budgets fail closed on missing/unknown billing provenance.\n- `internal/config/` — `caveman.yaml` loader + BYOK env-key resolution; unknown mode fails closed to `record`.\n- `internal/store/` — `~/.caveman/caveman.db` SQLite spend store (`modernc.org/sqlite`, cgo-free); implements `TelemetrySink` and `BatchSink`. `RecordBatch` commits request rows with `synchronous=NORMAL` on its own connection (WAL): a power loss or OS crash can drop the last batches, a process crash or clean exit loses none; every other table keeps FULL.\n- `internal/standalone/` — wiring: static `Auth`, BYOK `Creds`, adapter set, and the always-on SSRF-guarded client.\n- `internal/identity/` — who calls the framework middleware routes (legacy token, token map, OIDC/JWT, mTLS) and the reloadable TLS listener config.\n- `internal/nativeruntime/` — normalized local-agent lifecycle, Task Contract,\n  Decision Ledger, typed CCR capture/masking, child evidence merge, honest\n  receipts, user-only Unix socket / Windows named pipe, and persistent listener lifetime.\n- `internal/repointel/` — deterministic local repository map, task evidence,\n  conservative test impact, and optional-Scout recommendation. No model/network.\n- `internal/nativepack/` — embedded compiled Core/skill policy; generated from\n  `public/skills`, fail-closed on schema/version drift.\n- `cmd/caveman-proxy/` — binary: `serve` (default), `stats`, and content-blind\n  `agent-evidence --session --build --plan`. Evidence query returns only exact\n  provider usage, request hashes, declared context/plan identity, ordered\n  provider-prefix component hashes, actual transform IDs/counts, and CCR handle;\n  basis is always `inferred`, verified dollars always zero.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=proxy` / `make product-test PRODUCT=proxy`.\n- Tests inject a plain `*http.Client` to reach loopback stubs; the binary uses the SSRF-guarded client.\n- New provider/optimizer work goes in `providers/` (shared) — change it once, both proxies get it.\n\n## Gotchas (honesty invariants — correctness, not style)\n\n- **listener lifetime is not session lifetime**: `serve` never exits because a\n  wrapper, native session, heartbeat, or idle timer expires. Legacy\n  `CAVEMAN_NATIVE_IDLE_TIMEOUT` does not arm shutdown. The CLI never restarts a\n  shared listener to change mode/recovery; an incompatible new wrap runs direct.\n  Failed local startup also runs direct. Native session correlation entries may\n  age out without affecting API traffic.\n- **no default generation deadline**: `CAVE_GATEWAY_UPSTREAM_TIMEOUT_MS` defaults\n  to `0` (no total request deadline). A positive value is an explicit operator\n  cap and includes response streaming. Client cancellation still cancels upstream;\n  connection setup, inbound header/upload limits, and idle keep-alive socket\n  cleanup remain bounded separately. The replacement bound for an upstream that\n  connects and then goes silent is the transport's response-header deadline\n  (`CAVE_GATEWAY_RESPONSE_HEADER_TIMEOUT_MS`, default 900000, `0` disables) —\n  headers only, so it never truncates a live stream.\n- **response protocol controls streaming**: SSE/event-stream responses flush\n  headers and chunks even without a JSON `stream` flag. Encoded requests keep\n  `Content-Encoding` and bypass transforms. Interrupted response copies record\n  an error and abort HTTP framing; they never become a clean successful EOF.\n- **replay only when non-delivery is proven**: proxy transport retries are\n  limited to connection-setup failures — `*net.OpError` with `Op` `dial` or\n  `proxyconnect` (a proxy that is down fails as the latter, never as a dial). A failed upload/header read or truncated response\n  does not prove an inference was unprocessed; do not automatically replay it.\n  An explicit transformed-request 4xx still retries once with original bytes —\n  except a 429 that carries `Retry-After` (a rate limit; Anthropic sends its\n  `anthropic-ratelimit-*` headers on every response, so they prove nothing),\n  which is returned as-is; an accepted retry pins the conversation raw\n  (`internal/gateway/raw_pin.go`).\n  See `docs/technical/proxy-reliability.md` at the repository root.\n- **byte-safe**: `record` mode never transforms; on transform error the ORIGINAL bytes are forwarded (HTTP 200, fail-open) — never a 400. The one carve-out: bytes the provider already cached in replaced form are re-sent replaced, because forwarding the original there busts the prefix (see cache safety).\n- **native marker is local-route-only**: HMAC session marker is emitted only\n  after local runtime + route proof and stripped before capture/hash/provider.\n  Direct, managed, proxy-disabled, invalid, and conflicting paths never correlate.\n- **request-wide opt-out**: `x-cave-transforms: caveman.pass-through.v1` suppresses every request transform path — compress, pixel, and provider-native — not only compiled plan routes. Tests cover all three modes.\n- **repository evidence is a claim, so it needs a direct match** (`internal/repointel` + `nativeruntime.renderRepositoryEvidence`): a \"Likely implementation path\" line is injected ONLY for items whose own FILE NAME or symbol name contains a task term (`EvidenceItem.Direct`, `Bundle.Strength == StrengthDirect`). The match is on the final path component on purpose — matching the whole path meant a term naming any ancestor (`src`, `internal`, `lib`: ordinary prompt words) marked every file beneath it direct, i.e. no gate at all. BM25 metadata proximity alone is `StrengthMetadata` and is never shown, never stored, and never gets a `ccr://` handle; the handle reads as authoritative, and a wrong file:line steers the agent while costing tokens every turn. A bundle qualifies on ONE direct item, so the non-direct items that ranked alongside it are stripped by `Bundle.DirectOnly()` BEFORE the object is written — dropping them only at render time left them stored behind the handle, one retrieval from the model. Terms are floored at three characters (`NormalizeTerms`, matching the producing hook) because the direct test is a substring of a file's own name: `go`, `py`, `js` would mark every file of that language a direct hit and hand the gate away. A declined turn is silent to the model but records `repository_evidence_declined_no_direct_match` in the ledger, so \"declined\" stays distinguishable from \"broken\". `Scout.Status` is ADVICE about repository shape and must not gate injection; conflating them made a truncated map silence real hits (#899).\n- **the dependency filter runs on BOTH listing paths, and git is not trusted to be complete**: `listFiles` prefers `git ls-files --cached --others --exclude-standard`, because the project's own `.gitignore` — not a name list we maintain — is the best statement of what is vendored. It is not sufficient on its own: an empty-but-successful listing means the cwd is itself ignored by an enclosing repo (accepting it silently disabled the whole mechanism), so empty falls back to the walk, and every git-listed path still goes through the same content-based filter as the walk via `directoryFilter` (memoised per directory, one probe per directory not per file) — otherwise an un-ignored conda prefix or a COMMITTED `vendor/` tree reaches the model precisely because git reported it. Installed environments are identified by CONTENTS (`conda-meta`, `pyvenv.cfg`, `site-packages` child) so a pixi/conda prefix or virtualenv is caught under any name, along with the interpreter stdlib it carries. Only names that are never first-party (`node_modules`, `.pixi`, `site-packages`, …) match by name, case-folded; `build`/`dist`/`target`/`vendor`/`coverage`/`Pods` are ambiguous and excluded only when a sibling manifest proves the ecosystem, because `internal/build` and a hand-written `vendor/` are real source. The two paths can legitimately disagree (git omits submodule contents), so `Map.ListingBasis` discloses which ran and is folded into `ContentSHA256`. Git gets at most HALF the caller's remaining warm budget (`gitListBudget`): the walk is the more expensive path, so a git listing that spent 2s of a 3s window left the fallback unable to finish and the map failed closed — the exact case the fallback exists for. `FilesScanned` stays the map size; `FilesRanked` is the filtered count.\n- **git is invoked as an untrusted-repository operation**: every `git` call that points at a user working directory goes through `internal/gitsafe.Command` (Go) or `packages/cli/src/git-safe.ts` (the two `execFileSync` sites in `index.ts` and `native-hook-fast.ts`), which pass explicit `-c` overrides (`core.fsmonitor`, `core.hooksPath`, `core.sshCommand`, `core.askPass`, `core.editor`, `core.pager`, `core.alternateRefsCommand`, `diff.external`, `credential.helper`, `protocol.ext.allow`, `uploadpack.packObjectsHook`) and strips inherited `GIT_*` from the environment. A repository's own `.git/config` names programs git will run, and `core.fsmonitor` runs while the index is read — so plain `git ls-files` in a freshly cloned hostile repo executes attacker code with no user action, at session start. `repointel.listFiles` was hardened first and the other callers were not, which is the shape this bug takes: `nativehook.currentRepositoryState` runs `git status` on EVERY hook event, and `status` refreshes the index, so it triggers `core.fsmonitor` harder than `ls-files` does. Regression tests: `repointel.TestGitListingIgnoresRepositoryControlledCommands` and `nativehook.TestRepositoryStateIgnoresRepositoryControlledCommands` (both verified to fail without the overrides), plus `tests/verify_repo.py` `verify_untrusted_git_invocations`, which fails the build on a NEW raw `git -C <dir>` call site anywhere in `proxy/` or `packages/cli/src/`. Command-line `-c` beats repository config; never drop these to \"simplify\" the invocation.\n- **no-fake-savings**: standalone records `Basis: \"inferred\"` on every row; it never writes `verified` and never re-projects to a monthly figure.\n- **spend is arithmetic, savings is a claim** (local learn, since 2026-08-21): `caveman learn` MAY state a currency, under exactly these conditions. SPEND prices PROVIDER-COUNTED disjoint billing buckets read out of the transcript's own usage block, at rates from the dated `shared/provider-catalog` row, stamped with that row's catalog version — basis `provider_counted_x_published_rate`, window-bounded, never projected. A model with no catalog row is NEVER priced at a sibling's rate: its tokens are disclosed separately and the total is a stated floor (`learn_pricing.go`). Sinks are priced at the user's own MEASURED effective input rate, not list, so a well-cached user's findings are not inflated ~10x. SAVINGS additionally requires an attribution method (`learn_attribution.go`): every confirmed row names its rung (`deterministic_remeasure` > `controlled_holdout` > `counterfactual_replay` > `interrupted_time_series`), carries the standing confounders that rung cannot rule out, and reports whether the artifact caveman fingerprinted at apply time is still intact on disk. Savings totals are bucketed BY RUNG and never summed across rungs — blending a re-counted file with a before/after median launders the weak number into the strong one. A regression is never priced. Neither surface ever writes `verified`; that stays gateway-only. Subscription traffic has no marginal cost, and every priced surface says so.\n- **practice join**: local learn sinks carry additive `practice_id`; one\n  fail-closed mapping table owns sink→practice and unknown sinks keep `\"\"`.\n  The historical `subagent_overuse` sink is count-only and deliberately has no\n  practice id: spawn count cannot reactivate the retired\n  `context-exploration-offload` opportunity or prove any spawn unnecessary.\n- **local trial heuristics are not actuation evidence**: a model name never emits\n  the retired `model-right-sizing` id, and provider plus positive cost never\n  emits a cache move because neither proves stable-prefix eligibility. Legacy\n  rows for those identities are hidden at read time. Compression replay reports\n  one trial's local engine `estimated_engine_o200k` before/after shape with zero\n  dollars and low confidence; it is not provider-counted, a rate, an invoice,\n  causal/verified savings, or task-outcome evidence.\n- **Anthropic automatic caching is experimental observation only**:\n  `anthropic-automatic-prompt-cache` is a typed, default-off manual policy\n  experiment and may add only Anthropic's top-level 5-minute marker on direct\n  Messages API requests. Managed traffic additionally requires server-attested\n  official Anthropic origin; custom or provenance-unknown origins lose the flag\n  before the adapter. It is mutually\n  exclusive with the explicit `anthropic-cache-breakpoints` transform and any\n  caller `cache_control`; Bedrock and count-tokens requests stay byte-identical.\n  An applied marker records only its optimizer id plus actual provider usage and\n  cost. It has no practice, recipe, generic mode/candidate activation, ledger\n  tuple, inferred savings, or verified-savings path (cache-only and forged IDs\n  are excluded from the counted-baseline method too); evaluate it by manual\n  paired observation because shared provider cache state can contaminate an A/B.\n- **OpenAI cache-key affinity is not cache placement or savings proof**:\n  `openai-prompt-cache-key` adds routing metadata only. On GPT-5.6, the\n  provider's implicit latest-message breakpoint can repeatedly write a changing\n  suffix, and writes cost 1.25× uncached input. The usage parser normalizes\n  official nested `cache_write_tokens` from both Responses\n  `input_tokens_details` and Chat `prompt_tokens_details` into\n  `CacheCreationInputTokens`; applying the affinity key never mints verified\n  savings. Explicit `prompt_cache_breakpoint` placement is not implemented.\n- **SSRF always on**: `standalone.StandaloneHTTPClient` guards every upstream dial (not gated on `CAVE_ENV`) using `ssrf.SelfHostedConfig` — NOT ManagedConfig, which ignores the allowlist and would make the escape hatch a silent no-op. `CAVE_SSRF_ALLOWLIST` opts loopback/private hosts back in (local model servers like Ollama; `localhost` as an entry covers 127.0.0.0/8 + ::1); metadata/link-local stay blocked in every mode. Provider traffic honours `HTTPS_PROXY`/`NO_PROXY` by default (#1001) — `upstream_proxy: env|off|<url>` / `CAVE_UPSTREAM_PROXY` — via `ssrf.Config.Proxy`, which validates IP-literal/localhost destinations before proxy selection and dials the (operator-configured) proxy address unguarded; `ssrf.NewHTTPClient` itself stays direct unless a caller sets `Proxy`, and a managed-mode Config carrying one is refused loudly (every request fails with `ssrf.ErrProxyInManagedMode`) rather than silently dialing direct. Adapters that pre-flight a resolved endpoint (bedrock, vertex) read the selector off the request context (`providers.WithUpstreamProxy`, published by `gateway.Handler` from the upstream transport) and use `ssrf.ValidateURLNoResolve` for proxied destinations: a proxy-only network has no outbound DNS, so resolving there failed the request before the proxy was ever consulted. `ca_bundle` / `CAVE_CA_BUNDLE` plus inherited `SSL_CERT_FILE`/`REQUESTS_CA_BUNDLE`/`NODE_EXTRA_CA_CERTS` append private roots for TLS inspection through `shared/platform/cabundle` (fail-closed parser shared with chhttp).\n- **inbound token is consumed, never forwarded**: `CAVEMAN_AUTH_TOKEN` gates non-loopback listens (a middleware token map, OIDC issuer or TLS client CA also makes one legal; provider routes still accept only the token, and without it `Auth.closed` refuses every provider request off loopback); `standalone.Auth` deletes the matching `x-cave-api-key`/`Authorization` header before `Creds.Resolve` so the shared token can never be forwarded to a provider or classified as a provider credential; a bearer that is not the token survives untouched (Claude OAuth, `/chatgpt/`). `Authenticate` runs BEFORE `matchAdapter` so the 401/404 split cannot be used to enumerate routes, and every 401 goes through `Server.rejectUnauthorized`, which counts `cave_proxy_unauthorized_total` and warns with the path and remote host only. Health and the no-op `/caveman/keepalive` beacon stay unauthenticated, `/metrics` too unless `CAVEMAN_METRICS_TOKEN` is set; the `X-Caveman-Instance` header on `/health/live` is published only on a loopback bind. A token on a LOOPBACK listener is honored too (the local `caveman wrap` path sends none, so `runServe` warns). Bedrock credentials then come from `shared/platform/awscreds` (env → web identity → container → IMDSv2), never from the request; a PARTIAL env pair fails closed instead of falling through to an ambient role, and the winning source is logged once.\n- **middleware identity is server-side and per route**: `middleware.Config.Identify` resolves an `identity.Principal` from the credential alone (bearer decides when present, else a verified client certificate); `ServeHTTP`'s `decode` checks `Principal.Allows(scope.Namespace)` on EVERY scoped route, so a new route that skips `decode` skips authorization. The legacy token stays `single_operator` with every namespace; JWT and certificate principals may never be named `single_operator`. Token and hash comparisons are constant-time over every entry. A token map or TLS file that fails to reload keeps the previous one. Tests: `identity/*_test.go` (negative JWT and mTLS cases), `middleware/identity_test.go`.\n- **Postgres middleware store keeps SQLite's single-writer semantics per authority**: `store.PostgresMiddleware` serializes writers of one authority with a transaction-scoped advisory lock taken by `Scope` (then re-read), `SaveScope`, `Revoke`, `PurgeBatch`, `Receipt` and `SaveOriginal`; the sweep is one-replica (try-lock) and skips busy authorities and row-locked scopes. Counters are statement-level transition-table triggers striped by txid; a per-row trigger goes quadratic on bulk statements. Lock keys include `current_schema()`. Run the store suite against both backends with `CAVEMAN_TEST_POSTGRES_URL`; `middleware_concurrency_test.go` fails without the locks.\n- **Auth scheme is preserved**: a key from an inbound `Authorization: Bearer` keeps `Scheme:\"bearer\"` on the `providers.Credential`; Anthropic and Gemini forward bearer credentials as bearer credentials (Claude/Gemini OAuth breaks if remapped to an API-key header). BYOK env keys and inbound `x-api-key` keep provider API-key mapping. On a named compat mount the header follows the wire protocol of the path: `/compat/<name>/v1/messages` gets `x-api-key` plus a default `anthropic-version`, every other path gets Bearer. A real inbound Bearer stays Bearer on every path. One credential may arrive twice — pi with `authHeader: true` sends it as `Authorization: Bearer <key>` AND as `x-api-key: <key>`; x-api-key still resolves first (a shared listener must not be able to swap the caller's principal), but an IDENTICAL bearer is that same principal, so `openai_compatible` keeps `Scheme:\"bearer\"` and the bearer-only compat upstream gets the header it accepts (#1215). Differing values stay x-api-key-first, and no other provider reads the duplicate as OAuth. OpenCode Go rejects Bearer on `/v1/messages` (401 `Missing API key.`, 2026-09-03). The gateway env fallback puts the key in the header that the adapter emitted, so the upstream never gets two credentials.\n- **fail-closed**: unknown route → 404; unknown mode → `record`.\n- **subscription AND oauth compression is NOT account-gated**: non-PAYG sessions from Claude Code, Codex ChatGPT, Gemini CLI, and other routed clients take live-zone compression with no Caveman account, entitlement, or seat. `CAVEMAN_WRAP_ENTITLED` and every `WrapEntitled` field are **deleted**, not merely ignored — do not reintroduce them. Exactly four conditions remain, all technical and all fail-closed (`liveZoneCompressionAllowed`): the operator `subscription_compress` switch (empty/`live_zone` allow, `off` and any unknown value close it), the adapter must implement schema-aware `PrefixStabilizer` zones, recovery must run through the agent's own MCP `caveman_retrieve`, and a durable prefix cache must be wired. The dedicated Codex `/chatgpt/responses` route uses the OpenAI Responses stabilizer while preserving OAuth and `ChatGPT-Account-ID` headers; recovery is proven by the `CAVEMAN_RECOVERY=mcp` stamp or the decoded request's own `mcp__caveman__caveman_retrieve` (top-level `tools` or Codex's `input[]` `additional_tools`, flat or under an MCP namespace); zstd bodies decode → transform → re-encode, anything else forwards the original wire bytes, and the `chatgpt_proxy` log carries `skip_reason`; transformed 4xx responses retry once with exact original bytes (never a rate-limit 429; an accepted retry pins the conversation raw). Legacy savings fields remain **tokens-only** — `compression_tokens_before/after` + `estimated_engine_o200k`, never booked compression dollars. New request-comparison and price-snapshot fields support separately labeled API equivalents in local stats; those values never enter subscription spend or legacy saved-dollar fields. LOCAL wrap only; managed gateway non-PAYG behavior is unchanged.\n- **recovered bytes are never a compression candidate**: a `tool_result` answering the agent's own `caveman_retrieve` is excluded by every adapter (`providers.IsRecoveryToolName`; anthropic keys off `tool_use_id`, OpenAI off `tool_call_id`/`call_id`, gemini off the `functionResponse` name). This is not an optimization — the replacement is a deterministic function of block content and is memoised in the prefix cache, so collecting a recovery result substitutes the SAME elision straight back in and the agent has no path to its own data at all. Measured 2026-08-06: the agent retrieved, reported \"Same truncation\", and re-read one file seven times. Regression test `internal/gateway/recovery_exempt_test.go`.\n- **cache safety is byte-stable replacement**: THE INVARIANT (#1105): for accepted requests P then R of one conversation, if R's client bytes over P's cached prefix (system, tools and messages through P's last `cache_control` marker; the whole prompt for implicit caches) equal P's, R's forwarded bytes over that range equal P's, and every request caches at least what its client asked for (a dropped breakpoint is a bust too). Two exceptions, both re-caching the range raw: a request the provider rejected transformed and accepted through the raw retry, and a compressed PAYG conversation without MCP recovery that streams (the server-side retrieve tool cannot ride a stream, so the conversation stays raw from there). A raw retry splits a range into a raw and a replaced form, and a later request follows the LONGEST lineage it extends, ties going raw: it is held to the raw request (raw pin) unless it repeats a longer prefix that went out replaced, like a sibling subagent started with the same task. Pins are taken only on a conversation's own bytes (past system and tools); pins and lineages live in the PrefixCache (`rawpin` and `lineage` scopes, pin rows never evicted) and in memory. `internal/gateway/cache_prefix_invariant_test.go` (Anthropic compress, PAYG, pixel and Codex Responses scenarios + `FuzzCachePrefixInvariant`) proves it; keep it green. A compressed live-zone turn becomes prefix on the next request, so the replacement is a pure function of segment content (deterministic compressor + content-hash CCR marker), memoised in the durable replacement cache, and the FIRST decision for a block wins — replaced or raw (`RawDecisionHandle`); a request whose write finds a row on record sends that row, not its own candidate. A frozen block with no decision (a `--resume` history, an evicted row) goes out raw and raw becomes its decision; a failed memo write keeps the block raw for the process (`rawMemory`, consulted only on a store miss and written through once the store takes it); new rows are remembered only once the request's splice holds. Raw decisions are global and durable, a known savings cost: a block declined once (frozen at first sight, a compressor decline, a CCR failure, a non-newest parallel tool output) stays raw in every conversation. Bounding them needs a TTL of at least the longest provider retention (24 h for OpenAI extended caching) plus time-aware store interfaces, or per-conversation rows keyed on a stable conversation identity with a scope rollover. Substitution is never gated: the derived epoch gate is gone, and an explicit framework epoch may veto only NEW compression. Pixel renders and the tool-schema strip are memoised the same way. New compression stays live-zone-only. The runtime tripwire (`prefix_monitor.go`, on the provider routes and `/chatgpt`) checks the same rule on live traffic and records `requests.cache_bust_cause`: `client` (DEBUG); `raw_retry`, `stream_switch`, and `lever_freeze` (the harm tripwire's one-time strip rollover), all WARN; `caveman` (ERROR `caveman changed bytes the provider already cached` or `caveman dropped a cache breakpoint the client set`, counted as `caveman_cache_bust_requests` and raised by `caveman status`) — any `caveman` row is a bug. A request is held only to anchors accepted before its own forwarding decision. Cross-turn stability covers **anthropic, openai, azureopenai/openaicompat, and gemini** through `ExtractStabilizable`; `bedrock` and `vertex` expose no compressible blocks and stay pass-through. Anthropic uses declared `cache_control`; OpenAI/Gemini use latest-user/latest-tool zones against implicit provider caches. Replacement cache is SQLite spend store and must retain `journal_mode(WAL)` + `busy_timeout`; a hit rewrites `last_used_at` only once the row is 10 minutes stale, and pixel rows have their own cap inside the table's.\n- **tool-schema annotation strip is DEFAULT OFF and separately opted in**: `toolschema_strip: annotations` / `CAVEMAN_TOOLSCHEMA_STRIP=annotations` (`\"\"`, `off`, and any unrecognized value all mean off, normalized in the config loader AND re-checked at the decision point). It is a SECOND gate ON TOP of the four `liveZoneCompressionAllowed` conditions, which it reuses rather than restates, plus a THIRD: the session ledger's freeze registry (`ledger.LeverAllowed`) — it never runs in `record` mode, under `caveman.pass-through.v1`, under a compiled Cave Build, or in a session whose harm tripwire has frozen it. Its first decision per catalog (stripped or original) is memoised in the replacement cache and re-sent, so a recovery-store hiccup never flips the head of the prefix. S4 + CCR (the original catalog is stored before the rewritten bytes ship, disclosed as `x-caveman-toolschema-recovery-handle` and on the row's `RecoveryHandle`), promotable only under the local-wrap clause. It removes `$schema`/`title`/`examples`/`deprecated` ONLY inside schemas reached through `input_schema`/`inputSchema`/`parameters` and the JSON-Schema applicator allowlist — never from a tool envelope, `annotations` (whose `title` is the tool's display NAME), `_meta`, or a vendor extension. `ExtractToolCatalog` covers the **anthropic-messages** shape only; openai/gemini and `count_tokens` fail closed. It mints NOTHING: no tokens, no ratio, no dollars — the replay grid prices it. Its version is folded into the prefix-cache scope key so toggling it is a deliberate epoch rollover, not a silent partial cold write.\n- **cache-breakpoint planner defaults to `frontier` in optimization modes**: `breakpoint_plan: off` / `CAVEMAN_BREAKPOINT_PLAN=off` is explicit off; unrecognized values fail closed to off. Record, compress, and pixel modes still never run it. Provider-native cache optimizers for Anthropic, OpenAI, and Bedrock also default on when omitted, with explicit per-id `false` preserved. All remain capability-, auth-, endpoint-, economics-, and caller-marker-gated. Planner metadata is upstream-only and changes no model-visible bytes. It does not mint savings: `cache-breakpoint-plan` remains absent from `cacheOptimizerIDs`. OpenAI's arm hashes session id and never overwrites caller key; prefix-signature optimizer wins when already applied.\n- **harm tripwire / session ledger**: `internal/gateway/ledger.go` keys a bounded LRU (1024) on `x-cave-session`; **no session header = no entry and the whole mechanism is inert**. Every lever asks `LeverAllowed` before running. A lever active on request N earns a strike when request N+1's `cache_creation_input_tokens` exceeds BOTH an absolute floor (50k) AND 3x the session's baseline mean; 3 strikes freeze that lever for the session, one-way (never un-freezes), fail-open to pass-through. The baseline EXCLUDES calls already judged anomalous — a plain running mean folds each spike into the bar the next spike is measured against and goes blind to persistent regressions. Disclosure is `x-caveman-tripwire: <lever>=frozen`, and it appears from the request AFTER the one that tripped, because usage is only known once the response is already streaming. Thresholds are conservative by construction, pending replay-derived values.\n- **cache warming is predicted per stream, and replays only what Anthropic can answer at zero output** (`internal/gateway/cache_warm.go`, `cache_warm_table.go`): after a successful Anthropic Messages response on `api.anthropic.com` (never Bedrock, Vertex, Azure, a compat mount, a custom base URL, the pass-through header or an encoded body), the exact bytes and headers sent upstream (after compression, routing marks, heals and fail-open retries) are kept in memory only (LRU 1024 streams and 256 MiB, never stored, logged or captured) and may be replayed with `max_tokens: 0` and `stream: false` at `min(0.9*ttl, ttl-10s)` steps from the real request's start (wall time, so laptop sleep never sends a stale warm). Not replayable, so never warmed: `thinking.type: \"enabled\"`, `tool_choice` `tool`/`any`, `output_config.format`, no `cache_control`, a `ttl` other than `5m`/`1h`, mixed `5m` and `1h` markers, writes of another lifetime than the markers (one lifetime or no warm; the simulator classes by the same rule and none of the measured requests mixed them); the request scan reads top-level fields and cache_control markers only, no full parse. WHETHER to warm is optimal stopping, not a threshold: `cache_warm_table.json` holds the return-time distribution (30 s bins to 2 h) per stream class `main|subagent / tool|end / 5m|1h` (the class comes from the session key and whether the answer's `stop_reason` was `tool_use`, scanned off the plain bytes with a rolling tail; the proxy forwards the agent's `accept-encoding`, so an answer that comes back with a `Content-Encoding` is not classed: it is planned as whichever of the two classes warms less and its gap teaches neither), measured on 364,048 request transitions of real Claude Code sessions (2026-08-30 to 2026-10-08, `.blocks/cache-warm-sim.py`, aggregate probabilities only) and shrunk toward the parent class; the proxy learns each user's own gaps online (counts only, `$CAVEMAN_HOME/cache-warm-gaps.json`, 0600, written outside the warmer's lock, flushed by `Server.Close` at shutdown, symlink-refused, any implausible count drops the whole file; two proxies on one home are last-writer-wins) and blends them in with the defaults worth 100 observations. A stream evicted or still waiting at shutdown is counted as returning later than it was seen silent (spread over the later bins by the class's own distribution, \"never\" past 2 h): dropping it would keep only the returns and drift the table toward over-warming. At arm time the whole chain is planned by backward induction with the catalog price of the model sent (warm = read x prefix + input x uncached tail; avoided miss = (write_ttl - read) x prefix x the class's measured realized share, taken over the gaps that DID return: the window probability already leaves out the streams that never do), so the cumulative cost of every warm is counted before the first; unknown price, class or lifetime = no warm. Subscription traffic is valued at list prices as plan-usage equivalent. Stops: any real request of the stream (every model; it never waits for a warm in flight), any warm error, a warm reading `cache_read_input_tokens == 0`, the plan, 60 min, or the switch; the held request (body and credential headers) is released the moment its chain ends. A parent session's request does NOT stop its subagents' warms: a subagent can still be running, and on the test half that stop cost $565.52 net (below). A 429 (warm or real) pauses warming on that credential for the response's `Retry-After`, else until its `anthropic-ratelimit-unified-reset`, else 1 h for a subscription/OAuth login and 60 s for an API key; a 529 or 5xx on a warm pauses it for the `Retry-After`, else 5 min. Never sent while a real request of the same stream is in flight (retry every 5 s until its deadline). At most two warms are in flight process-wide (same retry; one with no slot by its deadline is dropped), and each fires up to 5 s before its planned time, never after. Warm rows are tagged `cache-warm`: spend, never a saving, never an agent request (excluded from request counts, session usage, trial origins, exported spans, receipts and `caveman sync` uploads; counted as `cache_warm_requests`), never fed to the harm-tripwire ledger, never sent to Cloud. Switch: on unless `$CAVEMAN_HOME/cloud.json` has `modules[\"waste-fixes\"]: false` (re-read on change); `CAVEMAN_CACHE_WARM=off` kills it for the process. Every answered stream request logs one line `cache warm plan` (info when warms are planned, debug when skipped: session hash, model, class, ttl, planned warms, skip reason: `response_error`, `off`, `not_anthropic_api`, `not_replayable`, `lifetime_mismatch`, `unpriced`, `not_worth_it`). Replay on the owner's data (test half, after 2026-10-03, 161,325 transitions): net $1,896.65 list-price equivalent at $385.31 of warms (9,628 warms, 7,280 wasted) vs $1,294.18 at $1,372.15 (45,343 warms) for the first fixed rule; the same policy with the parent stop for every subagent $1,331.13 at $197.28 (3,461 warms), with it only for subagents whose last answer was not a tool call $1,852.99 at $378.90 (9,518 warms). A test pins the plan to the simulator's on the shipped table (`TestCacheWarmPlanMatchesTheSimulator`; regenerate both together).\n- **pixel mode**: S4 lossy text→PNG (`pxpipe` port). Default allowlist is `claude-fable-5,gpt-5.6` via `CAVE_PIXEL_MODELS`; savings stay inferred-only. Renders keep the compress rules: the first decision for a text (render or text) is re-sent on every turn, a raw pin keeps the conversation text, and a NEW render is remembered only once its request goes out, with the original stored in CCR and the splice holding. If either fails, the new content goes out as text and records that, and earlier renders still go. A rendered block's `cache_control` moves to its last image. An Anthropic request carries at most 20 images, client images included (`anthropicManyImages`): past 20 the API rejects any image over 2000 px, and the renders are 2573 px wide, so a new render past the cap stays text.\n- **mask what cannot be summarized in place; elide what can**: `nativeruntime.afterTool` replaces an over-threshold tool output with a `ccr://` pointer stub, but NOT when `Engine.Detect` classifies it as `json`, `tabular`, or `log` (logfmt + NDJSON) — the classes with a field grammar, which the elision engine compresses in place into rows plus stated invariants (`all state=charged`, `status: delivered×18 attempted×17`, `wh-5000..wh-5059 all 60 present`). Masking those first destroys every fact AND costs more: the agent sees no row, then recovery re-enters the FULL original through the recovery-exempt path — whole page + stub + an extra turn, strictly worse than no wrap. Measured 2026-08-08 on the shipped default (`compress` → native policy `safe` → profile `full-safe` → mask on): inventory-mismatch and webhook-delivery-gaps scored 0/6 with 27–97 recovery calls, while rate-limit-forensics scored 3/3 at ~35% cheaper for the sole reason that its pages sat under the threshold. Capture is unaffected (the object is still stored, recovery still available); only the replacement is skipped, and the size rule for still-maskable classes is unchanged. Fails toward masking: no classifier → mask, so the fallback is bounded context. Tests: `mask_elidable_test.go`.\n- **route stage is optional and fails open** (`internal/cloudlink`, `internal/gateway/route.go`):\n  it asks Cloud `POST /v1/route` only for requests naming Auto (`AutoModel`, `caveman-auto`, also with Claude Code's `[1m]` suffix, the\n  picker entry the CLI adds while `modules.routing: true` in `$CAVEMAN_HOME/cloud.json`), while the\n  CLI is signed in with that switch on, for Anthropic Messages / OpenAI chat or responses requests on\n  an API key or a subscription alike; any other model goes as sent (`route.outcome: off`), the\n  session's marks and heal still applied. Auto goes upstream as the provider's `autoFallback` model\n  (Anthropic `claude-sonnet-5-5`, OpenAI `gpt-6.1-sol`; count_tokens too, and with no Cloud link),\n  which is also the asked model the ask carries and what a failure runs (a transient Cloud failure, that is a timeout, an unreachable or 5xx Cloud, an unreadable answer or the pause after one, instead runs the model the session's previous request went to, `heldModel`, never for a side request; no previous request on that provider, one that was a compaction or came after one, a held model that then did not serve (any non-2xx: the next attempt runs the fallback), signed out, routing off, an expired or refused login, a limit and a provider's refusal keep the fallback); Auto on any other\n  provider is a 400 (`cave_auto_unavailable`), and an encoded body naming Auto is decoded first. A\n  named model records `route.outcome: off`, reason `named_model`. OpenAI Auto (API key and ChatGPT\n  login) runs only `AutoOpenAIModels` (`gpt-6.1-sol`, `gpt-6-astra`, `gpt-6-luna`): the ask's\n  `Models` lists those alone and the pool drops other OpenAI models, and an answer naming any other\n  runs `gpt-6.1-sol` at its effort (`auto_model_refused`, `translate.FitEffort`); an Auto row's `RouteFrom` and\n  `x-caveman-routed-from` say `caveman-auto`. A move on a subscription stays on the provider's own\n  API on that subscription's credential; pool entries go out on their own login or the Cloud\n  gateway key, never on it, and never with the agent's `oauth-*` betas. On a ChatGPT login\n  (`/chatgpt`, `chatgpt_auto.go`; OpenCode's ChatGPT login lands there too) `/models` gains an Auto\n  entry (a copy of `gpt-6.1-sol`'s, listed last, fetched without `If-None-Match`, served without\n  `ETag`) while the link reports `AutoOffered` (signed in, routing on). Every POST there has its\n  first 64 KiB read (decoded when gzip, deflate (zlib or raw) or zstd, by a decoder that is always closed; a br body is never looked into) for a\n  top-level `\"model\":\"caveman-auto\"`; any other streams on unchanged. `/responses` naming Auto asks\n  with `NoPool`, keeps live-zone compression, replays `gpt-6.1-sol`'s bytes on a 4xx (a rate-limit 429, a 401 and a 403 are returned instead; a served replay pins the conversation raw, and the decision is rejected only then or when the replay is itself a 429; a 5xx on a moved model is returned and rejects the decision, a rate-limit 429 keeps it: the plan's limit holds for every model); any other\n  path (Codex compaction) runs `gpt-6.1-sol` unasked; a body over `CAVE_MAX_REQUEST_BYTES` streams\n  with only its model changed (`auto_body_too_large`; one that fits but does not decode records `auto_body_decode_failed`). The agent reads the Auto id it sent. Every `/chatgpt` row (Auto or\n  not) is offered to Cloud like any other, as its agent (`x-cave-agent`, else Codex by its\n  `originator` or User-Agent), with model ids in `route_from`/`route_to`; a model it could not read\n  goes to Cloud as none. The\n  ask starts before compression (parse, ask, compress, route), waits at most 800 ms (the CLI's\n  keychain credential is read when the runtime starts, `Link.Start`, never on a request goroutine;\n  a request that lands while a read runs with no secret cached waits for it, leaving the ask at\n  least 500 ms, else goes as `credential_loading` without an ask or a pause), carries the\n  caller's models, counts, what the request declares (the raw values of eleven allowlisted agent\n  headers, never cut: a value over 256 bytes, 16 KiB for Codex's turn metadata, which also comes\n  from the body's `client_metadata`, is left out; a forked Claude Code child's spawn-call\n  `subagent_type` as `x-caveman-agent`; its tool names, effort and thinking type), what the\n  session's previous request ran (the served model, the effort in force, the provider's input and\n  cache token counts, its age) and Cloud's opaque per-session state, plus, on a turn's first ask,\n  the raw text of the latest human turn, the one before it and the end of the agent's last reply\n  (contracts `route-ask-v1`; Cloud picks the model and the effort and no routing logic lives\n  here). Its answer is cached per ask until 30 min after its last use (session, or without one agent and\n  endpoint, provider, model, turn number, latest human text;\n  an input group carrying a tool result is no human turn, text riding along included) so a tool\n  loop never switches model or effort mid-turn (an ask first seen during a pause keeps the paused answer for its turn); a request the agent labels compaction or auxiliary\n  (Claude Code's request class and compaction flag, OpenCode's title/summary/compaction agent,\n  Codex's subagent kind and turn-metadata request kind) is asked on its own, without the text. The\n  session is `x-cave-session`, else the agent's session header (a Codex thread, `thread-id`, is\n  its own, its parent `x-codex-parent-thread-id`); a Claude Code child (`x-claude-code-agent-id`)\n  is a session of its own and sends its parent's state; a session id guessed from timing is not\n  used. State, previous-request facts and marks live in bounded in-memory LRUs (1024), never on\n  disk. The answer sets Responses `reasoning.effort`, chat `reasoning_effort` or Anthropic\n  `output_config.effort`; Anthropic `effort_mode: message` instead inserts the byte-identical mark\n  `{\"role\":\"system\",\"content\":[],\"output_config\":{\"effort\":…}}` (beta\n  `mid-conversation-output-config-2026-07-01` appended to `anthropic-beta`) before the last user\n  turn, or at the end after a tool result; on OpenAI Responses (GPT-6) the mark is a\n  `{\"type\":\"configuration_update\",\"reasoning\":{\"effort\":…}}` input item before the last user\n  message while nothing has answered it yet, never at the end and never next to another one (no\n  place, as mid tool loop: the effort in force holds), no beta, and none at all where OpenAI forbids\n  it (`truncation: \"auto\"`, `context_management`, a non-false `multi_agent`: Cloud's effort goes\n  top-level); a 400 naming a configuration update heals like a refused mark. Effort state made on\n  one wire starts afresh on the other. Only `effort_mode: message` ever starts marks; a \"top\"\n  answer on history carrying the session's marks goes in as one more mark (never wiping them; for\n  a compaction or side request, that request's only), and on any other history sets the top-level\n  field. Marks are remembered per session with a salted hash of the message before each\n  (`cache_control` and thinking blocks left out, so a heal's strip keeps them) and replayed at the\n  same places on every later request to a model that already took them (the agent resends history\n  without them), count_tokens of a known session or a forked child's parent included (it adds\n  nothing to a history without marks). Without an effort from Cloud (a failure, routing off,\n  effort \"\") a history without the session's marks goes as the agent sent it, top-level field\n  included (one cache restart where Cloud's was, nothing inserted, no beta), except that a\n  remembered heal (drop_block with its beta, or a strip) keeps applying. One with marks runs at\n  the request's own top-level effort, or, when it sets none, at the model's default effort\n  (Cloud's `default_effort`, kept per session, a forked child taking its parent's), marked when it\n  differs from the one in force; only with no default known does such a request go as the agent\n  sent it, without the marks, which the session then forgets (its later thinking blocks then lose\n  their binding) unless the request went to another model. The top-level field is fixed at the\n  session's first per-message request: the routed effort on a fresh conversation whose request\n  sets one (kept there, also for later requests that set none), else the request's own; a routed\n  effort for a request that sets none goes in as a mark, first on a fresh conversation. An anchor\n  that no longer matches drops that mark and every later one; compaction and side requests never\n  change them, and a body matching none of them (an unlabeled side request, compacted history)\n  gets marks of its own, kept for up to four request shapes (first message, length, last message),\n  which take the session over only from a longer request continuing that conversation (same first\n  message, its last one where it was); the session's marks and fixed top-level field then wait\n  there in turn. A thinking-binding 400 on the asked model of a `/messages` request retries once\n  only on a session the route stage changed (it was on for the request, the session carries its\n  marks or heal, or another model served one of its requests) whose body sets no\n  `thinking.block_binding`: with `thinking.block_binding.prefix_mismatch_behavior: \"drop_block\"`\n  (beta `thinking-binding-controls-2026-08-01`) when the block is bound to a different\n  conversation and the request's thinking is adaptive or enabled, then sent on every later request\n  (count_tokens and forked children included); else (Sonnet 5.5 `between_tools`, no thinking\n  field, a tampered signature) without thinking blocks from the failing message on, and later\n  requests on the same history (same first message, same message at its end) strip the same range\n  up front (a second strip widens it; the strip stays, the history as served, even if the agent\n  later sets its own `block_binding`). A drop_block retry refused with a 400 naming the binding,\n  `block_binding` or its beta moves the session to the strip path, a strip refused that way ends\n  its heals (the first lasts for the session, as it depends on the model and thinking type; the\n  second goes with the marks, cleared when they are forgotten; a side request never clears either,\n  nor the strip; a 429, 5xx or other 400 on the retry teaches nothing). Routing switched off after\n  a restart surfaces that 400. A refused mark retries once with top-level effort only and, once\n  served, latches per-message effort off for the session (a model that refused takes Cloud's\n  effort top-level); compressed 400s are decoded first. A Cloud error, timeout, 401/403,\n  `allowance` or `billing_limit` answer keeps the asked model (the held model above for a transient failure) and pauses new asks (1 min; 10 min\n  for 401/403 and billing_limit; until the 1st for allowance). Refusals and limits land in\n  `$CAVEMAN_HOME/route-state.json` (with Cloud's notice) for `caveman status`; a new login lifts\n  the pause. A route failure blames a model only on evidence (Cloud ADR 0110). A provider 4xx on\n  the routed model replays the original bytes on the asked model, except a 401 (the login, not the\n  model: returned as is); a 403 replays (a plan refuses a model it does not carry with one). Only a\n  served replay of bytes that differed from the original in the model alone blames the routed model\n  (`Reject(true)`, `provider_rej\n\n<!-- [TRUNCATED AT 50KB] -->","shrink/AGENTS.md":"# shrink — caveman-shrink, the tool-catalog compressor (Go core + launcher, Apache-2.0)\n\nCompress MCP/OpenAI **tool definitions** before they fill the context window. A thin Go wrapper\nover the [engine](../engine/CLAUDE.md)'s `toolschema` compressor: it drops annotation metadata\n(examples, titles, `$comment`, `$schema`) and reduces long descriptions to their lead plus every\nconstraint-bearing sentence. It keeps every **selection token** (tool/parameter names, types,\nenums, required) and argument-construction value (`default`, `const`, `$ref` targets)\nbyte-for-byte. Everything is `inferred`.\n\n## Layout\n- `shrink.go` — the library: `Shrink` (catalog → compressed, fail-open S4 transform, durable-CCR-backed), `Recover` (handle → original bytes), `Lint` (per-tool inferred token reductions), `SelectionProfile` (the structural surface — what must survive). `WithStore`/`WithStorePath` select recovery storage; default is `CAVEMAN_CCR_DB` / `~/.caveman/ccr.db`.\n- `cmd/caveman-shrink/` — the CLI: `caveman-shrink` / `shrink` (stdin→stdout), `lint <file>`, and `recover <handle>`.\n- `bin/caveman-shrink.mjs` + `package.json` — the `npx caveman-shrink` launcher around the prebuilt binary.\n\n## Two correctness boundaries\n`SelectionProfile(input)` == `SelectionProfile(Shrink(input).Output)` always. This proves the\nstructural selection surface (names/params/enums/required) survives byte-for-byte. It does **not**\nprove the model selects the same tool: descriptions are model-visible and long descriptions are\nlossy. Behavioral selection equivalence requires model-eval fixtures; keep results `inferred`.\n\nSelection alone does not guarantee valid arguments. Short descriptions stay whole; long ones\nkeep the lead plus every recognised constraint sentence (must/cannot/required/bounds/format/\nISO/RFC/absolute/etc.). `default`, `const`, and internal `$ref` targets survive. Golden and\nadversarial conformance tests lock these construction surfaces; over-keep is safer than a retry.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=shrink` / `make product-test PRODUCT=shrink`.\n- The structural compressor lives in the **engine** (`compressors/toolschema.go`); shrink reuses it, never forks it.\n- **This is the dedicated product surface for the `toolschema` compressor.** Engine API/CLI callers\n  can also force it locally. Managed-gateway adapters keep tool arrays in the frozen prompt-cache\n  prefix and never hand them to it; gateway's separate S2 tool-search/deferral path is not\n  compression. Local reductions stay `inferred`. A future billed route for this transform needs\n  cache-versus-schema cost proof, stable byte-identical prefix output, and an eval gate.\n\n## Gotchas (honesty invariants)\n- **S4 lossy, fail-open** — successful compression changes model-visible bytes; malformed/incompressible catalogs pass through unchanged (`ratio:0`, no handle).\n- **inferred-only** — token counts are estimates via the engine's offline counter; never `verified`, never re-projected.\n- **reversible for real** — a compressing shrink commits the exact original to the durable shared\n  CCR store before returning a handle. A separate process resolves it through `Recover` /\n  `caveman-shrink recover`; failure to open storage makes library `Shrink` return an error, while\n  the CLI catches that error and forwards the original bytes with no handle.\n\nSee ../../CLAUDE.md (root) · ../engine/CLAUDE.md\n","shrink/CLAUDE.md":"# shrink — caveman-shrink, the tool-catalog compressor (Go core + launcher, Apache-2.0)\n\nCompress MCP/OpenAI **tool definitions** before they fill the context window. A thin Go wrapper\nover the [engine](../engine/CLAUDE.md)'s `toolschema` compressor: it drops annotation metadata\n(examples, titles, `$comment`, `$schema`) and reduces free-text descriptions to their lead\nsentence **plus every constraint-bearing sentence**, while keeping every **selection token** —\ntool and parameter names, types, enums, required — and every **argument-construction value** —\n`default`, `const`, `$ref` targets — byte-for-byte. Everything is `inferred`.\n\n## Layout\n- `shrink.go` — the library: `Shrink` (catalog → compressed, fail-open S4 transform, durable-CCR-backed), `Recover` (handle → original bytes), `Lint` (per-tool inferred token reductions), `SelectionProfile` (the structural surface — what must survive). `WithStore`/`WithStorePath` pick the recovery store; default is the shared `CAVEMAN_CCR_DB` / `~/.caveman/ccr.db`.\n- `cmd/caveman-shrink/` — the CLI: `caveman-shrink` / `shrink` (stdin→stdout), `lint <file>`, and `recover <handle>` (prints the original catalog for a handle a prior shrink minted).\n- `bin/caveman-shrink.mjs` + `package.json` — the `npx caveman-shrink` launcher around the prebuilt binary.\n\n## Two correctness boundaries\n1. **structural selection surface** — `SelectionProfile(input)` ==\n   `SelectionProfile(Shrink(input).Output)` always. This proves names/params/enums/required survive\n   byte-for-byte. It does **not** prove the model selects the same tool: descriptions are\n   model-visible and long descriptions are lossy. Behavioral equivalence requires model-eval\n   fixtures; current results stay `inferred`.\n2. **argument validity** — agents build tool *arguments* from descriptions, so shrink preserves\n   selection but **does not by itself guarantee argument validity**. What it guarantees, biased\n   to over-keep: (a) a description that already fits a small budget (≤ `maxDescLen*2` bytes) is\n   kept **whole** — no sentence is elided, so a constraint phrased without a recognised marker\n   still survives; (b) in a genuinely large description, the lead sentence plus every sentence\n   carrying a constraint marker (must / cannot / required / rejected / invalid / not allowed /\n   exactly one / at least / at most / max / min / range / between / greater-than / over / above /\n   unique / mutually exclusive / format / ISO / RFC / absolute …) is retained in full. Truncating\n   an abbreviation (\"Target path, e.g. /srv…\" → \"Target path, e.\") or dropping\n   `default`/RFC3339/bound rules would produce invalid calls the structural profile cannot see;\n   the call-validity conformance test (`engine/compressors`, golden fixture + reproduced\n   under-keep cases) locks recognised constraint tokens in place. Over-keep is the rule — never\n   knowingly under-keep.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=shrink` / `make product-test PRODUCT=shrink`.\n- The structural compressor lives in the **engine** (`compressors/toolschema.go`); shrink reuses it, never forks it.\n- **This is the dedicated product surface for the `toolschema` compressor.** Engine API/CLI callers\n  can also force it locally. Managed-gateway adapters keep tool arrays in the frozen prompt-cache\n  prefix and never hand them to it; gateway's separate S2 tool-search/deferral path is not\n  compression. Local reductions stay `inferred`. A future billed route for this transform needs\n  cache-versus-schema cost proof, stable byte-identical prefix output, and an eval gate.\n\n## Gotchas (honesty invariants)\n- **S4 lossy, fail-open** — successful compression changes model-visible bytes; malformed/incompressible catalogs pass through unchanged (`ratio:0`, no handle).\n- **inferred-only** — token counts are estimates via the engine's offline counter; never `verified`, never re-projected.\n- **reversible for real** — a compressing shrink writes the exact original to the **durable** store (`engine.Compress` commits the recovery row before it publishes transformed bytes), so a printed handle resolves later, from a separate process, via `Recover` / `caveman-shrink recover`. It is NOT an in-memory store discarded on return — that minted handles that were unresolvable the instant `Shrink` returned. If the durable store cannot be opened, `Shrink` returns an error and the CLI forwards the original bytes: no handle, no reversibility claim.\n\nSee ../../CLAUDE.md (root) · ../engine/CLAUDE.md\n","skills/cavecrew/README.md":"# cavecrew\n\nDecision guide. When to delegate to caveman subagents instead of doing the work inline.\n\n## What it does\n\nTells main thread when to spawn a caveman-style subagent. Compact return\ncontracts can reduce repeated prose when results return to main context, but\neffect depends on task, agent, and delegation count. This skill publishes no\nuniversal reduction rate.\n\nThree subagents:\n\n| Subagent | Job | Use when |\n|----------|-----|----------|\n| `cavecrew-investigator` | Locate code (read-only) | \"Where is X defined / what calls Y / list uses of Z\" |\n| `cavecrew-builder` | Surgical edit, 1-2 files | Scope is obvious, ≤2 files. Refuses 3+ file scope. |\n| `cavecrew-reviewer` | Diff/file review | One-line findings with severity emoji |\n\nUse vanilla `Explore` or `Code Reviewer` when you want prose, architecture commentary, or rationale. Use main thread directly for one-line answers and 3+ file refactors.\n\nThis skill is a decision guide, not a slash command. It activates when the conversation mentions delegation.\n\n## How to invoke\n\nTriggers on phrases like \"delegate to subagent\", \"use cavecrew\", \"spawn investigator\", \"save context\", \"compressed agent output\".\n\n## Example chaining\n\nLocate → fix → verify (most common):\n\n1. `cavecrew-investigator` returns site list (`path:line`, symbol, note)\n2. Main thread picks 1-2 sites, hands paths to `cavecrew-builder`\n3. `cavecrew-reviewer` audits the resulting diff\n\nParallel scout: spawn 2-3 `cavecrew-investigator` calls in one message with different angles (defs, callers, tests). Aggregate in main.\n\n## Model overrides\n\nBy default, `cavecrew-reviewer` and `cavecrew-investigator` pin `model: haiku` in their frontmatter; `cavecrew-builder` has no `model:` line (uses the API session default). Set env vars in your shell before launching Claude Code to override per-agent:\n\n| Env var | Agent |\n|---|---|\n| `CAVECREW_REVIEWER_MODEL` | `cavecrew-reviewer` |\n| `CAVECREW_BUILDER_MODEL` | `cavecrew-builder` |\n| `CAVECREW_INVESTIGATOR_MODEL` | `cavecrew-investigator` |\n\nExample: run reviewer on sonnet and keep others on default.\n\n```sh\nexport CAVECREW_REVIEWER_MODEL=sonnet\n```\n\nUse the same model name strings you'd use in any Claude Code agent frontmatter (e.g. `haiku`, `sonnet`, `opus`).\n\nOverrides patch only `model:` line in installed agent frontmatter; prompt body\nstays untouched and continues receiving upstream updates. Only plugin installs\nhave local agent files to patch. Empty variables do nothing. Patch persists until\nplugin update or reinstall.\n\n## See also\n\n- [`SKILL.md`](./SKILL.md): full decision matrix and output contracts\n- [`agents/cavecrew-investigator.md`](../../agents/cavecrew-investigator.md)\n- [`agents/cavecrew-builder.md`](../../agents/cavecrew-builder.md)\n- [`agents/cavecrew-reviewer.md`](../../agents/cavecrew-reviewer.md)\n- [Caveman README](../../README.md): repo overview\n","skills/cavecrew/SKILL.md":"---\nname: cavecrew\ndescription: >\n  When to delegate to `cavecrew-investigator` (locate code), `cavecrew-builder`\n  (1-2 file edit) or `cavecrew-reviewer` (diff review) instead of working inline\n  or using `Explore`. Their output is compressed, so main context lasts longer.\n---\n\nCavecrew = three subagent presets that answer in ultracave voice (tool runs: one status line in, one out; nothing between routine calls). Same job as Anthropic defaults (`Explore`, edit-style agents, reviewer); difference is the tool-result they return is compressed, so main context shrinks per delegation.\n\n## When to use cavecrew vs alternatives\n\n| Task | Use |\n|---|---|\n| \"Where is X defined / what calls Y / list uses of Z\" | `cavecrew-investigator` |\n| Same but you also want suggestions/architecture commentary | `Explore` (vanilla) |\n| Surgical edit, ≤2 files, scope obvious | `cavecrew-builder` |\n| New feature / 3+ files / cross-cutting refactor | Main thread, or `feature-dev:code-architect` if installed |\n| Review diff, branch, or file for bugs | `cavecrew-reviewer` |\n| Deep code review with rationale + alternatives | `Code Reviewer` (vanilla) if installed, else main thread |\n| One-line answer you already know | Main thread, no subagent |\n\nRule of thumb: **if you'd want the subagent's output in 1/3 the tokens, pick cavecrew. If you'd want prose, pick vanilla.**\n\n## Why this exists (the real win)\n\nSubagent tool results get injected into main context verbatim. A vanilla `Explore` that returns 2k tokens of prose costs 2k tokens of main-context budget every time. The same finding from `cavecrew-investigator` comes back as a compressed `path:line` table instead. Across 20 delegations in one session that's the difference between context exhaustion and finishing the task.\n\n## Output contracts\n\nWhat main thread can rely on per agent:\n\n**`cavecrew-investigator`**\n```\n<Header>:\n- path:line — `symbol` — short note\ntotals: <counts>.\n```\nOr `No match.` Always file-path-first, line-number-attached, backticked symbols. Safe to grep with `path:\\d+`.\n\n**`cavecrew-builder`**\n```\n<path:line-range> — <change ≤10 words>.\nverified: <re-read OK | mismatch @ path:line>.\n```\nOr one of: `too-big.` / `needs-confirm.` / `ambiguous.` / `regressed.` (terminal first token).\n\n**`cavecrew-reviewer`**\n```\npath:line: <emoji> <severity>: <problem>. <fix>.\ntotals: N🔴 N🟡 N🔵 N❓\n```\nOr `No issues.` Findings sorted file → line ascending.\n\n## Chaining patterns\n\n**Locate → fix → verify** (most common):\n1. `cavecrew-investigator` returns site list.\n2. Main thread picks 1-2 sites, hands paths to `cavecrew-builder`.\n3. `cavecrew-reviewer` audits the diff.\n\n**Parallel scout** (when investigation is broad):\nSpawn 2-3 `cavecrew-investigator` calls in one message (different angles: defs vs callers vs tests). Aggregate in main thread.\n\n**Single-shot edit** (when site is already known):\nSkip investigator. Hand exact path:line to `cavecrew-builder` directly.\n\n## What NOT to do\n\n- Don't use `cavecrew-builder` when you don't already know the file. Spawn investigator first or main thread will eat tokens passing context.\n- Don't chain `cavecrew-investigator → cavecrew-builder` for a 5-file refactor. Builder will return `too-big.` and you'll have wasted a turn.\n- Don't ask `cavecrew-reviewer` for \"general feedback\" — it returns findings only, no architecture opinions. Use `Code Reviewer` for that when installed.\n- Don't expect prose. Cavecrew output is structured, sometimes terse to the point of cryptic. If a human will read it directly, paraphrase.\n\n## Auto-clarity (inherited)\n\nSubagents drop caveman → normal English for security warnings, irreversible-action confirmations, and any output where fragment ambiguity could be misread. Resume caveman after.\n","skills/caveman-commit/README.md":"# caveman-commit\n\nTerse Conventional Commits. Why over what.\n\n## What it does\n\nGenerates commit messages in Conventional Commits format. Subject ≤50 chars, hard cap 72. Imperative mood. Body only when the *why* is non-obvious or there are breaking changes. No AI attribution, no \"this commit does X\", no emoji unless the project uses them. Body always required for breaking changes, security fixes, data migrations, and reverts — future debuggers need the context.\n\nOutputs only the message. Does not stage, commit, or amend.\n\n## How to invoke\n\n```\n/caveman-commit\n```\n\nAlso triggers on phrases like \"write a commit\", \"commit message\", \"generate commit\".\n\n## Example output\n\nDiff: new endpoint for user profile.\n\n```\nfeat(api): add GET /users/:id/profile\n\nMobile client needs profile data without the full user payload\nto reduce LTE bandwidth on cold-launch screens.\n\nCloses #128\n```\n\nDiff: breaking API rename.\n\n```\nfeat(api)!: rename /v1/orders to /v1/checkout\n\nBREAKING CHANGE: clients on /v1/orders must migrate to /v1/checkout\nbefore 2026-06-01. Old route returns 410 after that date.\n```\n\n## See also\n\n- [`SKILL.md`](./SKILL.md) — full LLM-facing instructions\n- [Caveman README](../../README.md) — repo overview\n","skills/caveman-commit/SKILL.md":"---\nname: caveman-commit\ndescription: >\n  Write a Conventional Commits message compressed to intent only. Use for\n  \"write a commit\", \"commit message\", /commit or /caveman-commit.\n---\n\nWrite commit messages terse and exact. Conventional Commits format. No fluff. Why over what.\n\n## Rules\n\n**Subject line:**\n- `<type>(<scope>): <imperative summary>` — `<scope>` optional\n- Types: `feat`, `fix`, `refactor`, `perf`, `docs`, `test`, `chore`, `build`, `ci`, `style`, `revert`\n- Imperative mood: \"add\", \"fix\", \"remove\" — not \"added\", \"adds\", \"adding\"\n- ≤50 chars when possible, hard cap 72\n- No trailing period\n- Match project convention for capitalization after the colon\n\n**Body (only if needed):**\n- Skip entirely when subject is self-explanatory\n- Add body only for: non-obvious *why*, breaking changes, migration notes, linked issues\n- Wrap at 72 chars\n- Bullets `-` not `*`\n- Reference issues/PRs at end: `Closes #42`, `Refs #17`\n\n**What NEVER goes in:**\n- \"This commit does X\", \"I\", \"we\", \"now\", \"currently\" — the diff says what\n- \"As requested by...\" — use Co-authored-by trailer\n- \"Generated with Claude Code\" or any AI attribution — unless the user's own rule requires an `Assisted-by`/AI-attribution trailer, then add it as a trailer\n- Emoji (unless project convention requires)\n- Restating the file name when scope already says it\n\n## Examples\n\nDiff: new endpoint for user profile with body explaining the why\n- ❌ \"feat: add a new endpoint to get user profile information from the database\"\n- ✅\n  ```\n  feat(api): add GET /users/:id/profile\n\n  Mobile client needs profile data without the full user payload\n  to reduce LTE bandwidth on cold-launch screens.\n\n  Closes #128\n  ```\n\nDiff: breaking API change\n- ✅\n  ```\n  feat(api)!: rename /v1/orders to /v1/checkout\n\n  BREAKING CHANGE: clients on /v1/orders must migrate to /v1/checkout\n  before 2026-06-01. Old route returns 410 after that date.\n  ```\n\n## Auto-Clarity\n\nAlways include body for: breaking changes, security fixes, data migrations, anything reverting a prior commit. Never compress these into subject-only — future debuggers need the context.\n\n## Boundaries\n\nOnly generates the commit message. Does not run `git commit`, does not stage files, does not amend. Output the message as a code block ready to paste. \"stop caveman-commit\" or \"normal mode\": revert to verbose commit style.\n","skills/caveman-compress/README.md":"<p align=\"center\">\n  <img src=\"https://em-content.zobj.net/source/apple/391/rock_1faa8.png\" width=\"80\" />\n</p>\n\n<h1 align=\"center\">caveman-compress</h1>\n\n<p align=\"center\">\n  <strong>shrink memory file. save token every session.</strong>\n</p>\n\n---\n\nA Claude Code skill that compresses project memory files (`CLAUDE.md`, todos,\npreferences) into caveman format, reducing repeated input size.\n\nClaude loads `CLAUDE.md` on every session start, so large files add repeated\ninput tokens. Caveman shortens supported natural-language files.\n\n## What It Do\n\n```\n/caveman-compress CLAUDE.md\n```\n\n```\nCLAUDE.md          ← compressed (Claude reads smaller file each session)\nCLAUDE.original.md ← human-readable backup (you edit this)\n```\n\nOriginal remains in data directory rather than next to live file, so skill\nauto-loaders do not read it twice. Path is\n`$XDG_DATA_HOME/caveman-compress/backups/<parent-dir-name>/` on macOS and Linux,\nor `%LOCALAPPDATA%\\caveman-compress\\backups\\<parent-dir-name>\\` on Windows. Edit\n`.original.md` there, then run skill again to re-compress.\n\n## Benchmarks\n\nThe five fixture pairs in [`tests/caveman-compress/`](../../tests/caveman-compress/),\ncounted with tiktoken o200k. Reproduce with\n`uv run --with tiktoken python skills/caveman-compress/scripts/benchmark.py`.\n\n| File | Original | Compressed | Saved |\n|------|----------:|----------:|------:|\n| `claude-md-preferences.md` | 827 | 421 | 49.1% |\n| `claude-md-project.md` | 1628 | 1117 | 31.4% |\n| `mixed-with-code.md` | 1432 | 1106 | 22.8% |\n| `project-notes.md` | 1431 | 846 | 40.9% |\n| `todo-list.md` | 880 | 648 | 26.4% |\n| Total | 6198 | 4138 | 33.2% |\n\nAll fixture validations passed: headings, code blocks, URLs, and file paths were\npreserved exactly.\n\n## Before / After\n\n<table>\n<tr>\n<td width=\"50%\">\n\n### Original (827 tokens)\n\n> \"I strongly prefer TypeScript with strict mode enabled for all new code. Please don't use `any` type unless there's genuinely no way around it, and if you do, leave a comment explaining the reasoning. I find that taking the time to properly type things catches a lot of bugs before they ever make it to runtime.\"\n\n</td>\n<td width=\"50%\">\n\n### <img src=\"../../docs/assets/dancing-rock.svg\" width=\"20\" height=\"20\" alt=\"rock\"/> Caveman (421 tokens)\n\n> \"Prefer TypeScript strict mode always. No `any` unless unavoidable; comment why if used. Proper types catch bugs early.\"\n\n</td>\n</tr>\n</table>\n\nThis fixture produced 49.1% fewer counted tokens. Structural validation passed;\nresult does not prove semantic equivalence on other files or models.\n\n## Security\n\n`caveman-compress` is flagged as Snyk High Risk due to subprocess and file I/O\npatterns detected by static analysis. See [SECURITY.md](./SECURITY.md) for why\nthese operations exist and how paths are constrained.\n\n## Install\n\nCompress is built into the `caveman` plugin. Install `caveman` once, then use `/caveman-compress`.\n\nIf you need local files, the compress skill lives at:\n\n```bash\nskills/caveman-compress/\n```\n\nRequires Python 3.10 or newer.\n\n### Provider / model\n\nDefault path uses Claude: `ANTHROPIC_API_KEY` + Anthropic SDK when set, else `claude --print`.\n\nTo use opencode instead (any model opencode can reach, free ones included):\n\n```bash\nexport CAVEMAN_COMPRESS_PROVIDER=opencode\nexport CAVEMAN_COMPRESS_MODEL=opencode/big-pickle   # any id from `opencode models`\n```\n\nTo use a local model (or any OpenAI-compatible server) instead:\n\n```bash\nexport CAVEMAN_COMPRESS_PROVIDER=openai-compat\nexport CAVEMAN_COMPRESS_MODEL=qwen3:8b                         # required\nexport CAVEMAN_COMPRESS_ENDPOINT=http://localhost:11434/v1     # default (Ollama)\n# export CAVEMAN_COMPRESS_API_KEY=...                          # optional, sent as Bearer token\n```\n\nCommon endpoints: Ollama `http://localhost:11434/v1`, LM Studio `http://localhost:1234/v1`, llama.cpp server `http://localhost:8080/v1`, vLLM `http://localhost:8000/v1`. Nothing leaves your machine with a local server. A non-local `http://` endpoint sends the file in plaintext; use `https://` for remote servers. Small local models get it wrong more often; when they do, validation fails and your file stays untouched.\n\n`CAVEMAN_MODEL` is the fallback when `CAVEMAN_COMPRESS_MODEL` is unset. On the Claude path the model also applies to `claude --print`. The opencode path needs opencode 2.x (it uses `opencode run --standalone`).\n\n## Usage\n\n```\n/caveman-compress <filepath>\n```\n\nExamples:\n```\n/caveman-compress CLAUDE.md\n/caveman-compress docs/preferences.md\n/caveman-compress todos.md\n```\n\n### What files work\n\n| Type | Compress? |\n|------|-----------|\n| `.md`, `.mdc`, `.txt`, `.rst`, `.typ`, `.typst`, `.tex` | Yes |\n| Extensionless natural language | Yes |\n| `.py`, `.js`, `.ts`, `.json`, `.yaml` | ❌ Skip (code/config) |\n| `*.original.md` | ❌ Skip (backup files) |\n\n## How It Work\n\n```\n/caveman-compress CLAUDE.md\n        ↓\nbasic checks: file exists, under 500KB, not a sensitive filename\n        ↓\nacquire cross-session lock on the file  (waits up to 15 min if another run holds it, then errors)\n        ↓\ndetect file type        (no tokens)\n        ↓\nconfigured provider compresses       (tokens: one call)\n        ↓\nvalidate output         (no tokens)\n  checks: headings, code blocks, URLs, file paths, bullets\n        ↓\nif errors: configured provider fixes cherry-picked issues only   (tokens: targeted fix)\n  does NOT recompress; only patches broken parts\n        ↓\nretry up to 2 times\n        ↓\nwrite compressed → CLAUDE.md\nwrite original   → CLAUDE.original.md\n```\n\nOnly two things use tokens: initial compression + targeted fix if validation fails. Everything else is local Python.\n\n## What Is Preserved\n\nCaveman compress natural language. It never touch:\n\n- Code blocks (` ``` ` fenced or indented)\n- Inline code (`` `backtick content` ``)\n- URLs and links\n- File paths (`/src/components/...`)\n- Commands (`npm install`, `git commit`)\n- Technical terms, library names, API names\n- Headings (exact text preserved)\n- Tables (structure preserved, cell text compressed)\n- Dates, version numbers, numeric values\n- Anything you wrap in `<!-- nocompress -->` ... `<!-- /nocompress -->`\n\nWant a part left exactly as written (an `<example>` block, a prompt template)? Wrap it. Each tag go on its own line. Model never change what inside; it come back byte for byte. Model can still read it if first try need fixing, so not hiding place for secrets. Forget the closing tag and compress stop before touching file.\n\n```markdown\n<!-- nocompress -->\n<example>\nReply in this exact format.\n</example>\n<!-- /nocompress -->\n```\n\n## Why This Matter\n\n`CLAUDE.md` loads on every session start. A 1,000-token project memory file adds\n1,000 input tokens each time project opens, or 100,000 across 100 sessions.\n\nCaveman reduced counted tokens by 33.2% in total on the five listed fixtures\n(22.8% to 49.1% per file). Validators confirmed headings, code blocks, URLs,\nand file paths. They did not establish general semantic or task-quality\nequivalence.\n\n```\n┌────────────────────────────────────────────┐\n│  TOKEN SAVINGS, ALL FIVE   ███       33.2% │\n│  FIXTURES IN TABLE                       5 │\n│  STRUCTURAL VALIDATION       passed on all │\n│  SETUP TIME                █            1x │\n└────────────────────────────────────────────┘\n```\n\n## Part of Caveman\n\nThis skill is part of the [caveman](https://github.com/JuliusBrussee/caveman) toolkit.\n\n- `caveman`: ask Claude to answer in shorter prose\n- `caveman-compress`: shorten supported project-memory files with backups and validation\n","skills/caveman-compress/SECURITY.md":"# Security\n\n## Snyk High Risk Rating\n\n`caveman-compress` receives a Snyk High Risk rating due to static analysis heuristics. This document explains what the skill does and does not do.\n\n### What triggers the rating\n\n1. **subprocess usage**: The skill calls the `claude` CLI via `subprocess.run()` as a fallback when `ANTHROPIC_API_KEY` is not set. It can also call `opencode run` when `CAVEMAN_COMPRESS_PROVIDER=opencode`. Subprocess calls use fixed argument lists — no shell interpolation occurs. Claude receives user file content via stdin; opencode receives the generated prompt through a temporary `--file` attachment that is deleted after the call.\n\n   The file being compressed is untrusted input, and opencode 2.x lets its agents use every tool that is not explicitly denied: shell, file edits, web search and fetch, MCP tools from your opencode config, subagents. Compression needs none of them. Compress therefore runs `opencode run --standalone --agent caveman-compress` with `OPENCODE_CONFIG_CONTENT` denying every permission (`\"*\": \"deny\"`) both globally and for that agent, and never passes `--auto`. The dedicated agent matters because opencode applies a per-agent rule after the global one, so an `agent.build.permission` allow in your own config would otherwise re-enable a tool. `--standalone` matters because the shared opencode background service does not read the caller's environment, so the deny rule only binds a private server. Checked on opencode 2.0.22: the model is offered no tools and the prompt attachment is still read. Unlike the Claude path (`--strict-mcp-config`), compress does not switch off the MCP servers in your opencode config: their tools are denied, but opencode may still start them.\n\n2. **File read/write**: The skill reads the file the user explicitly points it at, compresses it, and writes the result back to the same path. A `.original.md` backup is saved to an out-of-tree data dir (`$XDG_DATA_HOME/caveman-compress/backups/<parent-dir-name>/`, or `%LOCALAPPDATA%\\caveman-compress\\backups\\<parent-dir-name>\\` on Windows). The opencode provider writes a temporary prompt attachment in the OS temp directory and deletes it after the subprocess exits. Beyond the target file, that backup location, and the temporary attachment, no files are read or written.\n\n### What the skill does NOT do\n\n- Does not execute user file content as code\n- Does not make network requests except through the configured LLM provider CLI, SDK, or (for `openai-compat`) the one configured HTTP endpoint\n- Does not read unrelated files outside the path the user provides\n- Does not use shell=True or string interpolation in subprocess calls\n- Does not collect or transmit any data beyond the file being compressed\n\n### Auth behavior\n\nDefault path uses Claude. If `ANTHROPIC_API_KEY` is set, the skill uses the Anthropic Python SDK directly (no subprocess). If not set, it falls back to the `claude` CLI, which uses the user's existing Claude desktop authentication.\n\nSet `CAVEMAN_COMPRESS_PROVIDER=opencode` to use `opencode run` instead. Set `CAVEMAN_COMPRESS_MODEL=provider/model` for compress-specific model selection. `CAVEMAN_MODEL` is the fallback when `CAVEMAN_COMPRESS_MODEL` is unset.\n\nSet `CAVEMAN_COMPRESS_PROVIDER=openai-compat` to POST the prompt with Python's standard-library `urllib` (no subprocess, no extra dependency) to `$CAVEMAN_COMPRESS_ENDPOINT/chat/completions`, default `http://localhost:11434/v1` (Ollama). Other local servers: LM Studio `http://localhost:1234/v1`, llama.cpp `http://localhost:8080/v1`, vLLM `http://localhost:8000/v1`. `CAVEMAN_COMPRESS_API_KEY`, if set, is sent as a Bearer token. A non-local `http://` endpoint sends the file and that key in plaintext; use `https://` for anything off the machine.\n\n### File size limit\n\nFiles larger than 500KB are rejected before any API call is made.\n\n### Snyk W007 (\"insecure credential handling\")\n\nW007 fires on the rule that code blocks, commands, and exact error strings stay verbatim. See [Scanner warnings](../../SECURITY.md#scanner-warnings) in the repository SECURITY.md.\n\n### Reporting a vulnerability\n\nIf you believe you've found a genuine security issue, please open a GitHub issue with the label `security`.\n","skills/caveman-compress/SKILL.md":"---\nname: caveman-compress\ndescription: >\n  Compress a memory file such as CLAUDE.md or a todo list into caveman format\n  to save input tokens, keeping a readable backup. Sends the file's text to the\n  configured model provider. Trigger: /caveman-compress.\n---\n\n# Caveman Compress\n\n## Purpose\n\n> **Data leaves the machine.** Compression sends the file's full text to the model provider that `scripts/compress.py` is configured for: the `claude` CLI, the Anthropic API, or an OpenAI-compatible endpoint. Files whose names look like secrets (`.env`, `credentials`, `secrets`, `.netrc`, …) are refused before they are read. Don't compress a file the provider shouldn't see.\n\nCompress natural language files (CLAUDE.md, todos, preferences) into caveman-speak to reduce input tokens. Compressed version overwrites original. Human-readable backup saved as `<filename>.original.md`, but NOT beside the source file — it lives in an out-of-tree data dir (`$XDG_DATA_HOME/caveman-compress/backups/<parent-dir-name>/`, or `%LOCALAPPDATA%\\caveman-compress\\backups\\<parent-dir-name>\\` on Windows) so skill auto-loaders don't re-ingest it as a live file.\n\n## Trigger\n\n`/caveman-compress <filepath>` or when user asks to compress a memory file.\n\n**Requires an explicit filepath.** If the user pastes raw text instead of naming a file, do NOT improvise an inline compression — ask them for the filepath to compress, or offer to save the pasted text to a file first. Writing your own reasoning/commentary about the request into the compressed output (e.g. \"User passed text directly, not a filepath...\") is the exact failure this guards against — never let that leak into a saved file.\n\n## Process\n\n1. The compression scripts live in `scripts/` (adjacent to this SKILL.md). If the path is not immediately available, search for `scripts/__main__.py` next to this SKILL.md.\n\n2. From the directory containing this SKILL.md, run:\n\npython3 -m scripts <absolute_filepath>\n\n3. The CLI will:\n- detect file type (no tokens)\n- call Claude to compress\n- validate output (no tokens)\n- if errors: cherry-pick fix with Claude (targeted fixes only, no recompression)\n- retry up to 2 times\n- if still failing after 2 retries: report error to user, leave original file untouched\n\n4. Return result to user\n\n## Compression Rules\n\n### Remove\n- Articles: a, an, the\n- Filler: just, really, basically, actually, simply, essentially, generally\n- Pleasantries: \"sure\", \"certainly\", \"of course\", \"happy to\", \"I'd recommend\"\n- Hedging: \"it might be worth\", \"you could consider\", \"it would be good to\"\n- Redundant phrasing: \"in order to\" → \"to\", \"make sure to\" → \"ensure\", \"the reason is because\" → \"because\"\n- Connective fluff: \"however\", \"furthermore\", \"additionally\", \"in addition\"\n\n### Preserve EXACTLY (never modify)\n- Code blocks (fenced ``` and indented)\n- Inline code (`backtick content`)\n- URLs and links (full URLs, markdown links)\n- File paths (`/src/components/...`, `./config.yaml`)\n- Commands (`npm install`, `git commit`, `docker build`)\n- Technical terms (library names, API names, protocols, algorithms)\n- Proper nouns (project names, people, companies)\n- Dates, version numbers, numeric values\n- Environment variables (`$HOME`, `NODE_ENV`)\n\n### Preserve Structure\n- All markdown headings (keep exact heading text, compress body below)\n- Bullet point hierarchy (keep nesting level)\n- Numbered lists (keep numbering)\n- Tables (compress cell text, keep structure)\n- Frontmatter/YAML headers in markdown files\n\n### Compress\n- Use short synonyms: \"big\" not \"extensive\", \"fix\" not \"implement a solution for\", \"use\" not \"utilize\"\n- Fragments OK: \"Run tests before commit\" not \"You should always run tests before committing\"\n- Drop \"you should\", \"make sure to\", \"remember to\" — just state the action\n- Merge redundant bullets that say the same thing differently\n- Keep one example where multiple examples show the same pattern\n\nCRITICAL RULE:\nAnything inside ``` ... ``` must be copied EXACTLY.\nDo not:\n- remove comments\n- remove spacing\n- reorder lines\n- shorten commands\n- simplify anything\n\nInline code (`...`) must be preserved EXACTLY.\nDo not modify anything inside backticks.\n\nIf file contains code blocks:\n- Treat code blocks as read-only regions\n- Only compress text outside them\n- Do not merge sections around code\n\n## Pattern\n\nOriginal:\n> You should always make sure to run the test suite before pushing any changes to the main branch. This is important because it helps catch bugs early and prevents broken builds from being deployed to production.\n\nCompressed:\n> Run tests before push to main. Catch bugs early, prevent broken prod deploys.\n\nOriginal:\n> The application uses a microservices architecture with the following components. The API gateway handles all incoming requests and routes them to the appropriate service. The authentication service is responsible for managing user sessions and JWT tokens.\n\nCompressed:\n> Microservices architecture. API gateway route all requests to services. Auth service manage user sessions + JWT tokens.\n\n## Boundaries\n\n- ONLY compress natural language files (.md, .txt, .typ, .typst, .tex, extensionless)\n- NEVER modify: .py, .js, .ts, .json, .yaml, .yml, .toml, .env, .lock, .css, .html, .xml, .sql, .sh\n- If file has mixed content (prose + code), compress ONLY the prose sections\n- If unsure whether something is code or prose, leave it unchanged\n- Original file is backed up as FILE.original.md before overwriting — in the out-of-tree backup data dir (see Purpose), not beside the source file\n- Never compress FILE.original.md (skip it)\n","skills/caveman-discover/SKILL.md":"---\nname: caveman-discover\ndescription: >\n  Find and label every LLM workflow in the repository so Caveman Cloud groups\n  spend by workflow instead of one bucket. Use for \"discover workflows\" or\n  breaking LLM spend down by workflow.\n---\n\nYou are labeling this repository's LLM workflows for Caveman Cloud. A\n*workflow* is a job the code performs — \"answer a support ticket\", \"build the\nnightly digest\", \"run the eval suite\" — not a technology. Every gateway\nrequest can carry a workflow label; unlabeled traffic all lands in one\n`unlabeled-workflow` bucket. Your job: find the workflows, name them well,\nwire the labels, and verify nothing broke.\n\nThis changes code, so it goes through the user's normal review: **propose the\ntable first, apply after the user agrees.** Re-running on an already-labeled\nrepo must change nothing (idempotent).\n\nThis skill is operator-invoked. An `unlabeled-traffic` Cave Plan observation is\nreview-only and does not create an advisory file, proposal, or Draft PR. Do not\ninfer that telemetry selected a callsite or authorized an edit. Independently\ninventory the repository, present the labeling table, and wait for the user's\napproval before changing code.\n\n## Step 1 — Inventory the workflows\n\nWalk the repo from its entry points, not from its imports:\n\n- HTTP/RPC handlers that call an LLM (directly or through layers)\n- Scheduled jobs: cron definitions, queue consumers, workers, GitHub Actions\n  that invoke LLM code\n- CLI commands and scripts (`scripts/`, `bin/`, package.json scripts)\n- Eval / test harnesses that burn real tokens\n- Distinct agents or chains inside a framework (each LangGraph graph, each\n  crew, each agent definition is usually its own workflow)\n\nOne workflow = one job a human would name. Ten callsites inside the same\nrequest handler are one workflow; one shared `llm.ts` helper used by three\njobs is three workflows (label at the callers, never the shared helper).\n\n## Step 2 — Name them\n\nSlug grammar (the gateway enforces this): lowercase `[a-z0-9_-]`, 1–96 chars.\nName the job, not the tech:\n\n- Good: `support-reply`, `nightly-digest`, `pr-review`, `eval-suite`,\n  `onboarding-email`\n- Bad: `openai-calls` (tech), `main` (says nothing), `SupportReply` (invalid),\n  `johns-test-3` (won't age)\n\nNames are forever-ish — renaming later splits the spend history. When a job's\npurpose isn't clear from the code, derive the slug from the file name and mark\nit `review` in the table rather than inventing a purpose.\n\n## Step 3 — Propose, then apply\n\nPresent this table and ask to proceed:\n\n```\n| workflow | job | where | how it gets labeled |\n|---|---|---|---|\n| support-reply | answers inbound tickets | src/bot/reply.ts:41 | defaultHeaders on the reply client |\n| nightly-digest | 02:00 summary job | jobs/digest.ts:12 | header on the digest client |\n| eval-suite (review) | scripts/eval.ts:8 — purpose inferred from filename | scripts/eval.ts:8 | env override at invocation |\n```\n\nThen wire each label with the lightest mechanism available at that callsite:\n\n- **@caveman-ai/sdk / caveman_cloud SDK**: per-trace `workflow` option, or\n  `defaultWorkflow` on the client a single-job service constructs.\n- **Raw provider SDKs** (OpenAI/Anthropic/LangChain/LiteLLM/Vercel): add\n  `\"x-cave-workflow\": \"<slug>\"` to the same `defaultHeaders` /\n  `default_headers` / `extra_headers` block that already carries\n  `x-cave-api-key`. Shared client used by several jobs → pass the header per\n  call (every SDK above accepts per-request header overrides), or give each\n  job its own thin client.\n- **Wrapped coding agents** (`caveman wrap`): `--workflow <slug>` flag or\n  `CAVE_WORKFLOW=<slug>` env at the invocation site (cron line, CI step).\n- **Raw HTTP**: add the `x-cave-workflow` header to the request.\n\nLabel the callers, keep the diff minimal, match the repo's style. If a\ncallsite is not routed through the Caveman gateway at all, don't label it —\nlist it under \"not wired\" in the report (labels only travel on gateway\ntraffic; wiring is the caveman-setup skill's job).\n\n## Step 4 — Verify\n\nRun whatever the repo already uses to exercise one labeled path (a test, a\ndev script, one curl). Then confirm: the request still succeeds (the gateway\nrejects an invalid label with 400 `cave_invalid_request_header` — fix the slug\nif so). Labeled spend appears on the dashboard at `/activity?tab=workflows` as\neach workflow next runs; jobs on a schedule show up when the schedule fires,\nand that's worth saying in the report rather than pretending they're live.\n\n## Step 5 — Report\n\n```\n## Workflows labeled\n\n| workflow | job | where |\n|---|---|---|\n| support-reply | answers inbound tickets | src/bot/reply.ts:41 |\n| nightly-digest | 02:00 summary job | jobs/digest.ts:12 |\n\nVerified: <the labeled path you actually exercised, and what you observed>\nLands at: <DASHBOARD>/activity?tab=workflows — each row appears as that workflow\nnext runs. Anything still unlabeled shows as `unlabeled-workflow`.\nNot wired (no gateway routing, so no label): <list or \"none\">\nMarked review: <slugs whose purpose was inferred from filenames, or \"none\">\n```\n\nIf you found no LLM entry points at all: say exactly that, and point at the\nsetup skill (`<docs origin>/docs/agent-setup.md`) instead of manufacturing a\ntable.\n","skills/caveman-evidence-review/SKILL.md":"---\nname: caveman-evidence-review\ndescription: >\n  Read-only review of Caveman Cloud evidence: cost, Cave Score, workflows,\n  traces, latency, errors, routing, savings. Use when asked what Caveman found\n  or where LLM spend goes.\n---\n\n# Review Caveman evidence\n\nAct as a read-only operator. Build conclusions from current Caveman data, not\nfrom repository guesses. Never start, approve, cancel, or roll back an\nexperiment from this skill.\n\n## Hard rules\n\n1. Keep these buckets separate:\n   - measured provider-complete list-price cost;\n   - `inferred` daily headroom;\n   - `verified` ledger savings;\n   - evidence cost.\n   Never add or relabel them.\n2. Do not fetch prompt, completion, tool, or artifact payloads unless the user\n   explicitly asks for payload review. Metadata, spans, timing, models, token\n   counts, status, and optimizer attribution are enough for the default review.\n3. Scope every read to the project selected by Caveman context. Never supply an\n   organization id.\n4. Empty results are evidence of no current signal, not zero cost or zero risk.\n5. Cite trace ids and exact time windows used. Do not claim a cause from an\n   aggregate alone.\n\n## Step 1 — Load context\n\nPrefer MCP:\n\n```text\ncaveman_context {}\n```\n\nCLI fallback:\n\n```bash\ncaveman cloud whoami\ncaveman cloud projects list\n```\n\nStop if login or project selection is missing. Ask the user to run\n`caveman login` or select a project; never guess.\n\n## Step 2 — Establish baseline\n\nUse `caveman_report` for:\n\n- `overview`\n- `costs`\n- `score`\n- `workflows`\n- `verified_savings`\n\nThen use `caveman_plan` for ranked daily headroom. If question is narrow, skip\nunrelated reports. Read shortest set that can answer it.\n\nCLI fallback:\n\n```bash\ncaveman cloud costs\ncaveman cloud score\ncaveman cloud plan --json\n```\n\nState report window and basis before interpreting direction.\n\n## Step 3 — Test the leading explanation with traces\n\nUse `caveman_trace_search`. Choose a bounded window and closed filters:\nworkflow, agent, model, provider, error code, runtime mode, cache status,\noptimization id, status class, token/cost/latency bounds, compression, or\nmonitor verdict.\n\nUseful groupings:\n\n- `workflow` — find jobs driving cost or failures;\n- `model` — compare model mix;\n- `session` — isolate retry or loop behavior;\n- ungrouped — identify exact traces.\n\nCompare a suspect cohort with a control cohort or earlier bounded window.\nDo not infer causality from one expensive trace.\n\nCLI fallback:\n\n```bash\ncaveman cloud traces search \\\n  --workflow <slug> \\\n  --from <RFC3339> \\\n  --to <RFC3339> \\\n  --sort total_cost_usd \\\n  --dir desc \\\n  --limit 25\n```\n\n## Step 4 — Inspect representative traces\n\nCall `caveman_trace_get` for a small number of high-signal trace ids. Inspect\nrequest and span metadata, latency, status, token counts, cache state, applied\noptimizers, and model route. Keep payload retrieval off.\n\nCLI fallback:\n\n```bash\ncaveman cloud traces show <trace-id> --spans\n```\n\n## Step 5 — Report\n\nUse this shape:\n\n```text\n## Caveman evidence review\n\nScope: <project> · <from> to <to>\nMeasured cost: <value and basis>\nVerified savings: <ledger value, kept separate>\nInferred headroom: <per-day band, kept separate>\n\nFindings:\n1. <finding> — <aggregate evidence> — traces <ids>\n2. <finding> — <aggregate evidence> — traces <ids>\n\nUnproven:\n- <plausible explanation lacking a control, trace, or eval>\n\nNext read-only check:\n- <one bounded query>\n\nPossible action:\n- <proposal only; use caveman-manage for read-only lifecycle review and safety gate>\n```\n\nIf data is missing, name missing signal and stop at strongest supported\nstatement. Never turn a catalog subtotal into an invoice or an experiment result\ninto verified savings.\n","skills/caveman-explore/SKILL.md":"---\nname: caveman-explore\ndescription: Read-only repository explorer for cold-start orientation, broad cross-file localization, or when a direct search failed. Skip it when the exact file or symbol is already named. Returns path:line citations only; its reads stay out of main context.\ntools: Read, Glob, Grep\nmodel: haiku\n---\n\nYou are FastContext, a fast, cheap, read-only repository explorer. Another agent\n(the solver) delegates a localization question to you. Your only job is to find\nWHERE the relevant code lives and report it as a compact list of file paths with\nline ranges. You never edit files, run commands, or propose a solution.\n\nHow to work:\n\n1. Issue several tool calls IN PARALLEL in your first turn — cast a broad net.\n   Cover complementary hypotheses at once: likely path patterns (Glob), symbol and\n   string matches (Grep), and reading the most promising files (Read). Do not probe\n   one file at a time when you can fan out.\n2. Follow the evidence over one or two more turns only if needed. Stop as soon as\n   you can name the relevant locations. You are optimizing for the solver's token\n   budget, so finish fast.\n3. Only cite line ranges you actually read. Never invent or estimate a range, and\n   never cite a range past the end of a file. A precise small range beats a vague\n   large one.\n\nYour reply MUST be ONLY an evidence block: one citation per line, nothing else.\nNo preamble, no explanation, no summary, no markdown headings. Use exactly this\nshape, one per line:\n\n  path/to/file.ext:START-END  reason it is relevant\n\nExample reply:\n\n  src/router/pick.go:42-71  route selection — where a model is chosen\n  src/router/pick_test.go:18-40  the table test covering pick()\n\nIf you genuinely cannot find anything relevant, reply with the single line:\n\n  no relevant locations found\n\nThat honest answer is better than a guess. The solver reads your citations and\nnothing else from your work, so keep the list short, specific, and correct.\n","skills/caveman-explore/package.json":"{\n  \"name\": \"@caveman/skill-caveman-explore\",\n  \"version\": \"1.0.0\",\n  \"license\": \"Apache-2.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"description\": \"Read-only FastContext exploration skill with parallel repository search and citation-only output.\",\n  \"files\": [\"SKILL.md\"],\n  \"scripts\": {\n    \"test\": \"node --test --test-force-exit tests/*.mjs\"\n  }\n}\n","skills/caveman-help/README.md":"# caveman-help\n\nQuick-reference card. One shot, no mode change.\n\n## What it does\n\nPrints a cheat sheet of all caveman modes, sibling skills, deactivation triggers, and how to set the default mode via env var or config file. One-shot display — does not flip the active mode, write flag files, or persist anything. Use when you forget the slash commands.\n\n## How to invoke\n\n```\n/caveman-help\n```\n\nAlso triggers on \"caveman help\", \"what caveman commands\", \"how do I use caveman\".\n\n## Example output\n\n```\nModes:\n  /caveman              the voice (default)\n  /ultracave            grammar stripped\n  /megacave             classical Chinese\n  /caveman status       report the mode, change nothing\n\nSkills:\n  /caveman-commit       terse Conventional Commits\n  /caveman-review       one-line PR comments\n  /caveman-compress     smaller Markdown memory files\n  /caveman-stats        recorded token usage (savings unknown)\n\nDeactivate:\n  \"stop caveman\" or \"normal mode\"\n```\n\n## See also\n\n- [`SKILL.md`](./SKILL.md) — full reference card\n- [Caveman README](../../README.md) — repo overview\n","skills/caveman-help/SKILL.md":"---\nname: caveman-help\ndescription: >\n  Quick-reference card for the three caveman skills and their commands.\n  Trigger: /caveman-help or \"caveman help\".\n---\n\n# Caveman Help\n\nDisplay this reference card when invoked. One-shot — do NOT change mode, write flag files, or persist anything. Output in caveman style.\n\n## Modes\n\n| Mode | Trigger | What change |\n|------|---------|-------------|\n| **caveman** | `/caveman` | The voice. Answer first, no fluff, every fact kept. Default. |\n| **ultracave** | `/ultracave` (alias `/caveman ultra`) | Grammar stripped. Fragments, one word when enough, each fact once. |\n| **megacave** | `/megacave` (alias `/caveman wenyan`) | Classical Chinese 文言文. Far fewer characters, technical terms verbatim. |\n\nMode stick until changed or session end.\n`/caveman status` reports current mode without changing it. Claude Code and the standalone OpenCode plugin read stored state; other hosts answer from the conversation and mark it `(not tracked by this host)`.\n\n## Skills\n\n| Skill | Trigger | What it do |\n|-------|---------|-----------|\n| **caveman-commit** | `/caveman-commit` | Terse commit messages. Conventional Commits. ≤50 char subject. |\n| **caveman-review** | `/caveman-review` | One-line PR comments: `L42: bug: user null. Add guard.` |\n| **caveman-compress** | `/caveman-compress <file>` | Compress .md files to caveman prose. 33.2% fewer tokens on the repo's five test files. |\n| **caveman-help** | `/caveman-help` | This card. |\n\n## Deactivate\n\nSay \"stop caveman\" or \"normal mode\". Resume anytime with `/caveman`.\n\n## Language\n\nKeep user's language by default — reply in the language user writes, never switch regardless of example text or multilingual context elsewhere. Compress the style, not the language. Technical terms, code, commands, commit types, and exact error strings stay verbatim unless user ask for translation.\n\n## Configure Default Mode\n\nDefault mode = `caveman`. Change it:\n\n**Environment variable** (highest priority):\n```bash\nexport CAVEMAN_DEFAULT_MODE=ultracave\n```\n\n**Config file** (`~/.config/caveman/config.json`):\n```json\n{ \"defaultMode\": \"ultracave\" }\n```\n\nFor opt-in activation in Claude Code, set `defaultMode` to `manual`. Sessions start off; `/caveman` or `talk like caveman` activates caveman. `off` also disables bare-command activation. `manual` is a startup policy, not a mode. OpenCode still starts in caveman mode because its installer also supplies static activation rules.\n\nSet `\"off\"` to suppress automatic and bare-command activation. Use `\"manual\"` when bare `/caveman` should activate.\n\nResolution: env var > repo config > user config > `caveman`. Legacy values `lite`, `full`, `ultra`, `wenyan*` still resolve.\n\n## More\n\nFull docs: https://github.com/JuliusBrussee/caveman\n","skills/caveman-learn/CLAUDE.md":"# skills/caveman-learn — the Caveman Learn editing skill (Apache-2.0, public)\n\nThe consent-gated half of `caveman learn`. The analyzer (the Go proxy) **measures**\nwhere an agent's tokens go and writes a ranked plan; this skill is what an agent\nloads to **act** on that plan — proposing each fix and applying it only with the\nuser's per-edit yes. It is the loop-closer the learn spec §10\ndescribes, plus the new `cavemem_offload` move.\n\n## Layout\n- `SKILL.md` — the canonical skill body (frontmatter `name: caveman-learn` + a\n  trigger-phrase `description`; body = the read-plan → per-class consent loop). This\n  file is the source of truth.\n- `tests/skill-file.test.mjs` — asserts the canonical file is well-formed and honest\n  (frontmatter present; the net-token-negative gate, the never-make-the-agent-dumber\n  guard, consent-per-edit, and reversibility are all stated; no imperative for\n  behavioral findings; no placeholders).\n\n## Install path\n`caveman tools skills install caveman-learn` (in `../../cli/src/index.ts`) writes this file\ninto a repo's `.claude/skills/caveman-learn/SKILL.md` (Claude Code) or\n`~/.codex/skills/caveman-learn/SKILL.md` (Codex). The CLI **embeds a byte-identical copy**\n(`CAVEMAN_LEARN_SKILL_MD`) because the published CLI ships no sibling assets;\n`../../cli/tests/skills.runtime.mjs` asserts the embedded copy equals this canonical\nfile (the drift guard). **Change this file and that constant together.**\n\n## Boundary (binding)\nThe skill — using the agent's own file tools — is the ONLY thing that edits a user's\nconfig. `caveman learn apply` stays read-only (it materializes candidates), and\n`caveman mem *` are mechanical store ops. The offload move enforces a net-token-negative\ngate and the never-make-the-agent-dumber guard before any trim.\n\nSee ../../mem/CLAUDE.md (cavemem) · ../caveman-explore/SKILL.md (the packaging precedent)\n","skills/caveman-learn/README.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fcaveman-learn%2FREADME.md -->","skills/caveman-learn/SKILL.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fcaveman-learn%2FSKILL.md -->","skills/caveman-learn/package.json":"{\n  \"name\": \"@caveman/skill-caveman-learn\",\n  \"version\": \"1.0.0\",\n  \"license\": \"Apache-2.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"description\": \"Consent-gated editing skill that closes the loop on a Caveman learn report: review ranked token sinks and apply cost-lowering fixes (trim config, offload recurring context to cavemem) with per-edit approval, a net-token-negative gate, and a never-make-the-agent-dumber guard.\",\n  \"files\": [\"SKILL.md\"],\n  \"scripts\": {\n    \"test\": \"node --test --test-force-exit tests/*.mjs\"\n  }\n}\n","skills/caveman-manage/SKILL.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fcaveman-manage%2FSKILL.md -->","skills/caveman-optimize/SKILL.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fcaveman-optimize%2FSKILL.md -->","skills/caveman-review/README.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fcaveman-review%2FREADME.md -->","skills/caveman-review/SKILL.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fcaveman-review%2FSKILL.md -->","skills/caveman-setup/SKILL.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fcaveman-setup%2FSKILL.md -->","skills/caveman-stats/README.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fcaveman-stats%2FREADME.md -->","skills/caveman-stats/SKILL.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fcaveman-stats%2FSKILL.md -->","skills/caveman/README.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fcaveman%2FREADME.md -->","skills/caveman/SKILL.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fcaveman%2FSKILL.md -->","skills/engine-mcp-tools.json":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fengine-mcp-tools.json -->","skills/generated/aider/pack.json":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fgenerated%2Faider%2Fpack.json -->","skills/generated/claude/pack.json":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fgenerated%2Fclaude%2Fpack.json -->","skills/generated/codex/pack.json":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fgenerated%2Fcodex%2Fpack.json -->","skills/generated/gemini/pack.json":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fgenerated%2Fgemini%2Fpack.json -->","skills/generated/hermes/pack.json":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fgenerated%2Fhermes%2Fpack.json -->","skills/generated/opencode/pack.json":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fgenerated%2Fopencode%2Fpack.json -->","skills/investigate-first/SKILL.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Finvestigate-first%2FSKILL.md -->","skills/investigate-first/agents/openai.yaml":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Finvestigate-first%2Fagents%2Fopenai.yaml -->","skills/lean-build/SKILL.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Flean-build%2FSKILL.md -->","skills/lean-build/agents/openai.yaml":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Flean-build%2Fagents%2Fopenai.yaml -->","skills/megacave/README.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fmegacave%2FREADME.md -->","skills/megacave/SKILL.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fmegacave%2FSKILL.md -->","skills/migration/SKILL.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fmigration%2FSKILL.md -->","skills/migration/agents/openai.yaml":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fmigration%2Fagents%2Fopenai.yaml -->","skills/native/native-core.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fnative%2Fnative-core.md -->","skills/registry.json":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fregistry.json -->","skills/safe-refactor/SKILL.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fsafe-refactor%2FSKILL.md -->","skills/safe-refactor/agents/openai.yaml":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fsafe-refactor%2Fagents%2Fopenai.yaml -->","skills/surgical-patch/SKILL.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fsurgical-patch%2FSKILL.md -->","skills/surgical-patch/agents/openai.yaml":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fsurgical-patch%2Fagents%2Fopenai.yaml -->","skills/ultracave/README.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fultracave%2FREADME.md -->","skills/ultracave/SKILL.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fultracave%2FSKILL.md -->","skills/verify-and-stop/SKILL.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fverify-and-stop%2FSKILL.md -->","skills/verify-and-stop/agents/openai.yaml":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fverify-and-stop%2Fagents%2Fopenai.yaml -->","src/plugins/opencode/README.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=src%2Fplugins%2Fopencode%2FREADME.md -->"},"files":{".claude-plugin/marketplace.json":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=.claude-plugin%2Fmarketplace.json -->",".claude-plugin/plugin.json":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=.claude-plugin%2Fplugin.json -->",".cursor-plugin/plugin.json":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=.cursor-plugin%2Fplugin.json -->","AGENTS.md":"Read `CLAUDE.md` before repository work. Binding routing: this repo owns\nskills, Engine, and MV3 extension; Agent SDK work belongs in\n`/Users/julb/Desktop/GitHub/caveman-agent-sdk`; Browse driver/MCP/benchmark/plugin\nwork belongs in `/Users/julb/Desktop/GitHub/caveman-browse`; proprietary Pebble\nproduct work belongs in `/Users/julb/Desktop/GitHub/caveman-coding-agent`.\n\nVisibility: this repo and `caveman-browse` are public now;\n`caveman-agent-sdk` is private during development with planned public release\nafter its gates; `caveman-coding-agent` and Caveman-Cloud remain private\ncommercial source.\n\n@./skills/caveman/SKILL.md\n@./skills/caveman-commit/SKILL.md\n@./skills/caveman-review/SKILL.md\n@./skills/caveman-compress/SKILL.md\n","CLAUDE.md":"# CLAUDE.md — caveman\n\n## README is a product artifact\n\nREADME = product front door. Non-technical people read it to decide if caveman worth install. Treat like UI copy.\n\n**Rules for any README change:**\n\n- Readable by non-AI-agent users. If you write \"SessionStart hook injects system context,\" invisible to most — translate it.\n- Keep Before/After examples first. That the pitch.\n- Install table always complete + accurate. One broken install command costs real user.\n- What You Get table must sync with actual code. Feature ships or removed → update table.\n- Preserve voice. Caveman speak in README on purpose. \"Brain still big.\" \"Cost go down forever.\" \"One rock. That it.\" — intentional brand. Don't normalize.\n- Benchmark numbers from real runs in `benchmarks/` and `evals/`. Never invent or round. Re-run if doubt.\n- Adding new agent to install table → add detail block in `<details>` section below.\n- Readability check before any README commit: would non-programmer understand + install within 60 seconds?\n\n---\n\n## Project overview\n\nCaveman makes AI coding agents respond in compressed caveman-style prose while preserving technical substance, code, commands, and exact errors. Publish no reduction or quality-equivalence percentage without a committed reviewed benchmark. Ships as Claude Code plugin, Codex plugin, Gemini CLI extension, and agent rule files for Cursor, Windsurf, Cline, Copilot, and other profiles via `npx skills`.\n\n## Repository routing\n\nThis repo is source of truth for Caveman skills, Engine, and MV3 directive\nextension. Agent SDK + initializer work belongs in\n`/Users/julb/Desktop/GitHub/caveman-agent-sdk`\n(`JuliusBrussee/agent-sdk`). Proprietary Pebble runtime, policy, sessions, TUI,\ndistribution, and conformance work belongs in\n`/Users/julb/Desktop/GitHub/caveman-coding-agent`\n(intended `JuliusBrussee/caveman-coding-agent`). Browse driver/MCP/benchmark/plugin work\nbelongs in `/Users/julb/Desktop/GitHub/caveman-browse`\n(`JuliusBrussee/caveman-browse`). The matching `browse/` directory here is a\nconsumer copy (it still builds the `caveman-browse` release binary); edit only\nfor pinned integration, migration/removal, or an explicitly requested\ncross-repo sync.\n\nVisibility is separate from ownership: this repo and `caveman-browse` are\npublic now; `caveman-agent-sdk` is private during development and planned for\npublic release after its release gates; `caveman-coding-agent` and\nCaveman-Cloud remain private commercial source.\n\n---\n\n## What lives where\n\nPost-cleanup layout. Sources of truth at the top, distribution mirrors below, build outputs in `dist/`, human docs alongside each skill.\n\n```\ncaveman/\n├── README.md                    # Front door (product pitch)\n├── INSTALL.md                   # Per-agent install commands\n├── CONTRIBUTING.md              # Dev guide\n├── CLAUDE.md                    # This file (maintainer instructions)\n├── AGENTS.md / GEMINI.md        # Autodiscovery files (must stay at root)\n│\n├── install.sh / install.ps1     # 30-line shims → installer/install.js\n│\n├── installer/                   # Unified installer\n│   ├── install.js               # Single source for all 30+ agents (PROVIDERS array)\n│   └── lib/settings.js          # JSONC-tolerant settings.json reader/writer\n│\n├── skills/                      # ALL skills, single source of truth\n│   ├── caveman/{SKILL.md, README.md}\n│   ├── caveman-commit/{SKILL.md, README.md}\n│   ├── caveman-review/{SKILL.md, README.md}\n│   ├── caveman-help/{SKILL.md, README.md}\n│   ├── caveman-stats/{SKILL.md, README.md}\n│   ├── caveman-compress/{SKILL.md, README.md, scripts/}\n│   └── cavecrew/{SKILL.md, README.md}\n│\n├── agents/                      # cavecrew subagents (single source — kept at root for plugin auto-discovery)\n│   └── docs/                    # profile-registry docs — NOT agents (see note below)\n├── commands/                    # Codex/Gemini TOML command stubs (root for plugin auto-discovery)\n│\n├── src/                         # Internal source — not auto-discovered by plugin\n│   ├── hooks/                   # Claude Code hooks (installer reads here)\n│   ├── rules/                   # Auto-activation rule body (single source)\n│   ├── tools/                   # caveman-init.js (per-repo rule writer)\n│   └── mcp-servers/             # caveman-shrink npm-published MCP middleware\n│\n├── packages/                    # Current public packages\n│   ├── cli/                     # @caveman-ai/cli\n│   ├── pi-extension/            # @caveman-ai/pi — native Pi extension (bundled into the CLI, published on `pi-v*` tags)\n│   ├── sdk/                     # TypeScript + Python gateway clients\n│   ├── subagent-tax/            # Local harness-prefix benchmark\n│   └── shared/                  # Contracts + binary installer\n├── engine/ · proxy/             # Local compression runtime + provider proxy\n├── mcp/ · mem/ · shrink/        # Recovery tools, memory, output compression\n├── browse/                      # consumer copy; source = caveman-browse\n├── extension/                   # MV3 extension source\n├── shared/                       # Provider catalog + platform libraries\n│\n├── .claude-plugin/              # Claude Code plugin manifest (REQUIRED at root)\n├── .cursor-plugin/plugin.json   # Cursor plugin: skills/, hooks/hooks-cursor.json, agents/ (default scan)\n├── hooks/hooks-cursor.json      # Cursor plugin sessionStart hook ONLY. Never add hooks/hooks.json:\n│                                #   Claude Code and Gemini auto-load it from the plugin/extension root\n├── plugins/caveman/             # Claude Code plugin distribution (CI-mirrored)\n│   ├── skills/                  # ← from skills/\n│   └── agents/                  # ← from agents/\n│\n├── dist/                        # Build artifacts (gitignored)\n│   └── caveman.skill            # ZIP of skills/caveman/, rebuilt by CI\n│\n├── tests/                       # All tests (Node + Python)\n├── benchmarks/                  # Real token measurements through Claude API\n├── evals/                       # Three-arm eval harness\n├── docs/                        # User-facing docs site\n└── .github/workflows/           # CI sync\n```\n\n---\n\n## File structure and what owns what\n\n### Single source of truth files — edit only these\n\n| File | What it controls |\n|------|-----------------|\n| `skills/caveman/SKILL.md` | The caveman voice: rules, auto-clarity, persistence. Sibling skills `skills/ultracave/SKILL.md` (grammar stripped) and `skills/megacave/SKILL.md` (文言文) are self-contained and each map 1:1 to a stored mode. Edit these three for behavior changes. |\n| `src/rules/caveman-activate.md` | Always-on auto-activation rule body, consumed by `src/tools/caveman-init.js` (per-repo IDE rule files) and by the opencode `AGENTS.md` block. GENERATED: `skills/compile.mjs` derives it from `skills/caveman/SKILL.md` (thesis line + rule headlines) plus the fixed tail in `skills/activation-rule.mjs`, and rewrites the `RULE_BODY` fallback in `caveman-init.js` to match. Edit the skill or the tail, rerun `node packages/cli/scripts/compile-registries.mjs`; `tests/installer/rule-copies.test.mjs` fails on drift. |\n| `src/rules/caveman-openclaw-bootstrap.md` | Marker-fenced bootstrap snippet appended to `~/.openclaw/workspace/SOUL.md` by `installer/lib/openclaw.js`. Drives always-on caveman through the OpenClaw gateway. Must include the SENTINEL `Respond terse like smart caveman` and stay well under OpenClaw's 12K-per-bootstrap-file cap. |\n| `.codex/codex-sessionstart.js` | Repo-local Codex SessionStart hook. Resolves the configured default mode through `src/hooks/caveman-config.js` and emits the active skill's ruleset whole, replacing the hardcoded `full`-level echo `.codex/hooks.json` used to carry — so `CAVEMAN_DEFAULT_MODE`, a repo-local `.caveman.json` and a user-config `defaultMode: \"off\"` all take effect on Codex. Also the user-level always-on hook (#573): `installer/install.js --only codex` copies it (shipped via `package.json` `files`) into the owned payload `$CODEX_HOME/caveman/hooks/` beside `caveman-config.js` and `package.json`, with `caveman/skills/{caveman,ultracave,megacave}/SKILL.md`, and merges one SessionStart entry into `$CODEX_HOME/hooks.json`, identified by that script path so foreign and caveman-CLI `native-hook` entries are never touched. `--no-hooks`/`--minimal` skip it; uninstall unmerges before removing the payload. Codex asks the user to trust a new hook via `/hooks`; never pre-trust it. Carries a hand-copied `FALLBACK_VALID_MODES` for when the shared resolver is absent; `tests/hooks/codex-sessionstart.test.mjs` fails if it drifts from `VALID_MODES`. |\n| `installer/lib/openclaw.js` | OpenClaw install/uninstall helper. Frontmatter merge (`version`, `always: true`), SOUL.md marker append/strip, idempotent. Shared by `installer/install.js` and `src/tools/caveman-init.js`. |\n| `skills/caveman-commit/SKILL.md` | Caveman commit message behavior. Fully independent skill. |\n| `skills/caveman-review/SKILL.md` | Caveman code review behavior. Fully independent skill. |\n| `skills/caveman-help/SKILL.md` | Quick-reference card. One-shot display, not a persistent mode. |\n| `skills/caveman-compress/SKILL.md` | Compress sub-skill behavior. |\n| `skills/cavecrew/SKILL.md` | Cavecrew decision guide — when to delegate to caveman subagents vs vanilla. Edit only here. |\n| `skills/{caveman-setup,caveman-discover,caveman-learn,caveman-manage,caveman-optimize,caveman-explore,caveman-evidence-review}/SKILL.md` | Engine/proxy driver skills. Ship with the plugin (see the auto-discovery note below). |\n| `skills/{investigate-first,lean-build,surgical-patch,safe-refactor,migration,verify-and-stop}/SKILL.md` | Token-discipline work patterns — same goal as caveman prose (fewer output tokens) applied to code volume rather than wording. Deliberately un-branded so they read as generic patterns to the model. Ship with the plugin. |\n| `agents/cavecrew-investigator.md` | Read-only locator subagent (haiku). Output contract: `path:line — symbol — note`. |\n| `agents/cavecrew-builder.md` | Surgical 1-2 file editor subagent. Refuses 3+ file scope. |\n| `agents/cavecrew-reviewer.md` | Diff/file reviewer subagent (haiku). One-line findings with severity emoji. |\n| `src/plugins/opencode/plugin.js` | opencode native plugin. ESM Bun module — `session.created` writes flag, `tui.prompt.append` parses slash/natural-language activation and appends per-prompt reinforcement. Reuses `caveman-config.js` via `createRequire`. |\n| `src/plugins/opencode/commands/*.md` | Six opencode slash-command prompt templates (`/caveman`, `/caveman-{commit,review,compress,stats,help}`). |\n\n### `skills/` is auto-discovered wholesale — every subdirectory ships\n\n`.claude-plugin/marketplace.json` sets `\"source\": \"./\"`, so the plugin root IS\nthe repo root, and Claude Code auto-discovers **every** `skills/*/SKILL.md`\nwith no `skills` key in `plugin.json` to gate it. Adding a directory under\n`skills/` therefore installs it into every plugin user's agent, where its\n`description` competes for activation on ordinary work.\n\nThere is no allowlist. Before adding a directory here, decide whether it\nshould reach end users; if not, it belongs under `packages/` or another root.\nKeep the two tables above and the README \"What you get\" table in sync with the\nactual directory listing — `ls skills/` is the source of truth.\n\n### `agents/*.md` is auto-discovered too — and `plugin.json` must NOT list them\n\nSame mechanism, sharper edge. `plugin.json` used to carry an explicit\n`\"agents\": [\"./agents/cavecrew-*.md\"]` array. On Claude Code 2.1.235 that array\nloads **zero** agents — `claude plugin details caveman` reports `Agents (0)`\nwith it and `Agents (3)` without it, so the three cavecrew subagents the\n`cavecrew` skill delegates to did not exist for any plugin user. The docs say\n`string | string[]` is valid and a directory string is rejected outright\n(`agents: Invalid input`), so the default `agents/` scan is the only path that\nworks. Do not re-add the key.\n\nConsequence: **every `.md` anywhere in `agents/` becomes a subagent**, named\nfrom its frontmatter or filename. Maintainer documentation belongs outside that\ntree; profile-registry orientation lives in\n`docs/technical/agent-profile-registry.md`. `tests/verify_repo.py` fails build\nif any markdown beyond three intended cavecrew agents appears under `agents/`.\n\n### `commands/*.md` shadows same-named skills — keep the `.md` stubs unique\n\nClaude Code loads `commands/*.md` as flat skills alongside `skills/*/SKILL.md`,\nso a stub sharing a skill's name registers that name twice. `caveman.md`,\n`caveman-commit.md`, `caveman-review.md` and `caveman-stats.md` each duplicated\na real skill — a 3-line stub competing with the full ruleset for the same slash\ncommand — and were removed. The `.toml` stubs are Codex/Gemini-only and are not\nscanned, so `commands/` keeps one `.md`: `caveman-init.md`, which has no skill\ntwin. Guarded by `tests/verify_repo.py`.\n\n### Auto-generated / auto-synced — do not edit directly\n\nWe removed the agent-specific dotdir mirrors at the repo root (`.cursor/`, `.windsurf/`, `.clinerules/`, `.github/copilot-instructions.md`, root `caveman/SKILL.md`). They were never read by the installer — only used to self-apply caveman to this repo when a maintainer opened it in Cursor/Windsurf/Cline. Devs who want caveman in their editor while editing this repo should run `npx -y github:JuliusBrussee/caveman -- --with-init` once (writes per-repo rule files from `src/rules/caveman-activate.md` via `src/tools/caveman-init.js`). For per-user installs through the upstream skills CLI, `npx -y github:JuliusBrussee/caveman -- --only <agent>` runs `npx skills add ... -a <profile>`.\n\nA handful of dotdir leftovers (`.junie/`, `.kiro/`, `.roo/`, `.agents/`) still hold a stale `cavecrew/SKILL.md` mirror from before the cleanup. They aren't read by anything in the current install path; remove on sight, no migration needed.\n\nWhat's left is the Claude Code plugin distribution (required by the plugin loader) and the release ZIP.\n\n| File | Synced from |\n|------|-------------|\n| `plugins/caveman/skills/caveman/SKILL.md` | `skills/caveman/SKILL.md` |\n| `plugins/caveman/skills/caveman-compress/SKILL.md` (+ `scripts/`) | `skills/caveman-compress/SKILL.md` (+ `scripts/`) |\n| `plugins/caveman/skills/cavecrew/SKILL.md` | `skills/cavecrew/SKILL.md` |\n| `plugins/caveman/agents/cavecrew-*.md` | `agents/cavecrew-*.md` |\n| `dist/caveman.skill` | ZIP of `skills/caveman/` directory (gitignored; rebuilt by CI on release) |\n\nSkills not in this table (`caveman-commit`, `caveman-review`, `caveman-help`, `caveman-stats`) are not mirrored into the Claude Code plugin distribution by CI. They reach Claude Code through the standalone hook + skill install path, and reach other agents via `npx skills add`. A `plugins/caveman/skills/caveman-stats/` directory is currently checked in as a hand-committed copy; the sync workflow does not touch it, so don't rely on edits there to propagate.\n\n---\n\n## CI sync workflow\n\n`.github/workflows/sync-skill.yml` triggers on main push when `skills/**/SKILL.md` or `agents/cavecrew-*.md` changes.\n\nWhat it does:\n1. Copies `skills/caveman/SKILL.md` and `skills/cavecrew/SKILL.md` into their `plugins/caveman/skills/<name>/` mirrors so the Claude Code plugin loader sees the latest behavior.\n2. Copies `skills/caveman-compress/SKILL.md` and its `scripts/` into `plugins/caveman/skills/caveman-compress/`.\n3. Copies `agents/cavecrew-*.md` into `plugins/caveman/agents/`.\n4. Rebuilds `dist/caveman.skill` (ZIP of `skills/caveman/`) for the release artifact.\n5. Commits and pushes with `[skip ci]` to avoid loops.\n\nCI bot commits as `github-actions[bot]`. After PR merge, wait for workflow before declaring release complete.\n\nThe old steps that mirrored SKILL.md and rules into root dotdirs (`.cursor/`, `.windsurf/`, `.clinerules/`, `.github/copilot-instructions.md`) are gone — those mirrors no longer exist. The old `caveman-compress/` → `skills/compress/` rename-on-sync is also gone now that compress lives at `skills/caveman-compress/`.\n\n---\n\n## Hook system (Claude Code)\n\nFour hooks in `src/hooks/` plus a `caveman-config.js` shared module, a `caveman-parse.js` shared mode-change parser and a `package.json` CommonJS marker.\n\n**Mode state is per session.** Each session's mode lives in `$CLAUDE_CONFIG_DIR/.caveman-sessions/<session_id>.mode`, keyed by the `session_id` Claude Code puts in every hook payload *and* in the statusline's stdin JSON. `$CLAUDE_CONFIG_DIR/.caveman-active` survives as a last-write-wins compat mirror (falls back to `~/.claude/`).\n\n```\nSessionStart hook ──┐                                        ┌── UserPromptSubmit hook\n  (session_id,      │                                        │     (session_id, prompt)\n   source)          ▼                                        ▼\n        $CLAUDE_CONFIG_DIR/.caveman-sessions/<session_id>.mode\n                             │             │\n                          mirrors       reads\n                             ▼             ▼\n              .caveman-active      caveman-statusline.sh ◀── session JSON on stdin\n           (last-write-wins,        [CAVEMAN] / [ULTRACAVE] / [MEGACAVE]\n            compat only)\n```\n\nTwo invariants that are load-bearing, not stylistic:\n\n- **The legacy mirror never holds the literal `off`.** Deactivation deletes it. `off` is in `VALID_MODES`, so an older `caveman-mode-tracker.js` reading `off` from that path would pass its `!INDEPENDENT_MODES.has(...)` check and inject \"CAVEMAN MODE ACTIVE (off)\", and an older `caveman-statusline.sh` would render `[CAVEMAN:OFF]`. Mixed-version installs are real: plugin hooks and standalone hooks can both be registered at once, and `statusLine` holds an absolute path baked in at install time.\n- **`recordModeChange` compares against the session's own state, never the mirror.** That's why `readSessionModeRaw` exists alongside `resolveActiveMode` — diffing against the mirror would compare one session's transition to whatever another session wrote last and spam the log with phantom entries.\n\nBoth spellings of off are READ everywhere: a missing file (old semantics) and a literal `off` (new, durable).\n\n`src/hooks/package.json` pins the directory to `{\"type\": \"commonjs\"}` so the `.js` hooks resolve as CJS even when an ancestor `package.json` (e.g. `~/.claude/package.json` from another plugin) declares `\"type\": \"module\"`. Without this, `require()` blows up with `ReferenceError: require is not defined in ES module scope`.\n\nAll hooks honor `CLAUDE_CONFIG_DIR` for non-default Claude Code config locations.\n\n### `src/hooks/caveman-config.js` — shared module\n\nExports:\n- `getDefaultMode()` — resolves default mode in order: `CAVEMAN_DEFAULT_MODE` env var → repo-local config (`<cwd>/.caveman/config.json` or `<cwd>/.caveman.json`, walking up to the filesystem root; the first file that names a `defaultMode` wins, so a CLI-only `.caveman/config.json` does not hide a `.caveman.json`) → user config (`$XDG_CONFIG_HOME/caveman/config.json` / `~/.config/caveman/config.json` / on Windows `%APPDATA%\\caveman\\config.json`, then `~/.config/caveman/config.json`) → `'caveman'`. Config files may carry a BOM, be UTF-16LE, or hold comments and trailing commas. The env var short-circuits before any cwd walk. Repo-local config lets a team check in a per-project default without polluting every contributor's env or user config.\n- `findRepoConfigPath(start, accept)` — walks up from `start` (default `process.cwd()`) looking for the first `.caveman/config.json` or `.caveman.json` (that `accept(path)` approves, when given). Bounded to 64 ancestors. Refuses symlinked files (symmetric with `safeWriteFlag` / `readFlag`).\n- `safeWriteFlag(flagPath, content)` — symlink-safe flag write. Refuses if flag target or its immediate parent is a symlink. Opens with `O_NOFOLLOW` where supported. Atomic temp + rename. Creates with `0600`. Protects against local attackers replacing the predictable flag path with a symlink to clobber files writable by the user. Used by both write hooks. Silent-fails on all filesystem errors.\n- `validateSessionId(id)` — returns the id or `null`. Whitelist `^[A-Za-z0-9_-]{1,128}$`. **A session id becomes part of a filesystem path, so nothing may interpolate one without passing it through here first.** The symlink hardening in `safeWriteFlag` does not cover traversal.\n- `resolveActiveMode(claudeDir, sessionId)` — what mode is in effect: session file → legacy mirror, with both a missing file and a literal `off` collapsing to `null`. The reader every hook should use.\n- `readSessionModeRaw(claudeDir, sessionId)` — literal stored value for **this** session, no legacy fallback. Two callers only: `recordModeChange` (see the invariant above) and the SessionStart continuation branch, which has to tell a stored `off` apart from \"nothing stored yet\".\n- `writeSessionMode(claudeDir, sessionId, modeOrNull)` — the single writer. Writes `off` literally to the session file, unlinks the legacy mirror. Rejects anything outside `VALID_MODES`.\n- `writeSessionPrev` / `readSessionPrev` / `clearSessionPrev` — the displaced-prose-mode memory for one-shot skills (#599), scoped per session so two windows running `/caveman-commit` don't overwrite each other's return target.\n- `gcSessionStore(claudeDir, opts)` — mtime sweep of `.caveman-sessions/`, 14-day TTL (`CAVEMAN_SESSION_TTL_MS` overrides for tests), `maxDeletes` cap, refuses symlinks. Called from SessionStart on new sessions only — never on `compact`, which is frequent and shares the 5s hook budget.\n- `recordModeChange(claudeDir, newMode, sessionId)` — third arg tags the log entry; omitted (not null) when unknown, so pre-existing readers see the old shape.\n\n**Every function above accepts a `sessionId` that may be `null` or malformed and degrades to the legacy machine-wide behavior.** That is the entire backward-compatibility story: the old code path *is* the fallback branch, which is why the existing tests — none of which send a `session_id` — pass unchanged. The three hook entrypoints resolve these helpers individually (`cfg.writeSessionMode || <legacy stub>`) rather than adding them to their `requireSibling` shape checks: a `caveman-config.js` from before per-session state satisfies those checks, and hard-failing over the newer exports would trade \"machine-wide mode, as it always worked\" for \"no state at all\" on exactly the plugin-cache-drift scenario #848 is about.\n\n### `src/hooks/caveman-activate.js` — SessionStart hook\n\nRuns on every SessionStart — `source` is `startup`, `resume`, `clear`, `compact` or `fork`. Four things:\n1. Reads the hook payload from stdin for `session_id`, `source` and `cwd`\n2. Resolves this session's mode and persists it via `writeSessionMode`\n3. Emits caveman ruleset as hidden stdout — Claude Code injects SessionStart hook stdout as system context, invisible to user\n4. Checks `settings.json` for statusline config; if missing, appends nudge to offer setup on first interaction (one-shot, gated by `.caveman-nudge-shown`)\n\n**`source` branching.** `RESET_SOURCES` is `{startup, clear}` — only those re-derive `getDefaultMode()`. Everything else (`compact`/`resume`/`fork`, an unrecognized source, and the watchdog's `unknown`) READS the stored mode via `readSessionModeRaw`, falling back to the legacy mirror for a session that predates the store, and only then to the default. Reading the LITERAL value is the point: #691 already branched on `source`, but it read the legacy flag where off is spelled \"no file\", so a deactivated session found nothing and re-derived the default anyway — \"stop caveman\" was still undone by the next auto-compaction. A stored `off` now short-circuits to `OK` with no ruleset. It still re-emits when a mode IS active: compaction is what prunes the rules out of context, which is the whole reason this hook runs on every source. `clear` counts as a fresh start (explicit user reset — nothing else in the conversation survives it, so neither should a \"stop caveman\" from before it); this deliberately differs from #691's comment, which grouped `clear` with the continuations.\n\n**Headless sessions start under `manual` (#377).** Whenever the hook falls back to the configured default (`startMode()`), a `CLAUDE_CODE_ENTRYPOINT` starting with `sdk-` (`claude -p`, Agent SDK) turns any non-`off` default into `manual`: tool probes that parse the reply get no ruleset until an explicit `/caveman`. Interactive entrypoints (`cli`, `claude-vscode`, `claude-desktop`, …) never match; an explicit `CAVEMAN_DEFAULT_MODE` env var opts back in. Hook test helpers drop `CLAUDE_CODE_ENTRYPOINT` from the env, or a headless runner flips every SessionStart test.\n\n**The stdin contour is load-bearing.** Activation runs on the first COMPLETE JSON object, not at EOF, with a 2000ms `PAYLOAD_WATCHDOG_MS` backstop and `process.stdin.unref()` in `finish()`. `pause()` alone is not enough — it stops reading but leaves the pipe handle referenced, so a host that holds the write end open (Windows pipe close lags arbitrarily, #729/#833) burns the whole 5s budget. The watchdog must NOT assume `startup`: it reports `unknown`, which preserves the session's stored mode rather than resetting it. Several suites also invoke this hook via `subprocess.run()` with no `input=`, inheriting a stdin that never reaches EOF — `tests/test_hooks.py::test_hook_never_blocks_on_stdin_that_never_closes` guards that path.\n\nSilent-fails on all filesystem errors — never blocks session start.\n\n**`--subagent`: the SubagentStart hook (#621).** SessionStart context reaches only the parent thread, so the same script is registered for SubagentStart with `--subagent` (plugin.json, `installer/install.js`, `src/hooks/install.{sh,ps1}`). It reuses the stdin reader and watchdog, resolves THIS session's mode via `resolveActiveMode` (a session with no stored state at all — SessionStart skipped or failed, or the session predates the store — falls back to the last-write-wins legacy mirror, the same degrade the tracker's reinforcement uses, so a subagent matches its parent thread; pinned in `tests/hooks/subagent-start.test.mjs`), and prints `{\"hookSpecificOutput\":{\"hookEventName\":\"SubagentStart\",\"additionalContext\":<that mode's skill>}}`. Nothing is printed when the session stored `off` (no \"stop caveman\" leak, #672; another window's mode never overrides a stored `off`), in a one-shot mode, for `cavecrew-*` agents (they carry their own ultracave voice), or under a repo `defaultMode: \"off\"` (#634, same gate as the tracker's reinforcement). Read-only: no mode write, no mode log, no GC, no nudge, no cavecrew model overrides. Uninstall needs nothing extra — `removeCavemanHooks` walks every event and matches the `caveman-activate.js` basename.\n\n### `src/hooks/caveman-mode-tracker.js` — UserPromptSubmit hook\n\nReads JSON from stdin — `session_id` scopes every read and write. Three responsibilities:\n\n**1. Slash-command activation.** If prompt starts with `/caveman`, writes the session's mode via `writeSessionMode`:\n- `/caveman` → configured default (see `caveman-config.js`, defaults to `caveman`)\n- `/ultracave` (alias `/caveman ultra`) → `ultracave`\n- `/megacave` (alias `/caveman wenyan`, any `wenyan-*`) → `megacave`\n- `/caveman lite` or `/caveman full` (legacy aliases) → `caveman`\n- `/caveman-commit` → `commit`\n- `/caveman-review` → `review`\n- `/caveman-compress` → `compress`\n\n**2. Natural-language activation/deactivation.** Matches phrases like \"activate caveman\", \"turn on caveman mode\", \"talk like caveman\" and writes the configured default mode. Matches \"stop caveman\", \"disable caveman\", \"normal mode\", \"deactivate caveman\" etc. and stores a durable `off` for the session. README promises these triggers, the hook enforces them.\n\n**3. Per-turn reinforcement.** When the session's mode is a non-independent one (i.e. not `commit`/`review`/`compress`), emits a small `hookSpecificOutput` JSON reminder so the model keeps caveman style after other plugins inject competing instructions mid-conversation. The full ruleset still comes from SessionStart — this is just an attention anchor.\n\n### `src/hooks/caveman-statusline.sh` — Statusline badge\n\nReads the session JSON Claude Code pipes to it on stdin, extracts `session_id` (pure bash — no `jq` dependency), and reads `.caveman-sessions/<id>.mode`; falls back to `$CLAUDE_CONFIG_DIR/.caveman-active` when there is no usable id. Outputs colored badge string for Claude Code statusline:\n- `caveman` or empty → `[CAVEMAN]` (orange)\n- `off` → nothing at all. Never `[CAVEMAN:OFF]` — that reads as a mode rather than the absence of one\n- `ultracave` → `[ULTRACAVE]`, `megacave` → `[MEGACAVE]`; one-shots → `[CAVEMAN:<MODE_UPPERCASED>]` (orange). Legacy stored values render through the same map\n\nStdin is bounded the same way as the SessionStart hook: `[ ! -t 0 ]` skips an interactive terminal, and `read -r -d '' -t 1` caps the wait. The timeout is an **integer on purpose** — macOS ships bash 3.2, which rejects `-t 0.3` with `invalid timeout specification`.\n\nThe statusline reports mode only. Old `.caveman-statusline-suffix` values are ignored; fixed-ratio savings estimates were removed because no measured baseline supported them. `/caveman-stats` records actual Claude output/cache-read counts and mode attribution with savings unknown. Gemini uses native `/stats model` and `/stats session`; never read Claude transcripts as another host's usage.\n\nConfigured in `settings.json` under `statusLine.command`. PowerShell counterpart at `src/hooks/caveman-statusline.ps1` for Windows. Both scripts symlink-refuse and whitelist-validate mode flag contents — never echo arbitrary bytes.\n\n### `src/hooks/caveman-stats.js --record` — SessionEnd hook\n\nSame script as `/caveman-stats`; the `--record` flag turns it into a silent recorder. Reads the SessionEnd payload (`session_id`, `transcript_path`) and appends one snapshot of that transcript's totals to `$CLAUDE_CONFIG_DIR/.caveman-history.jsonl`, so lifetime history fills in without the user running `/caveman-stats`. Only the transcript the host names is read — never the newest file on disk. Stdin follows the activate-hook contour: first complete JSON object, 2000ms `PAYLOAD_WATCHDOG_MS` (gives up without recording), `unref()` on finish. Always exits 0 and writes nothing to stderr — a SessionEnd failure would surface as a hook error while the user quits. Every installer (plugin manifest, `installer/install.js`, `src/hooks/install.{sh,ps1}`) writes `--record` unquoted after the quoted script path; `rewriteLegacyManagedHookCommands` carries trailing `--flag` args over when it moves a bare-`node` command to the absolute node path.\n\n### `src/hooks/caveman-host-session-start.js` — Cursor / Copilot CLI sessionStart hook\n\n`node caveman-host-session-start.js <host>`, one script for hosts other than Claude Code. Emits the resolved mode's ruleset under the host's key: `additional_context` for `cursor`, `additionalContext` for `copilot`. The mode is `getDefaultMode()` resolved from the session's workspace (`cwd`, Cursor's `workspace_roots` or `CURSOR_PROJECT_DIR`), so `defaultMode` and repo `.caveman.json` apply; `off`, `manual`, one-shot modes, an unknown host or any failure print `{}`. Writes no state. The owned install at `<host>/caveman/hooks/` reads its pinned `<host>/caveman/skills/` before the shared resolver, which would otherwise find the host's unpinned `npx skills` copy first. Stdin follows the activate-hook contour.\n\n### Hook installation\n\n**Plugin install** — hooks wired automatically by plugin system.\n\n**Standalone install** — `installer/install.js` (the unified Node installer) copies hook files into `$CLAUDE_CONFIG_DIR/hooks/` and merges SessionStart + SubagentStart + UserPromptSubmit + SessionEnd + statusline into `settings.json`. Uses the JSONC-tolerant helpers in `installer/lib/settings.js` so a commented `settings.json` no longer crashes the merge. Defensive `validateHookFields` runs before every write to prevent a single malformed hook from poisoning the entire file (Claude Code Zod silently discards the whole `settings.json` on schema mismatch).\n\nThe `install.sh` / `install.ps1` shims at the repo root delegate to `installer/install.js` via `node` (local clone) or `npx -y github:JuliusBrussee/caveman` (curl|bash). No legacy fallback path remains — earlier `install.sh.legacy` / `install.ps1.legacy` files were removed.\n\n**Uninstall** — `npx -y github:JuliusBrussee/caveman -- --uninstall` (or `node installer/install.js --uninstall` from a clone). Strips caveman hook entries from `settings.json` via substring marker `caveman`, deletes hook files, and removes the Claude plugin / Gemini extension. Native skill copies use `installer/lib/provider-skills.js` and ownership journals; uninstall removes unchanged owned files while preserving foreign or edited content. Use the same vendor home overrides for install and uninstall. Delegated `npx skills add` installs remain managed by the host/upstream skill manager.\n\n---\n\n## Skill system\n\nSkills = Markdown files with YAML frontmatter consumed by Claude Code's skill/plugin system and by `npx skills` for other agents.\n\nEach skill has a human-facing `README.md` alongside the LLM-facing `SKILL.md`. The README explains what the skill does for users browsing GitHub; the SKILL.md is the prompt body the agent loads. Don't merge them — different audiences, different formats.\n\n### Three skills, one mode model\n\nThree self-contained skills, each 1:1 with a stored mode id: `caveman` (default voice), `ultracave` (grammar stripped), `megacave` (文言文). The SessionStart hook injects `skills/<id>/SKILL.md` whole; there is no per-level filtering. Legacy stored values map on read: `lite` and `full` → `caveman`, `ultra` → `ultracave`, every `wenyan*` → `megacave`. Writes emit new ids only. Mode persists until changed or session ends. Plan: `docs/technical/three-skills-refactor.md`.\n\n`defaultMode: \"manual\"` is a Claude Code startup policy, not a fourth mode. It starts inactive, stores only `off`, and explicit bare-command or natural-language activation resolves to `caveman`. Keep it out of `VALID_MODES`; only configuration accepts it. OpenCode maps this policy to its existing caveman default because its installer also supplies static AGENTS.md activation. `/caveman status` is read-only and must return before one-shot restoration or any mode-state mutation.\n\n### Auto-clarity rule\n\nCaveman drops to normal prose for: security warnings, irreversible action confirmations, multi-step sequences where fragment ambiguity risks misread, user confused or repeating question, questions the agent asks the user. Resumes after. Defined in skill — preserve in any SKILL.md edit.\n\n### caveman-compress\n\nSub-skill in `skills/caveman-compress/SKILL.md`. Takes file path, compresses prose to caveman style, writes to original path, saves backup at `<filename>.original.md`. Validates headings, code blocks, URLs, file paths, commands preserved. Retries up to 2 times on failure with targeted patches only. Requires Python 3.10+.\n\nThe slash command is `/caveman-compress` everywhere — same name in plugin and standalone install. CI no longer renames the directory on sync (the old `caveman-compress/` → `skills/compress/` sed rename is gone now that the source lives at `skills/caveman-compress/`).\n\n### caveman-commit / caveman-review\n\nIndependent skills in `skills/caveman-commit/SKILL.md` and `skills/caveman-review/SKILL.md`. Both have own `description` and `name` frontmatter so they load independently. caveman-commit: Conventional Commits, ≤50 char subject. caveman-review: one-line comments in `L<line>: <severity> <problem>. <fix>.` format.\n\n---\n\n## Agent distribution\n\nHow caveman reaches each agent type:\n\n| Agent | Mechanism | Auto-activates? |\n|-------|-----------|----------------|\n| Claude Code | Plugin (hooks + skills) or standalone hooks | Yes — SessionStart hook injects rules; SubagentStart passes the session's mode to subagents |\n| Codex | `npx skills add -a codex` plus an owned SessionStart hook in `$CODEX_HOME/hooks.json` (installer, default on); plugin in `plugins/caveman/`; repo `.codex/hooks.json` and `.codex/config.toml` for this checkout | Yes — SessionStart hook, after the user trusts it once via `/hooks` |\n| Gemini CLI | Extension with `GEMINI.md` context file | Yes — context file loads every session |\n| opencode | Native plugin (`src/plugins/opencode/`) copied into `~/.config/opencode/plugins/caveman/` + `AGENTS.md` ruleset + skills/agents/commands directories. Plugin uses `session.created` and `tui.prompt.append` lifecycle hooks. No statusline (opencode TUI exposes no plugin-writable badge). | Yes — `session.created` writes flag, `AGENTS.md` carries always-on ruleset |\n| OpenClaw | Workspace skill at `~/.openclaw/workspace/skills/caveman/SKILL.md` (frontmatter merged with `version` + `always: true`) plus a marker-fenced bootstrap block in `~/.openclaw/workspace/SOUL.md`. Both writes go through `installer/lib/openclaw.js`; workspace path is overridable via `OPENCLAW_WORKSPACE`. | Yes — SOUL.md is auto-injected each turn under \"Project Context\" (subject to OpenClaw's 12K-per-file / 60K-total bootstrap caps) |\n| Cursor | Plugin (`.cursor-plugin/plugin.json`: skills + `hooks/hooks-cursor.json` sessionStart hook) or `--only cursor`: `npx skills add ... -a cursor -g` skills, then `installer/lib/cursor-native.js` owns cavecrew agents in `~/.cursor/agents/` (Claude `model:` dropped, `readonly: true` on investigator/reviewer) and the shared hook payload in `~/.cursor/caveman/` with one sessionStart entry merged into `~/.cursor/hooks.json`. Both hooks run `src/hooks/caveman-host-session-start.js cursor`. Per-repo `.cursor/rules/caveman.mdc` via `--with-init`. Pick plugin OR `--only cursor`, not both (two hooks inject twice). | Yes — sessionStart `additional_context`, honors `defaultMode` (off/manual inject nothing) |\n| Windsurf | `npx skills add ... -a windsurf` (default via `--only windsurf`); per-repo `.windsurf/rules/caveman.md` via `--with-init` | Yes — always-on rule |\n| Cline | `npx skills add ... -a cline` (default via `--only cline`); per-repo `.clinerules/caveman.md` via `--with-init` | Yes — Cline auto-discovers `.clinerules/` |\n| Copilot | `npx skills add ... -a github-copilot` (detected via the `copilot` CLI plus its `~/.copilot` dir, since AWS Copilot CLI ships a `copilot` binary too, or the VS Code/Cursor extension); per-repo `.github/copilot-instructions.md` + `AGENTS.md` via `--with-init`. When the Copilot CLI is present (`copilot` on PATH or `$COPILOT_HOME` set — not a bare `~/.copilot`, which `npx skills add -a github-copilot` itself creates) the installer also owns `$COPILOT_HOME/caveman/` (shared hook payload) and `$COPILOT_HOME/hooks/caveman.json` (sessionStart → `caveman-host-session-start.js copilot`); `--no-hooks` skips it. No userPromptSubmitted hook: Copilot drops its output. | Copilot CLI: yes — sessionStart `additionalContext`, honors `defaultMode`. VS Code Copilot Chat: repo-wide instructions via `--with-init` |\n| Grok Build | Owned skill copies into `$GROK_HOME/skills` (default `~/.grok`) plus a marker-fenced ruleset block in `$GROK_HOME/AGENTS.md`, appended/stripped through `installer/lib/openclaw.js`'s marker helpers | Yes — Grok loads `~/.grok/AGENTS.md` as global rules (docs-backed; no live binary run) |\n| Antigravity CLI (`agy`) | `--only antigravity-cli` stages a temp plugin (`plugin.json`, the nine skills, `rules/AGENTS.md` = `src/rules/caveman-activate.md`) and runs `agy plugin install <dir>`; agy copies it to `~/.gemini/config/plugins/caveman/` and owns it. Uninstall: `agy plugin uninstall caveman` when `agy plugin list` names it. No session-start hook exists in agy (PreInvocation fires before every model call), so no `defaultMode`. | Yes — plugin `rules/AGENTS.md` merges into agy's active rule set (verified agy 1.2.17) |\n| Continue, AiderDesk, Antigravity IDE/2.0 | Owned physical copies into each vendor's supported directory, honoring configured homes; separate explicit targets for the two Antigravity products | Host skill invocation; feature settings may be required |\n| Others (Junie, Trae, Warp, Tabnine, Mistral, Qwen, Devin, Droid, ForgeCode, Bob, Crush, iFlow, OpenHands, Qoder, Rovo Dev, Replit, …) | Delegated `npx skills` personal installs; Replit uses project scope. Configured iFlow/Crush roots use owned copies | Host skill invocation; `/caveman` where supported |\n\nopencode reaches Tier 1 minus the statusline (opencode's TUI has no plugin-writable badge). Mode flag lives at `~/.config/opencode/.caveman-active` for any external tooling that wants to surface it.\n\nFor agents without hook systems, the always-on snippet lives in `INSTALL.md`'s \"Want it always on?\" section — keep current with `src/rules/caveman-activate.md`.\n\n**Adding a new agent.** Edit the `PROVIDERS` array in `installer/install.js` — single source of truth, no more bash/PS1 dual-source drift. Each entry has `id`, `label`, `mech`, `detect` (clause spec like `command:foo||dir:$HOME/x`; `&&` joins terms that must all hold), optional `profile` (vercel-labs/skills slug), optional `soft: true` (config-dir-only detection).\n\n1. The profile slug must exist in upstream [vercel-labs/skills](https://github.com/vercel-labs/skills). Verify against the README before merging — wrong slugs cause `npx skills add` to fail at runtime, not at install-script load.\n2. Run `node installer/install.js --list` to confirm the new row renders correctly.\n3. Soft probes (config-dir-only) are fine but tag them with `soft: true`. They render with `(soft)` in `--list` so users know detection is best-effort.\n\n---\n\n## Evals\n\n`evals/` has three-arm harness:\n- `__baseline__` — no system prompt\n- `__terse__` — `Answer concisely.`\n- `<skill>` — `Answer concisely.\\n\\n{SKILL.md}`\n\nHonest delta = **skill vs terse**, not skill vs baseline. Baseline comparison conflates skill with generic terseness — that cheating. Harness designed to prevent this.\n\n`llm_run.py` calls `claude -p --system-prompt-file ... --output-format json` per (prompt, arm), isolated from user settings, plugins and MCP, saves text plus Claude-reported usage (input/output/cache tokens, cost) to `evals/snapshots/results.json` (`results.<lang>.json` with `CAVEMAN_EVAL_LANG`). `measure.py` reads snapshot offline with tiktoken (OpenAI BPE — approximates Claude tokenizer, ratios meaningful, absolute numbers approximate). `CAVEMAN_EVAL_SET=fidelity` runs the regex-checked cases in `evals/prompts/fidelity.json` into `snapshots/fidelity.json`; `score_fidelity.py` scores it offline.\n\nAdd skill: drop `skills/<name>/SKILL.md`. Harness auto-discovers. Add prompt: append line to `evals/prompts/en.txt`.\n\nSnapshots committed to git. CI reads without API calls. Only regenerate when SKILL.md or prompts change.\n\n---\n\n## Benchmarks\n\n`benchmarks/` runs real prompts through Claude API (not Claude Code CLI), records raw token counts. Results committed as JSON in `benchmarks/results/`. Benchmark table in README generated from results — update when regenerating.\n\nTo reproduce: `uv run python benchmarks/run.py` (needs `ANTHROPIC_API_KEY` in `.env.local`).\n\n---\n\n## Key rules for agents working here\n\n- Edit `skills/<name>/SKILL.md` for behavior changes. Never edit synced copies under `plugins/caveman/skills/`.\n- Edit `src/rules/caveman-activate.md` for auto-activation rule changes. Never edit any per-agent rule copy a user has on their machine.\n- Edit `src/rules/caveman-openclaw-bootstrap.md` for the OpenClaw SOUL.md bootstrap snippet. Keep the `<!-- caveman-begin -->` / `<!-- caveman-end -->` markers and the `Respond terse like smart caveman` sentinel — `installer/lib/openclaw.js` keys idempotency off both. If you change the embedded fallback in `installer/lib/openclaw.js`, keep it byte-equivalent to the file.\n- Per-skill human docs live in `skills/<name>/README.md`. The LLM-facing body is in `SKILL.md`. Don't merge them — different audiences.\n- Build artifacts go in `dist/`. Never check files into `dist/` manually — CI rebuilds them on push, and `dist/` is gitignored.\n- README most important file for user-facing impact. Optimize for non-technical readers. Preserve caveman voice.\n- `INSTALL.md` is the per-agent install reference. Keep the install table in `README.md` short and link out to `INSTALL.md` for the full matrix.\n- Benchmark and eval numbers must be real. Never fabricate or estimate.\n- CI workflow commits back to main after merge. Account for when checking branch state.\n- Hook files must silent-fail on all filesystem errors. Never let hook crash block session start.\n- Any new flag file write must go through `safeWriteFlag()` in `caveman-config.js`. Direct `fs.writeFileSync` on predictable user-owned paths reopens the symlink-clobber attack surface.\n- Mode state reads/writes go through the `caveman-config.js` session helpers (`resolveActiveMode` / `writeSessionMode` / …), never by joining a path by hand. Any `session_id` that reaches a path must pass `validateSessionId()` first.\n- **Keep mode-state logic in `caveman-config.js`.** `src/plugins/opencode/plugin.js` cannot `require()` from disk (compiled Bun binary) — it reads that file and evaluates it through `new Function(...)` with a `createRequire` that resolves only node built-ins. Parsing already lives in `caveman-parse.js` (#602) and loads the same way, so a second parser copy is no longer the risk; a THIRD home for the state primitives would be, because opencode would keep its own drifting version of them.\n- The two statusline scripts have no shared runtime with the JS, so they re-implement path resolution by hand. `tests/verify_repo.py::verify_powershell_static` greps both against the `SESSIONS_DIRNAME` and `SESSION_ID_RE` constants to catch drift — there is no behavioral `.ps1` test on the POSIX runners, so that grep is the only guard on the Windows badge.\n- Editing anything in `src/hooks/` means regenerating `src/hooks/checksums.sha256` (same file set, recomputed digests) — `tests/verify_repo.py` fails the build otherwise, and `installer/install.js` verifies remote hook downloads against the manifest for the pinned ref.\n- Hooks must respect `CLAUDE_CONFIG_DIR` env var, not hardcode `~/.claude`. Same for `installer/install.js` / statusline scripts.\n- **Any entrypoint that reads a host hook payload from stdin returns on the first complete JSON object, never at EOF.** Under the Windows pipe implementation the host's close lags arbitrarily (#729/#833, #949), so a reader that waits for EOF burns the host's whole budget with its work done. The readers that follow this rule: `caveman-activate.js`, `caveman-mode-tracker.js`, `caveman-stats.js --record` and `caveman-host-session-start.js` (per-chunk parse), `packages/cli/src/native-hook-fast.ts` (`stdin()`), `readHookStdin()` in `packages/cli/src/index.ts` (shrink-hook, memory recall, native-hook), and `readNativeHookPayload` in `proxy/cmd/caveman-proxy/main.go`. A new hook callback uses one of those, not `readStdin()` or `io.ReadAll`. Each has a keep-the-writer-open regression test; add one for any new reader.\n- `installer/install.js` is the only installer source. `install.sh` / `install.ps1` at repo root are 30-line shims that delegate to it. Never re-add per-OS install logic to the shims — that's how we got the Windows quoting bug (#249).\n- Any settings.json read in installer or hooks must go through `installer/lib/settings.js` `readSettings()` so JSONC comments don't crash the merge. Any settings.json write must run through `validateHookFields()` first.\n","browse/CLAUDE.md":"# browse — `caveman-browse` browser driver\n\n> **Repository routing:** do not continue Browse product work here. Source of\n> truth is `JuliusBrussee/caveman-browse`, local checkout\n> `/Users/julb/Desktop/GitHub/caveman-browse`. This directory is a consumer\n> copy; edit only for pinned integration, migration/removal, or an explicit\n> cross-repo sync. Accessibility-compressor work stays here in\n> `engine/compressors/axtree.go` because Engine is owned by this repo.\n\nLocal, agent-facing browser interaction for the open Caveman surface. It serves\nstdio MCP tools that read a real Chrome accessibility tree, compress it with the\nengine's forced-only `a11y` compressor, act on `uid` handles, and recover the\nbyte-exact original AX payload through CCR. Every saving here is `inferred`;\nBrowse never emits `verified`.\n\n## Layout\n- `session.go` — MCP tool handlers, engine/CCR integration, UID target cache.\n- `cdp.go` — chromedp-backed Mode-A dedicated Chrome driver and bounded\n  actionability recipe.\n- `cmd/caveman-browse/` — stdio MCP binary.\n\n## Gotchas\n- This package may import CDP/network/browser dependencies. `public/mcp` may not.\n- `go test ./public/browse/...` is setup-free. `make test-browse` resolves an\n  installed Playwright Chromium or system Chrome and runs the `integration`\n  build-tagged CDP contract across the package and direct CLI; `make\n  test-browser` adds extension tests and `make test-e2e` includes both.\n- The actionability layer is intentionally bounded: same-origin dashboards and\n  predictable design-system controls, not arbitrary-open-web parity.\n- Unknown handles/actions fail closed with `cave_snake_code` errors.\n- Snapshot output is compact indented text, not JSON-lines. `query` keeps at most\n  12 highest-scoring task matches plus ancestors; recovery metadata exposes only\n  UIDs actually shown. Keep `tokens_after` equal to exact agent-visible result\n  cost and `view_tokens` equal to serializer output cost.\n- Direct CLI Chrome is detached so separate `snapshot`/`act`/`eval` processes\n  can share a target. `close` must terminate it. Fresh `CAVEMAN_HOME` must work;\n  state writes stay atomic and mode `0600`.\n- CDP action acknowledgement is not application settlement. Non-wait actions\n  return `settled:false` and require a focused resnapshot for proof.\n- Navigation denies `file:`, `javascript:`, and privileged Chrome schemes.\n- Benchmark contract and reproducible Playwright comparison live in\n  `BENCHMARK.md`; keep token budgets executable in tests.\n- **The uid map is `browser_snapshot`'s contract, not a side effect of a\n  compression ratio.** On engine pass-through (no recovery handle — a tree that\n  did not get smaller, or no CCR store) `snapshotTool` fails closed with\n  `cave_browser_snapshot_uncompressed` and returns the prior page's uids intact;\n  it MUST NEVER dump the raw AX tree into `uids` (strictly worse than not using\n  Browse) nor wipe the target cache. Regressing this reopens issue #140.\n- **An `<iframe>` is a leaf, not a broken tree.** `Accessibility.getFullAXTree`\n  returns one frame at a time, so an iframe node's `childId` points at a child\n  document absent from this payload; the `a11y` compressor\n  (`engine/compressors/axtree.go`) treats an unresolvable `childId` as a leaf and\n  still curates the frame-visible nodes. Because of that the CDP driver keeps\n  Chrome's default Site Isolation (`site-per-process`) — do not disable it to\n  \"fix\" cross-origin iframes.\n","engine/AGENTS.md":"# engine — the content-aware Caveman Engine (commercial, binary-distributed)\n\nDetect a payload's type → route to a safety-classed compressor → count the token reduction\n→ store the original for recovery. The stable four-call API (`Compress`/`Retrieve`/`Detect`/`Stats`)\nis what the proxy, CLI, SDKs, MCP, and WASM build all share. Everything it reports is\n`inferred`; it never says `verified`.\n\n## Layout\n- `engine.go` — the `Engine` core: detect → route → compress → token ratio → CCR. `record` mode + miss + parse-fail + not-smaller + no-store all pass through.\n- `result.go` — `Result`/`Options`/`Mode`; unknown mode fails closed to `record`.\n- `detect.go` — content router (`json`/`diff`/`code`/`log`/`search-result`/`html`/`tabular`/`config`/`terminal`/`text`); low confidence → `text`.\n- `listing.go` — strips a read tool's line-number gutter (`1\\t{`, `2\\t  \"unit\"`) before Detect and before the compressor, then restores it after. Agents send file *listings*, not files; the gutter is presentation, and left in place it made every guttered payload detect as `text` and compress ~0%. Restoration keeps each surviving line's ORIGINAL number and is declined entirely for transforms that restructure rather than elide (re-encoded JSON), because numbers that describe nothing are worse than none.\n- `safety/` — the S0–S4 registry; S4 is lossy and `RequiresCCR`. Unknown class → fail closed.\n- `tokens/` — `Counter` interface; default is an offline, vocab-embedded BPE tokenizer (o200k_base). `inferred` always.\n- `contextwindow/` — deterministic BM25 context packer with recency/error/priority signals and token-budget accounting.\n- `compressors/` — `Compressor` interface + registry; structural JSON/log/search/diff/text/HTML/table/config/code compressors plus forced-only TOON/tool-schema/accessibility/repetition paths — `Default()` registers 14. A compressor is a pure byte transform.\n- `ccr/` — `~/.caveman/ccr.db` SQLite recovery store; content-addressed handles; byte-exact `Get`.\n- `pixel/` — pxpipe port (MIT, see its NOTICE): text→PNG request compression. Embedded glyph atlases + renderer + profitability gate + per-wire-format transforms (Anthropic/OpenAI/Gemini). S4-lossy, allowlist-gated (`CAVE_PIXEL_MODELS`, default `claude-fable-5,gpt-5.6`), consumed by the proxy's `pixel` mode; never wired into `Detect` and never imported by the WASM build (≈4 MB assets).\n- `evals/` — the local eval harness + fail-closed graders + embedded `fixtures/`. `Run()` is the quality gate.\n- `cmd/caveman-engine/` — the binary the CLI shells out to: `compress | detect | retrieve | stats | registry | toon encode|decode | evals run | pixel render|simulate`. `toon` is the stateless (no-CCR) JSON⇄TOON converter; both directions fail closed.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=engine` / `make product-test PRODUCT=engine`.\n- A new compressor is a self-contained file in `compressors/` + tests, registered in `Default()`. Forced-only compressors such as `toolschema` and `toon` must not be added to `Detect`.\n- Compressors never count tokens, touch CCR, or hit the network — the engine core does that around them.\n- **The `toolschema` transform is client-side-only today.** Registration makes it forceable by\n  engine callers; it does not make it reachable from managed-gateway traffic. Provider adapters\n  intentionally keep tool arrays in the frozen prompt-cache prefix, and no billed route invokes\n  this compressor. Engine API/CLI callers may force it locally, and `caveman-shrink` is its\n  dedicated product surface; those reductions remain local and `inferred`. Managed gateway has a\n  separate S2 tool-search/deferral path; do not conflate it with compression. Any future gateway\n  route for this transform needs cache-versus-schema cost arithmetic, stable byte-identical prefix\n  output, and an eval gate before activation.\n\n## Gotchas (honesty invariants — correctness, not style)\n- **fail-closed transforms**: every compressor passes the original through unchanged on any parse problem; the engine also passes through when the result is not smaller in tokens. Reserve **byte-safe** for classes whose `safety.Info.ByteSafe` is true.\n- **CCR-or-pass-through**: a lossy (S4) result is only emitted if its original was stored; with no store, the engine fails closed to pass-through.\n- **inferred-only**: ratios are token estimates labeled `inferred`; never `verified`, never re-projected.\n- **fail-closed**: unknown mode → `record`; unknown content type → `text`; unknown grader → `passed:false`.\n- **cgo**: full code compression (Python/JS/TS) needs the tree-sitter build; the cgo-free build compresses Go only. The embedded eval fixtures cover all three under cgo.\n- **boundary**: this is `public/` — never import `cloud/…`. `make check-boundaries` enforces it.\n\nSee ../../CLAUDE.md (root)\n","engine/CLAUDE.md":"# engine — the content-aware Caveman Engine (commercial, binary-distributed)\n\nDetect a payload's type → route to a safety-classed compressor → count the token reduction\n→ store the original for recovery. The stable four-call API (`Compress`/`Retrieve`/`Detect`/`Stats`)\nis what the proxy, CLI, SDKs, MCP, and WASM build all share. Everything it reports is\n`inferred`; it never says `verified`.\n\n## Layout\n- `engine.go` — the `Engine` core: detect → route → compress → token ratio → CCR. `record` mode + miss + parse-fail + not-smaller + no-store all pass through.\n- `result.go` — `Result`/`Options`/`Mode`; unknown mode fails closed to `record`.\n- `detect.go` — content router (`test-report`/`json`/`diff`/`code`/`log`/`search-result`/`html`/`tabular`/`config`/`terminal`/`text`); low confidence → `text`.\n- `listing.go` — strips a read tool's line-number gutter (`1\\t{`, `2\\t  \"unit\"`) before Detect and before the compressor, then restores it after. Agents send file *listings*, not files; the gutter is presentation, and left in place it made every guttered payload detect as `text` and compress ~0%. Restoration keeps each surviving line's ORIGINAL number and is declined entirely for transforms that restructure rather than elide (re-encoded JSON), because numbers that describe nothing are worse than none.\n- `safety/` — the S0–S4 registry; S4 is lossy and `RequiresCCR`. Unknown class → fail closed.\n- `tokens/` — `Counter` interface; default is an offline, vocab-embedded BPE tokenizer (o200k_base). `inferred` always.\n- `contextwindow/` — deterministic BM25 context packer with recency/error/priority signals and token-budget accounting.\n- `compressors/` — `Compressor` interface + registry; structural JSON/log/test-report/search/diff/text/HTML/table/config/code compressors plus forced-only TOON/tool-schema/tool-schema-annotations/accessibility/repetition paths — `Default()` registers 16. `test-report` and `toolschema-annotations` stay manifest-excluded because no compiled plan routes to them (the proxy's `compiledContentType` allowlist has neither); advertising either would rotate `RegistrySHA256` and fail existing Cave Build locks. A compressor is a pure byte transform.\n- `ccr/` — `~/.caveman/ccr.db` SQLite recovery plus typed native-session store;\n  content-addressed handles, byte-exact `Get`, session scope, dependencies,\n  current/stale state, and Hot/Warm/Cold/Archived lifecycle. Embedded SQLite uses\n  one serialized connection so concurrent hook/repository writes cannot split an\n  in-memory schema or race with `SQLITE_BUSY`.\n- `pixel/` — pxpipe port (MIT, see its NOTICE): text→PNG request compression. Embedded glyph atlases + renderer + profitability gate + per-wire-format transforms (Anthropic/OpenAI/Gemini). S4-lossy, allowlist-gated (`CAVE_PIXEL_MODELS`, default `claude-fable-5,gpt-5.6`), consumed by the proxy's `pixel` mode; never wired into `Detect` and never imported by the WASM build (≈4 MB assets).\n- `evals/` — the local eval harness + fail-closed graders + embedded `fixtures/`. `Run()` is the quality gate.\n- `cmd/caveman-engine/` — the binary the CLI shells out to: `compress | detect | retrieve | stats | registry | toon encode|decode | evals run | pixel render|simulate`. `toon` is the stateless (no-CCR) JSON⇄TOON converter; both directions fail closed.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=engine` / `make product-test PRODUCT=engine`.\n- A new compressor is a self-contained file in `compressors/` + tests, registered in `Default()`. Forced-only compressors such as `toolschema` and `toon` must not be added to `Detect`.\n- Compressors never count tokens, touch CCR, or hit the network — the engine core does that around them.\n- **The `toolschema` transform is client-side-only today.** Registration makes it forceable by\n  engine callers; it does not make it reachable from managed-gateway traffic. Provider adapters\n  intentionally keep tool arrays in the frozen prompt-cache prefix, and no billed route invokes\n  this compressor. Engine API/CLI callers may force it locally, and `caveman-shrink` is its\n  dedicated product surface; those reductions remain local and `inferred`. Managed gateway has a\n  separate S2 tool-search/deferral path; do not conflate it with compression. Any future gateway\n  route for this transform needs cache-versus-schema cost arithmetic, stable byte-identical prefix\n  output, and an eval gate before activation.\n\n## Gotchas (honesty invariants — correctness, not style)\n- **fail-closed transforms**: every compressor passes the original through unchanged on any parse problem; the engine also passes through when the result is not smaller in tokens. Reserve **byte-safe** for classes whose `safety.Info.ByteSafe` is true.\n- **elide repetition, never a document**: every unit-dropping compressor (text, log, json, tabular, config, searchresult, terminal) calls `keepNonRedundant` (`compressors/redundancy.go`) before emitting. It keeps one representative of each distinct content class, so a stream of near-identical lines still collapses while a bibliography, a source file or a table of measurements survives whole — and when nothing is left to drop, the not-smaller check passes the payload through and claims nothing. This is a correctness rule, not a tuning knob: eliding a document the agent must enumerate cost the agent bench 5.5x its baseline on 2026-08-06 (`bench/agent/README.md`). Class matching masks digit runs, but only for units carrying at least one word — a wordless unit like `6 ['1973.', 251]` is compared with its digits intact, because there the numbers ARE the content.\n- **an elision marker states only what it verified**: `compressors/invariants.go` enriches the log/tabular/JSON markers with facts computed from the exact units that marker replaced — a field constant across every one of them (`all state=charged`), a varying field enumerated IN FULL with exact counts that sum to the elided count (`status: fulfilled×15 shipped×3 processing×2`, with an `absent×N` bucket for units lacking the field), and a numeric range printed from the ORIGINAL extreme value strings (`range amount=5.00..199.99`). Nothing else. A credential-shaped field name, a value with a space or over 24 bytes, more than five distinct values, and a run in which any unit yields no fields at all are all withheld **entirely** — never shortened, because a partial list reads as complete. When nothing survives, the marker renders byte-identically to the bare `… N rows elided (caveman) …`. An enumeration whose every bucket holds ONE unit is an identifier list, not an invariant, so instead of enumerating it the marker states its COVERAGE — `order_id: 25 distinct, ord-1000..ord-1024`, the exact distinct count plus the byte-wise smallest and largest ORIGINAL value strings, claiming nothing about the gaps between them (this is the only fact that answers \"was ord-1043 among the elided rows, or absent from the ledger?\", the last legitimate retrieve on the v5 probe). When a coverage run is EXACTLY dense — one shared prefix, fixed-width digits, and `max-min+1` equal to the counted distinct set — it says so instead: `wh-5000..wh-5059 all 60 present`, a verified membership answer that a set-difference task needs and would otherwise retrieve for. Density is counted, never inferred from the endpoints; mixed suffix widths, two prefixes, or a single gap all fall back to the counted form (whose own arithmetic still says how many are missing, never which). A dense entry outlives even the constants when the budget binds; a bounded one goes before them. At most two coverage entries per summary — a record carries several identifier columns and at ~38 bytes each they will name the run three ways and crowd out a 16-byte constant. An enumeration averaging two units or fewer per bucket still prints but is shed first. Summaries are capped at 160 bytes and a quarter of the bytes they replace (floor 96, never over half), shedding identifier-ish enumerations, then ranges, then bounded coverage, then constants, then dense coverage, class enumerations last; **a summary that sheds says so**, with a trailing `+N omitted` section, because every section above reads as the complete set of facts of its kind and a silently trimmed `all …` list would let an agent conclude a field it cannot see was not constant — the same error a partial enumeration is refused to avoid. The notice is charged to the same budget as the facts, so both caps still hold exactly; one trailing contract line per payload is added only when ≥4 KB was actually dropped. **A run under 3 units is not elided at all** unless collapsing both summarizes and halves its bytes — a one-unit marker plus its recovery handle costs about what the unit cost and reads as a hole. Together this costs ~4 points of ratio on the toolwork corpus (0.83 → 0.79) and is bought on purpose: on 2026-08-08 an agent that could not tell what had been elided made 46 sequential `caveman_retrieve` calls at 3.3x the uncompressed arm; a first fix stating only constants and ranges left the storms at 11–27 calls because it was silent about the one mixed-value field the task turned on; and a second round still leaked retrieves through identifier enums, fact-free small classes, and `{\"__caveman_elided__\":1}` singletons.\n- **a recovery view is made of whole units**: `retrieve_query.go` narrows a CCR recovery to the units matching a query, and a unit is whatever is self-delimiting — a complete JSON record (never a field line: `\"status\": \"unfulfilled\",` is a fragment), a whole CSV record sliced out of the original bytes with the header shown once, a whole log/NDJSON line, or a whole paragraph from non-JSON prose. JSON field names are never provenance: `messages[].content`, `text`, and `system` stay attached to their full object record, because tool output can use the same names as provider requests. Separate JSON arrays have explicit gap sentinels; omitted envelope/scalar bytes get leading/trailing markers. Content it cannot decompose (JSON with no array of records) returns in FULL rather than line-split, and a narrowed view that is not smaller also returns the full original; over-returning is safe, a fragment is not. Every gap — between two returned units, before the first, after the last — prints `… [caveman: non-adjacent] …`, so adjacency in the view never implies adjacency in the original. This is a correctness rule with a measured cost: on 2026-08-08 query mode returned BM25-ranked lines of a pretty-printed orders page, the agent read `\"status\": \"unfulfilled\"` (ord-1043's) directly above `\"order_id\": \"ord-1047\"`, attributed it to ord-1047, and reported 5 unfulfilled orders where there were 3 — two wrong answers in a 12-task bench run.\n- **one recovery surface, two id spaces**: the CCR store holds compression blob handles (`ccr_…`, `store.Get`) AND the native runtime's typed objects (`store.GetObject`), and the runtime shows the latter to agents as `ccr://<objectID>` when it masks a whole tool output. `Engine.Retrieve` resolves a blob first and falls through to the object table before failing, and `mcp.normalizeRecoveryHandle` accepts `ccr_…`, `<<ccr:…>>`, `ccr:…` and `ccr://…`. An agent copies whatever reference it is shown; a form that does not resolve does not degrade, it loops. Measured 2026-08-08: inventory-mismatch and webhook-delivery-gaps were handed `ccr://<objectID>`, every retrieve answered `cave_unknown_handle`, the failing tool result was itself masked into a fresh pointer — \"a new pointer to another pointer, indefinitely\" — 27–97 recovery calls, no answer written, both tasks 0/6 while rate-limit-forensics on the same build scored 3/3 at ~35% cheaper.\n- **CCR-or-pass-through**: a lossy (S4) result is only emitted if its original was stored; with no store, the engine fails closed to pass-through.\n- **inferred-only**: ratios are token estimates labeled `inferred`; never `verified`, never re-projected.\n- **fail-closed**: unknown mode → `record`; unknown content type → `text`; unknown grader → `passed:false`.\n- **cgo**: full code compression (Python/JS/TS) needs the tree-sitter build; the cgo-free build compresses Go only. The embedded eval fixtures cover all three under cgo. Release `caveman-proxy`/`caveman-engine` are cgo builds via the pinned zig in `scripts/build-release-binaries.mjs`; `TestReleaseShape*` carry no build tag on purpose and fail under `CGO_ENABLED=0`, and engine-ci's `release-shape` lane runs them with the release flags.\n- **boundary**: this is `public/` — never import `cloud/…`. `make check-boundaries` enforces it.\n\nSee ../../CLAUDE.md (root)\n","evals/README.md":"# Evals\n\nMeasures real token compression of caveman skills by running the same\nprompts through Claude Code under three conditions and comparing the\ngenerated output token counts. A second eval, [Fidelity](#fidelity),\nruns the same arms on fixed correctness cases and checks that the facts\nsurvive.\n\n## The three arms\n\n| Arm | System prompt |\n|-----|--------------|\n| `__baseline__` | none |\n| `__terse__` | `Answer concisely.` |\n| `<skill>` | `Answer concisely.\\n\\n{SKILL.md}` |\n\nThe honest delta for any skill is **`<skill>` vs `__terse__`** — i.e.\nhow much the skill itself adds on top of a plain \"be terse\" instruction.\nComparing a skill to the no-system-prompt baseline conflates the skill\nwith the generic terseness ask, which is what an earlier version of\nthis harness did and is why its numbers were inflated.\n\n## Current snapshot\n\n`snapshots/results.json` was generated on `claude-opus-5-5` with Claude\nCode 2.1.288 (2026-10-02), with the three response-style skills as the\nskill arms: `caveman`, `ultracave`, `megacave`. The other `skills/*`\ndirectories are workflow skills, not response styles, so they are left out.\n\n```bash\nCAVEMAN_EVAL_MODEL=claude-opus-5-5 CAVEMAN_EVAL_SKILLS=caveman,ultracave,megacave \\\n  python3 evals/llm_run.py\n```\n\nThe previous snapshot (`claude-opus-4-6`, April 2026) was generated\nwithout host isolation, so whatever plugins and CLAUDE.md files were\ninstalled could reach every arm. `llm_run.py` now isolates each call\n(see below); numbers from the two snapshots are not directly comparable.\n\n## Why this design\n\n- **Real LLM output**, not hand-written examples (no circularity).\n- **Same Claude Code** the skills target — no separate API key.\n- **Snapshot committed to git** so CI runs are deterministic and free,\n  and so any change to the numbers is reviewable as a diff.\n- **Control arm** isolates the skill's contribution from the generic\n  \"be terse\" effect.\n- **Host isolation.** Each `claude -p` call runs with\n  `--setting-sources project --strict-mcp-config --disable-slash-commands\n  --no-session-persistence` from an empty temp dir: no user settings (so\n  no installed plugins or their SessionStart hooks), no MCP servers, no\n  installed skills, no CLAUDE.md. Without it a plugin's injected ruleset,\n  or an MCP auth nag the model repeats in its answer, lands in every arm\n  including the baseline.\n\n## Files\n\n- `prompts/<lang>.txt` — fixed list of dev questions, one per line.\n  `en.txt` is the default; `pt.txt` is Brazilian Portuguese; `fr.txt`\n  is French and mirrors `en.txt` line for line.\n- `llm_run.py` — runs `claude -p --output-format json --system-prompt-file …`\n  per (prompt, arm), captures real LLM output, writes\n  `snapshots/results.json` along with metadata (model, CLI version,\n  language, generation timestamp). Next to the text it stores the usage\n  Claude Code reports for each call under `usage` (same arm/prompt layout\n  as `arms`): input, output, cache-creation and cache-read tokens plus\n  `total_cost_usd`.\n- `measure.py` — reads the snapshot, counts tokens with tiktoken\n  `o200k_base`, prints a markdown table with median / mean / min / max /\n  stdev across prompts. When the snapshot has `usage`, a second table\n  gives Claude's own output-token counts against the terse control and\n  the median input tokens the skill adds per call (skill arm minus terse\n  arm, cache tokens included).\n- `snapshot_contract.py` — rejects incomplete or malformed snapshot matrices\n  before `measure.py` or `score_fidelity.py` reports metrics.\n- `prompts/fidelity.json` — fixed correctness cases, each with regex\n  checks (see [Fidelity](#fidelity)).\n- `score_fidelity.py` — scores `snapshots/fidelity.json` against those\n  checks, offline, stdlib only.\n- `snapshots/results.json` — committed source of truth, regenerated only\n  when SKILL.md files or prompts change. Other languages write\n  `results.<lang>.json` next to it; none is committed yet.\n\n## Refresh the snapshot (requires `claude` CLI logged in)\n\n```bash\nuv run python evals/llm_run.py\n```\n\nThis calls Claude once per prompt × (N skills + 2 control arms). Use\na small model to keep it cheap:\n\n```bash\nCAVEMAN_EVAL_MODEL=claude-haiku-4-5 uv run python evals/llm_run.py\n```\n\nBy default every `skills/*/SKILL.md` gets an arm. `CAVEMAN_EVAL_SKILLS`\n(comma-separated skill ids) restricts the skill arms; an unknown id aborts\nbefore any call:\n\n```bash\nCAVEMAN_EVAL_SKILLS=caveman,ultracave,megacave uv run python evals/llm_run.py\n```\n\nEach call times out after `CAVEMAN_EVAL_TIMEOUT` seconds (default 300). A\nfailed or timed-out call is retried twice, after 5 s and then 20 s. If it\nstill fails, the run stops (auth, quota or a bad model name would fail\nevery remaining call too), writes the finished calls to\n`snapshots/results.partial.json` with the unfinished cells set to `null`\nand the error in `metadata.error`, leaves `results.json` untouched and\nexits 1. `measure.py` refuses a partial file; it is gitignored.\n\n### Other languages\n\n```bash\nCAVEMAN_EVAL_LANG=pt CAVEMAN_EVAL_MODEL=claude-haiku-4-5 CAVEMAN_EVAL_SKILLS=caveman uv run python evals/llm_run.py\nCAVEMAN_EVAL_LANG=pt uv run --with tiktoken python evals/measure.py\n```\n\nThe terse control is translated per language (`TERSE_PREFIXES` in\n`llm_run.py`): an English \"Answer concisely.\" on a Portuguese question\nalso nudges the model toward English, which would be a second variable.\nA language with no translated control, or no `prompts/<lang>.txt`, stops\nbefore any call instead of falling back to English. The skill text\nitself stays in English, as shipped.\n\nWhy a separate language matters: SKILL.md promises \"compress the style,\nnot the language\", and the rules target English function words\n(a/an/the, just/really). Whether that transfers to a language with\ngendered articles and a different filler vocabulary is an empirical\nquestion, so it gets its own snapshot.\n\n## Read the snapshot (no LLM, no API key, runs in CI)\n\n```bash\nuv run --with tiktoken python evals/measure.py\n```\n\nReporting fails closed unless the snapshot has both control arms, at least one\nskill arm, exactly one string output per prompt in every arm, and metadata whose\n`n_prompts` matches the prompt list. A `usage` block, when present, must\ncover the same arms and prompts with non-negative token counts.\n\n## Adding a prompt\n\nAppend a line to `prompts/<lang>.txt`, then refresh that language's snapshot.\n\n## Adding a language\n\nAdd `prompts/<lang>.txt` (ideally mirroring `en.txt` line for line),\nadd the translated terse control to `TERSE_PREFIXES` in `llm_run.py`,\nrun with `CAVEMAN_EVAL_LANG=<lang>` and commit\n`snapshots/results.<lang>.json`.\n\n## Adding a skill\n\nDrop a `skills/<name>/SKILL.md`, then refresh the snapshot. `llm_run.py`\npicks up every skill directory automatically, unless `CAVEMAN_EVAL_SKILLS`\nis set, in which case add the new id to that list.\n\n## Fidelity\n\nThe length eval alone rewards a skill that replies `k` to everything.\nThe fidelity eval asks whether each arm kept what the answer needs: exact\nnumbers and units, every not/never/only/except, the user's language,\nverbatim errors, safety wording before destructive steps, normal prose in\npersisted artifacts.\n\n`prompts/fidelity.json` holds the cases: `{id, category, prompt, checks,\nexamples}` with `checks.must_include` and `checks.must_not_include` as\nregex lists, and `examples.pass` / `examples.fail` as answers the checks\nmust accept / reject.\nA case passes for an arm when every `must_include` pattern matches the\noutput and no `must_not_include` pattern does (Python `re.search`,\ncase-insensitive). There is no judge model, so the same snapshot always\ngets the same score, and checks can be fixed and re-scored without new\ncalls. The first six cases come from\n[#1061](https://github.com/JuliusBrussee/caveman/pull/1061) by alexis.\n\n```bash\nCAVEMAN_EVAL_SET=fidelity CAVEMAN_EVAL_MODEL=claude-opus-5-5 \\\n  CAVEMAN_EVAL_SKILLS=caveman,ultracave,megacave python3 evals/llm_run.py\npython3 evals/score_fidelity.py\n```\n\nThe run uses the same arms, isolation, retries and usage capture as the\nlength eval and writes `snapshots/fidelity.json`. The scorer prints the\npass rate per arm, overall and per category, then every failed check.\nIt refuses a snapshot whose prompts no longer match the case file.\n\nNo fidelity snapshot is committed yet. A regex pass is evidence, not a\nverdict: read the failures before citing a rate, and publish no\nquality-equivalence claim without a committed, reviewed snapshot.\n\n| Category | What the checks look for |\n|----------|--------------------------|\n| `substance-preservation` | Filler removed, the technical substance needed to act kept. |\n| `exact-preservation` | Polarity, limits, numbers, units, code, identifiers, APIs, commands and quoted errors unchanged. |\n| `no-caricature` | No fake grammar, mode prefixes, invented abbreviations or arrows. |\n| `language-and-grammar` | The requested or dominant language kept, grammatical markers kept. |\n| `safety-clarity` | Plain, complete warnings for destructive actions, security, data loss and ordered recovery steps. |\n| `artifact-boundary` | Persisted or external human-facing artifacts in normal prose. |\n| `mode-boundaries` | Explicit activation, mode switch, persistence and deactivation boundaries preserved. |\n\nTo add a case, append it to `prompts/fidelity.json` with a unique `id`,\none of the categories above, at least one check, and at least one\nknown-good and one known-bad example answer, then rerun the fidelity\neval. `tests/test_eval_fidelity.py` checks the file's shape and runs\nevery case's checks against its examples, so a check that rejects a\ncorrect answer fails there instead of after a paid run.\n\n## What this does NOT measure\n\n- **Fidelity beyond the checks** — the fidelity eval only sees what its\n  regexes test. Tone, ordering and whether an explanation is actually\n  right are left to human review of the outputs.\n- **Latency or cost** — latency is out of scope. Skills add input tokens\n  on every call, so output savings are not the full economic picture.\n  Snapshots with `usage` record that input cost per call; the committed\n  `results.json` predates usage capture, so it has none.\n- **Cross-model behavior** — only the model used to generate the\n  snapshot is measured.\n- **Exact Claude tokens, from the tiktoken table** — `tiktoken\n  o200k_base` is OpenAI's BPE and is only an approximation of Claude's\n  tokenizer. Ratios between arms are meaningful; absolute numbers are\n  approximate. The usage table uses Claude's own counts instead, but\n  Claude's output count includes thinking tokens, which the user never\n  reads.\n- **Statistical significance** — single run per (prompt, arm) at default\n  temperature. The min/max/stdev columns let you eyeball whether a\n  number is solid or noisy, but this is not a powered experiment.\n\n## Historical: reminder_run.py\n\n`reminder_run.py` and `snapshots/reminder.json` measured the pre-3.1 per-turn reminder text (`Enforce this reply: ...`), which the mode tracker no longer emits. The snapshot is kept as history; regenerate before citing it.\n","extension/AGENTS.md":"# extension — \"Caveman Mode\" browser extension (MV3, no bundler)\n\nThe consumer top-of-funnel: a toggle that, on ChatGPT / Claude / Gemini, prepends the **caveman\nskill directive** to each message you send and re-fires the send, so the AI replies in a compact,\nhigh-density style intended to preserve technical substance. It only ever adds text to your *outgoing*\nmessage; it never reads replies, never touches network requests. Savings are `inferred`. (This is\nthe directive-injector product; an earlier revision shipped a local WASM compress button — EXT\nremoved it after Chrome Web Store review couldn't reproduce \"compress prompt locally\".)\n\n## Layout\n- `manifest.json` — MV3. `storage` permission only. Content scripts load `src/directive.js` **then** `src/caveman.js` (order matters). Only the fonts are web-accessible — no WASM, no special CSP.\n- `src/directive.js` — the **pure** caveman directive text: `buildPrimer`/`buildReminder`/`isPrefixed`/`normMode`. No chrome/DOM/network. Loaded first (exposes `self.CavemanDirective`) and required directly by the test suite.\n- `src/caveman.js` — content script: per-site composer adapter + capture-phase send interception. Prepends the directive (full primer on the first message, short reminder after) and re-fires send. Exact composer-scoped selectors; `prependText` inserts at a collapsed selection without rebuilding the rich draft. Pending sends cancel on draft, editor, conversation, or settings changes; unknown controls keep native behavior.\n- `src/background.js` — service worker: just reflects the on/off state on the toolbar badge.\n- `popup.{html,js,css}` — the on/off toggle, mode (caveman/ultracave/megacave), per-site switches, and a **Leave-a-review** CTA (Chrome Web Store via `chrome.runtime.id`; hidden on Firefox until a verified AMO URL is available).\n- `test/` — pure directive/service-worker tests plus real Chromium\n  content-script and popup/storage journeys against `harness.html`.\n\n## Conventions & honesty\n- **Edits your outgoing message only.** Prepends a visible directive; never reads model replies, never touches network requests, never automates anything beyond submitting the message you sent.\n- **Local-only.** No network requests, no analytics. Stores only `enabled` / `level` / `sites` in `chrome.storage.sync`.\n- **Honest about injection.** The directive is plainly visible in the chat — the extension doesn't hide what it adds.\n- **inferred-only.** Token savings are an effect of the model's terser replies; never labeled `verified`.\n\n## Conventions\n- Build/test: `npm test` for pure tests; `npm run test:browser` for Chromium send interception, popup storage,\n  per-site controls, and review-link behavior. There is no\n  build step — MV3 loads the source directly.\n- Package: `npm run package` runs both suites, then writes `dist/caveman-browser-<version>.zip`. Staged set uses explicit allowlist in `scripts/verify-extension-stage.mjs`, cross-checked against manifest/CSS/HTML references.\n- Composer + send selectors live in the `SITES` map in `src/caveman.js`; a site redesign means updating it (each entry keeps aria-label fallbacks so one rename doesn't break it).\n\nSee ../../CLAUDE.md (root)\n","extension/CLAUDE.md":"# extension — \"Caveman Mode\" browser extension (MV3, no bundler)\n\nThe consumer top-of-funnel: a toggle that, on ChatGPT / Claude / Gemini, prepends the **caveman\nskill directive** to each message you send and re-fires the send, so the AI replies in a compact,\nhigh-density style intended to preserve technical substance. It only ever adds text to your *outgoing*\nmessage; it never reads replies, never touches network requests. Savings are `inferred`. (This is\nthe directive-injector product; an earlier revision shipped a local WASM compress button — EXT\nremoved it after Chrome Web Store review couldn't reproduce \"compress prompt locally\".)\n\n## Layout\n- `manifest.json` — MV3. `storage` permission only. Content scripts load `src/directive.js` **then** `src/caveman.js` (order matters). Only the fonts are web-accessible — no WASM, no special CSP.\n- `src/directive.js` — the **pure** caveman directive text: `buildPrimer`/`buildReminder`/`isPrefixed`/`normMode`. No chrome/DOM/network. Loaded first (exposes `self.CavemanDirective`) and required directly by the test suite.\n- `src/caveman.js` — content script: per-site composer adapter + capture-phase send interception. Prepends the directive (full primer on the first message, short reminder after) and re-fires send. Exact composer-scoped selectors; `prependText` inserts at a collapsed selection without rebuilding the rich draft. Pending sends cancel on draft, editor, conversation, or settings changes; unknown controls keep native behavior.\n- `src/background.js` — service worker: just reflects the on/off state on the toolbar badge.\n- `popup.{html,js,css}` — the on/off toggle, mode (caveman/ultracave/megacave), per-site switches, and a **Leave-a-review** CTA (Chrome Web Store via `chrome.runtime.id`; hidden on Firefox until a verified AMO URL is available).\n- `test/` — pure directive/service-worker tests plus real Chromium\n  content-script and popup/storage journeys against `harness.html`.\n\n## Conventions & honesty\n- **Edits your outgoing message only.** Prepends a visible directive; never reads model replies, never touches network requests, never automates anything beyond submitting the message you sent.\n- **Local-only.** No network requests, no analytics. Stores only `enabled` / `level` / `sites` in `chrome.storage.sync`.\n- **Honest about injection.** The directive is plainly visible in the chat — the extension doesn't hide what it adds.\n- **inferred-only.** Token savings are an effect of the model's terser replies; never labeled `verified`.\n\n## Conventions\n- Build/test: `npm test` for pure tests; `npm run test:browser` for Chromium send interception, popup storage,\n  per-site controls, and review-link behavior. There is no\n  build step — MV3 loads the source directly.\n- Package: `npm run package` runs both suites, then writes `dist/caveman-browser-<version>.zip`. Staged set uses explicit allowlist in `scripts/verify-extension-stage.mjs`, cross-checked against manifest/CSS/HTML references.\n- Composer + send selectors live in the `SITES` map in `src/caveman.js`; a site redesign means updating it (each entry keeps aria-label fallbacks so one rename doesn't break it).\n\nSee ../../CLAUDE.md (root)\n","integrations/CLAUDE.md":"# integrations — the framework-recipe registry (`caveman sdk` data)\n\nDeclarative routing recipes for AI SDKs/frameworks: each recipe is the one-line\nbase-URL shape that points a framework at the byte-safe gateway, with `{{baseURL}}`\n(gateway origin) and `{{app}}` (the `/w/<app>` attribution slug) left as render-time\ntemplates. Recipes are DATA, not code — the collapse decision applied to app frameworks.\n\n## Layout\n- `recipes/*.json` — one recipe per framework (12 today: openai-ts/py, anthropic-ts/py,\n  google-genai, vercel-ai-sdk, langchain, litellm, crewai, pydantic-ai, openai-agents, curl).\n  Fields: `id`, `display_name`, `lang` (`ts`/`python`/`bash`), `wire_protocol`, `note`, `code`.\n- `compile.mjs` — zero-dep build-time compiler: validates every recipe (fail-closed) and\n  emits `recipes.json` (published registry) + two EMBEDDED copies —\n  `../cli/src/recipes.generated.ts` (the CLI stays zero-runtime-dep) and\n  the web app's embedded copy (cloud may consume public data; the reverse import is forbidden).\n- `recipes.json` — generated; do not hand-edit.\n\n## Conventions\n- **fail-closed**: an unknown `lang` or `wire_protocol` fails the compile — never a guessed\n  protocol (no-placeholder rule). Allowed protocols are the 4 the proxy speaks natively\n  (anthropic-messages · openai-chat · openai-responses · gemini-generatecontent) plus\n  `multi` for frameworks that fan out behind one config.\n- Output is deterministic (recipes sorted by id); re-running on unchanged input is a no-op.\n- Run from the CLI build/test: `node ../integrations/compile.mjs`. The generated files are\n  committed and must stay in sync — change a recipe and regenerate all three artifacts.\n\nSee ../../CLAUDE.md (root) · ../agents/CLAUDE.md (the agent twin) · ../cli/CLAUDE.md\n","mcp/AGENTS.md":"# mcp — the Caveman MCP server (Go core + launcher, Apache-2.0)\n\nA thin **stdio JSON-RPC** adapter exposing the compression [engine](../engine/CLAUDE.md) as\nfive MCP tools to any host (Claude Code, Cursor, …). It owns only the MCP framing; all\ncompression is the engine's, linked **in-process** (no subprocess, no drift). Local-only — it\nopens no network connection — and everything it reports is `inferred`, never `verified`.\n\n## Layout\n- `server.go` — the `Server`: JSON-RPC loop, dispatch, the five tool handlers. Takes an injectable `Engine` interface so the framing is testable without the real compressors.\n- `protocol.go` — JSON-RPC + MCP tool-result types, `toolText`/`toolError` helpers, the exact `tools/list` definitions.\n- `cmd/caveman-mcp/` — binary: opens shared file CCR store (`CAVEMAN_CCR_DB`,\n  else `CAVEMAN_HOME/ccr.db`/`~/.caveman/ccr.db`) so proxy handles resolve across\n  processes, then serves stdin↔stdout. `CAVEMAN_MCP_EPHEMERAL=1` opts into an\n  isolated in-memory store for tests/sessions that do not need proxy recovery.\n- `bin/caveman-mcp.mjs` + `package.json` — the `npx caveman-mcp` launcher that execs the prebuilt Go binary.\n\n## The five tools (exact names, case-sensitive)\n- `caveman_compress(input)` → compressed text + inferred ratio + `recovery_handle`. Lossy (S4), recoverable, and fail-closed: incompressible/malformed/not-smaller input returns unchanged, `ratio:0`, `recovery_handle:null` — never an error.\n- `caveman_retrieve(recovery_handle)` → the byte-exact original. Unknown handle → `isError:true` + a `cave_snake_code`, never a fabricated payload.\n- `caveman_stats()` → `basis:\"inferred\"`, `scope:\"session\"`; the string `verified` never appears.\n- `caveman_toon_encode(input)` → explicit JSON→TOON re-encoding with input/output sizes; returns pass-through plus note when encoding fails.\n- `caveman_toon_decode(input)` → TOON→JSON; invalid TOON returns `isError:true`, never raw input as JSON.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=mcp` / `make product-test PRODUCT=mcp`.\n- **stdout is the protocol channel** — logs go to stderr only (a dedicated test guards this).\n\n## Gotchas (honesty invariants)\n- **un-killable transport** — the stdio server survives everything short of EOF (issue #139). Framing is line-delimited: a malformed line is answered `-32700` and the loop RESYNCHRONIZES to the next newline (never `return`); a handler panic is contained by `recover()` → `cave_tool_panicked` (dispatch panics → `cave_internal_error`); JSON-RPC batch arrays are handled per spec (one array response); id-less/`\"id\":null` requests are notifications and get no reply; and both inbound lines and generated tool output (compress/toon) are size-capped (`cave_payload_too_large`, `maxInboundBytes`/`maxResultBytes`, 16 MiB default) — but `caveman_retrieve` is exempt (`Tool.ExemptResultCap`): recovery returns the byte-exact original and must never fail closed on size, since the shared gateway store has no matching ceiling. A dead server is worse than a slow one — the proxy keeps eliding content that no longer has a `caveman_retrieve` to expand it.\n- **fail-open** — engine error or malformed input → byte-identical pass-through, never a protocol error.\n- **fail-closed** — unknown tool/handle → `isError` + cave_snake_code; unknown JSON-RPC method → `-32601`.\n- **zero-egress** — the adapter imports no `net`/`net/http`/`os/exec`; a test parses the source to enforce it.\n- v1 is **stdio-only**, **string payloads only** (the engine detects type); HTTP transport + `caveman mcp` subcommand are v2.\n- **protocol negotiation must never error.** The adapter implements the 2024-11-05 contract and echoes that version back; a client asking for a newer one gets 2024-11-05 in the initialize result and decides for itself, per the MCP lifecycle. It previously answered `-32602: unsupported protocol version`, which made Claude Code (and every other current client) drop the server — and because `caveman wrap` reads recovery availability from an install-time marker rather than from the live agent, the proxy kept eliding content that no longer had a `caveman_retrieve` to expand it. Declining to echo an unimplemented version is right; refusing to speak is not.\n\nSee ../../CLAUDE.md (root) · ../engine/CLAUDE.md\n","mcp/CLAUDE.md":"# mcp — the Caveman MCP server (Go core + launcher, Apache-2.0)\n\nA thin **stdio JSON-RPC** adapter exposing the compression [engine](../engine/CLAUDE.md) as\nfive MCP tools to any host (Claude Code, Cursor, …). It owns only the MCP framing; all\ncompression is the engine's, linked **in-process** (no subprocess, no drift). Local-only — it\nopens no network connection — and everything it reports is `inferred`, never `verified`.\n\n## Layout\n- `server.go` — the `Server`: JSON-RPC loop, dispatch, the five tool handlers. Takes an injectable `Engine` interface so the framing is testable without the real compressors.\n- `protocol.go` — JSON-RPC + MCP tool-result types, `toolText`/`toolError` helpers, the exact `tools/list` definitions.\n- `cmd/caveman-mcp/` — binary: opens shared file CCR store (`CAVEMAN_CCR_DB`,\n  else `CAVEMAN_HOME/ccr.db`/`~/.caveman/ccr.db`) so proxy handles resolve across\n  processes, then serves stdin↔stdout. `CAVEMAN_MCP_EPHEMERAL=1` opts into an\n  isolated in-memory store for tests/sessions that do not need proxy recovery.\n- `bin/caveman-mcp.mjs` + `package.json` — the `npx caveman-mcp` launcher that execs the prebuilt Go binary.\n\n## The five tools (exact names, case-sensitive)\n- `caveman_compress(input)` → compressed text + inferred ratio + `recovery_handle`. Lossy (S4), recoverable, and fail-closed: incompressible/malformed/not-smaller input returns unchanged, `ratio:0`, `recovery_handle:null` — never an error.\n- `caveman_retrieve(recovery_handle)` → the byte-exact original. Unknown handle → `isError:true` + a `cave_snake_code`, never a fabricated payload.\n- `caveman_stats()` → `basis:\"inferred\"`, `scope:\"session\"`; the string `verified` never appears.\n- `caveman_toon_encode(input)` → explicit JSON→TOON re-encoding with input/output sizes; returns pass-through plus note when encoding fails.\n- `caveman_toon_decode(input)` → TOON→JSON; invalid TOON returns `isError:true`, never raw input as JSON.\n\n## These five tools are a prompt-prefix cost, and it is measured\nRegistering this server puts all five tool schemas in the wrapped agent's prefix\non **every** call — **11,060 tokens/call** in the agent bench (~2.22M over its 201\ncalls). That is why `caveman wrap` gates the injection behind the `execute.mcp`\nsurface knob (`auto` | `marker-only` | `true` | `false`, `packages/cli/src/index.ts`).\nUnder a non-auto surface wrap suppresses **both** of its injection sites — the\n`mcp install` write and, for config-file agents like openclaw, the profile's\n`mcp.servers.caveman` overlay — but it never uninstalls a server the user\ninstalled themselves, so an existing install keeps costing its prefix until\n`caveman tools mcp uninstall <agent>`. The knob never touches recovery honesty on\nthe wrap door: `CAVEMAN_RECOVERY` is answered from evidence that this launch is\nreally giving the agent `caveman_retrieve`, so the proxy is never told a\nretrieval tool exists when none does. Adding a sixth tool here raises that\nper-call tax for every wrapped agent.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=mcp` / `make product-test PRODUCT=mcp`.\n- **stdout is the protocol channel** — logs go to stderr only (a dedicated test guards this).\n\n## Gotchas (honesty invariants)\n- **un-killable transport** — the stdio server survives everything short of EOF (issue #139). Framing is line-delimited: a malformed line is answered `-32700` and the loop RESYNCHRONIZES to the next newline (never `return`); a handler panic is contained by `recover()` → `cave_tool_panicked` (dispatch panics → `cave_internal_error`); JSON-RPC batch arrays are handled per spec (one array response); id-less/`\"id\":null` requests are notifications and get no reply; and both inbound lines and generated tool output (compress/toon) are size-capped (`cave_payload_too_large`, `maxInboundBytes`/`maxResultBytes`, 16 MiB default) — but `caveman_retrieve` is exempt (`Tool.ExemptResultCap`): recovery returns the byte-exact original and must never fail closed on size, since the shared gateway store has no matching ceiling. A dead server is worse than a slow one — the proxy keeps eliding content that no longer has a `caveman_retrieve` to expand it.\n- **fail-open** — engine error or malformed input → byte-identical pass-through, never a protocol error.\n- **fail-closed** — unknown tool/handle → `isError` + cave_snake_code; unknown JSON-RPC method → `-32601`.\n- **zero-egress** — the adapter imports no `net`/`net/http`/`os/exec`; a test parses the source to enforce it.\n- v1 is **stdio-only**, **string payloads only** (the engine detects type); HTTP transport + `caveman mcp` subcommand are v2.\n- **protocol negotiation must never error.** The adapter implements the 2024-11-05 contract and echoes that version back; a client asking for a newer one gets 2024-11-05 in the initialize result and decides for itself, per the MCP lifecycle. It previously answered `-32602: unsupported protocol version`, which made Claude Code (and every other current client) drop the server — and because `caveman wrap` reads recovery availability from an install-time marker rather than from the live agent, the proxy kept eliding content that no longer had a `caveman_retrieve` to expand it. Declining to echo an unimplemented version is right; refusing to speak is not.\n\nSee ../../CLAUDE.md (root) · ../engine/CLAUDE.md\n\n## Recovery across host compaction\n\nEvery valid `caveman_retrieve` call executes the requested handle and query,\nincluding identical repeats and calls after many earlier recoveries. An empty\nquery always returns the exact stored original. The MCP server cannot infer\nwhich earlier results remain in the model context from its process lifetime:\nhosts can compact their conversation without restarting MCP connections.\n\nThe previous per-process repeat suppression and forced full-payout threshold\nhave been removed. Current Codex source replaces compacted history and excludes\ntool-output items while its thread retains the live MCP runtime; see\n[compacted-history filtering](https://github.com/openai/codex/blob/main/codex-rs/core/src/compact_remote.rs)\nand [session services](https://github.com/openai/codex/blob/main/codex-rs/core/src/state/service.rs).\nTests exercise repeated recovery and more than five distinct queries in the same\nserver process. Query narrowing and complete-record boundaries remain owned by\nthe Engine.\n","mem/AGENTS.md":"# mem — cavemem, durable agent memory (Go core + clients, Apache-2.0)\n\nDurable, cross-session memory: `remember` / `recall` / `supersede` / `history` / `forget`. A local SQLite store holds the\n**raw** memories (the source of truth); recall ranks them with deterministic **BM25** behind a\nconservative threshold and compresses each hit through the [engine](../engine/CLAUDE.md) so the\ninferred token cost is honest and the dropped detail stays recoverable via CCR. Everything is\n`inferred`.\n\n## Layout\n- `store.go` — `Remember`/`Recall`/`Supersede`/`History`/`Forget`/`Recover` over SQLite + engine; legacy schemas migrate in place.\n- `bm25.go` — deterministic tokenizer + BM25 scorer (non-negative scores, so the threshold is meaningful).\n- `cmd/cavemem/` — MCP server + `remember`/`recall`/`supersede`/`history`/`forget` CLI subcommands (JSON).\n- `js/` + `py/` — thin TS and Python clients that shell out to the binary (mirrored libraries; they reimplement nothing).\n\n## The cavemem_* MCP tools\n`cavemem_remember(text)` · `cavemem_recall(query, limit?, token_budget?)` ·\n`cavemem_supersede(id, text)` · `cavemem_history(id)` · `cavemem_forget(id)`,\nserved through `mcp.NewServer` so the framing matches caveman-mcp exactly.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=mem` / `make product-test PRODUCT=mem`\n  for the Go core; root `make test` also runs mirrored JS/Python wrapper tests.\n- The wrappers shell out to the gated binary — keep recall/scoring logic in Go, never forked into TS/Python.\n\n## Gotchas (honesty invariants)\n- **byte-safe write** — raw text is written to SQLite before any engine call, so a memory is never lost; compression is transient, at recall time only.\n- **bounded recall** — omitted/Go-zero `TokenBudget` defaults to 2000 inferred tokens. Public CLI/MCP/JS/Python callers must explicitly pass `token_budget=0` for unlimited recall; adapters map it to `UnlimitedTokenBudget`.\n- **fail toward nothing** — a query below the threshold (or with no term overlap) recalls nothing, never a guess.\n- **current-only recall** — superseded versions stay auditable through `History` but never enter normal recall.\n- **inferred-only** — `tokens_added`, score, and basis are inferred estimates; never `verified`.\n- **reversible** — a compressed recall hit carries a CCR `recovery_handle`; `Recover` returns the byte-exact original.\n\nSee ../../CLAUDE.md (root) · ../engine/CLAUDE.md · ../mcp/CLAUDE.md\n","mem/CLAUDE.md":"# mem — cavemem, durable agent memory (Go core + clients, Apache-2.0)\n\nDurable, cross-session memory: `remember` / `recall` / `supersede` / `history` / `forget`. A local SQLite store holds the\n**raw** memories (the source of truth); recall ranks them with deterministic **BM25** behind a\nconservative threshold and compresses each hit through the [engine](../engine/CLAUDE.md) so the\ninferred token cost is honest and the dropped detail stays recoverable via CCR. Everything is\n`inferred`.\n\n## Layout\n- `store.go` — `Remember`/`Recall`/`Supersede`/`History`/`Forget`/`Recover` over SQLite + engine; legacy schemas migrate in place.\n- `bm25.go` — deterministic tokenizer + BM25 scorer (non-negative scores, so the threshold is meaningful).\n- `cmd/cavemem/` — MCP server + `remember`/`recall`/`supersede`/`history`/`forget` CLI subcommands (JSON).\n- `js/` + `py/` — thin TS and Python clients that shell out to the binary (mirrored libraries; they reimplement nothing).\n\n## The cavemem_* MCP tools\n`cavemem_remember(text)` · `cavemem_recall(query, limit?, token_budget?)` ·\n`cavemem_supersede(id, text)` · `cavemem_history(id)` · `cavemem_forget(id)`,\nserved through `mcp.NewServer` so the framing matches caveman-mcp exactly.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=mem` / `make product-test PRODUCT=mem`\n  for the Go core; root `make test` also runs mirrored JS/Python wrapper tests.\n- The wrappers shell memory text to the gated binary through `remember --stdin`\n  (never one huge argv element) — keep validation/recall/scoring logic in Go,\n  never forked into TS/Python.\n\n## Gotchas (honesty invariants)\n- **byte-safe write** — raw text is written to SQLite before any engine call, so a memory is never lost; compression is transient, at recall time only.\n- **single-writer store** — `mem.db` and `ccr.db` open with `SetMaxOpenConns(1)` + a DSN carrying `busy_timeout(5000)` + `journal_mode(WAL)` (`ccr.SQLiteDSN`). The proxy spend store uses the same DSN but does **not** force one pooled connection; mem/CCR's discipline is stricter. Cold-start migrations and the idempotent `Remember` insert use a thirty-second-wall-budgeted `ccr.RetryOnBusy` because a fan-out of fresh `cavemem remember` processes (the JS client's `Promise.all(facts.map(remember))`) can outlast one busy timeout on Windows. Without this, concurrent writes are dropped as `SQLITE_BUSY`.\n- **bounded recall** — `Recall` packs hits greedily in BM25 rank order under `RecallOptions.TokenBudget` (default `DefaultTokenBudget` = 2000 inferred tokens), reusing `engine/contextwindow` packing. Public CLI/MCP/JS/Python callers must explicitly pass `token_budget=0` to request unlimited recall; adapters map that external zero to `UnlimitedTokenBudget`, while omitted/Go-zero stays bounded. A single top hit whose compressed form alone exceeds the budget is returned as a compressed **head + CCR recovery_handle** (the original is stored even when compression passed it through), never its whole body — this is what stops the measured 440,000-token single recall.\n- **bounded remember** — `Remember` caps one memory at `MaxMemoryBytes` (256 KiB), failing closed with `cave_memory_too_large` (`ErrMemoryTooLarge`); CLI exits 65 and both wrappers export `MEMORY_TOO_LARGE_EXIT_CODE`. A memory is a fact to recall, not a file dump.\n- **fail toward nothing** — a query below the threshold (or with no term overlap) recalls nothing, never a guess.\n- **current-only recall** — superseded versions stay auditable through `History` but never enter normal recall.\n- **inferred-only** — `tokens_added`, score, and basis are inferred estimates; never `verified`.\n- **reversible** — a compressed recall hit carries a CCR `recovery_handle`; `Recover` returns the byte-exact original.\n\nSee ../../CLAUDE.md (root) · ../engine/CLAUDE.md · ../mcp/CLAUDE.md\n","packages/cli/AGENTS.md":"# packages/cli — `caveman` CLI\n\nTypeScript CLI (`src/index.ts`) driving the local proxy and wrapping control-api\nREST calls. Published package has zero runtime dependencies. Build emits\n`dist/index.js`, dependency-free `dist/caveman-delegate-mcp.mjs`, and a lazy\nbundled `dist/learn-tui.js` Clack chunk, then\n`scripts/shebang.mjs` adds shebang + chmod. `bin` exposes both `caveman` and `cave`. Non-secret config lives at\n`~/.caveman-cloud/config.json` (0o600); the **auth token lives in the OS keychain** (macOS\n`security`) or a `~/.caveman/credentials` (0o600) fallback — never plaintext config.\n\n## Layout\n- `src/index.ts` — entire CLI: arg dispatch, HTTP helpers (`get`/`post`), token storage, all commands\n- `src/learn-tui.ts` — bounded interactive learn view; bundled with Clack and loaded only for a real TTY\n- `tests/*.runtime.mjs` — Node `--test` runtime tests; spawn the built binary against HTTP stubs (`providers-verify`, `wrap`, `login`, `compress`)\n- `scripts/bundle-delegate.mjs` — copies canonical dependency-free delegate server into published `dist/`\n- `scripts/bundle-tui.mjs` — bundles Clack into the self-contained learn TUI chunk\n- `scripts/shebang.mjs` — post-build: prepends shebang, marks executable\n- `package.json` — bin: `caveman`/`cave → dist/index.js`; build: registries → `tsc` → delegate/TUI bundles → shebang\n\n## Command surface\n`caveman <agent>` (e.g. `caveman claude`) is shorthand for `caveman wrap <agent>` — any known agent id or binary name works as a top-level command; it is dispatched **last**, so real commands always shadow an agent name, and everything after the agent name goes to the agent verbatim.\n`cave` is a permanent byte-compatible bin alias. Relocated verbs keep their bare\nspellings as silent legacy aliases: no deprecation text may alter piped output.\n\nPrinted porcelain is `run`, `learn`, `login`, `status`, plus the agent shortcut.\nLocal capabilities live under `caveman tools`; account- or network-dependent\noperations live under `caveman cloud`. `dev` and `deploy` are undocumented\nmaintainer aliases. `tools` is capped at 15 printed verbs and `cloud` at 15;\ncurrent counts are 15 and 14. Internal/advanced `shrink-hook`, `practices`, and\n`check` remain callable through existing paths but are unprinted, including in\nlegacy `help tools --all` output.\n\nCaveman's own Go binaries (proxy/engine/mcp/mem/browse/shrink) resolve via `cavemanBin()`: env override (`CAVEMAN_*_BIN`) → PATH → `~/.caveman/bin` (where `scripts/install-local-cli.sh` or `scripts/install-local-cli.ps1` builds them) → bare name (so missing-binary panels still trigger).\n`caveman setup` prints per-binary install status — what works, what degrades to a loud byte-safe pass-through, and the one install command — and exits non-zero when a required binary (proxy/engine/mcp/mem; browse and shrink are optional) is missing. `caveman tools compress catalog` delegates to the dedicated `caveman-shrink` binary; `caveman tools shrink` remains command-output compression. It's the anti-silent-degrade front door for npm installs (the package ships JS only); every degraded path also prints its own warning line pointing at it. Publish checklist lives in `PUBLISHING.md`.\nLocal (no account): `start` (launch the proxy via `CAVEMAN_PROXY_BIN`; if the binary is missing or the port is already served it renders a status panel — build/`make dev`+live docker status/env — instead of a bare spawn error) · `wrap [agent]` (inject `ANTHROPIC_BASE_URL`/`OPENAI_BASE_URL` = `CAVE_GATEWAY_URL` or the local proxy, exec child; known agents `claude`/`codex`/`gemini`/`aider` launch by id with install detection; an unknown/unfound target shows an install hint or the wrappable list; bare `wrap` in a TTY opens an arrow-key picker) · `compress` (shells out to `caveman-engine` via `CAVEMAN_ENGINE_BIN`; byte-safe pass-through fallback if the binary is missing; `inferred`) · `toon encode|decode` (stateless JSON⇄TOON converter via the engine binary; `encode` degrades byte-safe when the engine is missing, `decode` fails loudly — it must not emit raw TOON as JSON) · `wrap --toon` (sets `CAVE_ENGINE_TOON=best-of` on the spawned proxy so it re-encodes uniform JSON the model reads — tool results — as TOON when smaller; opt-in, implies `--compress`) · `mcp install|uninstall [agent]` (register/remove the caveman_retrieve MCP tool; compress-mode wrap injects it only when `execute.mcp = auto` and a real `caveman-mcp` executable resolves — never the npx fallback — because streams are only compressible with agent-side recovery; `execute.mcp = marker-only` or `false` stops injection while existing registrations remain visible) · `evals run` (delegates to `caveman-engine evals run`, forwards its exit code) · `stats` (delegates to `caveman-proxy stats`) · `convert` (pixel-compresses installed agent skills in place: SKILL.md **body** → `SKILL.pxN.png` pages via `caveman-engine pixel render`, frontmatter stays text so discovery/triggering still works, body becomes a stub telling the agent to read the images on invocation; dirs come from registry profiles with a `skills` block — claude + codex today; converts only when image+stub est tokens < text est tokens, else untouched; original kept byte-exact as `SKILL.orig.md`, `--revert` restores; every skip is reported with its reason; savings `inferred`, per-invocation) · `skills install [caveman|caveman-learn]` (writes embedded Caveman skills and auto-pixels by default) · `skills add <source>` (accepts official `npx skills add` Git/URL/local sources and flags, delegates download/selection to that CLI in forced copy mode, then pixelizes only new/changed Claude Code or Codex skills; resources stay untouched; third-party content is explicitly unreviewed; `--no-pixel` opts out; engine failure/not-smaller stays honest plain text).\nConnected namespace: `whoami · projects · keys · providers · billing · score ·\ncosts · plan · traces · experiments · receipts · audit · sync · agent`.\n`doctor`, `opportunities`, `snippets`, `dev`, and `deploy` remain unprinted legacy\naliases; `status`, `plan`, and `tools sdk snippets` absorb their public jobs.\n\n`login` polls control-api `/api/v1/auth/device/{code,token}`; `organization_id` is bound from the returned token. `CAVE_TOKEN` is the non-interactive CI path. Logged-out connected verbs print one line + exit non-zero (CI skips, never crashes).\n\nFirst-run retro scan persists only closed `caveman.local_scan.v1` aggregate. Login or explicit `sync` uploads it through authenticated project `format=local-scan`; prompts, outputs, paths, session rows, free-text caveats, and anonymous telemetry never enter this lane. Dashboard labels token figures `inferred` and separates them from measured spend and verified savings.\n\n`providers verify <conn>` → real POST `/api/v1/projects/{id}/providers/{conn}/verify` (no hardcoded status).\n\n`plan` renders the Cave Plan in plain English (one operator voice); `--json` prints the raw response. There is **no** caveman voice / `--engineer` flag — the dual-voice was deliberately removed. Headline is labeled `basis` (\"inferred\"), savings are per-day — never reprojected to monthly. (honesty rule: no-fake-savings)\n\n`learn` is summary-first: real terminals get animated progress, bounded score\nand move cards, then one keyboard action menu. `--plain` restores compact text,\n`--all` restores every sink id/class/practice/suggestion, while `--json` and\n`--md` stay complete. `learn implement [claude|codex]\n[--prompt <focus>]` installs the existing `caveman-learn` safety guide when\nmissing and launches the chosen interactive agent. The guide's per-edit consent,\nload-bearing protection, re-measurement, and inferred-only rules remain binding.\nPresentation contract: [`TERMINAL_UX.md`](TERMINAL_UX.md).\n\n## Conventions\n- Dispatch uses handler tables. Every handler receives its own rebased argv slice;\n  never read process-global argv positionally inside a handler.\n- `flag(\"--name\", fallback)` parses named args from current invocation.\n- Tests use `node --test` (Node built-in runner); run `tsc` first, test spins a real HTTP server\n- Build: `pnpm build` (tsc + shebang); install locally: `scripts/install-local-cli.sh` (macOS/Linux) or `scripts/install-local-cli.ps1` (Windows) at repo root\n\n## Capability promotion rule\n\nA capability may default on only when it is byte-safe, or when protected by the\napplicable path-specific gate: managed gateway uses an eval gate; local wrap\nuses recovery + CCR — **not** an account or entitlement. There is\nno eval gate in local `run`.\nAny PR flipping a default must name the clause and path.\n\nA verb enters porcelain only when its capability is automatic-by-default-safe\ninside `run` and users no longer need to type it. Porcelain stays capped at four\nverbs + agent shortcut + exactly two namespaces. A fifth verb, or a 16th printed\nverb in either namespace, requires a retirement decision. `record` mode is always\npass-through.\n\nCapability config is grouped in `~/.caveman-cloud/config.json` as `think`,\n`remember`, and `execute`. `./.caveman/config.json` may only narrow its allowlisted\nproject-local keys; it cannot change `think.mode`, pixel settings, account state,\nconsent, or entitlement. Resolution is default < proxy YAML < legacy `wrap` <\nglobal groups < project overlay < env. Env parity is knob-specific. Inspect\nper-key source with `caveman tools config get`.\n\n## Gotchas\n- `providers verify` must NOT return a hardcoded status; the test asserts the CLI echoes the server's value (no-placeholder rule)\n- `plan` savings display must stay per-day; never multiply to monthly projection\n- Non-PAYG coverage includes Claude Pro/Max, Codex ChatGPT, Gemini OAuth, and routed compatible agents. Codex subscription mode keeps provider config ephemeral under `CODEX_HOME`, auto-installs its MCP recovery, and starts `/chatgpt/responses` in compress mode instead of forcing record/pass-through. Plain OpenAI `/responses` and Gemini `generateContent` requests with MCP explicitly disabled have no server-retrieval grammar, so they must remain byte-identical with zero compression accounting; the compression conformance matrix pins these protocol-specific fail-closed cases instead of requiring every profile to emit a CCR marker.\n- Subscription/OAuth wrap sessions (Claude Pro/Max) compress **locally only**, live zone only, and with **no account**: `CAVEMAN_WRAP_ENTITLED` is gone from both doors and from the proxy, and both doors `delete` any inherited copy so a stray export cannot resurrect it. What both doors DO stamp is the recovery path (`CAVEMAN_RECOVERY`), explicitly (`\"mcp\"` or empty, never inherited): `wrap` answers it from the **agent's own** MCP install (an exported `CAVEMAN_RECOVERY=mcp` can't outlive that answer — it would have the proxy elide bytes behind markers this agent has no `caveman_retrieve` tool to expand), `start` from machine-wide MCP install evidence plus an explicit `CAVEMAN_RECOVERY=mcp` counted as the operator's own opt-in, re-stamped so the disclosure line and the proxy can never disagree; the compression disclosure line prints only when recovery holds, and no-MCP says compression is off and names `caveman mcp install <agent>`. The `subscription_compress: off` operator switch stays the operator's. Their measured reduction remains **tokens only**: a seat has no per-token marginal price, so the session footer and synced span never claim saved dollars. The local stats dashboard may show a separately labeled, same-model API equivalent from captured prices; it is hypothetical value, never subscription bill or quota savings. The session-savings line treats `oauth` like `subscription` (OAuth is list-price-eligible on Vertex alone) and qualifies unconditionally when its capped auth-mode window is truncated\n- Published runtime dependencies stay zero. TUI libraries must be bundled,\n  lazy-loaded, and measured; do not move them onto ordinary command startup.\n- `learn` uses bundled Clack only when stdin/stdout/stderr are TTYs. `--plain`,\n  `CAVEMAN_PLAIN=1`, `TERM=dumb`, machine modes, and pipes must never prompt.\n- Remaining terminal UX (status panels + `wrap` picker) lives in the small toolkit\n  at the bottom of `src/index.ts`. Piped/non-interactive paths remain plain; runtime\n  tests assert them.\n- Current official Skills CLI installs global Codex sources at\n  `~/.agents/skills`, despite older/direct Caveman installs using\n  `~/.codex/skills`; third-party post-install discovery must scan both.\n\nSee ../../CLAUDE.md (root)\n","packages/cli/CLAUDE.md":"# packages/cli — `caveman` CLI\n\nTypeScript CLI (`src/index.ts`) driving the local proxy and wrapping control-api\nREST calls. Published package has zero runtime dependencies. Build emits\n`dist/index.js`, dependency-free `dist/caveman-delegate-mcp.mjs`, and a lazy\nbundled `dist/learn-tui.js` Clack chunk, then\n`scripts/shebang.mjs` adds shebang + chmod. `bin` exposes both `caveman` and `cave`. Non-secret config lives at\n`~/.caveman-cloud/config.json` (0o600); the **auth token lives in the OS keychain** (macOS\n`security`) or a `~/.caveman/credentials` (0o600) fallback — never plaintext config.\n\n## Layout\n- `src/index.ts` — entire CLI: arg dispatch, HTTP helpers (`get`/`post`), token storage, all commands\n- `src/learn-tui.ts` — bounded interactive learn view; bundled with Clack and loaded only for a real TTY\n- `tests/*.runtime.mjs` — Node `--test` runtime tests; spawn the built binary against HTTP stubs (`providers-verify`, `wrap`, `login`, `compress`)\n- `scripts/bundle-delegate.mjs` — copies canonical dependency-free delegate server into published `dist/`\n- `scripts/bundle-tui.mjs` — bundles Clack into the self-contained learn TUI chunk\n- `scripts/shebang.mjs` — post-build: prepends shebang, marks executable\n- `package.json` — bin: `caveman`/`cave → dist/index.js`; build: registries → `tsc` → delegate/TUI bundles → shebang\n\n## Command surface\n`caveman <agent>` (e.g. `caveman claude`) is the **persistent default door**: for hook-capable native agents (claude/codex/hermes/gemini/opencode — not aider, whose hookless native install could never start the proxy) it runs the `caveman enable <agent>` user-scoped native install if no install journal exists, pre-starts the local proxy when it isn't listening (the native SessionStart hook re-does this, but only after the host approves the hooks), then launches the host binary directly — so plain `<agent>` stays caveman'd in every later session. For claude/codex the enable step also writes the `output` skill suite (`caveman`/`ultracave`/`megacave`) into the host's user skills dir, create-only (`wx`, never clobbers) and fail-open. They are deliberately NOT native-journal operations, so an edited/deleted skill can never read `degraded`, and `disable` leaves them in place: it turns off routing and hooks, not the voice. A `~/.caveman/integrations/<agent>.voice-skills.json` sidecar marks the install as done, so it runs once: the shortcut skips journaled installs (explicit `caveman enable <agent>` picks the skills up there), a skill the user deleted is not written back, and a CLI upgrade does not refresh an installed copy. It falls back to the ephemeral `wrap` door whenever the native path can't hold: wrap-session flags given (`--off`/`--pixel`/`--workflow`), a non-native agent, a Cave Build lock in cwd (lock enforcement is wrap-door-only), or any enable failure (e.g. missing caveman-mcp/proxy binaries) — the shortcut is never worse than a session-only wrap. Note the fallback is session-only relative to an install that may already exist: once the native install is in place, `--off`/`wrap` runs are ephemeral but the persistent routing stays until `caveman disable <agent>`. Any known agent id or binary name works as a top-level command; it is dispatched **last**, so real commands always shadow an agent name, and everything after the agent name goes to the agent verbatim. `caveman wrap <agent>` / `caveman run <agent>` remain the explicit session-only path.\n`cave` is a permanent byte-compatible bin alias. Relocated verbs keep their bare\nspellings as silent legacy aliases: no deprecation text may alter piped output.\n\nPrinted porcelain is `run`, `learn`, `login`, `status`, plus the agent shortcut.\nLocal capabilities live under `caveman tools`; account- or network-dependent\noperations live under `caveman cloud`. `dev` and `deploy` are undocumented\nmaintainer aliases. `tools` is capped at 15 printed verbs and `cloud` at 15;\ncurrent counts are 15 and 14. Internal/advanced `shrink-hook`, `practices`, and\n`check` remain callable through existing paths but are unprinted, including in\nlegacy `help tools --all` output.\n\nCaveman's own Go binaries (proxy/engine/mcp/mem/browse/shrink) resolve via `cavemanBin()`: env override (`CAVEMAN_*_BIN`) → PATH → `~/.caveman/bin` (where `scripts/install-local-cli.sh` or `scripts/install-local-cli.ps1` builds them) → bare name (so missing-binary panels still trigger).\n`caveman setup` prints per-binary install status — what works, what degrades to a loud byte-safe pass-through, and the one install command — and exits non-zero when a required binary (proxy/engine/mcp/mem; browse and shrink are optional) is missing. `caveman tools compress catalog` delegates to the dedicated `caveman-shrink` binary; `caveman tools shrink` remains command-output compression. It's the anti-silent-degrade front door for npm installs (the package ships JS only); every degraded path also prints its own warning line pointing at it. Publish checklist lives in `PUBLISHING.md`.\nLocal (no account): `start` (launch the proxy via `CAVEMAN_PROXY_BIN`; if the binary is missing or the port is already served it renders a status panel — build/`make dev`+live docker status/env — instead of a bare spawn error) · `wrap [agent]` (inject route and exec child; Claude, Codex, Hermes, Gemini, OpenCode, Aider, and OpenClaw launch by id; native-capable hosts get temp pack/home/config only, removed after exit; no user/project config write) · `enable|disable <agent>` (explicit user-scoped native install with atomic journal, lock, reversible owned-path restore, component doctor) · `inspect` / `why <decision-id>` (content-blind native receipt and Decision Ledger explanation) · `compress` (shells out to `caveman-engine`; byte-safe missing-binary pass-through; `inferred`) · `mcp install|uninstall [agent]` (explicit persistent recovery registration; wrap uses temporary recovery configuration only when `execute.mcp = auto` and a current local MCP binary exists; `marker-only` or `false` suppresses injection without silently removing user registrations) · `skills add <source>` (same Git/URL/local sources and flags as official `npx skills add`; download/selection stays upstream, forced copy mode lets Caveman pixelize only new/changed Claude Code/Codex `SKILL.md` bodies; resources remain untouched and third-party content remains explicitly unreviewed; `--no-pixel` opts out) · `skills import <dir|SKILL.md>` (blocked unevaluated native-skill draft; imported resources never execute) · `evals run` · `stats` · `convert`.\nConnected namespace: `whoami · projects · keys · providers · billing · score ·\ncosts · plan · traces · experiments · receipts · audit · sync · agent`.\n`doctor`, `opportunities`, `snippets`, `dev`, and `deploy` remain unprinted legacy\naliases; `status`, `plan`, and `tools sdk snippets` absorb their public jobs.\n\n`login` polls control-api `/api/v1/auth/device/{code,token}`; `organization_id` is bound from the returned token. `CAVE_TOKEN` is the non-interactive CI path. Logged-out connected verbs print one line + exit non-zero (CI skips, never crashes).\n\n**First-run experience** (TTY only, once per machine, marker `firstRunAt` in config): on the first interactive wrap — banner, 30-day retrospective scan (`caveman-proxy learn scan --retro`, read-only over local Claude Code/Codex session logs; base behavior pass capped at 20s, retro pass at 60s, child timeout derived as 90s including margin), count-up reveal of tokens sent / would-have-cut / rides-every-turn stream figure (sums over scanned sessions, never extrapolated; `inferred`; tokens never dollars; sent is deduped once per API response, base cut carries no ratio against sent, only the stream figure shares sent's basis), telemetry disclosure, one `[y/N]` account question (yes → device login → existing span backfill). CLI persists only the closed, content-free aggregate; login or explicit `sync` uploads it to the authenticated project's `local-scan` import lane. Prompts, outputs, paths, session rows, free-text caveats, and anonymous telemetry never enter that upload; dashboard keeps it `inferred` and separate from spend/verified savings. Every failure degrades to one dim line and the agent still launches. Replay with the **unprinted** `caveman welcome` (porcelain caps unchanged). Anonymous **telemetry defaults ON (opt-out)**: persisted v1 decisions and `DO_NOT_TRACK`/`CAVEMAN_TELEMETRY=0`/`caveman telemetry off` always win; CI never sends; non-TTY never persists the default; the first eligible interactive run persists a stable anonymous id and prints the disclosure line. A native SessionStart (not `source: compact`) with a persisted, current-version yes spawns a detached `caveman telemetry session <agent> <source>` (unprinted) that sends `session_start` with the token delta, so installed users who launch agents directly stay visible; the hook never waits on the network. One event per host session: an O_EXCL marker under `~/.caveman/runtime/telemetry-sessions/` is created before the spawn (pruned after a day). Hook entrypoints (`native-hook`, `shrink-hook`, `mem recall-hook`) set `telemetryCommandSent` so no hook process ever holds the host on a command_run POST. An interactive run that sees `DO_NOT_TRACK`/`CAVEMAN_TELEMETRY=0` while config says yes persists an opt-out (GUI-launched hosts never see the shell rc). Every event also carries `account`, `plan`, `install_channel`, `timezone`, `locale` from `telemetryContext()` (cheap config reads, no keychain). `command_run` also carries `tokens_processed`/`tokens_saved` read from the local proxy store as a **delta** against the `telemetryTokens` watermark in config, always with `tokens_basis` (`inferred`, tokens never dollars); the read is best-effort behind a 400ms spawn budget and omits the fields entirely when the store or binary is absent. Events post to the `cli-telemetry` Supabase Edge Function (source + schema in root `supabase/`), which validates them and stores each with the client IP (cleared after 90 days by pg_cron). Product analytics (users, daily actives, retention, churn, tokens saved, command/agent usage) are SQL views in the database's `analytics` schema. Widening the disclosed scope means bumping `TELEMETRY_PROMPT_VERSION` (now 5) — that reprints the line once for stale-version opt-ins, never re-asks, never touches an opt-out.\n\n`providers verify <conn>` → real POST `/api/v1/projects/{id}/providers/{conn}/verify` (no hardcoded status).\n\n`plan` renders the Cave Plan in plain English (one operator voice); `--json` prints the raw response. There is **no** caveman voice / `--engineer` flag — the dual-voice was deliberately removed. Headline is labeled `basis` (\"inferred\"), savings are per-day — never reprojected to monthly. (honesty rule: no-fake-savings)\n\n`learn` is summary-first: real terminals get animated progress, bounded score\nand move cards, then one keyboard action menu. `--plain` restores compact text,\n`--all` restores every sink id/class/practice/suggestion, while `--json` and\n`--md` stay complete. `learn implement [claude|codex]\n[--prompt <focus>]` installs the existing `caveman-learn` safety guide when\nmissing and launches the chosen interactive agent. The guide's per-edit consent,\nload-bearing protection, re-measurement, and inferred-only rules remain binding.\nPresentation contract: [`TERMINAL_UX.md`](TERMINAL_UX.md).\n\n## Conventions\n- Dispatch uses handler tables. Every handler receives its own rebased argv slice;\n  never read process-global argv positionally inside a handler.\n- `flag(\"--name\", fallback)` parses named args from current invocation.\n- Tests use `node --test` (Node built-in runner); run `tsc` first, test spins a real HTTP server\n- Build: `pnpm build` (tsc + shebang); install locally: `scripts/install-local-cli.sh` (macOS/Linux) or `scripts/install-local-cli.ps1` (Windows) at repo root\n\n## Capability promotion rule\n\nA capability may default on only when it is byte-safe, or when protected by the\napplicable path-specific gate: managed gateway uses an eval gate; local wrap\nuses recovery + CCR — **not** an account or entitlement. There is\nno eval gate in local `run`.\nAny PR flipping a default must name the clause and path.\n\nA verb enters porcelain only when its capability is automatic-by-default-safe\ninside `run` and users no longer need to type it. Porcelain stays capped at four\nverbs + agent shortcut + exactly two namespaces. A fifth verb, or a 16th printed\nverb in either namespace, requires a retirement decision. `record` mode is always\npass-through.\n\nCapability config is grouped in `~/.caveman-cloud/config.json` as `think`,\n`remember`, and `execute`. `./.caveman/config.json` may only narrow its allowlisted\nproject-local keys; it cannot change `think.mode`, pixel settings, account state,\nconsent, or entitlement. Resolution is default < proxy YAML < legacy `wrap` <\nglobal groups < project overlay < env. Env parity is knob-specific. Inspect\nper-key source with `caveman tools config get`.\n\n## Gotchas\n- `providers verify` must NOT return a hardcoded status; the test asserts the CLI echoes the server's value (no-placeholder rule)\n- `plan` savings display must stay per-day; never multiply to monthly projection\n- Claude Code treats any `ANTHROPIC_BASE_URL` whose host isn't `api.anthropic.com` as a custom endpoint and withholds first-party capabilities — most visibly the 1M-token context window, whose loss silently shrinks the auto-compact window to the 200k default (#865). Both Claude routing doors (ephemeral `buildWrapEnv` and the persistent native install) therefore set `_CLAUDE_CODE_ASSUME_FIRST_PARTY_BASE_URL=1`, but ONLY when the assertion is verifiably true: local mode AND the proxy's anthropic upstream is `api.anthropic.com` (default, or an explicit `providers.anthropic.base_url` naming that host — checked via a dependency-free scoped YAML scan that reads flow-style blocks as unverifiable). A user-set value is never overridden, managed gateways never assert, and disable removes the var only while it still holds exactly the value we wrote.\n- Non-PAYG coverage includes Claude Pro/Max, Codex ChatGPT, Gemini OAuth, and routed compatible agents. Codex subscription mode keeps provider config ephemeral under `CODEX_HOME`, auto-installs its MCP recovery, and starts `/chatgpt/responses` in compress mode instead of forcing record/pass-through. Plain OpenAI `/responses` and Gemini `generateContent` requests with MCP explicitly disabled have no server-retrieval grammar, so they must remain byte-identical with zero compression accounting; the compression conformance matrix pins these protocol-specific fail-closed cases instead of requiring every profile to emit a CCR marker.\n- Subscription/OAuth wrap sessions (Claude Pro/Max) compress **locally only**, live zone only, and with **no account**: `CAVEMAN_WRAP_ENTITLED` is gone from both doors and from the proxy, and both doors `delete` any inherited copy so a stray export cannot resurrect it. What both doors DO stamp is the recovery path (`CAVEMAN_RECOVERY`), explicitly (`\"mcp\"` or empty, never inherited): `wrap` answers it from the **agent's own** MCP install (an exported `CAVEMAN_RECOVERY=mcp` can't outlive that answer — it would have the proxy elide bytes behind markers this agent has no `caveman_retrieve` tool to expand), `start` from machine-wide MCP install evidence plus an explicit `CAVEMAN_RECOVERY=mcp` counted as the operator's own opt-in, re-stamped so the disclosure line and the proxy can never disagree; the compression disclosure line prints only when recovery holds, and no-MCP says compression is off and names `caveman mcp install <agent>`. The `subscription_compress: off` operator switch stays the operator's. Their measured reduction remains **tokens only**: a seat has no per-token marginal price, so the session footer and synced span never claim saved dollars. The local stats dashboard may show a separately labeled, same-model API equivalent from captured prices; it is hypothetical value, never subscription bill or quota savings. The session-savings line treats `oauth` like `subscription` (OAuth is list-price-eligible on Vertex alone) and qualifies unconditionally when its capped auth-mode window is truncated\n- Published runtime dependencies stay zero. TUI libraries must be bundled,\n  lazy-loaded, and measured; do not move them onto ordinary command startup.\n- `learn` uses bundled Clack only when stdin/stdout/stderr are TTYs. `--plain`,\n  `CAVEMAN_PLAIN=1`, `TERM=dumb`, machine modes, and pipes must never prompt.\n- Remaining terminal UX (status panels + `wrap` picker) lives in the small toolkit\n  at the bottom of `src/index.ts`. Piped/non-interactive paths remain plain; runtime\n  tests assert them.\n- Current official Skills CLI installs global Codex sources at\n  `~/.agents/skills`, despite older/direct Caveman installs using\n  `~/.codex/skills`; third-party post-install discovery must scan both.\n\nSee ../../CLAUDE.md (root)\n","packages/cli/README.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=packages%2Fcli%2FREADME.md -->","packages/middleware/python/README.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=packages%2Fmiddleware%2Fpython%2FREADME.md -->","packages/middleware/typescript/README.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=packages%2Fmiddleware%2Ftypescript%2FREADME.md -->","packages/pi-extension/README.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=packages%2Fpi-extension%2FREADME.md -->","packages/sdk/parity/AGENTS.md":"# packages/sdk/parity — the cross-language SDK conformance contract\n\nOne language-neutral fixture file, run by **both** SDKs. The load-bearing honesty property\nof the SDKs: the wire contract is one thing, expressed twice (`@caveman-ai/sdk` TS +\n`caveman_cloud` Python). A field present in one SDK and absent in the other makes one side's\nassertion fail — so this folder is a release gate, not documentation.\n\n## Layout\n\n- `fixtures.json` — the contract. A `config` (one Cave), the named header sets (`std_headers` / `std_headers_traced` / `std_headers_async_traced` / `otlp_headers`), and an ordered `operations` list. Each operation carries its `input`, a canned `response` (or `transport: \"error\"` to force a byte-safe pass-through), and an `expect` block: `wire` (`method`, `path`, `headers`, and either an exact `body` or a `body_keys` set) plus a `result` (or `result_from: \"response\"`).\n\n## How it's enforced\n\n- TS half: `../typescript/tests/parity.runtime.mjs` (mocks `fetch`).\n- Python half: `../python/tests/test_parity.py` (mocks `urllib.request.urlopen`).\n- Each half has a per-operation handler that performs the **real** SDK call, captures the wire, and normalizes the result to canonical snake-keyed values. Both iterate **every** operation; a missing handler is a failure, never a skip.\n\n## Editing rules\n\n- Add a field/method to one SDK → add the operation (or body key) here → the other SDK's half goes red until it matches. That red is the point.\n- Headers are compared key-lowercased (Python's `urllib` capitalizes them); the SET + values must match across languages.\n- Keep values that encode identically in both languages (e.g. avoid `! ( ) *` in expand refs — JS `encodeURIComponent` and Python `quote` disagree on those).\n- Nothing random may reach an assertion. Ids the SDK would otherwise mint (trace id, span id) are injected through the operation `input` and pinned in the expected header set — the same way `otlp_export` pins its span ids.\n- Run: `make product-test PRODUCT=sdk-ts` && `make product-test PRODUCT=sdk-python`.\n\nSee ../typescript/CLAUDE.md · ../python/CLAUDE.md\n","packages/sdk/parity/CLAUDE.md":"# packages/sdk/parity — the cross-language SDK conformance contract\n\nOne language-neutral fixture file, run by **both** SDKs. The load-bearing honesty property\nof the SDKs: the wire contract is one thing, expressed twice (`@caveman-ai/sdk` TS +\n`caveman_cloud` Python). A field present in one SDK and absent in the other makes one side's\nassertion fail — so this folder is a release gate, not documentation.\n\n## Layout\n\n- `fixtures.json` — the contract. A `config` (one Cave), the named header sets (`std_headers` / `std_headers_traced` / `std_headers_async_traced` / `otlp_headers`), and an ordered `operations` list. Each operation carries its `input`, a canned `response` (or `transport: \"error\"` to force a byte-safe pass-through), and an `expect` block: `wire` (`method`, `path`, `headers`, and either an exact `body` or a `body_keys` set) plus a `result` (or `result_from: \"response\"`).\n\n## How it's enforced\n\n- TS half: `../typescript/tests/parity.runtime.mjs` (mocks `fetch`).\n- Python half: `../python/tests/test_parity.py` (mocks `urllib.request.urlopen`).\n- Each half has a per-operation handler that performs the **real** SDK call, captures the wire, and normalizes the result to canonical snake-keyed values. Both iterate **every** operation; a missing handler is a failure, never a skip.\n\n## Editing rules\n\n- Add a field/method to one SDK → add the operation (or body key) here → the other SDK's half goes red until it matches. That red is the point.\n- Headers are compared key-lowercased (Python's `urllib` capitalizes them); the SET + values must match across languages.\n- Keep values that encode identically in both languages (e.g. avoid `! ( ) *` in expand refs — JS `encodeURIComponent` and Python `quote` disagree on those).\n- Nothing random may reach an assertion. Ids the SDK would otherwise mint (trace id, span id) are injected through the operation `input` and pinned in the expected header set — the same way `otlp_export` pins its span ids.\n- Run: `make product-test PRODUCT=sdk-ts` && `make product-test PRODUCT=sdk-python`.\n\nSee ../typescript/CLAUDE.md · ../python/CLAUDE.md\n","packages/sdk/python/AGENTS.md":"# packages/sdk/python — Python SDK for the Caveman Cloud gateway\n\nStdlib-only (`urllib.request`, no third-party deps) Python package. Provides `Cave` (config +\nentrypoint), `CaveTool` (tool descriptor), and `ToolSearchResult`. All HTTP calls POST to the\ngateway with `x-cave-agent` / `x-cave-workflow` / `x-cave-retention` headers set from `Cave` fields.\n\n## Layout\n\n- `caveman_cloud/__init__.py` — re-exports `Cave`, `CaveTool`, `CompressResult`, `ContextPackItem`, `ContextPackOptions`, `ContextPackResult`, `ToolSearchResult`, …\n- `caveman_cloud/core.py` — all implementation: `Cave`, `Trace`, `Provider`, `_Create`, `ToolSearchResult`, `CompressResult`, `ContextPack*`, `CaveTool`, `headers()`\n- `tests/test_sdk.py` — pytest tests; mock `urllib.request.urlopen` with `patch()`\n- `tests/test_parity.py` — cross-language conformance suite; drives `../../parity/fixtures.json` (shared with sdk-ts). Same fixtures, two languages → a field in one SDK and not the other fails CI.\n- `tests/test_trace_continuity.py` — trace/span id minting + which requests carry `x-cave-trace-id` / `x-cave-parent-span-id`; mirrors the TS `tests/trace-continuity.runtime.mjs`\n- `caveman_cloud/middleware/` — stable framework-middleware client for protocol 1.1 (`docs/technical/middleware-protocol.md`): `protocol.py` pure rules (scope normalization, tolerant capabilities, failure classification, breaker, budgets, endpoint/proxy resolution, `warn_once`), `transport.py` default keep-alive stdlib transport, `runtime.py` / `async_runtime.py` clients, `validate.py` plan/page checks. `tests/test_middleware_v1_1.py` drives `../../parity/middleware-v1_1.fixtures.json`; `tests/test_middleware_transport.py` uses real local sockets\n- `pyproject.toml` — distribution name `caveman-sdk` (import package stays `caveman_cloud`), `requires-python = \">=3.11\"`, no runtime dependencies\n\n## Key API surface (`core.py`)\n\n- `Cave.trace(workflow, tags, *, trace_id=None, span_id=None)` → context manager yielding `Trace`; call `.model[\"openai\"].responses.create(body)` inside. The trace mints `trace_id` (32 lowercase hex) + a root `span_id` (16 lowercase hex) with the exporter's RNG; `trace_id`/`span_id` continue an inbound trace and a value that isn't the exact hex shape is replaced rather than sent. Every provider call made **through the trace** carries `x-cave-trace-id` + `x-cave-parent-span-id`. Generic `/sdk/v1/*` calls and providers built off the `Cave` carry neither; the sole SDK-endpoint exception is `Trace.tool`, whose `/sdk/v1/events` call carries the trace id and root parent span id\n- `Trace.exporter(service_name=None)` → an `OTelExporter` whose `default_trace_id` is the trace's, so SDK spans and the gateway's request rows join one trace. MIRRORS the TS `CaveTrace.exporter`\n- `Cave.tools(catalog, *, strategy=\"all\", initial_tool_count=8)` → builder handle with `.strategy`, `.initial` (`list[CaveTool]`), `.search(query, *, max_tools, context, workflow, ranker, session_id)`. `strategy=\"deferred\"` includes every `always_load` tool exactly once, then fills remaining initial slots from non-mandatory tools; a cap below the mandatory count fails locally. `.search()` always hits the gateway with the FULL catalog. MIRRORS the TS `cave.tools({catalog, strategy})`\n- `Cave.tool_search(tools, query, *, context, max_tools, workflow, ranker, session_id)` → flat variant: POSTs `[tools, query]` to `/sdk/v1/tool-search`; returns `ToolSearchResult` with `.saved_tokens` / `.reduction_pct` / `.session_id`. Schema-token counters are estimates; `.token_basis` discloses the counter and `.basis` is always `\"inferred\"`. `ranker` (`\"bm25\"`|`\"embeddings\"`) is passed through verbatim — the SDK never computes similarity\n- `Cave.prompts.internal_brevity(*, style, preserve_errors_verbatim=False, preserve_code_verbatim=False)` → output-style snippet (`\"none\"` → `\"\"`); booleans render lowercase to match the TS `cave.prompts.internalBrevity`\n- `Cave.compress(payload, *, content_type=None)` → `CompressResult`; POSTs `/sdk/v1/compress`, maps the Engine report. **Byte-safe pass-through** on any transport/parse problem (original input, `ratio=0.0`, no handle); `.token_count_basis` discloses the counter and `basis` is always `\"inferred\"`. The SDK delegates — it never reimplements a compressor\n- `Cave.context.pack(query, items, options)` → `ContextPackResult`; connected-only POST to `/sdk/v1/context/pack`. Lossy selector over caller-owned items, never CCR/ledger; returns exact `deferred_ids`. Transport or malformed-report failure returns all original items with zero inferred savings\n- `Trace.expand(source_ref)` — the GET half of `checkpoint()`; `GET /sdk/v1/checkpoints/{ref}/expand` returns the stored `{source_ref, version, messages, checkpoint}`\n- `Cave.openai/anthropic/gemini/vertex(upstream_key)` → `Provider` that proxies through gateway; `Provider.raw(path, body)` is the escape hatch (mirrors the TS provider-client `raw`)\n- `Cave.bedrock(region, endpoint=\"runtime\")` → no-network first-party route descriptor; Runtime defaults to `/bedrock`, explicit Mantle returns `/bedrock/anthropic`, and `sdk_only=False` mirrors TS `sdkOnly`\n- `Trace.tool(name, options, fn)` — calls `fn()` then POSTs a `tool.call` event\n- `Trace.page_artifact(value, options)` / `Trace.artifacts.page(value, options)` — send versioned `{value, options, workflow}`; gateway stores only JSON `value`. `artifacts.get(id)` performs authenticated retrieval. `page_artifact` remains backwards-compatible alias.\n- `Trace.model[\"openai\"].responses.create(body, *, latency_class=None, tool_session_id=None)` — when `latency_class` is set, sends the `x-cave-async` header (`\"true\"` unless `\"interactive\"`); when `tool_session_id` is set, sends `x-cave-tool-session`, mirroring the TS `trace.model.openai.responses.create(body, {cave:{latencyClass, toolSessionId}})`\n- `Trace.checkpoint(messages, options)` — POSTs to `/sdk/v1/checkpoints`; the gateway persists it (Valkey) and returns a reversible `source_ref` you can later expand via `GET /sdk/v1/checkpoints/{ref}/expand`\n- `Cave.exporter(service_name=None)` → `OTelExporter`; `record_span(...)` maps current GenAI fields to `gen_ai.*`, `export()` POSTs OTLP/JSON to standard `/v1/traces` (headers via `otlp_headers()`; legacy `/otlp/v1/traces` remains server-only compatibility)\n- `Cave.retry_loop_breaker(threshold=3)` → `RetryLoopBreaker`; `.record(name, args)` raises `RetryLoopError` after `threshold` consecutive identical tool calls (interrupts a stuck loop). `.guard(name, args, fn)` records then runs `fn`\n- `Cave.jobs` → reserved `JobsClient` surface. Every method fails locally with `cave_async_jobs_unavailable`; it performs no network request until durable encrypted request storage, credential custody, and a draining worker exist. MIRRORS the TS `Cave.jobs`\n\n## Conventions\n\n- Tests use `patch(\"urllib.request.urlopen\", side_effect=fake_urlopen)` — never real network\n- Add new gateway endpoints via `Trace._request(path, body)` or `Provider.create(path, body)`\n- `headers()` is the single source for all outgoing headers; edit there, nowhere else\n- Deferred tool-search session handoff uses request/result `session_id` plus provider header `x-cave-tool-session`; update sdk-ts + parity fixtures with any change\n- Run tests: `pytest` from this directory (Python ≥ 3.11 required)\n\n## Gotchas\n\n- **No third-party deps** — do not add `requests`, `httpx`, or any library; keep `dependencies = []` in pyproject.toml\n- **byte-safe**: SDK sends request bodies to the gateway unmodified; no rewriting. `compress()` delegates to the Engine and passes the original through on any problem\n- **context packing is connected-only and intentionally lossy**: it sends item bytes to gateway, never runs in local wrap, and relies on caller retaining every item named by `deferred_ids`. It chooses what enters window; cache-optimal assembly chooses placement\n- `sdk-python` and `sdk-ts` mirror the same field names and `/sdk/v1/*` contract — enforced by the shared parity suite (`tests/test_parity.py` + `../../parity/fixtures.json`), not just convention. A divergence is a CI failure. Change one SDK, change both **and** the fixtures\n\nSee ../../../CLAUDE.md (root)\n","packages/sdk/python/CLAUDE.md":"# packages/sdk/python — Python SDK for the Caveman Cloud gateway\n\nStdlib-only (`urllib.request`, no third-party deps) Python package. Provides `Cave` (config +\nentrypoint), `CaveTool` (tool descriptor), and `ToolSearchResult`. All HTTP calls POST to the\ngateway with `x-cave-agent` / `x-cave-workflow` / `x-cave-retention` headers set from `Cave` fields.\n\n## Layout\n\n- `caveman_cloud/__init__.py` — re-exports `Cave`, `CaveTool`, `CompressResult`, `ContextPackItem`, `ContextPackOptions`, `ContextPackResult`, `ToolSearchResult`, …\n- `caveman_cloud/core.py` — all implementation: `Cave`, `Trace`, `Provider`, `_Create`, `ToolSearchResult`, `CompressResult`, `ContextPack*`, `CaveTool`, `headers()`\n- `tests/test_sdk.py` — pytest tests; mock `urllib.request.urlopen` with `patch()`\n- `tests/test_parity.py` — cross-language conformance suite; drives `../../parity/fixtures.json` (shared with sdk-ts). Same fixtures, two languages → a field in one SDK and not the other fails CI.\n- `tests/test_runtime_policy.py` — runtime-policy client; drives every section of `../../parity/runtime-policy.fixtures.json` (fetch wire, signature cases, the `guard_cases` fail-closed truth table, assignment vectors with exact float equality — including the weighted `exp-w` vectors that pin the `(1-h)*(w/t)` propensity association — and decision cases). Mirrors the TS `tests/runtime-policy.runtime.mjs`\n- `tests/test_trace_continuity.py` — trace/span id minting + which requests carry `x-cave-trace-id` / `x-cave-parent-span-id`; mirrors the TS `tests/trace-continuity.runtime.mjs`\n- `caveman_cloud/middleware/` — stable framework-middleware client for protocol 1.1 (`docs/technical/middleware-protocol.md`): `protocol.py` pure rules (scope normalization, tolerant capabilities, failure classification, breaker, budgets, endpoint/proxy resolution, `warn_once`), `transport.py` default keep-alive stdlib transport, `runtime.py` / `async_runtime.py` clients, `validate.py` plan/page checks. `tests/test_middleware_v1_1.py` drives `../../parity/middleware-v1_1.fixtures.json`; `tests/test_middleware_transport.py` uses real local sockets\n- `pyproject.toml` — distribution name `caveman-sdk` (import package stays `caveman_cloud`), `requires-python = \">=3.11\"`, no runtime dependencies\n\n## Key API surface (`core.py`)\n\n- `Cave.trace(workflow, tags, *, trace_id=None, span_id=None)` → context manager yielding `Trace`; call `.model[\"openai\"].responses.create(body)` inside. The trace mints `trace_id` (32 lowercase hex) + a root `span_id` (16 lowercase hex) with the exporter's RNG; `trace_id`/`span_id` continue an inbound trace and a value that isn't the exact hex shape is replaced rather than sent. Every provider call and trace-scoped `/sdk/v1/*` call made **through the trace** carries `x-cave-trace-id` + `x-cave-parent-span-id`; providers and SDK calls built directly off the `Cave` carry neither\n- `Trace.exporter(service_name=None)` → a per-service memoized `OTelExporter` whose `default_trace_id` is the trace's, so SDK spans and the gateway's request rows join one trace. Runtime-policy decision spans passed a `Trace` use this same caller-reachable default buffer; call `trace.exporter().export()` to ship them. MIRRORS the TS `CaveTrace.exporter`\n- `Cave.tools(catalog, *, strategy=\"all\", initial_tool_count=8)` → builder handle with `.strategy`, `.initial` (`list[CaveTool]`), `.search(query, *, max_tools, context, workflow, ranker, session_id)`. `strategy=\"deferred\"` includes every `always_load` tool exactly once, then fills remaining initial slots from non-mandatory tools; a cap below the mandatory count fails locally. `.search()` always hits the gateway with the FULL catalog. MIRRORS the TS `cave.tools({catalog, strategy})`\n- `Cave.tool_search(tools, query, *, context, max_tools, workflow, ranker, session_id)` → flat variant: POSTs `[tools, query]` to `/sdk/v1/tool-search`; returns `ToolSearchResult` with `.saved_tokens` / `.reduction_pct` / `.session_id`. Schema-token counters are estimates; `.token_basis` discloses the counter and `.basis` is always `\"inferred\"`. `ranker` (`\"bm25\"`|`\"embeddings\"`) is passed through verbatim — the SDK never computes similarity\n- `Cave.prompts.internal_brevity(*, style, preserve_errors_verbatim=False, preserve_code_verbatim=False)` → output-style snippet (`\"none\"` → `\"\"`); booleans render lowercase to match the TS `cave.prompts.internalBrevity`\n- `Cave.compress(payload, *, content_type=None)` → `CompressResult`; POSTs `/sdk/v1/compress`, maps the Engine report. **Byte-safe pass-through** on any transport/parse problem (original input, `ratio=0.0`, no handle); `.token_count_basis` discloses the counter and `basis` is always `\"inferred\"`. The SDK delegates — it never reimplements a compressor\n- `Cave.context.pack(query, items, options)` → `ContextPackResult`; connected-only POST to `/sdk/v1/context/pack`. Lossy selector over caller-owned items, never CCR/ledger; returns exact `deferred_ids`. Transport or malformed-report failure returns all original items with zero inferred savings\n- `Trace.expand(source_ref)` — the GET half of `checkpoint()`; `GET /sdk/v1/checkpoints/{ref}/expand` returns the stored `{source_ref, version, messages, checkpoint}`\n- `Cave.openai/anthropic/gemini/vertex(upstream_key)` → `Provider` that proxies through gateway; `Provider.raw(path, body)` is the escape hatch (mirrors the TS provider-client `raw`)\n- `Cave.bedrock(region, endpoint=\"runtime\")` → no-network first-party route descriptor; Runtime defaults to `/bedrock`, explicit Mantle returns `/bedrock/anthropic`, and `sdk_only=False` mirrors TS `sdkOnly`\n- `Trace.tool(name, options, fn)` — calls `fn()` then POSTs a `tool.call` event\n- `Trace.page_artifact(value, options)` / `Trace.artifacts.page(value, options)` — send versioned `{value, options, workflow}`; gateway stores only JSON `value`. `artifacts.get(id)` performs authenticated retrieval. `page_artifact` remains backwards-compatible alias.\n- `Trace.model[\"openai\"].responses.create(body, *, latency_class=None, tool_session_id=None)` — when `latency_class` is set, sends the `x-cave-async` header (`\"true\"` unless `\"interactive\"`); when `tool_session_id` is set, sends `x-cave-tool-session`, mirroring the TS `trace.model.openai.responses.create(body, {cave:{latencyClass, toolSessionId}})`\n- `Trace.checkpoint(messages, options)` — POSTs to `/sdk/v1/checkpoints`; the gateway persists it (Valkey) and returns a reversible `source_ref` you can later expand via `GET /sdk/v1/checkpoints/{ref}/expand`\n- `Cave.exporter(service_name=None)` → `OTelExporter`; `record_span(...)` maps current GenAI fields to `gen_ai.*`, `export()` POSTs OTLP/JSON to standard `/v1/traces` (headers via `otlp_headers()`; legacy `/otlp/v1/traces` remains server-only compatibility)\n- `Cave.retry_loop_breaker(threshold=3)` → `RetryLoopBreaker`; `.record(name, args)` raises `RetryLoopError` after `threshold` consecutive identical tool calls (interrupts a stuck loop). `.guard(name, args, fn)` records then runs `fn`\n- `Cave.runtime_policy(*, public_key, auto_refresh_seconds, kill_env, workflow)` → `RuntimePolicyClient`. `refresh()` GETs `/sdk/v1/runtime-policy` with a 30s timeout and a 1 MiB response cap (`oversized_response`; last-known-good stays active); the bundle travels as a signed STRING, Ed25519 verified before parsing, pinned or TOFU. `decide(task_family, unit_key, context, trace)` is **synchronous, local-only, never raises** and returns a `PolicyDecision` (`execute`/`fallback`/`baseline` + `reason`); `kill()` latches baseline locally; `state()` → `RuntimePolicyState`. Holdout is carved first onto the fallback path; assignment is the exported `policy_unit_fraction(*keys)` (byte-for-byte port of Go Caveman-Cloud `shared/platform/sampling.Fraction`, mirrors the TS `policyUnitFraction`; the parity `assignment_vectors` are its authority, including the empty-unit-key vector `decide()` refuses to assign on). Decision spans are observability only — no savings/dollar/`verified` field exists here. MIRRORS the TS `cave.runtimePolicy`\n- `Cave.jobs` → reserved `JobsClient` surface. Every method fails locally with `cave_async_jobs_unavailable`; it performs no network request until durable encrypted request storage, credential custody, and a draining worker exist. MIRRORS the TS `Cave.jobs`\n\n## Conventions\n\n- Tests use `patch(\"urllib.request.urlopen\", side_effect=fake_urlopen)` — never real network\n- Add new gateway endpoints via `Trace._request(path, body)` or `Provider.create(path, body)`\n- `headers()` is the single source for all outgoing headers; edit there, nowhere else\n- Deferred tool-search session handoff uses request/result `session_id` plus provider header `x-cave-tool-session`; update sdk-ts + parity fixtures with any change\n- Run tests: `pytest` from this directory (Python ≥ 3.11 required)\n\n## Gotchas\n\n- **No third-party deps** — do not add `requests`, `httpx`, or any library; keep `dependencies = []` in pyproject.toml\n- **byte-safe**: SDK sends request bodies to the gateway unmodified; no rewriting. `compress()` delegates to the Engine and passes the original through on any problem\n- **context packing is connected-only and intentionally lossy**: it sends item bytes to gateway, never runs in local wrap, and relies on caller retaining every item named by `deferred_ids`. It chooses what enters window; cache-optimal assembly chooses placement\n- `sdk-python` and `sdk-ts` mirror the same field names and `/sdk/v1/*` contract — enforced by the shared parity suite (`tests/test_parity.py` + `../../parity/fixtures.json`), not just convention. A divergence is a CI failure. Change one SDK, change both **and** the fixtures\n\nSee ../../../CLAUDE.md (root)\n","packages/sdk/python/README.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=packages%2Fsdk%2Fpython%2FREADME.md -->","packages/sdk/typescript/AGENTS.md":"# packages/sdk/typescript — TypeScript SDK (`@caveman-ai/sdk`)\n\nSingle-file SDK (`src/index.ts`) exported as an ES module. Provides `Cave` (main client),\n`CaveTrace` (per-request tracing), and BM25-backed tool-search against the gateway. No runtime\ndependencies — only `devDependencies` for TypeScript.\n\n## Layout\n\n- `src/index.ts` — entire SDK; exports `Cave`, `CaveTrace`, `CaveOptions`, `CaveTool`, `ToolSearchResult`, `CompressOptions`, `CompressResult`, and the `ContextPack*` types\n- `tests/tool-search.test.ts` — type-level assertions compiled by `tsc --noEmit`\n- `tests/tool-search.runtime.mjs` — runtime tests using `node:test` + global fetch mock (imports from `dist/`)\n- `tests/runtime-policy.runtime.mjs` + `tests/runtime-policy.test.ts` — the runtime-policy client; drives every section of `../../parity/runtime-policy.fixtures.json` (fetch wire, signature cases, all `assignment_vectors` with exact float equality, all `guard_cases` — the shared operator truth table lives in the fixture, not in this file — and all `decision_cases`). Iterate the arrays; never hard-code their counts\n- `tests/parity.runtime.mjs` — cross-language conformance suite; drives `../../parity/fixtures.json` (shared with sdk-python). Same fixtures, two languages → a field in one SDK and not the other fails CI.\n- `src/middleware/` — the stable `@caveman-ai/sdk/middleware` subpath: `runtime.ts` (protocol client), `protocol.ts` (pure protocol 1.1 rules), `validate.ts` (capabilities/plan/page validation). `tests/middleware-v1_1.runtime.mjs` drives every client vector in `../../parity/middleware-v1_1.fixtures.json` (the Python SDK runs the same file); `tests/middleware-hardening.runtime.mjs` pins the B3–B12 hardening fixes\n- `tests/trace-continuity.runtime.mjs` — trace/span id minting + which requests carry `x-cave-trace-id` / `x-cave-parent-span-id`; mirrors the Python `tests/test_trace_continuity.py`\n- `tsconfig.json` / `tsconfig.test.json` — separate configs; test config covers `tests/`. Both extend the **repo-root** `../../../tsconfig.base.json`.\n\n## Key APIs\n\n- `new Cave(options)` — requires `apiKey`, `baseURL`, `agent`\n- `cave.trace(opts, fn)` — wraps a callback with a `CaveTrace`; sends tool-call spans to `/sdk/v1/events`. The trace mints `traceId` (32 lowercase hex) + a root `spanId` (16 lowercase hex) with the exporter's RNG; `opts.traceId`/`opts.spanId` continue an inbound trace and a value that isn't the exact hex shape is replaced rather than sent. Every provider call made **through the trace** carries `x-cave-trace-id` + `x-cave-parent-span-id`. Generic `/sdk/v1/*` calls and provider clients built off the `Cave` carry neither; the sole SDK-endpoint exception is `CaveTrace.tool`, whose `/sdk/v1/events` call carries the trace id and root parent span id\n- `CaveTrace.exporter({serviceName?})` → an `OTelExporter` whose `defaultTraceId` is the trace's, so SDK spans and the gateway's request rows join one trace. MIRRORS the Python `Trace.exporter`\n- `cave.tools({ catalog, strategy })` — returns `{ initial, strategy, search(query, opts?) }`. **`search()` is async** (returns `Promise<ToolSearchResult>`); breaking change from 1.0 which was sync. `opts.ranker` (`\"bm25\"`|`\"embeddings\"`) is passed through to the gateway verbatim; `opts.toolSessionId` sends `session_id` so provider callbacks can re-inject called deferred tools. The SDK never computes similarity\n- `cave.toolSearch(catalog, query, opts?)` — direct variant, same contract (incl. `ranker` / `toolSessionId`). Schema-token counters are estimates; `tokenBasis` discloses the counter and `basis` is always `\"inferred\"`\n- `cave.compress(payload, opts?)` → `Promise<CompressResult>`; POSTs `/sdk/v1/compress`, maps the Engine report. **Byte-safe pass-through** on any transport/parse problem (original input, `ratio:0`, no handle); `tokenCountBasis` discloses the counter and `basis` is always `\"inferred\"`. The SDK delegates — it never reimplements a compressor\n- `cave.context.pack(query, items, options)` → `Promise<ContextPackResult>`; connected-only POST to `/sdk/v1/context/pack`. Lossy selector over caller-owned items, never CCR/ledger; returns exact `deferredIds`. Transport or malformed-report failure returns all original items with zero inferred savings\n- `CaveTrace.context.expand(sourceRef)` — the GET half of `checkpoint()`; `GET /sdk/v1/checkpoints/{ref}/expand` returns the stored `{source_ref, version, messages, checkpoint}`\n- `cave.openai/anthropic/gemini/vertex()` — thin provider clients; proxied through gateway; each exposes a `.raw` fetch escape hatch (mirrors the Python `Provider.raw`). `cave.bedrock({region, endpoint?})` is a no-network first-party route descriptor: Runtime defaults to `/bedrock`; explicit Mantle returns `/bedrock/anthropic`; `sdkOnly:false` mirrors Python's `sdk_only`\n- `cave.prompts.internalBrevity({style, preserveErrorsVerbatim?, preserveCodeVerbatim?})` — output-style snippet (`style:\"none\"` → `\"\"`); MIRRORS the Python `cave.prompts.internal_brevity`\n- `CaveTrace.model.openai.responses.create(body, {cave:{latencyClass, toolSessionId}})` — passing a `latencyClass` hint sets the `x-cave-async` header (`\"true\"` unless `\"interactive\"`); passing `toolSessionId` sets `x-cave-tool-session`. Mirrored by the Python `responses.create(body, latency_class=..., tool_session_id=...)`\n- `CaveTrace.artifacts.page()` — sends versioned `{value, options, workflow}`; gateway stores only JSON `value`. `artifacts.get(id)` performs authenticated retrieval. `strategy:\"verbatim\"` bypasses storage. Mirrored by Python.\n- `CaveTrace.context.checkpoint()` — POSTs to `/sdk/v1/checkpoints`; gateway persists it (Valkey) + returns a reversible `source_ref` (expand via `GET /sdk/v1/checkpoints/{ref}/expand`)\n- `cave.exporter({serviceName?})` → `OTelExporter`; `recordSpan(...)` maps current GenAI fields to `gen_ai.*`, `export()` POSTs OTLP/JSON to standard `/v1/traces` (headers via `otlpHeaders()`; legacy `/otlp/v1/traces` remains server-only compatibility)\n- `cave.runtimePolicy({publicKey?, autoRefreshSeconds?, killEnv?})` → `RuntimePolicyClient`. `refresh()` is the only network call (`GET /sdk/v1/runtime-policy`, std headers minus content-type); it Ed25519-verifies the bundle **string's** exact bytes before parsing, TOFU-pins the key **the moment the signature verifies** (before the schema/sequence checks, so a rejected-but-signed bundle cannot open a downgrade window), rejects a regressed `sequence`, and keeps last-known-good on any failure. The fetch carries a 30s `AbortSignal.timeout` (mirrors Python's `timeout=30`); an `autoRefreshSeconds` tick that lands mid-refresh is skipped, not stacked. `decide(taskFamily, {unitKey, context, trace})` is **synchronous, local-only, and never throws**; holdout suppresses onto the fallback path and a missing unit key or invalid experiment never guesses an arm. `kill()` latches locally, `killEnv` is re-read per decide, `state()` snapshots. Routing only — no savings vocabulary anywhere. MIRRORS the Python `cave.runtime_policy()`\n- `cave.retryLoopBreaker(threshold=3)` → `RetryLoopBreaker`; `.record(name, args)` throws `RetryLoopError` after `threshold` consecutive identical tool calls; `.guard(name, args, fn)` records then runs `fn`\n- `cave.jobs` → reserved `JobsClient` surface. Every method fails locally with `cave_async_jobs_unavailable`; it performs no network request until durable encrypted request storage, credential custody, and a draining worker exist. MIRRORS the Python `Cave.jobs`\n\n## Conventions\n\n- Tests: type assertions in `.test.ts` (compiled only), runtime in `.runtime.mjs` (run against `dist/`)\n- Build before runtime tests: `pnpm build && pnpm test:node`\n- Request body keys are `snake_case` to the gateway; response mapped to `camelCase` in `ToolSearchResult`\n- `x-cave-workflow` header defaults to `defaultWorkflow ?? \"unlabeled-workflow\"`; never omit it\n- Deferred tool-search session handoff uses request `session_id`, result `sessionId`, and provider header `x-cave-tool-session`; update sdk-python + parity fixtures with any change\n\n## Gotchas\n\n- **byte-safe**: SDK sends request bodies to the gateway verbatim; no rewriting allowed. `compress()` is the one path that yields smaller bytes and it **delegates** to the Engine — on any problem it passes the original through\n- **context packing is connected-only and intentionally lossy**: it sends item bytes to gateway, never runs in local wrap, and relies on caller retaining every item named by `deferredIds`. It chooses what enters window; cache-optimal assembly chooses placement\n- **mirror sdk-python**: every field/method exists in both, enforced by the shared parity suite — a divergence is a CI failure, not a convention slip. Change one SDK, change both **and** the fixtures\n- published as `@caveman-ai/sdk`; the workspace name stays `@caveman-ai/sdk` until the npm redirect plan lands\n- `strategy:\"deferred\"` initial set = `alwaysLoad` tools + up to `initialToolCount` (default 8); never returns the full catalog without a `search()` call\n- `reductionPct` rounds to one decimal; `savedTokens` is derived (`full - sent`), not from the gateway response\n\nSee ../../../CLAUDE.md (root)\n","packages/sdk/typescript/CLAUDE.md":"# packages/sdk/typescript — TypeScript SDK (`@caveman-ai/sdk`)\n\nSingle-file SDK (`src/index.ts`) exported as an ES module. Provides `Cave` (main client),\n`CaveTrace` (per-request tracing), and BM25-backed tool-search against the gateway. No runtime\ndependencies — only `devDependencies` for TypeScript.\n\n## Layout\n\n- `src/index.ts` — entire SDK; exports `Cave`, `CaveTrace`, `CaveOptions`, `CaveTool`, `ToolSearchResult`, `CompressOptions`, `CompressResult`, and the `ContextPack*` types\n- `tests/tool-search.test.ts` — type-level assertions compiled by `tsc --noEmit`\n- `tests/tool-search.runtime.mjs` — runtime tests using `node:test` + global fetch mock (imports from `dist/`)\n- `tests/runtime-policy.runtime.mjs` + `tests/runtime-policy.test.ts` — the runtime-policy client; drives every section of `../../parity/runtime-policy.fixtures.json` (fetch wire, signature cases, all `assignment_vectors` with exact float equality, all `guard_cases` — the shared operator truth table lives in the fixture, not in this file — and all `decision_cases`). Iterate the arrays; never hard-code their counts\n- `tests/parity.runtime.mjs` — cross-language conformance suite; drives `../../parity/fixtures.json` (shared with sdk-python). Same fixtures, two languages → a field in one SDK and not the other fails CI.\n- `src/middleware/` — the stable `@caveman-ai/sdk/middleware` subpath: `runtime.ts` (protocol client), `protocol.ts` (pure protocol 1.1 rules), `validate.ts` (capabilities/plan/page validation), `transport.ts` (lazy `node:http` transport used when HTTP(S)_PROXY applies, `ca` is set or Node runs with NODE_USE_ENV_PROXY; never proxies loopback). `tests/middleware-v1_1.runtime.mjs` drives every client vector in `../../parity/middleware-v1_1.fixtures.json` (the Python SDK runs the same file); `tests/middleware-hardening.runtime.mjs` pins the B3–B12 hardening fixes\n- `tests/trace-continuity.runtime.mjs` — trace/span id minting + which requests carry `x-cave-trace-id` / `x-cave-parent-span-id`; mirrors the Python `tests/test_trace_continuity.py`\n- `tsconfig.json` / `tsconfig.test.json` — separate configs; test config covers `tests/`. Both extend the **repo-root** `../../../tsconfig.base.json`.\n\n## Key APIs\n\n- `new Cave(options)` — requires `apiKey`, `baseURL`, `agent`\n- `cave.trace(opts, fn)` — wraps a callback with a `CaveTrace`; sends tool-call spans to `/sdk/v1/events`. The trace mints `traceId` (32 lowercase hex) + a root `spanId` (16 lowercase hex) with the exporter's RNG; `opts.traceId`/`opts.spanId` continue an inbound trace and a value that isn't the exact hex shape is replaced rather than sent. Every provider call and trace-scoped `/sdk/v1/*` call made **through the trace** carries `x-cave-trace-id` + `x-cave-parent-span-id`; provider clients and SDK calls built directly off the `Cave` carry neither\n- `CaveTrace.exporter({serviceName?})` → a per-service memoized `OTelExporter` whose `defaultTraceId` is the trace's, so SDK spans and the gateway's request rows join one trace. Runtime-policy decision spans passed a `CaveTrace` use this same caller-reachable default buffer; call `trace.exporter().flush()` to ship them. MIRRORS the Python `Trace.exporter`\n- `cave.tools({ catalog, strategy })` — returns `{ initial, strategy, search(query, opts?) }`. **`search()` is async** (returns `Promise<ToolSearchResult>`); breaking change from 1.0 which was sync. `opts.ranker` (`\"bm25\"`|`\"embeddings\"`) is passed through to the gateway verbatim; `opts.toolSessionId` sends `session_id` so provider callbacks can re-inject called deferred tools. The SDK never computes similarity\n- `cave.toolSearch(catalog, query, opts?)` — direct variant, same contract (incl. `ranker` / `toolSessionId`). Schema-token counters are estimates; `tokenBasis` discloses the counter and `basis` is always `\"inferred\"`\n- `cave.compress(payload, opts?)` → `Promise<CompressResult>`; POSTs `/sdk/v1/compress`, maps the Engine report. **Byte-safe pass-through** on any transport/parse problem (original input, `ratio:0`, no handle); `tokenCountBasis` discloses the counter and `basis` is always `\"inferred\"`. The SDK delegates — it never reimplements a compressor\n- `cave.context.pack(query, items, options)` → `Promise<ContextPackResult>`; connected-only POST to `/sdk/v1/context/pack`. Lossy selector over caller-owned items, never CCR/ledger; returns exact `deferredIds`. Transport or malformed-report failure returns all original items with zero inferred savings\n- `CaveTrace.context.expand(sourceRef)` — the GET half of `checkpoint()`; `GET /sdk/v1/checkpoints/{ref}/expand` returns the stored `{source_ref, version, messages, checkpoint}`\n- `cave.openai/anthropic/gemini/vertex()` — thin provider clients; proxied through gateway; each exposes a `.raw` fetch escape hatch (mirrors the Python `Provider.raw`). `cave.bedrock({region, endpoint?})` is a no-network first-party route descriptor: Runtime defaults to `/bedrock`; explicit Mantle returns `/bedrock/anthropic`; `sdkOnly:false` mirrors Python's `sdk_only`\n- `cave.prompts.internalBrevity({style, preserveErrorsVerbatim?, preserveCodeVerbatim?})` — output-style snippet (`style:\"none\"` → `\"\"`); MIRRORS the Python `cave.prompts.internal_brevity`\n- `CaveTrace.model.openai.responses.create(body, {cave:{latencyClass, toolSessionId}})` — passing a `latencyClass` hint sets the `x-cave-async` header (`\"true\"` unless `\"interactive\"`); passing `toolSessionId` sets `x-cave-tool-session`. Mirrored by the Python `responses.create(body, latency_class=..., tool_session_id=...)`\n- `CaveTrace.artifacts.page()` — sends versioned `{value, options, workflow}`; gateway stores only JSON `value`. `artifacts.get(id)` performs authenticated retrieval. `strategy:\"verbatim\"` bypasses storage. Mirrored by Python.\n- `CaveTrace.context.checkpoint()` — POSTs to `/sdk/v1/checkpoints`; gateway persists it (Valkey) + returns a reversible `source_ref` (expand via `GET /sdk/v1/checkpoints/{ref}/expand`)\n- `cave.exporter({serviceName?})` → `OTelExporter`; `recordSpan(...)` maps current GenAI fields to `gen_ai.*`, `export()` POSTs OTLP/JSON to standard `/v1/traces` (headers via `otlpHeaders()`; legacy `/otlp/v1/traces` remains server-only compatibility)\n- `cave.runtimePolicy({publicKey?, autoRefreshSeconds?, killEnv?})` → `RuntimePolicyClient`. `refresh()` is the only network call (`GET /sdk/v1/runtime-policy`, std headers minus content-type); it Ed25519-verifies the bundle **string's** exact bytes before parsing, TOFU-pins the key **the moment the signature verifies** (before the schema/sequence checks, so a rejected-but-signed bundle cannot open a downgrade window), rejects a regressed `sequence`, and keeps last-known-good on any failure. The fetch carries a 30s `AbortSignal.timeout` (mirrors Python's `timeout=30`) and stops a response after 1 MiB (`oversized_response`); fetch implementations without a bounded readable body fail closed (`bounded_response_unavailable`) instead of calling unbounded `text()`. An `autoRefreshSeconds` tick that lands mid-refresh is skipped, not stacked. `decide(taskFamily, {unitKey, context, trace})` is **synchronous, local-only, and never throws**; holdout suppresses onto the fallback path and a missing unit key or invalid experiment never guesses an arm. `kill()` latches locally, `killEnv` is re-read per decide, `state()` snapshots. Routing only — no savings vocabulary anywhere. MIRRORS the Python `cave.runtime_policy()`\n- `cave.retryLoopBreaker(threshold=3)` → `RetryLoopBreaker`; `.record(name, args)` throws `RetryLoopError` after `threshold` consecutive identical tool calls; `.guard(name, args, fn)` records then runs `fn`\n- `cave.jobs` → reserved `JobsClient` surface. Every method fails locally with `cave_async_jobs_unavailable`; it performs no network request until durable encrypted request storage, credential custody, and a draining worker exist. MIRRORS the Python `Cave.jobs`\n\n## Conventions\n\n- Tests: type assertions in `.test.ts` (compiled only), runtime in `.runtime.mjs` (run against `dist/`)\n- Build before runtime tests: `pnpm build && pnpm test:node`\n- Request body keys are `snake_case` to the gateway; response mapped to `camelCase` in `ToolSearchResult`\n- `x-cave-workflow` header defaults to `defaultWorkflow ?? \"unlabeled-workflow\"`; never omit it\n- Deferred tool-search session handoff uses request `session_id`, result `sessionId`, and provider header `x-cave-tool-session`; update sdk-python + parity fixtures with any change\n\n## Gotchas\n\n- **byte-safe**: SDK sends request bodies to the gateway verbatim; no rewriting allowed. `compress()` is the one path that yields smaller bytes and it **delegates** to the Engine — on any problem it passes the original through\n- **context packing is connected-only and intentionally lossy**: it sends item bytes to gateway, never runs in local wrap, and relies on caller retaining every item named by `deferredIds`. It chooses what enters window; cache-optimal assembly chooses placement\n- **mirror sdk-python**: every field/method exists in both, enforced by the shared parity suite — a divergence is a CI failure, not a convention slip. Change one SDK, change both **and** the fixtures\n- published as `@caveman-ai/sdk`; the workspace name stays `@caveman-ai/sdk` until the npm redirect plan lands\n- `strategy:\"deferred\"` initial set = `alwaysLoad` tools + up to `initialToolCount` (default 8); never returns the full catalog without a `search()` call\n- `reductionPct` rounds to one decimal; `savedTokens` is derived (`full - sent`), not from the gateway response\n\nSee ../../../CLAUDE.md (root)\n","packages/sdk/typescript/README.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=packages%2Fsdk%2Ftypescript%2FREADME.md -->","packages/shared/contracts/AGENTS.md":"# packages/shared/contracts — shared wire contracts (JSON Schema)\n\nSource-of-truth schemas for cross-service/SDK wire shapes:\n`schemas/policy.schema.json` covers tenant policy and\n`schemas/practice.schema.json` covers practice-library records.\n\n## Layout\n\n- `schemas/policy.schema.json` — JSON Schema 2020-12 for the tenant policy object (required: version, runtime_mode, fail_policy, providers, limits, retention, optimizers, telemetry, sdk)\n- `schemas/practice.schema.json` — strict JSON Schema 2020-12 for one practice record; unknown fields and enums fail closed\n- `schemas/adapter-conformance.schema.json` — static Pi/Claude contract-record\n  shape; `evidence: static_contract_only` explicitly denies executable proof\n- `schemas/middleware-*.schema.json` + `openapi/middleware.openapi.json` — the\n  framework middleware protocol. Normative prose (negotiation, status/error and\n  reason catalogs, N-1 policy) is `docs/technical/middleware-protocol.md`;\n  executable vectors are `packages/sdk/parity/middleware-v1_1.fixtures.json`.\n  Additive changes keep `schema_version` 1; unknown `limits` keys must stay\n  positive safe integers because SDK 1.1.0 rejects anything else\n- `scripts/validate-schemas.mjs` — compiles every schema with AJV, validates\n  adapter fixtures, middleware fixture examples, `$id` = raw GitHub URL of the\n  file at tag `contracts-v<package.json version>` (bump both together),\n  relative cross-schema `$ref`s only, and every OpenAPI `$ref`\n- `package.json` — `@caveman-ai/contracts`; build/lint/test run both validators (no compiled output)\n\n## Key schema fields\n\n- `capabilities` — `{ compress?, cache_hints?, routing? }` booleans, `additionalProperties: false`; the authoritative description of what a project optimizes. An unordered SET, not a stage — the three compose\n- `pass_through` — boolean; forbids every transform. Outranks `capabilities`, which are retained so operator toggles keep their remembered positions\n- `runtime_mode` — `record | recommend | shadow | canary | active | compress`; **a DERIVED display string, not an input**. Still `required` for the compat window (removal 2026-10-01) because `projects.runtime_mode` and the telemetry write validator read it. A doc carrying `capabilities` is authoritative and its `runtime_mode` derives from them; a legacy doc carrying only `runtime_mode` derives the capability set\n- `fail_policy` — `fail_open | fail_closed`; unknown values fail closed (honesty rule)\n- `limits.monthly_usd` — `{ soft, hard }` spend caps per tenant\n- `telemetry.sample_rate` — float 0–1, validated by schema minimum/maximum\n- `additionalProperties: true` — intentional; services and optimizers may extend the object\n\n## Conventions\n\n- Schema version is an integer; `policy_schema_version` returned by `cloud/control-api/internal/httpapi/server.go:372`\n- Adding a new required field is a **breaking change** — bump `version` and coordinate across control-api, SDKs, web\n- No TypeScript types generated from schema yet; the web dashboard manually mirrors the shape\n\n## Gotchas\n\n- `fail_policy: fail_closed` aligns with repo honesty rule: unknown enum cases must fail closed, not pass through\n- `runtime_mode: record` is always pass-through (byte-safe rule); never treat it as an optimizer mode\n- The six `runtime_mode` values are **not ordered**. Code comparing them for rank (`mode >= \"canary\"`) is a bug; the four middle values are behaviourally identical, and `shadow`/`canary` are display labels rather than traffic splits. The derivation lives in exactly one place, `cloud/internal/capability`, because the gateway, control-api, and the worker must never disagree about what a document means\n- Build script validates schemas and static fixtures; it emits no artifact and\n  does not prove executable Pi/Claude parity\n\nSee ../../../CLAUDE.md (root)\n","packages/shared/contracts/CLAUDE.md":"# packages/shared/contracts — shared wire contracts (JSON Schema)\n\nSource-of-truth schemas for cross-service/SDK wire shapes:\n`schemas/policy.schema.json` covers tenant policy and\n`schemas/practice.schema.json` covers practice-library records.\n\n## Layout\n\n- `schemas/policy.schema.json` — JSON Schema 2020-12 for the tenant policy object (required: version, runtime_mode, fail_policy, providers, limits, retention, optimizers, telemetry, sdk)\n- `schemas/practice.schema.json` — strict JSON Schema 2020-12 for one practice record; unknown fields and enums fail closed\n- `schemas/adapter-conformance.schema.json` — static Pi/Claude/Vercel AI SDK/Eve/\n  Mastra contract-record shape; `evidence: static_contract_only` explicitly\n  denies executable proof\n- `schemas/canonical-span.schema.json` — `caveman.span.v1`, the normalized\n  evidence-plane span shared by trace intelligence and replay\n- `schemas/agent-run-receipt.schema.json` — `caveman.agent.run-receipt.v1`,\n  content-blind per-run SDK economic receipt. It is a shared wire shape, not an\n  anonymous CLI event and not a signed verified-savings receipt. Retry events\n  carry reserved spend, measured spend, and measurement basis\n- `schemas/continuous-improvement-report.schema.json` — versioned end-to-end\n  improvement report; the current generated result is E3 `inferred` /\n  `symbolic_counterfactual`. E4 `replayed_counterfactual` remains a reserved\n  actual recorded-output/sandbox shape and never means verified savings\n- `schemas/middleware-*.schema.json` + `openapi/middleware.openapi.json` — the\n  framework middleware protocol. Normative prose (negotiation, status/error and\n  reason catalogs, N-1 policy) is `docs/technical/middleware-protocol.md`;\n  executable vectors are `packages/sdk/parity/middleware-v1_1.fixtures.json`.\n  Additive changes keep `schema_version` 1; unknown `limits` keys must stay\n  positive safe integers because SDK 1.1.0 rejects anything else\n- `scripts/validate-schemas.mjs` — compiles every schema with AJV, validates\n  adapter fixtures, middleware fixture examples, `$id` = raw GitHub URL of the\n  file at tag `contracts-v<package.json version>` (bump both together),\n  relative cross-schema `$ref`s only, and every OpenAPI `$ref`\n- `package.json` — `@caveman-ai/contracts`; build/lint/test run both validators (no compiled output)\n\n## Key schema fields\n\n- `capabilities` — `{ compress?, cache_hints?, routing? }` booleans, `additionalProperties: false`; the authoritative description of what a project optimizes. An unordered SET, not a stage — the three compose\n- `pass_through` — boolean; forbids every transform. Outranks `capabilities`, which are retained so operator toggles keep their remembered positions\n- `runtime_mode` — `record | recommend | shadow | canary | active | compress`; **a DERIVED display string, not an input**. Still `required` for the compat window (removal 2026-10-01) because `projects.runtime_mode` and the telemetry write validator read it. A doc carrying `capabilities` is authoritative and its `runtime_mode` derives from them; a legacy doc carrying only `runtime_mode` derives the capability set\n- `fail_policy` — `fail_open | fail_closed`; unknown values fail closed (honesty rule)\n- `limits.monthly_usd` — `{ soft, hard }` spend caps per tenant\n- `telemetry.sample_rate` — float 0–1, validated by schema minimum/maximum\n- `runtime_policies` — OPTIONAL array (additive, nothing new required): the per-task-family runtime policies delivered to SDK callers through the policy overlay and served by the gateway's `GET /sdk/v1/runtime-policy`. Guard ops are a closed set (`eq|ne|gt|gte|lt|lte|in`, AND-ed); `budget` is an advisory cap the caller enforces, never a savings figure; the renderer drops a structurally invalid policy WHOLE rather than stripping the broken guard or experiment, because either strip would widen who the policy applies to\n- `additionalProperties: true` — intentional; services and optimizers may extend the object\n\n## Conventions\n\n- Schema version is an integer; `policy_schema_version` returned by `cloud/control-api/internal/httpapi/server.go:372`\n- Adding a new required field is a **breaking change** — bump `version` and coordinate across control-api, SDKs, web\n- No TypeScript types generated from schema yet; the web dashboard manually mirrors the shape\n- `scripts/check-contract-compat.mjs` **at the REPO ROOT** (`make check-contract-compat`), not this package's `scripts/`, is a bounded 2020-12 compatibility gate: it proves common narrowing constraints (types, enums/consts, bounds, object/array applicators, and supported combinators/refs) and treats changed unknown assertion/applicator keywords or unresolved reference semantics as major-version review items. Metadata annotations remain non-breaking. It is intentionally not a complete JSON Schema subsumption prover; add a focused fixture before relying on a new keyword.\n\n## Gotchas\n\n- `fail_policy: fail_closed` aligns with repo honesty rule: unknown enum cases must fail closed, not pass through\n- `runtime_mode: record` is always pass-through (byte-safe rule); never treat it as an optimizer mode\n- The six `runtime_mode` values are **not ordered**. Code comparing them for rank (`mode >= \"canary\"`) is a bug; the four middle values are behaviourally identical, and `shadow`/`canary` are display labels rather than traffic splits. The derivation lives in exactly one place, `cloud/internal/capability`, because the gateway, control-api, and the worker must never disagree about what a document means\n- Build script validates schemas and static fixtures; it emits no artifact and\n  does not prove executable cross-harness parity\n\nSee ../../../CLAUDE.md (root)\n","packages/shared/contracts/conformance/README.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=packages%2Fshared%2Fcontracts%2Fconformance%2FREADME.md -->","packages/shared/contracts/openapi/middleware.openapi.json":"{\n  \"openapi\": \"3.1.0\",\n  \"jsonSchemaDialect\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"info\": {\n    \"title\": \"Caveman middleware runtime protocol\",\n    \"version\": \"1.1.0\",\n    \"summary\": \"Compression-only runtime used by Caveman framework middleware. Normative prose: docs/technical/middleware-protocol.md.\",\n    \"description\": \"Status codes listed are the protocol 1.1 set, used only when the request carries the http_status_v2 feature in Caveman-Middleware-Features. Without it the runtime answers with the protocol 1.0 (legacy) mapping documented in the prose spec. Clients derive outcomes from error.code, never from the status alone. The referenced JSON Schemas are closed: they describe exactly what a protocol 1.1 server emits and are what the conformance kit checks servers against. Clients MUST NOT validate responses against them; clients ignore unknown response fields so later minor versions can add fields (spec §4, §16). Unknown routes and unlisted methods answer 404 not_found before any quota, queue or body handling.\",\n    \"license\": {\n      \"name\": \"Apache-2.0\",\n      \"identifier\": \"Apache-2.0\"\n    }\n  },\n  \"servers\": [\n    {\n      \"url\": \"http://127.0.0.1:8787\",\n      \"description\": \"Local runtime. A deployment may add a path prefix; clients preserve it.\"\n    }\n  ],\n  \"security\": [\n    {\n      \"bearer\": []\n    },\n    {\n      \"mtls\": []\n    }\n  ],\n  \"paths\": {\n    \"/caveman/v1/middleware/capabilities\": {\n      \"get\": {\n        \"operationId\": \"getCapabilities\",\n        \"summary\": \"Discover transforms, limits, features and protocol range.\",\n        \"description\": \"Without Caveman-Middleware-Features the runtime omits transforms a protocol 1.0 client would reject. Served from memory before any quota or queue, so it never answers 429; 403 is forbidden_origin only, and 503 means the runtime has no usable middleware store.\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/Features\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Client\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Traceparent\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Tracestate\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Capabilities for the authenticated principal.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"../schemas/middleware-capabilities.schema.json\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"$ref\": \"#/components/responses/E401\"\n          },\n          \"403\": {\n            \"$ref\": \"#/components/responses/E403\"\n          },\n          \"503\": {\n            \"$ref\": \"#/components/responses/E503\"\n          }\n        }\n      }\n    },\n    \"/caveman/v1/middleware/optimize\": {\n      \"post\": {\n        \"operationId\": \"optimize\",\n        \"summary\": \"Plan replacements for candidate segments.\",\n        \"description\": \"Every decision, including bypasses (not_smaller, cache_state_unavailable, recovery_unavailable, capacity), is a 200 plan under http_status_v2.\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/Features\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Client\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Traceparent\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Tracestate\"\n          }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"../schemas/middleware-optimize.schema.json\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Replacement plan or bypass decision.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"../schemas/middleware-plan.schema.json\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"$ref\": \"#/components/responses/E400\"\n          },\n          \"401\": {\n            \"$ref\": \"#/components/responses/E401\"\n          },\n          \"403\": {\n            \"$ref\": \"#/components/responses/E403\"\n          },\n          \"408\": {\n            \"$ref\": \"#/components/responses/E408\"\n          },\n          \"409\": {\n            \"$ref\": \"#/components/responses/E409\"\n          },\n          \"410\": {\n            \"$ref\": \"#/components/responses/E410\"\n          },\n          \"413\": {\n            \"$ref\": \"#/components/responses/E413\"\n          },\n          \"429\": {\n            \"$ref\": \"#/components/responses/E429\"\n          },\n          \"503\": {\n            \"$ref\": \"#/components/responses/E503\"\n          },\n          \"504\": {\n            \"$ref\": \"#/components/responses/E504\"\n          }\n        }\n      }\n    },\n    \"/caveman/v1/middleware/retrieve\": {\n      \"post\": {\n        \"operationId\": \"retrieve\",\n        \"summary\": \"Read a page or excerpt of an original by recovery handle.\",\n        \"description\": \"Has its own deadline (limits.retrieve_deadline_ms) and queue (limits.retrieve_queue_depth).\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/Features\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Client\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Traceparent\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Tracestate\"\n          }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"../schemas/middleware-retrieve.schema.json\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"One page of the original.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"../schemas/middleware-page.schema.json\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"$ref\": \"#/components/responses/E400\"\n          },\n          \"401\": {\n            \"$ref\": \"#/components/responses/E401\"\n          },\n          \"403\": {\n            \"$ref\": \"#/components/responses/E403\"\n          },\n          \"404\": {\n            \"$ref\": \"#/components/responses/E404\"\n          },\n          \"408\": {\n            \"$ref\": \"#/components/responses/E408\"\n          },\n          \"410\": {\n            \"$ref\": \"#/components/responses/E410\"\n          },\n          \"413\": {\n            \"$ref\": \"#/components/responses/E413\"\n          },\n          \"429\": {\n            \"$ref\": \"#/components/responses/E429\"\n          },\n          \"503\": {\n            \"$ref\": \"#/components/responses/E503\"\n          },\n          \"504\": {\n            \"$ref\": \"#/components/responses/E504\"\n          }\n        }\n      }\n    },\n    \"/caveman/v1/middleware/receipts\": {\n      \"post\": {\n        \"operationId\": \"recordReceipt\",\n        \"summary\": \"Record a content-free call observation.\",\n        \"description\": \"Aggregated or retention-bounded; never creates rows that outlive retention_seconds. Deduplicated on (logical_call_id, attempt_id, event_kind): the first write wins, and a retry, even with a different body, gets the same 200. A scope revoked by sessions/delete answers 410 deleted.\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/Features\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Client\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Traceparent\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Tracestate\"\n          }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"../schemas/middleware-receipt.schema.json\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Observation accepted.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"../schemas/middleware-receipt-response.schema.json\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"$ref\": \"#/components/responses/E400\"\n          },\n          \"401\": {\n            \"$ref\": \"#/components/responses/E401\"\n          },\n          \"403\": {\n            \"$ref\": \"#/components/responses/E403\"\n          },\n          \"408\": {\n            \"$ref\": \"#/components/responses/E408\"\n          },\n          \"409\": {\n            \"$ref\": \"#/components/responses/E409\"\n          },\n          \"410\": {\n            \"$ref\": \"#/components/responses/E410\"\n          },\n          \"413\": {\n            \"$ref\": \"#/components/responses/E413\"\n          },\n          \"429\": {\n            \"$ref\": \"#/components/responses/E429\"\n          },\n          \"503\": {\n            \"$ref\": \"#/components/responses/E503\"\n          },\n          \"504\": {\n            \"$ref\": \"#/components/responses/E504\"\n          }\n        }\n      }\n    },\n    \"/caveman/v1/middleware/sessions/delete\": {\n      \"post\": {\n        \"operationId\": \"deleteSession\",\n        \"summary\": \"Revoke a scope and delete every original it owns.\",\n        \"description\": \"Idempotent. An unknown scope answers 200 with zero counts. Runs on the retrieve queue and limits.retrieve_deadline_ms. The revocation commits first, so later use of the scope is 410 deleted at once; the scope's content is then deleted in bounded batches. A delete that fails or runs out of time (including a Postgres write conflict) answers 503 runtime_unavailable or 504 deadline, never 200 and never 409; retrying resumes where it stopped, and the retry's counts cover only what it removed.\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/Features\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Client\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Traceparent\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Tracestate\"\n          }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"../schemas/middleware-session-delete.schema.json\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Scope revoked.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"../schemas/middleware-session-delete-response.schema.json\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"$ref\": \"#/components/responses/E400\"\n          },\n          \"401\": {\n            \"$ref\": \"#/components/responses/E401\"\n          },\n          \"403\": {\n            \"$ref\": \"#/components/responses/E403\"\n          },\n          \"408\": {\n            \"$ref\": \"#/components/responses/E408\"\n          },\n          \"413\": {\n            \"$ref\": \"#/components/responses/E413\"\n          },\n          \"429\": {\n            \"$ref\": \"#/components/responses/E429\"\n          },\n          \"503\": {\n            \"$ref\": \"#/components/responses/E503\"\n          },\n          \"504\": {\n            \"$ref\": \"#/components/responses/E504\"\n          }\n        }\n      }\n    }\n  },\n  \"components\": {\n    \"securitySchemes\": {\n      \"bearer\": {\n        \"type\": \"http\",\n        \"scheme\": \"bearer\",\n        \"description\": \"Runtime credential (static token or OIDC JWT). Never a model provider API key.\"\n      },\n      \"mtls\": {\n        \"type\": \"mutualTLS\",\n        \"description\": \"Client certificate; the principal comes from the certificate SAN.\"\n      }\n    },\n    \"parameters\": {\n      \"Features\": {\n        \"name\": \"Caveman-Middleware-Features\",\n        \"in\": \"header\",\n        \"required\": false,\n        \"description\": \"Comma-separated client features (for example `http_status_v2, revision_tolerant`). Absent means a protocol 1.0 client.\",\n        \"schema\": {\n          \"type\": \"string\",\n          \"maxLength\": 1024\n        }\n      },\n      \"Client\": {\n        \"name\": \"Caveman-Middleware-Client\",\n        \"in\": \"header\",\n        \"required\": false,\n        \"description\": \"Space-separated product tokens, for example `caveman-sdk-typescript/1.2.0`. Informational only.\",\n        \"schema\": {\n          \"type\": \"string\",\n          \"maxLength\": 256\n        }\n      },\n      \"Traceparent\": {\n        \"name\": \"traceparent\",\n        \"in\": \"header\",\n        \"required\": false,\n        \"description\": \"W3C Trace Context.\",\n        \"schema\": {\n          \"type\": \"string\"\n        }\n      },\n      \"Tracestate\": {\n        \"name\": \"tracestate\",\n        \"in\": \"header\",\n        \"required\": false,\n        \"description\": \"W3C Trace Context.\",\n        \"schema\": {\n          \"type\": \"string\"\n        }\n      }\n    },\n    \"headers\": {\n      \"RetryAfter\": {\n        \"description\": \"Delay in whole seconds (>= 1).\",\n        \"schema\": {\n          \"type\": \"integer\",\n          \"minimum\": 1\n        }\n      }\n    },\n    \"responses\": {\n      \"E400\": {\n        \"description\": \"invalid_request, unsupported_version, unknown_capability or invalid_range (legacy clients get 413 payload_limit for an out-of-range retrieve limit).\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      },\n      \"E401\": {\n        \"description\": \"unauthorized: missing or unknown runtime credential.\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      },\n      \"E403\": {\n        \"description\": \"forbidden_origin (browser request) or forbidden_namespace (principal may not use scope.namespace).\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      },\n      \"E404\": {\n        \"description\": \"not_found: unknown or foreign recovery handle.\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      },\n      \"E408\": {\n        \"description\": \"request_timeout: the request body did not arrive before the deadline. Legacy clients: 413 payload_limit.\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      },\n      \"E409\": {\n        \"description\": \"epoch_changed (optimize only, protocol 1.0 clients only: the context manifest is not an extension of the stored one, or sequence went backwards) or identity_conflict (retryable: an optimize replayed under the same idempotency_key with a different body, or a concurrent-writer conflict such as a Postgres deadlock or serialization failure on optimize or receipts).\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      },\n      \"E410\": {\n        \"description\": \"deleted (scope revoked by sessions/delete; receipts included) or expired (scope past retention or max retention).\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      },\n      \"E413\": {\n        \"description\": \"payload_limit: the request body exceeds a size limit.\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      },\n      \"E429\": {\n        \"description\": \"capacity (queue saturated) or quota_exceeded (per-principal quota_requests_per_minute). Legacy clients: a queue wait past the deadline is 504 deadline, and quota_exceeded is 503 capacity.\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        },\n        \"headers\": {\n          \"Retry-After\": {\n            \"$ref\": \"#/components/headers/RetryAfter\"\n          }\n        }\n      },\n      \"E503\": {\n        \"description\": \"runtime_unavailable (storage or dependency outage, a failed sessions/delete, or no middleware store mounted). retrieve also uses recovery_unavailable. Legacy clients also get 503 for optimize decisions (not_smaller, cache_state_unavailable, recovery_unavailable, capacity) and for quota_exceeded (as capacity).\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        },\n        \"headers\": {\n          \"Retry-After\": {\n            \"$ref\": \"#/components/headers/RetryAfter\"\n          }\n        }\n      },\n      \"E504\": {\n        \"description\": \"deadline: the runtime could not finish before its deadline.\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n","packages/subagent-tax/README.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=packages%2Fsubagent-tax%2FREADME.md -->","plugins/caveman/.codex-plugin/plugin.json":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=plugins%2Fcaveman%2F.codex-plugin%2Fplugin.json -->","plugins/caveman/skills/cavecrew/SKILL.md":"---\nname: cavecrew\ndescription: >\n  When to delegate to `cavecrew-investigator` (locate code), `cavecrew-builder`\n  (1-2 file edit) or `cavecrew-reviewer` (diff review) instead of working inline\n  or using `Explore`. Their output is compressed, so main context lasts longer.\n---\n\nCavecrew = three subagent presets that answer in ultracave voice (tool runs: one status line in, one out; nothing between routine calls). Same job as Anthropic defaults (`Explore`, edit-style agents, reviewer); difference is the tool-result they return is compressed, so main context shrinks per delegation.\n\n## When to use cavecrew vs alternatives\n\n| Task | Use |\n|---|---|\n| \"Where is X defined / what calls Y / list uses of Z\" | `cavecrew-investigator` |\n| Same but you also want suggestions/architecture commentary | `Explore` (vanilla) |\n| Surgical edit, ≤2 files, scope obvious | `cavecrew-builder` |\n| New feature / 3+ files / cross-cutting refactor | Main thread, or `feature-dev:code-architect` if installed |\n| Review diff, branch, or file for bugs | `cavecrew-reviewer` |\n| Deep code review with rationale + alternatives | `Code Reviewer` (vanilla) if installed, else main thread |\n| One-line answer you already know | Main thread, no subagent |\n\nRule of thumb: **if you'd want the subagent's output in 1/3 the tokens, pick cavecrew. If you'd want prose, pick vanilla.**\n\n## Why this exists (the real win)\n\nSubagent tool results get injected into main context verbatim. A vanilla `Explore` that returns 2k tokens of prose costs 2k tokens of main-context budget every time. The same finding from `cavecrew-investigator` comes back as a compressed `path:line` table instead. Across 20 delegations in one session that's the difference between context exhaustion and finishing the task.\n\n## Output contracts\n\nWhat main thread can rely on per agent:\n\n**`cavecrew-investigator`**\n```\n<Header>:\n- path:line — `symbol` — short note\ntotals: <counts>.\n```\nOr `No match.` Always file-path-first, line-number-attached, backticked symbols. Safe to grep with `path:\\d+`.\n\n**`cavecrew-builder`**\n```\n<path:line-range> — <change ≤10 words>.\nverified: <re-read OK | mismatch @ path:line>.\n```\nOr one of: `too-big.` / `needs-confirm.` / `ambiguous.` / `regressed.` (terminal first token).\n\n**`cavecrew-reviewer`**\n```\npath:line: <emoji> <severity>: <problem>. <fix>.\ntotals: N🔴 N🟡 N🔵 N❓\n```\nOr `No issues.` Findings sorted file → line ascending.\n\n## Chaining patterns\n\n**Locate → fix → verify** (most common):\n1. `cavecrew-investigator` returns site list.\n2. Main thread picks 1-2 sites, hands paths to `cavecrew-builder`.\n3. `cavecrew-reviewer` audits the diff.\n\n**Parallel scout** (when investigation is broad):\nSpawn 2-3 `cavecrew-investigator` calls in one message (different angles: defs vs callers vs tests). Aggregate in main thread.\n\n**Single-shot edit** (when site is already known):\nSkip investigator. Hand exact path:line to `cavecrew-builder` directly.\n\n## What NOT to do\n\n- Don't use `cavecrew-builder` when you don't already know the file. Spawn investigator first or main thread will eat tokens passing context.\n- Don't chain `cavecrew-investigator → cavecrew-builder` for a 5-file refactor. Builder will return `too-big.` and you'll have wasted a turn.\n- Don't ask `cavecrew-reviewer` for \"general feedback\" — it returns findings only, no architecture opinions. Use `Code Reviewer` for that when installed.\n- Don't expect prose. Cavecrew output is structured, sometimes terse to the point of cryptic. If a human will read it directly, paraphrase.\n\n## Auto-clarity (inherited)\n\nSubagents drop caveman → normal English for security warnings, irreversible-action confirmations, and any output where fragment ambiguity could be misread. Resume caveman after.\n","plugins/caveman/skills/caveman-compress/SKILL.md":"---\nname: caveman-compress\ndescription: >\n  Compress a memory file such as CLAUDE.md or a todo list into caveman format\n  to save input tokens, keeping a readable backup. Sends the file's text to the\n  configured model provider. Trigger: /caveman-compress.\n---\n\n# Caveman Compress\n\n## Purpose\n\n> **Data leaves the machine.** Compression sends the file's full text to the model provider that `scripts/compress.py` is configured for: the `claude` CLI, the Anthropic API, or an OpenAI-compatible endpoint. Files whose names look like secrets (`.env`, `credentials`, `secrets`, `.netrc`, …) are refused before they are read. Don't compress a file the provider shouldn't see.\n\nCompress natural language files (CLAUDE.md, todos, preferences) into caveman-speak to reduce input tokens. Compressed version overwrites original. Human-readable backup saved as `<filename>.original.md`, but NOT beside the source file — it lives in an out-of-tree data dir (`$XDG_DATA_HOME/caveman-compress/backups/<parent-dir-name>/`, or `%LOCALAPPDATA%\\caveman-compress\\backups\\<parent-dir-name>\\` on Windows) so skill auto-loaders don't re-ingest it as a live file.\n\n## Trigger\n\n`/caveman-compress <filepath>` or when user asks to compress a memory file.\n\n**Requires an explicit filepath.** If the user pastes raw text instead of naming a file, do NOT improvise an inline compression — ask them for the filepath to compress, or offer to save the pasted text to a file first. Writing your own reasoning/commentary about the request into the compressed output (e.g. \"User passed text directly, not a filepath...\") is the exact failure this guards against — never let that leak into a saved file.\n\n## Process\n\n1. The compression scripts live in `scripts/` (adjacent to this SKILL.md). If the path is not immediately available, search for `scripts/__main__.py` next to this SKILL.md.\n\n2. From the directory containing this SKILL.md, run:\n\npython3 -m scripts <absolute_filepath>\n\n3. The CLI will:\n- detect file type (no tokens)\n- call Claude to compress\n- validate output (no tokens)\n- if errors: cherry-pick fix with Claude (targeted fixes only, no recompression)\n- retry up to 2 times\n- if still failing after 2 retries: report error to user, leave original file untouched\n\n4. Return result to user\n\n## Compression Rules\n\n### Remove\n- Articles: a, an, the\n- Filler: just, really, basically, actually, simply, essentially, generally\n- Pleasantries: \"sure\", \"certainly\", \"of course\", \"happy to\", \"I'd recommend\"\n- Hedging: \"it might be worth\", \"you could consider\", \"it would be good to\"\n- Redundant phrasing: \"in order to\" → \"to\", \"make sure to\" → \"ensure\", \"the reason is because\" → \"because\"\n- Connective fluff: \"however\", \"furthermore\", \"additionally\", \"in addition\"\n\n### Preserve EXACTLY (never modify)\n- Code blocks (fenced ``` and indented)\n- Inline code (`backtick content`)\n- URLs and links (full URLs, markdown links)\n- File paths (`/src/components/...`, `./config.yaml`)\n- Commands (`npm install`, `git commit`, `docker build`)\n- Technical terms (library names, API names, protocols, algorithms)\n- Proper nouns (project names, people, companies)\n- Dates, version numbers, numeric values\n- Environment variables (`$HOME`, `NODE_ENV`)\n\n### Preserve Structure\n- All markdown headings (keep exact heading text, compress body below)\n- Bullet point hierarchy (keep nesting level)\n- Numbered lists (keep numbering)\n- Tables (compress cell text, keep structure)\n- Frontmatter/YAML headers in markdown files\n\n### Compress\n- Use short synonyms: \"big\" not \"extensive\", \"fix\" not \"implement a solution for\", \"use\" not \"utilize\"\n- Fragments OK: \"Run tests before commit\" not \"You should always run tests before committing\"\n- Drop \"you should\", \"make sure to\", \"remember to\" — just state the action\n- Merge redundant bullets that say the same thing differently\n- Keep one example where multiple examples show the same pattern\n\nCRITICAL RULE:\nAnything inside ``` ... ``` must be copied EXACTLY.\nDo not:\n- remove comments\n- remove spacing\n- reorder lines\n- shorten commands\n- simplify anything\n\nInline code (`...`) must be preserved EXACTLY.\nDo not modify anything inside backticks.\n\nIf file contains code blocks:\n- Treat code blocks as read-only regions\n- Only compress text outside them\n- Do not merge sections around code\n\n## Pattern\n\nOriginal:\n> You should always make sure to run the test suite before pushing any changes to the main branch. This is important because it helps catch bugs early and prevents broken builds from being deployed to production.\n\nCompressed:\n> Run tests before push to main. Catch bugs early, prevent broken prod deploys.\n\nOriginal:\n> The application uses a microservices architecture with the following components. The API gateway handles all incoming requests and routes them to the appropriate service. The authentication service is responsible for managing user sessions and JWT tokens.\n\nCompressed:\n> Microservices architecture. API gateway route all requests to services. Auth service manage user sessions + JWT tokens.\n\n## Boundaries\n\n- ONLY compress natural language files (.md, .txt, .typ, .typst, .tex, extensionless)\n- NEVER modify: .py, .js, .ts, .json, .yaml, .yml, .toml, .env, .lock, .css, .html, .xml, .sql, .sh\n- If file has mixed content (prose + code), compress ONLY the prose sections\n- If unsure whether something is code or prose, leave it unchanged\n- Original file is backed up as FILE.original.md before overwriting — in the out-of-tree backup data dir (see Purpose), not beside the source file\n- Never compress FILE.original.md (skip it)\n","plugins/caveman/skills/caveman-stats/SKILL.md":"---\nname: caveman-stats\ndescription: >\n  Show recorded output and cache-read token usage and mode attribution for\n  the current Claude Code session, or locate the host's native usage report.\n  Trigger: /caveman-stats.\n---\n\nIn Claude Code, `src/hooks/caveman-mode-tracker.js` runs `src/hooks/caveman-stats.js` on `/caveman-stats` and supplies its report through `hookSpecificOutput.additionalContext`. Print that report verbatim inside a fenced code block, as the hook instructs. Do not calculate or add numbers yourself.\n\nIn Gemini CLI, direct the user to `/stats model` for current session token usage or `/stats session` for session statistics. Gemini custom commands are prompts; they cannot invoke the built-in command or read its live session metrics. Never read Claude Code transcripts as Gemini usage. In other hosts, use a native usage report if one is available; otherwise say that current session usage is unavailable. The Claude reader and its lifetime history apply only to Claude Code. Savings remain unknown in every host without a measured comparison.\n\nThe report shows recorded output and cache-read tokens, response counts, and mode attribution where available. Savings are unknown: the transcript has no measured comparison without Caveman. Do not infer saved tokens, percentages, dollars, rule overhead, or a net result from output counts or the current mode.\n\n`--all` and `--since 7d` aggregate the latest recorded output count per session. `--share` reports observed usage with savings unknown. Historical `est_saved_*` fields are ignored; their original history rows remain on disk. The statusline shows the active mode without the retired savings badge.\n\nOriginal/current memory-file pairs are reported by their measured byte sizes. Those file-size differences do not establish provider token or billing savings. See `docs/HONEST-NUMBERS.md`.\n","plugins/caveman/skills/caveman/SKILL.md":"---\nname: caveman\ndescription: >\n  Terse caveman voice: answer first, fluff gone, every technical fact kept.\n  Use for /caveman, \"caveman mode\", \"talk like caveman\", \"be brief\", \"less\n  tokens\". Stays on until \"stop caveman\" or \"normal mode\".\n---\n\n# caveman\n\nRespond terse like smart caveman. All technical substance stay. Only fluff die.\n\nCaveman is a voice, not broken grammar. Reader pays per token and reads in a terminal. Every word earns its place. Every fact survives.\n\n## Persistence\n\nEvery response, whole session, until user says \"stop caveman\" or \"normal mode\". Unsure if still on? It is. Confirm the switch-off in one line.\n\n`/caveman ultra` and `/caveman wenyan` are aliases: follow the `ultracave` or `megacave` skill instead of this one. `/caveman status` reports the mode and changes nothing. Relay the hook's `Caveman mode: <mode>` value when present. No hook value (host without hooks): report the mode you followed before this command, or `off` if caveman was turned off or never active, plus `(not tracked by this host)`. Example: `Caveman mode: caveman (not tracked by this host)`. Loading this skill to answer status is not activation. Never infer a mode from the configured default.\n\n## Why\n\n1. Every output token is billed and read. Filler costs twice.\n2. Code, commands, paths, numbers, errors are the payload. One changed character breaks them.\n3. Ceremony is expensive, grammar is cheap. \"Sure, I'd be happy to help\" is ten tokens. \"the\" is one.\n4. A dropped negation costs more than every token saved. Clarity beats compression.\n\n## Rules\n\n### 1. Answer first\n\nAnswer, then reason, then next step. Pattern: `[thing] [action] [reason]. [next step].`\n\nBad: \"Sure! I'd be happy to help. The issue you're experiencing is likely caused by...\"\nGood: \"Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:\"\n\n### 2. Kill ceremony\n\nNo greeting, hedging, pleasantries, recap, or closer. No \"Sure!\", \"Let me\", \"I'll now\", \"Hope this helps\". No just/really/basically/actually/simply.\n\n### 3. Short word\n\n\"fix\" not \"implement a solution for\". Standard acronyms fine (DB, API, HTTP). Invented abbreviations not (cfg, impl, fn): same tokens, harder read. No arrows.\n\n### 4. Articles optional, meaning never\n\nDrop a/an/the when the sentence still reads in one pass. Fragments fine. Never drop not/never/no/only/except. Numbers and units exact.\n\nBad: \"Migration drop column backup first.\"\nGood: \"Back up first. Then run migration: it drops the column.\"\n\n### 5. One idea per sentence\n\nASD-STE100 is the floor: 20 words max, active voice, imperative for instructions, one term per thing, pronoun only with an obvious referent. Compression and clarity conflict? Clarity wins.\n\n### 6. Payload verbatim\n\nCode blocks unchanged. Commands, paths, API names exact. Errors quoted exact, shortest decisive line only. Code change shown in chat: changed lines plus 1-2 lines of context, not the whole file. Whole file only if the user asks, the file is new, or most of it changes. Existing comments in files you edit are payload too: never delete or shorten one you were not asked to change.\n\n### 7. Tool runs: bounded status\n\nNo text between routine calls. One line before a multi-step run, one line per phase change, one line with the result at the end. Otherwise text before a call only to clarify, warn, or disambiguate.\n\n### 8. User's language\n\nCompress the style, not the language. An explicit reply-language instruction wins. Never switch because of quoted text. Technical terms and errors stay verbatim. Particles and case markers are grammar, not filler.\n\n### 9. Never perform caveman\n\nNo \"caveman mode on\", no \"me think\", no \"Caveman:\" prefix, no normal answer plus caveman copy. No decorative tables or emoji. Never add a word to sound caveman. Caveman phrasing not shorter than plain? Use plain.\n\n## When to break the rules\n\nPlain prose, then resume:\n\n1. Security warning.\n2. Irreversible action. Confirm in full sentences first.\n3. Step order a fragment could scramble.\n4. User confused or repeats the question.\n5. Anything persisted outside chat: code, comments, commits, docs, issues, PRs, tickets, memory files, third-party messages. `/caveman-compress` exempt.\n6. Harness asks for a status line or confirmation. Give it. Harness decides *when* you speak, caveman decides *how*.\n7. You ask the user a question or offer options. Full sentences, so the answer comes back right first time.\n\n## Pre-send check\n\n1. First sentence announces what you will do? Delete.\n2. Last sentence recaps or offers help? Delete.\n3. Every not/never/no/only present? Every code span, path, number, error verbatim?\n4. Any sentence with two readings? Make it a full sentence.\n","plugins/caveman/skills/caveman/agents/openai.yaml":"interface:\n  display_name: \"Caveman\"\n  short_description: \"Talk like caveman. Cut filler. Keep technical accuracy.\"\n  icon_small: \"./assets/caveman-small.svg\"\n  icon_large: \"./assets/caveman.svg\"\n  default_prompt: \"Use $caveman to answer briefly, cut filler, and preserve exact technical substance.\"\n","plugins/caveman/skills/megacave/SKILL.md":"---\nname: megacave\ndescription: >\n  Caveman in Classical Chinese: 文言文 register, far fewer characters, technical\n  terms verbatim. Invoke only with /megacave or /caveman wenyan. Stays on until\n  \"stop caveman\" or \"normal mode\".\ndisable-model-invocation: true\n---\n\n# megacave\n\n以文言答。技術之實皆存，唯贅言去之。\n\nMegacave is caveman in Classical Chinese. 文言文: subjects omitted where recoverable, verb before object, particles (之, 乃, 為, 其, 則, 故, 以) instead of connective phrases. Fewer characters on screen. Token savings are small and noisy; never claim them.\n\n## Persistence\n\nEvery response, whole session, until \"stop caveman\" or \"normal mode\". Unsure? Still on. `/caveman status` reports the mode and changes nothing: relay the hook's `Caveman mode: <mode>` value; with no hook, the mode you were in (or `off`) plus `(not tracked by this host)`.\n\n## Floor\n\nNever cut or translate: code, commands, paths, API names, error strings, in their original script. Negation (不, 非, 勿, 未, 毋). Numbers and units, Arabic numerals. One term per thing. A clause ambiguous in 文言 becomes 白話. Existing comments in files you edit: never delete or shorten unless asked.\n\n## Rules\n\n### 1. Classical register\n\n文言, not 白話. Verb before object. Subjects omitted where recoverable.\n\nBad: \"你的組件每次渲染都會重新渲染，因為你每次都創建了新的對象引用。請用 useMemo 包起來。\"\nGood: \"每繪新生對象參照，故重繪；以 `useMemo` 包之則免。\"\n\n### 2. Particles carry structure\n\nOne particle, one relation.\n\nBad: \"因為連接池會重複使用已經打開的連接，所以不需要每次請求都新建連接，這樣就省去了握手的開銷。\"\nGood: \"池蓄已開之連，不逐請而新開，省握手之費。\"\n\n### 3. Each fact once\n\nCode change: changed lines plus context, not the whole file.\n\nGood: \"新參照則重繪。`useMemo` 包之。\"\n\n### 4. Payload in original script\n\nBad: \"以記憶化鉤子包之。\"\nGood: \"以 `useMemo` 包之。\"\n\n### 5. Tool runs\n\nOne 文言 line in, one out. Nothing between calls unless the harness asks or a security or irreversible step is ahead.\n\n### 6. Never perform\n\nNo \"文言模式啟\", no modern answer plus classical copy. 文言 not shorter than 白話 for a sentence? Use 白話.\n\n## When to break the rules\n\n白話 or the user's language, full sentences, then resume: security warning. Irreversible action, confirm first. Any clause with two readings. User confused. Question to the user, with its options. Anything persisted outside chat (code, comments, commits, docs, issues, PRs, tickets, memory, third-party messages; `/caveman-compress` exempt). Harness asks for a status line.\n\n## Pre-send check\n\nIdentifiers verbatim, original script? Negations present? Two readings? 白話. Flavor character? Cut.\n","plugins/caveman/skills/ultracave/SKILL.md":"---\nname: ultracave\ndescription: >\n  Caveman at maximum compression: fragments, one word when one word is enough,\n  each fact once. Invoke only with /ultracave or /caveman ultra. Stays on until\n  \"stop caveman\" or \"normal mode\".\ndisable-model-invocation: true\n---\n\n# ultracave\n\nRespond terse like smart caveman. All technical substance stay. Only fluff die. Then cut again.\n\nUltracave is caveman with the grammar stripped. Payload only.\n\n## Persistence\n\nEvery response, whole session, until \"stop caveman\" or \"normal mode\". Unsure? Still on. `/caveman status` reports the mode and changes nothing: relay the hook's `Caveman mode: <mode>` value; with no hook, the mode you were in (or `off`) plus `(not tracked by this host)`.\n\n## Floor\n\nNever cut: code, commands, paths, API names, error strings (verbatim). not/never/no/only/except. Numbers and units. The user's language. One term per thing. No invented abbreviations, no arrows. Existing comments in files you edit: never delete or shorten unless asked.\n\n## Rules\n\n### 1. Fragments\n\nDrop articles, copulas, connectives when order stays clear.\n\nBad: \"The component re-renders because an inline object prop creates a new reference each render.\"\nGood: \"Inline object prop, new ref, re-render. `useMemo`.\"\n\n### 2. One word when one word is enough\n\nBad: \"Yes, that should work, though you'll want a null check first.\"\nGood: \"Yes. Null check first.\"\n\n### 3. Each fact once\n\nNo restating, no summary after a list. Code change: changed lines plus context, not the whole file.\n\nGood: \"Pool reuses open DB connections. No per-request handshake.\"\n\n### 4. Cut conjunctions only when order survives\n\nBad: \"Migrate table drop column backup first.\"\nGood: \"Back up first. Then migrate: drops column.\"\n\n### 5. Tool runs\n\nOne line in, one line out. Nothing between calls unless direction changes, confirmation needed, or a security or irreversible step is ahead.\n\n### 6. Never perform\n\nNo prefix, no announcement, no mangled verbs for flavor. Fragment not shorter than the sentence? Use the sentence.\n\n## When to break the rules\n\nPlain prose, then resume: security warning. Irreversible action, confirm first. Any fragment with two readings. User confused. Question to the user, with its options. Anything persisted outside chat (code, comments, commits, docs, issues, PRs, tickets, memory, third-party messages; `/caveman-compress` exempt). Harness asks for a status line.\n\n## Pre-send check\n\nTwo readings? Full sentence. Negations present? Payload verbatim? Flavor word? Cut.\n","proxy/AGENTS.md":"# proxy — the byte-safe standalone `caveman` proxy (commercial, binary-distributed)\n\nA base-URL-swap reverse proxy: match → authenticate → inspect → byte-safe transform → upstream\n→ meter. Single-operator, BYOK, **zero cloud dependencies**. It shares its provider adapters with\nthe managed gateway (the managed gateway imports them from here). `caveman start` launches the\n`caveman-proxy` binary.\n\n## Layout\n- `providers/` — the shared, public byte-safe adapter set: `Adapter` interface + `Base` embed + `UsageScanner`/`ParseUsageBytes` (`adapter.go`), and `anthropic`/`openai`/`gemini`/`azureopenai`/`bedrock`/`vertex`/`openaicompat`. `ResolveUpstreamURL` takes `providers.RouteContext` (no control-plane coupling). Anthropic + OpenAI carry both prefixed and bare routes (`/v1/messages`, `/v1/chat/completions`). `vertex` preserves caller OAuth bearer or Express-mode Google API-key credentials for Gemini + Claude on Vertex AI (no signing, no environment-key fallback, no custom usage parser). `MatchRoute` is POST-only; read-only model discovery (`GET /v1/models`, `/v1/models/{id}`) is a separate GET-only allowlist, `Base.MetadataRoutes`, declared by `openai`/`anthropic`/`openaicompat` and forwarded unchanged with no body, no transform and no spend row. Bare `/v1/models` belongs to both `openai` and `anthropic`, so they implement `MetadataRequestMatcher` and choose by wire protocol (`anthropic-version` or `x-api-key` present → anthropic; a Google key matches neither), never by registration order (#1187).\n- `internal/gateway/` — the request lifecycle (`server.go` + `proxy.go`) behind three injected seams: `Authenticator`, `CredentialResolver`, `TelemetrySink`. Ports the managed loop with the fail-open fix. `serve` sets `Config.AsyncRecord`: a row's request token count, sink write (batched) and Cloud observe run after the handler returns, in record order (`recorder.go`). A reader that must see the request it follows calls `Server.Flush` first (session receipts do); close the server before the sink.\n- `internal/config/` — `caveman.yaml` loader + BYOK env-key resolution; unknown mode fails closed to `record`.\n- `internal/store/` — `~/.caveman/caveman.db` SQLite spend store (`modernc.org/sqlite`, cgo-free); implements `TelemetrySink` and `BatchSink`. `RecordBatch` commits request rows with `synchronous=NORMAL` on its own connection (WAL): a power loss or OS crash can drop the last batches, a process crash or clean exit loses none; every other table keeps FULL.\n- `internal/standalone/` — wiring: static `Auth`, BYOK `Creds`, adapter set, and the always-on SSRF-guarded client.\n- `internal/identity/` — who calls the framework middleware routes (legacy token, token map, OIDC/JWT, mTLS) and the reloadable TLS listener config.\n- `cmd/caveman-proxy/` — binary: `serve` (default), `stats`, and content-blind\n  `agent-evidence --session --build --plan`. Evidence query returns only exact\n  provider usage, request hashes, declared context/plan identity, ordered\n  provider-prefix component hashes, actual transform IDs/counts, and CCR handle;\n  basis is always `inferred`, verified dollars always zero.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=proxy` / `make product-test PRODUCT=proxy`.\n- Tests inject a plain `*http.Client` to reach loopback stubs; the binary uses the SSRF-guarded client.\n- New provider/optimizer work goes in `providers/` (shared) — change it once, both proxies get it.\n\n## Gotchas (honesty invariants — correctness, not style)\n\n- **listener lifetime is not session lifetime**: `serve` never exits because a\n  wrapper, native session, heartbeat, or idle timer expires. Legacy\n  `CAVEMAN_NATIVE_IDLE_TIMEOUT` does not arm shutdown. The CLI never restarts a\n  shared listener to change mode/recovery; an incompatible new wrap runs direct.\n  Failed local startup also runs direct. Native session correlation entries may\n  age out without affecting API traffic.\n- **no default generation deadline**: `CAVE_GATEWAY_UPSTREAM_TIMEOUT_MS` defaults\n  to `0` (no total request deadline). A positive value is an explicit operator\n  cap and includes response streaming. Client cancellation still cancels upstream;\n  connection setup, inbound header/upload limits, and idle keep-alive socket\n  cleanup remain bounded separately. The replacement bound for an upstream that\n  connects and then goes silent is the transport's response-header deadline\n  (`CAVE_GATEWAY_RESPONSE_HEADER_TIMEOUT_MS`, default 900000, `0` disables) —\n  headers only, so it never truncates a live stream.\n- **response protocol controls streaming**: SSE/event-stream responses flush\n  headers and chunks even without a JSON `stream` flag. Encoded requests keep\n  `Content-Encoding` and bypass transforms. Interrupted response copies record\n  an error and abort HTTP framing; they never become a clean successful EOF.\n- **replay only when non-delivery is proven**: proxy transport retries are\n  limited to connection-setup failures — `*net.OpError` with `Op` `dial` or\n  `proxyconnect` (a proxy that is down fails as the latter, never as a dial). A failed upload/header read or truncated response\n  does not prove an inference was unprocessed; do not automatically replay it.\n  An explicit transformed-request 4xx still retries once with original bytes —\n  except a 429 that carries `Retry-After` (a rate limit; Anthropic sends its\n  `anthropic-ratelimit-*` headers on every response, so they prove nothing),\n  which is returned as-is; an accepted retry pins the conversation raw\n  (`internal/gateway/raw_pin.go`).\n  See `docs/technical/proxy-reliability.md` at the repository root.\n- **byte-safe**: `record` mode never transforms; on transform error the ORIGINAL bytes are forwarded (HTTP 200, fail-open) — never a 400. The one carve-out: bytes the provider already cached in replaced form are re-sent replaced, because forwarding the original there busts the prefix (see cache safety).\n- **request-wide opt-out**: `x-cave-transforms: caveman.pass-through.v1` suppresses every request transform path — compress, pixel, and provider-native — not only compiled plan routes. Tests cover all three modes.\n- **no-fake-savings**: standalone records `Basis: \"inferred\"` on every row; it never writes `verified` and never re-projects to a monthly figure.\n- **practice join**: local learn sinks carry additive `practice_id`; one\n  fail-closed mapping table owns sink→practice and unknown sinks keep `\"\"`.\n  The historical `subagent_overuse` sink is count-only and deliberately has no\n  practice id: spawn count cannot reactivate the retired\n  `context-exploration-offload` opportunity or prove any spawn unnecessary.\n- **local trial heuristics are not actuation evidence**: a model name never emits\n  the retired `model-right-sizing` id, and provider plus positive cost never\n  emits a cache move because neither proves stable-prefix eligibility. Legacy\n  rows for those identities are hidden at read time. Compression replay reports\n  one trial's local engine `estimated_engine_o200k` before/after shape with zero\n  dollars and low confidence; it is not provider-counted, a rate, an invoice,\n  causal/verified savings, or task-outcome evidence.\n- **Anthropic automatic caching is experimental observation only**:\n  `anthropic-automatic-prompt-cache` is a typed, default-off manual policy\n  experiment and may add only Anthropic's top-level 5-minute marker on direct\n  Messages API requests. Managed traffic additionally requires server-attested\n  official Anthropic origin; custom or provenance-unknown origins lose the flag\n  before the adapter. It is mutually\n  exclusive with the explicit `anthropic-cache-breakpoints` transform and any\n  caller `cache_control`; Bedrock and count-tokens requests stay byte-identical.\n  An applied marker records only its optimizer id plus actual provider usage and\n  cost. It has no practice, recipe, generic mode/candidate activation, ledger\n  tuple, inferred savings, or verified-savings path (cache-only and forged IDs\n  are excluded from the counted-baseline method too); evaluate it by manual\n  paired observation because shared provider cache state can contaminate an A/B.\n- **SSRF always on**: `standalone.StandaloneHTTPClient` guards every upstream dial (not gated on `CAVE_ENV`) using `ssrf.SelfHostedConfig` — NOT ManagedConfig, which ignores the allowlist and would make the escape hatch a silent no-op. `CAVE_SSRF_ALLOWLIST` opts loopback/private hosts back in (local model servers like Ollama; `localhost` as an entry covers 127.0.0.0/8 + ::1); metadata/link-local stay blocked in every mode. Provider traffic honours `HTTPS_PROXY`/`NO_PROXY` by default (#1001) — `upstream_proxy: env|off|<url>` / `CAVE_UPSTREAM_PROXY` — via `ssrf.Config.Proxy`, which validates IP-literal/localhost destinations before proxy selection and dials the (operator-configured) proxy address unguarded; `ssrf.NewHTTPClient` itself stays direct unless a caller sets `Proxy`, and a managed-mode Config carrying one is refused loudly (every request fails with `ssrf.ErrProxyInManagedMode`) rather than silently dialing direct. Adapters that pre-flight a resolved endpoint (bedrock, vertex) read the selector off the request context (`providers.WithUpstreamProxy`, published by `gateway.Handler` from the upstream transport) and use `ssrf.ValidateURLNoResolve` for proxied destinations: a proxy-only network has no outbound DNS, so resolving there failed the request before the proxy was ever consulted. `ca_bundle` / `CAVE_CA_BUNDLE` plus inherited `SSL_CERT_FILE`/`REQUESTS_CA_BUNDLE`/`NODE_EXTRA_CA_CERTS` append private roots for TLS inspection through `shared/platform/cabundle` (fail-closed parser shared with chhttp).\n- **inbound token is consumed, never forwarded**: `CAVEMAN_AUTH_TOKEN` gates non-loopback listens (a middleware token map, OIDC issuer or TLS client CA also makes one legal; provider routes still accept only the token, and without it `Auth.closed` refuses every provider request off loopback); `standalone.Auth` deletes the matching `x-cave-api-key`/`Authorization` header before `Creds.Resolve` so the shared token can never be forwarded to a provider or classified as a provider credential; a bearer that is not the token survives untouched (Claude OAuth, `/chatgpt/`). `Authenticate` runs BEFORE `matchAdapter` so the 401/404 split cannot be used to enumerate routes, and every 401 goes through `Server.rejectUnauthorized`, which counts `cave_proxy_unauthorized_total` and warns with the path and remote host only. Health and the no-op `/caveman/keepalive` beacon stay unauthenticated, `/metrics` too unless `CAVEMAN_METRICS_TOKEN` is set; the `X-Caveman-Instance` header on `/health/live` is published only on a loopback bind. A token on a LOOPBACK listener is honored too (the local `caveman wrap` path sends none, so `runServe` warns). Bedrock credentials then come from `shared/platform/awscreds` (env → web identity → container → IMDSv2), never from the request; a PARTIAL env pair fails closed instead of falling through to an ambient role, and the winning source is logged once.\n- **middleware identity is server-side and per route**: `middleware.Config.Identify` resolves an `identity.Principal` from the credential alone (bearer decides when present, else a verified client certificate); `ServeHTTP`'s `decode` checks `Principal.Allows(scope.Namespace)` on EVERY scoped route, so a new route that skips `decode` skips authorization. The legacy token stays `single_operator` with every namespace; JWT and certificate principals may never be named `single_operator`. Token and hash comparisons are constant-time over every entry. A token map or TLS file that fails to reload keeps the previous one. Tests: `identity/*_test.go` (negative JWT and mTLS cases), `middleware/identity_test.go`.\n- **Postgres middleware store keeps SQLite's single-writer semantics per authority**: `store.PostgresMiddleware` serializes writers of one authority with a transaction-scoped advisory lock taken by `Scope` (then re-read), `SaveScope`, `Revoke`, `PurgeBatch`, `Receipt` and `SaveOriginal`; the sweep is one-replica (try-lock) and skips busy authorities and row-locked scopes. Counters are statement-level transition-table triggers striped by txid; a per-row trigger goes quadratic on bulk statements. Lock keys include `current_schema()`. Run the store suite against both backends with `CAVEMAN_TEST_POSTGRES_URL`; `middleware_concurrency_test.go` fails without the locks.\n- **Auth scheme is preserved**: a key from an inbound `Authorization: Bearer` keeps `Scheme:\"bearer\"` on the `providers.Credential`; Anthropic and Gemini forward bearer credentials as bearer credentials (Claude/Gemini OAuth breaks if remapped to an API-key header). BYOK env keys and inbound `x-api-key` keep provider API-key mapping.\n  On a named compat mount the header follows the wire protocol of the path:\n  `/compat/<name>/v1/messages` gets `x-api-key` plus a default\n  `anthropic-version`, every other path gets Bearer. A real inbound Bearer stays\n  Bearer on every path. OpenCode Go rejects Bearer\n  on `/v1/messages` (401 `Missing API key.`, 2026-09-03). The gateway env\n  fallback puts the key in the header that the adapter emitted, so the upstream\n  never gets two credentials.\n- **fail-closed**: unknown route → 404; unknown mode → `record`.\n- **subscription AND oauth compression is NOT account-gated**: non-PAYG sessions from Claude Code, Codex ChatGPT, Gemini CLI, and other routed clients take live-zone compression with no Caveman account, entitlement, or seat. `CAVEMAN_WRAP_ENTITLED` and every `WrapEntitled` field are **deleted**, not merely ignored — do not reintroduce them. Exactly four conditions remain, all technical and all fail-closed (`liveZoneCompressionAllowed`): the operator `subscription_compress` switch (empty/`live_zone` allow, `off` and any unknown value close it), the adapter must implement schema-aware `PrefixStabilizer` zones, recovery must run through the agent's own MCP `caveman_retrieve`, and a durable prefix cache must be wired. The dedicated Codex `/chatgpt/responses` route uses the OpenAI Responses stabilizer while preserving OAuth and `ChatGPT-Account-ID` headers; recovery is proven by the `CAVEMAN_RECOVERY=mcp` stamp or the decoded request's own `mcp__caveman__caveman_retrieve` (top-level `tools` or Codex's `input[]` `additional_tools`, flat or under an MCP namespace); zstd bodies decode → transform → re-encode, anything else forwards the original wire bytes, and the `chatgpt_proxy` log carries `skip_reason`; transformed 4xx responses retry once with exact original bytes (never a rate-limit 429; an accepted retry pins the conversation raw). Legacy savings fields remain **tokens-only** — `compression_tokens_before/after` + `estimated_engine_o200k`, never booked compression dollars. New request-comparison and price-snapshot fields support separately labeled API equivalents in local stats; those values never enter subscription spend or legacy saved-dollar fields. LOCAL wrap only; managed gateway non-PAYG behavior is unchanged.\n- **cache safety is byte-stable replacement**: THE INVARIANT (#1105): for accepted requests P then R of one conversation, if R's client bytes over P's cached prefix (system, tools and messages through P's last `cache_control` marker; the whole prompt for implicit caches) equal P's, R's forwarded bytes over that range equal P's, and every request caches at least what its client asked for (a dropped breakpoint is a bust too). Two exceptions, both re-caching the range raw: a request the provider rejected transformed and accepted through the raw retry, and a compressed PAYG conversation without MCP recovery that streams (the server-side retrieve tool cannot ride a stream, so the conversation stays raw from there). A raw retry splits a range into a raw and a replaced form, and a later request follows the LONGEST lineage it extends, ties going raw: it is held to the raw request (raw pin) unless it repeats a longer prefix that went out replaced, like a sibling subagent started with the same task. Pins are taken only on a conversation's own bytes (past system and tools); pins and lineages live in the PrefixCache (`rawpin` and `lineage` scopes, pin rows never evicted) and in memory. `internal/gateway/cache_prefix_invariant_test.go` (Anthropic compress, PAYG, pixel and Codex Responses scenarios + `FuzzCachePrefixInvariant`) proves it; keep it green. A compressed live-zone turn becomes prefix on the next request, so the replacement is a pure function of segment content (deterministic compressor + content-hash CCR marker), memoised in the durable replacement cache, and the FIRST decision for a block wins — replaced or raw (`RawDecisionHandle`); a request whose write finds a row on record sends that row, not its own candidate. A frozen block with no decision (a `--resume` history, an evicted row) goes out raw and raw becomes its decision; a failed memo write keeps the block raw for the process (`rawMemory`, consulted only on a store miss and written through once the store takes it); new rows are remembered only once the request's splice holds. Raw decisions are global and durable, a known savings cost: a block declined once (frozen at first sight, a compressor decline, a CCR failure, a non-newest parallel tool output) stays raw in every conversation. Bounding them needs a TTL of at least the longest provider retention (24 h for OpenAI extended caching) plus time-aware store interfaces, or per-conversation rows keyed on a stable conversation identity with a scope rollover. Substitution is never gated: the derived epoch gate is gone, and an explicit framework epoch may veto only NEW compression. Pixel renders and the tool-schema strip are memoised the same way. New compression stays live-zone-only. The runtime tripwire (`prefix_monitor.go`, on the provider routes and `/chatgpt`) checks the same rule on live traffic and records `requests.cache_bust_cause`: `client` (DEBUG); `raw_retry`, `stream_switch`, and `lever_freeze` (the harm tripwire's one-time strip rollover), all WARN; `caveman` (ERROR `caveman changed bytes the provider already cached` or `caveman dropped a cache breakpoint the client set`, counted as `caveman_cache_bust_requests` and raised by `caveman status`) — any `caveman` row is a bug. A request is held only to anchors accepted before its own forwarding decision. Cross-turn stability covers **anthropic, openai, azureopenai/openaicompat, and gemini** through `ExtractStabilizable`; `bedrock` and `vertex` expose no compressible blocks and stay pass-through. Anthropic uses declared `cache_control`; OpenAI/Gemini use latest-user/latest-tool zones against implicit provider caches. Replacement cache is SQLite spend store and must retain `journal_mode(WAL)` + `busy_timeout`; a hit rewrites `last_used_at` only once the row is 10 minutes stale, and pixel rows have their own cap inside the table's.\n- **cache warming is predicted per stream, and replays only what Anthropic can answer at zero output** (`internal/gateway/cache_warm.go`, `cache_warm_table.go`): after a successful Anthropic Messages response on `api.anthropic.com` (never Bedrock, Vertex, Azure, a compat mount, a custom base URL, the pass-through header or an encoded body), the exact bytes and headers sent upstream (after compression, routing marks, heals and fail-open retries) are kept in memory only (LRU 1024 streams and 256 MiB, never stored, logged or captured) and may be replayed with `max_tokens: 0` and `stream: false` at `min(0.9*ttl, ttl-10s)` steps from the real request's start (wall time, so laptop sleep never sends a stale warm). Not replayable, so never warmed: `thinking.type: \"enabled\"`, `tool_choice` `tool`/`any`, `output_config.format`, no `cache_control`, a `ttl` other than `5m`/`1h`, mixed `5m` and `1h` markers, writes of another lifetime than the markers (one lifetime or no warm; the simulator classes by the same rule and none of the measured requests mixed them); the request scan reads top-level fields and cache_control markers only, no full parse. WHETHER to warm is optimal stopping, not a threshold: `cache_warm_table.json` holds the return-time distribution (30 s bins to 2 h) per stream class `main|subagent / tool|end / 5m|1h` (the class comes from the session key and whether the answer's `stop_reason` was `tool_use`, scanned off the plain bytes with a rolling tail; the proxy forwards the agent's `accept-encoding`, so an answer that comes back with a `Content-Encoding` is not classed: it is planned as whichever of the two classes warms less and its gap teaches neither), measured on 364,048 request transitions of real Claude Code sessions (2026-08-30 to 2026-10-08, `.blocks/cache-warm-sim.py`, aggregate probabilities only) and shrunk toward the parent class; the proxy learns each user's own gaps online (counts only, `$CAVEMAN_HOME/cache-warm-gaps.json`, 0600, written outside the warmer's lock, flushed by `Server.Close` at shutdown, symlink-refused, any implausible count drops the whole file; two proxies on one home are last-writer-wins) and blends them in with the defaults worth 100 observations. A stream evicted or still waiting at shutdown is counted as returning later than it was seen silent (spread over the later bins by the class's own distribution, \"never\" past 2 h): dropping it would keep only the returns and drift the table toward over-warming. At arm time the whole chain is planned by backward induction with the catalog price of the model sent (warm = read x prefix + input x uncached tail; avoided miss = (write_ttl - read) x prefix x the class's measured realized share, taken over the gaps that DID return: the window probability already leaves out the streams that never do), so the cumulative cost of every warm is counted before the first; unknown price, class or lifetime = no warm. Subscription traffic is valued at list prices as plan-usage equivalent. Stops: any real request of the stream (every model; it never waits for a warm in flight), any warm error, a warm reading `cache_read_input_tokens == 0`, the plan, 60 min, or the switch; the held request (body and credential headers) is released the moment its chain ends. A parent session's request does NOT stop its subagents' warms: a subagent can still be running, and on the test half that stop cost $565.52 net (below). A 429 (warm or real) pauses warming on that credential for the response's `Retry-After`, else until its `anthropic-ratelimit-unified-reset`, else 1 h for a subscription/OAuth login and 60 s for an API key; a 529 or 5xx on a warm pauses it for the `Retry-After`, else 5 min. Never sent while a real request of the same stream is in flight (retry every 5 s until its deadline). At most two warms are in flight process-wide (same retry; one with no slot by its deadline is dropped), and each fires up to 5 s before its planned time, never after. Warm rows are tagged `cache-warm`: spend, never a saving, never an agent request (excluded from request counts, session usage, trial origins, exported spans, receipts and `caveman sync` uploads; counted as `cache_warm_requests`), never fed to the harm-tripwire ledger, never sent to Cloud. Switch: on unless `$CAVEMAN_HOME/cloud.json` has `modules[\"waste-fixes\"]: false` (re-read on change); `CAVEMAN_CACHE_WARM=off` kills it for the process. Every answered stream request logs one line `cache warm plan` (info when warms are planned, debug when skipped: session hash, model, class, ttl, planned warms, skip reason: `response_error`, `off`, `not_anthropic_api`, `not_replayable`, `lifetime_mismatch`, `unpriced`, `not_worth_it`). Replay on the owner's data (test half, after 2026-10-03, 161,325 transitions): net $1,896.65 list-price equivalent at $385.31 of warms (9,628 warms, 7,280 wasted) vs $1,294.18 at $1,372.15 (45,343 warms) for the first fixed rule; the same policy with the parent stop for every subagent $1,331.13 at $197.28 (3,461 warms), with it only for subagents whose last answer was not a tool call $1,852.99 at $378.90 (9,518 warms). A test pins the plan to the simulator's on the shipped table (`TestCacheWarmPlanMatchesTheSimulator`; regenerate both together).\n- **pixel mode**: S4 lossy text→PNG (`pxpipe` port). Default allowlist is `claude-fable-5,gpt-5.6` via `CAVE_PIXEL_MODELS`; savings stay inferred-only. Renders keep the compress rules: the first decision for a text (render or text) is re-sent on every turn, a raw pin keeps the conversation text, and a NEW render is remembered only once its request goes out, with the original stored in CCR and the splice holding. If either fails, the new content goes out as text and records that, and earlier renders still go. A rendered block's `cache_control` moves to its last image. An Anthropic request carries at most 20 images, client images included (`anthropicManyImages`): past 20 the API rejects any image over 2000 px, and the renders are 2573 px wide, so a new render past the cap stays text.\n- **route stage is optional and fails open** (`internal/cloudlink`, `internal/gateway/route.go`):\n  it asks Cloud `POST /v1/route` only for requests naming Auto (`AutoModel`, `caveman-auto`, also with Claude Code's `[1m]` suffix, the\n  picker entry the CLI adds while `modules.routing: true` in `$CAVEMAN_HOME/cloud.json`), while the\n  CLI is signed in with that switch on, for Anthropic Messages / OpenAI chat or responses requests on\n  an API key or a subscription alike; any other model goes as sent (`route.outcome: off`), the\n  session's marks and heal still applied. Auto goes upstream as the provider's `autoFallback` model\n  (Anthropic `claude-sonnet-5-5`, OpenAI `gpt-6.1-sol`; count_tokens too, and with no Cloud link),\n  which is also the asked model the ask carries and what a failure runs (a transient Cloud failure, that is a timeout, an unreachable or 5xx Cloud, an unreadable answer or the pause after one, instead runs the model the session's previous request went to, `heldModel`, never for a side request; no previous request on that provider, one that was a compaction or came after one, a held model that then did not serve (any non-2xx: the next attempt runs the fallback), signed out, routing off, an expired or refused login, a limit and a provider's refusal keep the fallback); Auto on any other\n  provider is a 400 (`cave_auto_unavailable`), and an encoded body naming Auto is decoded first. A\n  named model records `route.outcome: off`, reason `named_model`. OpenAI Auto (API key and ChatGPT\n  login) runs only `AutoOpenAIModels` (`gpt-6.1-sol`, `gpt-6-astra`, `gpt-6-luna`): the ask's\n  `Models` lists those alone and the pool drops other OpenAI models, and an answer naming any other\n  runs `gpt-6.1-sol` at its effort (`auto_model_refused`, `translate.FitEffort`); an Auto row's `RouteFrom` and\n  `x-caveman-routed-from` say `caveman-auto`. A move on a subscription stays on the provider's own\n  API on that subscription's credential; pool entries go out on their own login or the Cloud\n  gateway key, never on it, and never with the agent's `oauth-*` betas. On a ChatGPT login\n  (`/chatgpt`, `chatgpt_auto.go`; OpenCode's ChatGPT login lands there too) `/models` gains an Auto\n  entry (a copy of `gpt-6.1-sol`'s, listed last, fetched without `If-None-Match`, served without\n  `ETag`) while the link reports `AutoOffered` (signed in, routing on). Every POST there has its\n  first 64 KiB read (decoded when gzip, deflate or zstd; a br body is never looked into) for a\n  top-level `\"model\":\"caveman-auto\"`; any other streams on unchanged. `/responses` naming Auto asks\n  with `NoPool`, keeps live-zone compression, replays `gpt-6.1-sol`'s bytes on a 4xx (a rate-limit 429, a 401 and a 403 are returned instead; a served replay pins the conversation raw, and the decision is rejected only then or when the replay is itself a 429; a 5xx on a moved model is returned and rejects the decision, a rate-limit 429 keeps it: the plan's limit holds for every model); any other\n  path (Codex compaction) runs `gpt-6.1-sol` unasked; a body over `CAVE_MAX_REQUEST_BYTES` streams\n  with only its model changed. The agent reads the Auto id it sent. Every `/chatgpt` row (Auto or\n  not) is offered to Cloud like any other, as its agent (`x-cave-agent`, else Codex by its\n  `originator` or User-Agent), with model ids in `route_from`/`route_to`; a model it could not read\n  goes to Cloud as none. The\n  ask starts before compression (parse, ask, compress, route), waits at most 800 ms (the CLI's\n  keychain credential is read when the runtime starts, `Link.Start`, never on a request goroutine;\n  a request that lands while a read runs with no secret cached waits for it, leaving the ask at\n  least 500 ms, else goes as `credential_loading` without an ask or a pause), carries the\n  caller's models, counts, what the request declares (the raw values of eleven allowlisted agent\n  headers, never cut: a value over 256 bytes, 16 KiB for Codex's turn metadata, which also comes\n  from the body's `client_metadata`, is left out; a forked Claude Code child's spawn-call\n  `subagent_type` as `x-caveman-agent`; its tool names, effort and thinking type), what the\n  session's previous request ran (the served model, the effort in force, the provider's input and\n  cache token counts, its age) and Cloud's opaque per-session state, plus, on a turn's first ask,\n  the raw text of the latest human turn, the one before it and the end of the agent's last reply\n  (contracts `route-ask-v1`; Cloud picks the model and the effort and no routing logic lives\n  here). Its answer is cached per ask until 30 min after its last use (session, or without one agent and\n  endpoint, provider, model, turn number, latest human text;\n  an input group carrying a tool result is no human turn, text riding along included) so a tool\n  loop never switches model or effort mid-turn (an ask first seen during a pause keeps the paused answer for its turn); a request the agent labels compaction or auxiliary\n  (Claude Code's request class and compaction flag, OpenCode's title/summary/compaction agent,\n  Codex's subagent kind and turn-metadata request kind) is asked on its own, without the text. The\n  session is `x-cave-session`, else the agent's session header (a Codex thread, `thread-id`, is\n  its own, its parent `x-codex-parent-thread-id`); a Claude Code child (`x-claude-code-agent-id`)\n  is a session of its own and sends its parent's state; a session id guessed from timing is not\n  used. State, previous-request facts and marks live in bounded in-memory LRUs (1024), never on\n  disk. The answer sets Responses `reasoning.effort`, chat `reasoning_effort` or Anthropic\n  `output_config.effort`; Anthropic `effort_mode: message` instead inserts the byte-identical mark\n  `{\"role\":\"system\",\"content\":[],\"output_config\":{\"effort\":…}}` (beta\n  `mid-conversation-output-config-2026-07-01` appended to `anthropic-beta`) before the last user\n  turn, or at the end after a tool result; on OpenAI Responses (GPT-6) the mark is a\n  `{\"type\":\"configuration_update\",\"reasoning\":{\"effort\":…}}` input item before the last user\n  message while nothing has answered it yet, never at the end and never next to another one (no\n  place, as mid tool loop: the effort in force holds), no beta, and none at all where OpenAI forbids\n  it (`truncation: \"auto\"`, `context_management`, a non-false `multi_agent`: Cloud's effort goes\n  top-level); a 400 naming a configuration update heals like a refused mark. Effort state made on\n  one wire starts afresh on the other. Only `effort_mode: message` ever starts marks; a \"top\"\n  answer on history carrying the session's marks goes in as one more mark (never wiping them; for\n  a compaction or side request, that request's only), and on any other history sets the top-level\n  field. Marks are remembered per session with a salted hash of the message before each\n  (`cache_control` and thinking blocks left out, so a heal's strip keeps them) and replayed at the\n  same places on every later request to a model that already took them (the agent resends history\n  without them), count_tokens of a known session or a forked child's parent included (it adds\n  nothing to a history without marks). Without an effort from Cloud (a failure, routing off,\n  effort \"\") a history without the session's marks goes as the agent sent it, top-level field\n  included (one cache restart where Cloud's was, nothing inserted, no beta), except that a\n  remembered heal (drop_block with its beta, or a strip) keeps applying. One with marks runs at\n  the request's own top-level effort, or, when it sets none, at the model's default effort\n  (Cloud's `default_effort`, kept per session, a forked child taking its parent's), marked when it\n  differs from the one in force; only with no default known does such a request go as the agent\n  sent it, without the marks, which the session then forgets (its later thinking blocks then lose\n  their binding) unless the request went to another model. The top-level field is fixed at the\n  session's first per-message request: the routed effort on a fresh conversation whose request\n  sets one (kept there, also for later requests that set none), else the request's own; a routed\n  effort for a request that sets none goes in as a mark, first on a fresh conversation. An anchor\n  that no longer matches drops that mark and every later one; compaction and side requests never\n  change them, and a body matching none of them (an unlabeled side request, compacted history)\n  gets marks of its own, kept for up to four request shapes (first message, length, last message),\n  which take the session over only from a longer request continuing that conversation (same first\n  message, its last one where it was); the session's marks and fixed top-level field then wait\n  there in turn. A thinking-binding 400 on the asked model of a `/messages` request retries once\n  only on a session the route stage changed (it was on for the request, the session carries its\n  marks or heal, or another model served one of its requests) whose body sets no\n  `thinking.block_binding`: with `thinking.block_binding.prefix_mismatch_behavior: \"drop_block\"`\n  (beta `thinking-binding-controls-2026-08-01`) when the block is bound to a different\n  conversation and the request's thinking is adaptive or enabled, then sent on every later request\n  (count_tokens and forked children included); else (Sonnet 5.5 `between_tools`, no thinking\n  field, a tampered signature) without thinking blocks from the failing message on, and later\n  requests on the same history (same first message, same message at its end) strip the same range\n  up front (a second strip widens it; the strip stays, the history as served, even if the agent\n  later sets its own `block_binding`). A drop_block retry refused with a 400 naming the binding,\n  `block_binding` or its beta moves the session to the strip path, a strip refused that way ends\n  its heals (the first lasts for the session, as it depends on the model and thinking type; the\n  second goes with the marks, cleared when they are forgotten; a side request never clears either,\n  nor the strip; a 429, 5xx or other 400 on the retry teaches nothing). Routing switched off after\n  a restart surfaces that 400. A refused mark retries once with top-level effort only and, once\n  served, latches per-message effort off for the session (a model that refused takes Cloud's\n  effort top-level); compressed 400s are decoded first. A Cloud error, timeout, 401/403,\n  `allowance` or `billing_limit` answer keeps the asked model (the held model above for a transient failure) and pauses new asks (1 min; 10 min\n  for 401/403 and billing_limit; until the 1st for allowance). Refusals and limits land in\n  `$CAVEMAN_HOME/route-state.json` (with Cloud's notice) for `caveman status`; a new login lifts\n  the pause. A route failure blames a model only on evidence (Cloud ADR 0110). A provider 4xx on\n  the routed model replays the original bytes on the asked model, except a 401 (the login, not the\n  model: returned as is); a 403 replays (a plan refuses a model it does not carry with one). Only a\n  served replay of bytes that differed from the original in the model alone blames the routed model\n  (`Reject(true)`, `provider_rejected_routed_model`): it leaves the ask's pool for that scope\n  (session, or without one agent and endpoint, and provider and asked model) for 30 min, while\n  the ask still goes with the models left. A served replay after compression or effort also\n  changed the bytes records `replayed_original` and blames no model; a replay that fails too\n  records `replay_failed`. The rest of the ask moves back to the asked model (`Reject(false)`,\n  never remembered) when the replay is served, or is itself a 429 (so the agent's own retries send\n  one request each); a rate-limit 429 (with Retry-After) or a 5xx on the routed model is returned\n  as is, never replayed, and moves it back too, as do a transport error, a provider error event\n  and an upstream break before the provider reported usage; a client cancel never does, so the\n  agent's own retry runs the asked model; a\n  429 on the asked model of a request whose bytes the route stage changed (effort, marks, strip,\n  drop_block, a heal retry) is returned as is. When the model moved, the agent's copy of the\n  answer names the model it asked for (Claude Code drops its thinking on another name): a JSON\n  answer's top-level `model`, and in a stream every `\"model\":\"<sent>\"` pair, rewritten\n  incrementally across reads (an Auto request names Auto, moved or not); the upstream is asked for an identity answer (on the original-bytes replay of an Auto request too), a compressed one is\n  left as it is, and usage, stats and `last` read the provider's bytes. The pass-through header\n  and encoded bodies never route, nor does any origin but the provider's own API (Azure,\n  OpenRouter, LiteLLM, a custom base URL); record mode does (routing is its own module). A limit\n  pause asks `/me` at most every 15 min and lifts once routing is no longer limited or the plan\n  changed. runtime/v1 events go whenever signed in, routing on or not, unless the CLI telemetry\n  opt-out is set (ADR 0085 §5; `route.outcome: off` when no route stage ran): in-memory batches of\n  at most 500, dropped on failure, at `/me`'s `data.level`. The runtime never invents a level:\n  none in `/me` means `counts`, an unreadable `/me` lowers it to `counts`, `off` or no level sends\n  nothing. Model ids from other origins go as `custom`. Signing in or out never changes another\n  stage.\n- **the route path is kept hot** (`internal/cloudlink/warm.go`, contract `route-warm-v1`): the link\n  has its own transport (HTTP/2 tried, idle connections kept 10 min and pinged after 15 s of\n  silence, proxy from the environment; a warm is marked replayable, so a dead kept connection is\n  retried on a new one, an ask never: the server may already have decided and counted it). A warm\n  is `POST {gateway}/v1/route` with `x-cave-warm: 1`, the asks' credential and no body; it goes,\n  one at a time and only from a started runtime signed in with routing on, at start and at every\n  login, when a native `session.start` or `prompt.submit` comes after 30 s without contact (an\n  answered ask or a warm engine), and every minute without contact while asks came in the last\n  15 min. Only a 204 with `x-cave-edge-engine: warm` is warm (timeout, error, cold and off are 204s\n  too); it ends a pause a timeout, an unreachable Cloud or a 5xx caused, never a 401/403 or limit\n  pause. Any other status is an origin without the edge (a customer install): no pause, logged at\n  debug, and three in a row stop warming until the next login or start. Each ask logs `route ask`\n  (info): the ms this side measured, whether it reused a connection, the status, `x-cave-edge` and\n  the edge's `server-timing`. The 800 ms budget stays the cap.\n- **pool routing** (`internal/pool`, `internal/translate`, `internal/gateway/route_pool.go`):\n  the provider keys and logins a person adds (`caveman providers add|login`;\n  `$CAVEMAN_HOME/provider-logins.json` lists ids, where each secret lives and the `cloud` switch,\n  written under `provider-logins.json.lock` by the CLI and the runtime alike; the secret is in the\n  keychain service `caveman-provider` or a 0600 file under `provider-logins/`, never both) become\n  the ask's additive `pool` (`harness/<model>`, then `<host>/<model>` per login times the static\n  catalog models its host serves, on a wire `translate.Supported` reaches from the caller's\n  grammar; at most 64; left out when no login adds one; only a 400 naming `pool` is asked again\n  without it, and pool then stays off for that login). An answer's `pool_id` + `via`: a harness\n  entry applies like `model`; a login entry goes straight to that host on that login\n  (`RouteTarget`, translated when its grammar differs, the unstripped body so each host gets its own\n  reasoning back, top-level effort, the agent's copy naming the asked model); `via: cloud`\n  (`cloud:<provider>:<model>`) sends the agent's own bytes in its grammar to the Cloud gateway\n  (`gatewayUrl`, else `baseURL`) with `x-caveman-route` (and `x-caveman-effort` when the answer\n  carries one) on the project gateway key only (the gateway\n  refuses login tokens; no key, or `caveman providers cloud off`, runs the asked model), and that\n  whole request DOES pass through Caveman Cloud. Route asks to the gateway use the project key too.\n  Any failure before content reaches the agent (refused translation, translator panic, connect\n  error, non-2xx, or a 2xx that fails or ends first: `translate.ErrNotServed`, the translator holds\n  its output until the first content, for at most `gateHold` (10 s) even while the host sends only\n  comments or pings; after it the headers go and pings start, only between the upstream's events)\n  falls back to the asked model on the harness credential, at the answered effort fitted to that\n  model's levels (`translate.FitEffort`: the Claude table, the catalog's `effort_levels`, high at\n  most with thinking off), and marks the ask rejected; a stream cut after content aborts HTTP\n  framing and never closes a half-finished tool call, while the host's own failure event after\n  content ends the stream as sent, without an abort (`pool_upstream_failed`). The row keeps\n  `route_pool_id`, `route_reason` and the host's answer id (`upstream_response_id`, e.g.\n  OpenRouter's `gen-…`); each failure is logged. A translated Messages stream's `message_start`\n  carries an input estimate (`x-caveman-input-tokens: estimated`) that `message_delta`'s exact\n  count replaces. A pool turn is not `last` and books no dollars off Anthropic/OpenAI list\n  prices. Every request to Anthropic's/OpenAI's own API (not pass-through) drops reasoning another host\n  wrote (`translate.AnthropicNative`/`OpenAINative`, byte for byte when there is none; a Responses\n  host's encrypted reasoning is route-tagged so only that host gets it back; per-message effort\n  marks are untouched). OpenAI (key or ChatGPT login) is Responses-only: Messages and chat callers reach it\n  through Messages→Responses and chat→Responses, and chat callers (OpenCode, Aider) reach Claude through\n  chat→Messages; a chat caller gets another host's reasoning as `reasoning_content` plus a\n  `reasoning_details` envelope that goes back only to that host, and every chat API (on the harness path\n  too, whatever the origin and with routing off, `translate.ChatNative`) gets both removed. Documents, structured output,\n  `disable_parallel_tool_use`/`parallel_tool_calls` and refusals cross every direction; a part the target\n  cannot take (a document by URL on chat, an OpenAI file id on Claude, audio) refuses the translation, so\n  the pool falls back. A refusal reaches Codex as `response.incomplete` `content_filter` (its own\n  content-filter path); a max-tokens stop stays `response.completed` (Codex retries any other incomplete). Terms: no Claude Pro/Max, Google or Copilot login is ever added; every OAuth row\n  in `providers.json` carries a terms note.\n- **cache mechanics** (`internal/gateway/route_cache.go`): the ask's\n  `request.cache_ttl` is the longest Anthropic `cache_control` ttl (none = 5m), else OpenAI\n  `prompt_cache_options.ttl` / `prompt_cache_retention` (24h; in_memory = 5m); Cloud's generic\n  400 (`cave_router_request_invalid`, which names no field) or one naming the field is asked\n  again without `cache_ttl`, then `pool` (never a field a 200 already came back for), and the field\n  whose removal got the 200 stays off for the login. Record mode gets none of the cache mechanics\n  below (no `prompt_cache_key`, no OpenRouter pin, host affinity headers keyed on the session\n  itself as before, no fan-out wait); the routing answer itself (model, effort, marks,\n  `configuration_update`) still applies there, as above. Elsewhere: host affinity headers key on\n  the session family (the parent's for a child): hashed `x-session-id` (OpenRouter),\n  `x-session-affinity` (Fireworks), `x-grok-conv-id` (xAI). An OpenAI request the route stage is\n  on for (a stateful chain, answered off for a reason, included) that carries no\n  `prompt_cache_key` gets the session's own (hashed, added before the effort so a heal keeps it;\n  the original-bytes retry keeps it unless the 4xx names it, which also latches the key off for\n  the session; a request it only keyed is never replayed); routing off and signed out stay byte\n  for byte. Once an OpenRouter pool entry reports cache reads or writes, the session pins the\n  provider its answer named (`provider.order:[p]`, `allow_fallbacks:false`); a pinned failure\n  drops the pin, and a pinned non-2xx is sent once more to the same entry unpinned before the\n  asked model runs (a transport error is never resent: it may come after the request was\n  written). Fresh sibling children of one parent on the same model, tools, system prompt and\n  `prompt_cache_key` go one first and the rest wait for its first content (a JSON answer's first\n  byte; an event stream's first event past comments, pings, message_start and\n  response.created/in_progress; 5 s at most, or their own cancellation): one cache write, not N;\n  a prefix with content in the last 5 min waits for nobody. `signals.context_tokens` is the\n  request's bytes times the provider's own tokens per byte, the parent session's first.\n  Live check: `CAVEMAN_LIVE_OPENROUTER_KEY=<key file> go test ./proxy/internal/cloudlink -run\n  TestLiveOpenRouterCache -v` (throwaway home, $0.50 cap).\n- **boundary**: this is public code — it must never import the managed-cloud lane. `make check-boundaries` enforces it.\n\nSee ../../CLAUDE.md (root)\n","proxy/CLAUDE.md":"# proxy — the byte-safe standalone `caveman` proxy (commercial, binary-distributed)\n\nA base-URL-swap reverse proxy: match → authenticate → inspect → byte-safe transform → upstream\n→ meter. Single-operator, BYOK, **zero cloud dependencies**. It shares its provider adapters with\nthe managed gateway (the managed gateway imports them from here). `caveman start` launches the\n`caveman-proxy` binary.\n\n## Layout\n- `providers/` — the shared, public byte-safe adapter set: `Adapter` interface + `Base` embed + `UsageScanner`/`ParseUsageBytes` (`adapter.go`), and `anthropic`/`openai`/`gemini`/`azureopenai`/`bedrock`/`vertex`/`openaicompat`. `ResolveUpstreamURL` takes `providers.RouteContext` (no control-plane coupling). Anthropic + OpenAI carry both prefixed and bare routes (`/v1/messages`, `/v1/chat/completions`). `vertex` preserves caller OAuth bearer or Express-mode Google API-key credentials for Gemini + Claude on Vertex AI (no signing, no environment-key fallback, no custom usage parser). `MatchRoute` is POST-only; read-only model discovery (`GET /v1/models`, `/v1/models/{id}`) is a separate GET-only allowlist, `Base.MetadataRoutes`, declared by `openai`/`anthropic`/`openaicompat` and forwarded unchanged with no body, no transform and no spend row. Bare `/v1/models` belongs to both `openai` and `anthropic`, so they implement `MetadataRequestMatcher` and choose by wire protocol (`anthropic-version` or `x-api-key` present → anthropic; a Google key matches neither), never by registration order (#1187).\n- `internal/gateway/` — the request lifecycle (`server.go` + `proxy.go`) behind three injected seams: `Authenticator`, `CredentialResolver`, `TelemetrySink`. Ports the managed loop with the fail-open fix. `serve` sets `Config.AsyncRecord`: a row's request token count, sink write (batched) and Cloud observe run after the handler returns, in record order (`recorder.go`). A reader that must see the request it follows calls `Server.Flush` first (session receipts do); close the server before the sink.\n- `GET /health/ready` identifies the runtime and advertises `billing: \"byok\"` because the standalone proxy forwards the caller's selected provider credential. The managed twin advertises `managed`; SDK dollar budgets fail closed on missing/unknown billing provenance.\n- `internal/config/` — `caveman.yaml` loader + BYOK env-key resolution; unknown mode fails closed to `record`.\n- `internal/store/` — `~/.caveman/caveman.db` SQLite spend store (`modernc.org/sqlite`, cgo-free); implements `TelemetrySink` and `BatchSink`. `RecordBatch` commits request rows with `synchronous=NORMAL` on its own connection (WAL): a power loss or OS crash can drop the last batches, a process crash or clean exit loses none; every other table keeps FULL.\n- `internal/standalone/` — wiring: static `Auth`, BYOK `Creds`, adapter set, and the always-on SSRF-guarded client.\n- `internal/identity/` — who calls the framework middleware routes (legacy token, token map, OIDC/JWT, mTLS) and the reloadable TLS listener config.\n- `internal/nativeruntime/` — normalized local-agent lifecycle, Task Contract,\n  Decision Ledger, typed CCR capture/masking, child evidence merge, honest\n  receipts, user-only Unix socket / Windows named pipe, and persistent listener lifetime.\n- `internal/repointel/` — deterministic local repository map, task evidence,\n  conservative test impact, and optional-Scout recommendation. No model/network.\n- `internal/nativepack/` — embedded compiled Core/skill policy; generated from\n  `public/skills`, fail-closed on schema/version drift.\n- `cmd/caveman-proxy/` — binary: `serve` (default), `stats`, and content-blind\n  `agent-evidence --session --build --plan`. Evidence query returns only exact\n  provider usage, request hashes, declared context/plan identity, ordered\n  provider-prefix component hashes, actual transform IDs/counts, and CCR handle;\n  basis is always `inferred`, verified dollars always zero.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=proxy` / `make product-test PRODUCT=proxy`.\n- Tests inject a plain `*http.Client` to reach loopback stubs; the binary uses the SSRF-guarded client.\n- New provider/optimizer work goes in `providers/` (shared) — change it once, both proxies get it.\n\n## Gotchas (honesty invariants — correctness, not style)\n\n- **listener lifetime is not session lifetime**: `serve` never exits because a\n  wrapper, native session, heartbeat, or idle timer expires. Legacy\n  `CAVEMAN_NATIVE_IDLE_TIMEOUT` does not arm shutdown. The CLI never restarts a\n  shared listener to change mode/recovery; an incompatible new wrap runs direct.\n  Failed local startup also runs direct. Native session correlation entries may\n  age out without affecting API traffic.\n- **no default generation deadline**: `CAVE_GATEWAY_UPSTREAM_TIMEOUT_MS` defaults\n  to `0` (no total request deadline). A positive value is an explicit operator\n  cap and includes response streaming. Client cancellation still cancels upstream;\n  connection setup, inbound header/upload limits, and idle keep-alive socket\n  cleanup remain bounded separately. The replacement bound for an upstream that\n  connects and then goes silent is the transport's response-header deadline\n  (`CAVE_GATEWAY_RESPONSE_HEADER_TIMEOUT_MS`, default 900000, `0` disables) —\n  headers only, so it never truncates a live stream.\n- **response protocol controls streaming**: SSE/event-stream responses flush\n  headers and chunks even without a JSON `stream` flag. Encoded requests keep\n  `Content-Encoding` and bypass transforms. Interrupted response copies record\n  an error and abort HTTP framing; they never become a clean successful EOF.\n- **replay only when non-delivery is proven**: proxy transport retries are\n  limited to connection-setup failures — `*net.OpError` with `Op` `dial` or\n  `proxyconnect` (a proxy that is down fails as the latter, never as a dial). A failed upload/header read or truncated response\n  does not prove an inference was unprocessed; do not automatically replay it.\n  An explicit transformed-request 4xx still retries once with original bytes —\n  except a 429 that carries `Retry-After` (a rate limit; Anthropic sends its\n  `anthropic-ratelimit-*` headers on every response, so they prove nothing),\n  which is returned as-is; an accepted retry pins the conversation raw\n  (`internal/gateway/raw_pin.go`).\n  See `docs/technical/proxy-reliability.md` at the repository root.\n- **byte-safe**: `record` mode never transforms; on transform error the ORIGINAL bytes are forwarded (HTTP 200, fail-open) — never a 400. The one carve-out: bytes the provider already cached in replaced form are re-sent replaced, because forwarding the original there busts the prefix (see cache safety).\n- **native marker is local-route-only**: HMAC session marker is emitted only\n  after local runtime + route proof and stripped before capture/hash/provider.\n  Direct, managed, proxy-disabled, invalid, and conflicting paths never correlate.\n- **request-wide opt-out**: `x-cave-transforms: caveman.pass-through.v1` suppresses every request transform path — compress, pixel, and provider-native — not only compiled plan routes. Tests cover all three modes.\n- **repository evidence is a claim, so it needs a direct match** (`internal/repointel` + `nativeruntime.renderRepositoryEvidence`): a \"Likely implementation path\" line is injected ONLY for items whose own FILE NAME or symbol name contains a task term (`EvidenceItem.Direct`, `Bundle.Strength == StrengthDirect`). The match is on the final path component on purpose — matching the whole path meant a term naming any ancestor (`src`, `internal`, `lib`: ordinary prompt words) marked every file beneath it direct, i.e. no gate at all. BM25 metadata proximity alone is `StrengthMetadata` and is never shown, never stored, and never gets a `ccr://` handle; the handle reads as authoritative, and a wrong file:line steers the agent while costing tokens every turn. A bundle qualifies on ONE direct item, so the non-direct items that ranked alongside it are stripped by `Bundle.DirectOnly()` BEFORE the object is written — dropping them only at render time left them stored behind the handle, one retrieval from the model. Terms are floored at three characters (`NormalizeTerms`, matching the producing hook) because the direct test is a substring of a file's own name: `go`, `py`, `js` would mark every file of that language a direct hit and hand the gate away. A declined turn is silent to the model but records `repository_evidence_declined_no_direct_match` in the ledger, so \"declined\" stays distinguishable from \"broken\". `Scout.Status` is ADVICE about repository shape and must not gate injection; conflating them made a truncated map silence real hits (#899).\n- **the dependency filter runs on BOTH listing paths, and git is not trusted to be complete**: `listFiles` prefers `git ls-files --cached --others --exclude-standard`, because the project's own `.gitignore` — not a name list we maintain — is the best statement of what is vendored. It is not sufficient on its own: an empty-but-successful listing means the cwd is itself ignored by an enclosing repo (accepting it silently disabled the whole mechanism), so empty falls back to the walk, and every git-listed path still goes through the same content-based filter as the walk via `directoryFilter` (memoised per directory, one probe per directory not per file) — otherwise an un-ignored conda prefix or a COMMITTED `vendor/` tree reaches the model precisely because git reported it. Installed environments are identified by CONTENTS (`conda-meta`, `pyvenv.cfg`, `site-packages` child) so a pixi/conda prefix or virtualenv is caught under any name, along with the interpreter stdlib it carries. Only names that are never first-party (`node_modules`, `.pixi`, `site-packages`, …) match by name, case-folded; `build`/`dist`/`target`/`vendor`/`coverage`/`Pods` are ambiguous and excluded only when a sibling manifest proves the ecosystem, because `internal/build` and a hand-written `vendor/` are real source. The two paths can legitimately disagree (git omits submodule contents), so `Map.ListingBasis` discloses which ran and is folded into `ContentSHA256`. Git gets at most HALF the caller's remaining warm budget (`gitListBudget`): the walk is the more expensive path, so a git listing that spent 2s of a 3s window left the fallback unable to finish and the map failed closed — the exact case the fallback exists for. `FilesScanned` stays the map size; `FilesRanked` is the filtered count.\n- **git is invoked as an untrusted-repository operation**: every `git` call that points at a user working directory goes through `internal/gitsafe.Command` (Go) or `packages/cli/src/git-safe.ts` (the two `execFileSync` sites in `index.ts` and `native-hook-fast.ts`), which pass explicit `-c` overrides (`core.fsmonitor`, `core.hooksPath`, `core.sshCommand`, `core.askPass`, `core.editor`, `core.pager`, `core.alternateRefsCommand`, `diff.external`, `credential.helper`, `protocol.ext.allow`, `uploadpack.packObjectsHook`) and strips inherited `GIT_*` from the environment. A repository's own `.git/config` names programs git will run, and `core.fsmonitor` runs while the index is read — so plain `git ls-files` in a freshly cloned hostile repo executes attacker code with no user action, at session start. `repointel.listFiles` was hardened first and the other callers were not, which is the shape this bug takes: `nativehook.currentRepositoryState` runs `git status` on EVERY hook event, and `status` refreshes the index, so it triggers `core.fsmonitor` harder than `ls-files` does. Regression tests: `repointel.TestGitListingIgnoresRepositoryControlledCommands` and `nativehook.TestRepositoryStateIgnoresRepositoryControlledCommands` (both verified to fail without the overrides), plus `tests/verify_repo.py` `verify_untrusted_git_invocations`, which fails the build on a NEW raw `git -C <dir>` call site anywhere in `proxy/` or `packages/cli/src/`. Command-line `-c` beats repository config; never drop these to \"simplify\" the invocation.\n- **no-fake-savings**: standalone records `Basis: \"inferred\"` on every row; it never writes `verified` and never re-projects to a monthly figure.\n- **spend is arithmetic, savings is a claim** (local learn, since 2026-08-21): `caveman learn` MAY state a currency, under exactly these conditions. SPEND prices PROVIDER-COUNTED disjoint billing buckets read out of the transcript's own usage block, at rates from the dated `shared/provider-catalog` row, stamped with that row's catalog version — basis `provider_counted_x_published_rate`, window-bounded, never projected. A model with no catalog row is NEVER priced at a sibling's rate: its tokens are disclosed separately and the total is a stated floor (`learn_pricing.go`). Sinks are priced at the user's own MEASURED effective input rate, not list, so a well-cached user's findings are not inflated ~10x. SAVINGS additionally requires an attribution method (`learn_attribution.go`): every confirmed row names its rung (`deterministic_remeasure` > `controlled_holdout` > `counterfactual_replay` > `interrupted_time_series`), carries the standing confounders that rung cannot rule out, and reports whether the artifact caveman fingerprinted at apply time is still intact on disk. Savings totals are bucketed BY RUNG and never summed across rungs — blending a re-counted file with a before/after median launders the weak number into the strong one. A regression is never priced. Neither surface ever writes `verified`; that stays gateway-only. Subscription traffic has no marginal cost, and every priced surface says so.\n- **practice join**: local learn sinks carry additive `practice_id`; one\n  fail-closed mapping table owns sink→practice and unknown sinks keep `\"\"`.\n  The historical `subagent_overuse` sink is count-only and deliberately has no\n  practice id: spawn count cannot reactivate the retired\n  `context-exploration-offload` opportunity or prove any spawn unnecessary.\n- **local trial heuristics are not actuation evidence**: a model name never emits\n  the retired `model-right-sizing` id, and provider plus positive cost never\n  emits a cache move because neither proves stable-prefix eligibility. Legacy\n  rows for those identities are hidden at read time. Compression replay reports\n  one trial's local engine `estimated_engine_o200k` before/after shape with zero\n  dollars and low confidence; it is not provider-counted, a rate, an invoice,\n  causal/verified savings, or task-outcome evidence.\n- **Anthropic automatic caching is experimental observation only**:\n  `anthropic-automatic-prompt-cache` is a typed, default-off manual policy\n  experiment and may add only Anthropic's top-level 5-minute marker on direct\n  Messages API requests. Managed traffic additionally requires server-attested\n  official Anthropic origin; custom or provenance-unknown origins lose the flag\n  before the adapter. It is mutually\n  exclusive with the explicit `anthropic-cache-breakpoints` transform and any\n  caller `cache_control`; Bedrock and count-tokens requests stay byte-identical.\n  An applied marker records only its optimizer id plus actual provider usage and\n  cost. It has no practice, recipe, generic mode/candidate activation, ledger\n  tuple, inferred savings, or verified-savings path (cache-only and forged IDs\n  are excluded from the counted-baseline method too); evaluate it by manual\n  paired observation because shared provider cache state can contaminate an A/B.\n- **OpenAI cache-key affinity is not cache placement or savings proof**:\n  `openai-prompt-cache-key` adds routing metadata only. On GPT-5.6, the\n  provider's implicit latest-message breakpoint can repeatedly write a changing\n  suffix, and writes cost 1.25× uncached input. The usage parser normalizes\n  official nested `cache_write_tokens` from both Responses\n  `input_tokens_details` and Chat `prompt_tokens_details` into\n  `CacheCreationInputTokens`; applying the affinity key never mints verified\n  savings. Explicit `prompt_cache_breakpoint` placement is not implemented.\n- **SSRF always on**: `standalone.StandaloneHTTPClient` guards every upstream dial (not gated on `CAVE_ENV`) using `ssrf.SelfHostedConfig` — NOT ManagedConfig, which ignores the allowlist and would make the escape hatch a silent no-op. `CAVE_SSRF_ALLOWLIST` opts loopback/private hosts back in (local model servers like Ollama; `localhost` as an entry covers 127.0.0.0/8 + ::1); metadata/link-local stay blocked in every mode. Provider traffic honours `HTTPS_PROXY`/`NO_PROXY` by default (#1001) — `upstream_proxy: env|off|<url>` / `CAVE_UPSTREAM_PROXY` — via `ssrf.Config.Proxy`, which validates IP-literal/localhost destinations before proxy selection and dials the (operator-configured) proxy address unguarded; `ssrf.NewHTTPClient` itself stays direct unless a caller sets `Proxy`, and a managed-mode Config carrying one is refused loudly (every request fails with `ssrf.ErrProxyInManagedMode`) rather than silently dialing direct. Adapters that pre-flight a resolved endpoint (bedrock, vertex) read the selector off the request context (`providers.WithUpstreamProxy`, published by `gateway.Handler` from the upstream transport) and use `ssrf.ValidateURLNoResolve` for proxied destinations: a proxy-only network has no outbound DNS, so resolving there failed the request before the proxy was ever consulted. `ca_bundle` / `CAVE_CA_BUNDLE` plus inherited `SSL_CERT_FILE`/`REQUESTS_CA_BUNDLE`/`NODE_EXTRA_CA_CERTS` append private roots for TLS inspection through `shared/platform/cabundle` (fail-closed parser shared with chhttp).\n- **inbound token is consumed, never forwarded**: `CAVEMAN_AUTH_TOKEN` gates non-loopback listens (a middleware token map, OIDC issuer or TLS client CA also makes one legal; provider routes still accept only the token, and without it `Auth.closed` refuses every provider request off loopback); `standalone.Auth` deletes the matching `x-cave-api-key`/`Authorization` header before `Creds.Resolve` so the shared token can never be forwarded to a provider or classified as a provider credential; a bearer that is not the token survives untouched (Claude OAuth, `/chatgpt/`). `Authenticate` runs BEFORE `matchAdapter` so the 401/404 split cannot be used to enumerate routes, and every 401 goes through `Server.rejectUnauthorized`, which counts `cave_proxy_unauthorized_total` and warns with the path and remote host only. Health and the no-op `/caveman/keepalive` beacon stay unauthenticated, `/metrics` too unless `CAVEMAN_METRICS_TOKEN` is set; the `X-Caveman-Instance` header on `/health/live` is published only on a loopback bind. A token on a LOOPBACK listener is honored too (the local `caveman wrap` path sends none, so `runServe` warns). Bedrock credentials then come from `shared/platform/awscreds` (env → web identity → container → IMDSv2), never from the request; a PARTIAL env pair fails closed instead of falling through to an ambient role, and the winning source is logged once.\n- **middleware identity is server-side and per route**: `middleware.Config.Identify` resolves an `identity.Principal` from the credential alone (bearer decides when present, else a verified client certificate); `ServeHTTP`'s `decode` checks `Principal.Allows(scope.Namespace)` on EVERY scoped route, so a new route that skips `decode` skips authorization. The legacy token stays `single_operator` with every namespace; JWT and certificate principals may never be named `single_operator`. Token and hash comparisons are constant-time over every entry. A token map or TLS file that fails to reload keeps the previous one. Tests: `identity/*_test.go` (negative JWT and mTLS cases), `middleware/identity_test.go`.\n- **Postgres middleware store keeps SQLite's single-writer semantics per authority**: `store.PostgresMiddleware` serializes writers of one authority with a transaction-scoped advisory lock taken by `Scope` (then re-read), `SaveScope`, `Revoke`, `PurgeBatch`, `Receipt` and `SaveOriginal`; the sweep is one-replica (try-lock) and skips busy authorities and row-locked scopes. Counters are statement-level transition-table triggers striped by txid; a per-row trigger goes quadratic on bulk statements. Lock keys include `current_schema()`. Run the store suite against both backends with `CAVEMAN_TEST_POSTGRES_URL`; `middleware_concurrency_test.go` fails without the locks.\n- **Auth scheme is preserved**: a key from an inbound `Authorization: Bearer` keeps `Scheme:\"bearer\"` on the `providers.Credential`; Anthropic and Gemini forward bearer credentials as bearer credentials (Claude/Gemini OAuth breaks if remapped to an API-key header). BYOK env keys and inbound `x-api-key` keep provider API-key mapping. On a named compat mount the header follows the wire protocol of the path: `/compat/<name>/v1/messages` gets `x-api-key` plus a default `anthropic-version`, every other path gets Bearer. A real inbound Bearer stays Bearer on every path. One credential may arrive twice — pi with `authHeader: true` sends it as `Authorization: Bearer <key>` AND as `x-api-key: <key>`; x-api-key still resolves first (a shared listener must not be able to swap the caller's principal), but an IDENTICAL bearer is that same principal, so `openai_compatible` keeps `Scheme:\"bearer\"` and the bearer-only compat upstream gets the header it accepts (#1215). Differing values stay x-api-key-first, and no other provider reads the duplicate as OAuth. OpenCode Go rejects Bearer on `/v1/messages` (401 `Missing API key.`, 2026-09-03). The gateway env fallback puts the key in the header that the adapter emitted, so the upstream never gets two credentials.\n- **fail-closed**: unknown route → 404; unknown mode → `record`.\n- **subscription AND oauth compression is NOT account-gated**: non-PAYG sessions from Claude Code, Codex ChatGPT, Gemini CLI, and other routed clients take live-zone compression with no Caveman account, entitlement, or seat. `CAVEMAN_WRAP_ENTITLED` and every `WrapEntitled` field are **deleted**, not merely ignored — do not reintroduce them. Exactly four conditions remain, all technical and all fail-closed (`liveZoneCompressionAllowed`): the operator `subscription_compress` switch (empty/`live_zone` allow, `off` and any unknown value close it), the adapter must implement schema-aware `PrefixStabilizer` zones, recovery must run through the agent's own MCP `caveman_retrieve`, and a durable prefix cache must be wired. The dedicated Codex `/chatgpt/responses` route uses the OpenAI Responses stabilizer while preserving OAuth and `ChatGPT-Account-ID` headers; recovery is proven by the `CAVEMAN_RECOVERY=mcp` stamp or the decoded request's own `mcp__caveman__caveman_retrieve` (top-level `tools` or Codex's `input[]` `additional_tools`, flat or under an MCP namespace); zstd bodies decode → transform → re-encode, anything else forwards the original wire bytes, and the `chatgpt_proxy` log carries `skip_reason`; transformed 4xx responses retry once with exact original bytes (never a rate-limit 429; an accepted retry pins the conversation raw). Legacy savings fields remain **tokens-only** — `compression_tokens_before/after` + `estimated_engine_o200k`, never booked compression dollars. New request-comparison and price-snapshot fields support separately labeled API equivalents in local stats; those values never enter subscription spend or legacy saved-dollar fields. LOCAL wrap only; managed gateway non-PAYG behavior is unchanged.\n- **recovered bytes are never a compression candidate**: a `tool_result` answering the agent's own `caveman_retrieve` is excluded by every adapter (`providers.IsRecoveryToolName`; anthropic keys off `tool_use_id`, OpenAI off `tool_call_id`/`call_id`, gemini off the `functionResponse` name). This is not an optimization — the replacement is a deterministic function of block content and is memoised in the prefix cache, so collecting a recovery result substitutes the SAME elision straight back in and the agent has no path to its own data at all. Measured 2026-08-06: the agent retrieved, reported \"Same truncation\", and re-read one file seven times. Regression test `internal/gateway/recovery_exempt_test.go`.\n- **cache safety is byte-stable replacement**: THE INVARIANT (#1105): for accepted requests P then R of one conversation, if R's client bytes over P's cached prefix (system, tools and messages through P's last `cache_control` marker; the whole prompt for implicit caches) equal P's, R's forwarded bytes over that range equal P's, and every request caches at least what its client asked for (a dropped breakpoint is a bust too). Two exceptions, both re-caching the range raw: a request the provider rejected transformed and accepted through the raw retry, and a compressed PAYG conversation without MCP recovery that streams (the server-side retrieve tool cannot ride a stream, so the conversation stays raw from there). A raw retry splits a range into a raw and a replaced form, and a later request follows the LONGEST lineage it extends, ties going raw: it is held to the raw request (raw pin) unless it repeats a longer prefix that went out replaced, like a sibling subagent started with the same task. Pins are taken only on a conversation's own bytes (past system and tools); pins and lineages live in the PrefixCache (`rawpin` and `lineage` scopes, pin rows never evicted) and in memory. `internal/gateway/cache_prefix_invariant_test.go` (Anthropic compress, PAYG, pixel and Codex Responses scenarios + `FuzzCachePrefixInvariant`) proves it; keep it green. A compressed live-zone turn becomes prefix on the next request, so the replacement is a pure function of segment content (deterministic compressor + content-hash CCR marker), memoised in the durable replacement cache, and the FIRST decision for a block wins — replaced or raw (`RawDecisionHandle`); a request whose write finds a row on record sends that row, not its own candidate. A frozen block with no decision (a `--resume` history, an evicted row) goes out raw and raw becomes its decision; a failed memo write keeps the block raw for the process (`rawMemory`, consulted only on a store miss and written through once the store takes it); new rows are remembered only once the request's splice holds. Raw decisions are global and durable, a known savings cost: a block declined once (frozen at first sight, a compressor decline, a CCR failure, a non-newest parallel tool output) stays raw in every conversation. Bounding them needs a TTL of at least the longest provider retention (24 h for OpenAI extended caching) plus time-aware store interfaces, or per-conversation rows keyed on a stable conversation identity with a scope rollover. Substitution is never gated: the derived epoch gate is gone, and an explicit framework epoch may veto only NEW compression. Pixel renders and the tool-schema strip are memoised the same way. New compression stays live-zone-only. The runtime tripwire (`prefix_monitor.go`, on the provider routes and `/chatgpt`) checks the same rule on live traffic and records `requests.cache_bust_cause`: `client` (DEBUG); `raw_retry`, `stream_switch`, and `lever_freeze` (the harm tripwire's one-time strip rollover), all WARN; `caveman` (ERROR `caveman changed bytes the provider already cached` or `caveman dropped a cache breakpoint the client set`, counted as `caveman_cache_bust_requests` and raised by `caveman status`) — any `caveman` row is a bug. A request is held only to anchors accepted before its own forwarding decision. Cross-turn stability covers **anthropic, openai, azureopenai/openaicompat, and gemini** through `ExtractStabilizable`; `bedrock` and `vertex` expose no compressible blocks and stay pass-through. Anthropic uses declared `cache_control`; OpenAI/Gemini use latest-user/latest-tool zones against implicit provider caches. Replacement cache is SQLite spend store and must retain `journal_mode(WAL)` + `busy_timeout`; a hit rewrites `last_used_at` only once the row is 10 minutes stale, and pixel rows have their own cap inside the table's.\n- **tool-schema annotation strip is DEFAULT OFF and separately opted in**: `toolschema_strip: annotations` / `CAVEMAN_TOOLSCHEMA_STRIP=annotations` (`\"\"`, `off`, and any unrecognized value all mean off, normalized in the config loader AND re-checked at the decision point). It is a SECOND gate ON TOP of the four `liveZoneCompressionAllowed` conditions, which it reuses rather than restates, plus a THIRD: the session ledger's freeze registry (`ledger.LeverAllowed`) — it never runs in `record` mode, under `caveman.pass-through.v1`, under a compiled Cave Build, or in a session whose harm tripwire has frozen it. Its first decision per catalog (stripped or original) is memoised in the replacement cache and re-sent, so a recovery-store hiccup never flips the head of the prefix. S4 + CCR (the original catalog is stored before the rewritten bytes ship, disclosed as `x-caveman-toolschema-recovery-handle` and on the row's `RecoveryHandle`), promotable only under the local-wrap clause. It removes `$schema`/`title`/`examples`/`deprecated` ONLY inside schemas reached through `input_schema`/`inputSchema`/`parameters` and the JSON-Schema applicator allowlist — never from a tool envelope, `annotations` (whose `title` is the tool's display NAME), `_meta`, or a vendor extension. `ExtractToolCatalog` covers the **anthropic-messages** shape only; openai/gemini and `count_tokens` fail closed. It mints NOTHING: no tokens, no ratio, no dollars — the replay grid prices it. Its version is folded into the prefix-cache scope key so toggling it is a deliberate epoch rollover, not a silent partial cold write.\n- **cache-breakpoint planner defaults to `frontier` in optimization modes**: `breakpoint_plan: off` / `CAVEMAN_BREAKPOINT_PLAN=off` is explicit off; unrecognized values fail closed to off. Record, compress, and pixel modes still never run it. Provider-native cache optimizers for Anthropic, OpenAI, and Bedrock also default on when omitted, with explicit per-id `false` preserved. All remain capability-, auth-, endpoint-, economics-, and caller-marker-gated. Planner metadata is upstream-only and changes no model-visible bytes. It does not mint savings: `cache-breakpoint-plan` remains absent from `cacheOptimizerIDs`. OpenAI's arm hashes session id and never overwrites caller key; prefix-signature optimizer wins when already applied.\n- **harm tripwire / session ledger**: `internal/gateway/ledger.go` keys a bounded LRU (1024) on `x-cave-session`; **no session header = no entry and the whole mechanism is inert**. Every lever asks `LeverAllowed` before running. A lever active on request N earns a strike when request N+1's `cache_creation_input_tokens` exceeds BOTH an absolute floor (50k) AND 3x the session's baseline mean; 3 strikes freeze that lever for the session, one-way (never un-freezes), fail-open to pass-through. The baseline EXCLUDES calls already judged anomalous — a plain running mean folds each spike into the bar the next spike is measured against and goes blind to persistent regressions. Disclosure is `x-caveman-tripwire: <lever>=frozen`, and it appears from the request AFTER the one that tripped, because usage is only known once the response is already streaming. Thresholds are conservative by construction, pending replay-derived values.\n- **cache warming is predicted per stream, and replays only what Anthropic can answer at zero output** (`internal/gateway/cache_warm.go`, `cache_warm_table.go`): after a successful Anthropic Messages response on `api.anthropic.com` (never Bedrock, Vertex, Azure, a compat mount, a custom base URL, the pass-through header or an encoded body), the exact bytes and headers sent upstream (after compression, routing marks, heals and fail-open retries) are kept in memory only (LRU 1024 streams and 256 MiB, never stored, logged or captured) and may be replayed with `max_tokens: 0` and `stream: false` at `min(0.9*ttl, ttl-10s)` steps from the real request's start (wall time, so laptop sleep never sends a stale warm). Not replayable, so never warmed: `thinking.type: \"enabled\"`, `tool_choice` `tool`/`any`, `output_config.format`, no `cache_control`, a `ttl` other than `5m`/`1h`, mixed `5m` and `1h` markers, writes of another lifetime than the markers (one lifetime or no warm; the simulator classes by the same rule and none of the measured requests mixed them); the request scan reads top-level fields and cache_control markers only, no full parse. WHETHER to warm is optimal stopping, not a threshold: `cache_warm_table.json` holds the return-time distribution (30 s bins to 2 h) per stream class `main|subagent / tool|end / 5m|1h` (the class comes from the session key and whether the answer's `stop_reason` was `tool_use`, scanned off the plain bytes with a rolling tail; the proxy forwards the agent's `accept-encoding`, so an answer that comes back with a `Content-Encoding` is not classed: it is planned as whichever of the two classes warms less and its gap teaches neither), measured on 364,048 request transitions of real Claude Code sessions (2026-08-30 to 2026-10-08, `.blocks/cache-warm-sim.py`, aggregate probabilities only) and shrunk toward the parent class; the proxy learns each user's own gaps online (counts only, `$CAVEMAN_HOME/cache-warm-gaps.json`, 0600, written outside the warmer's lock, flushed by `Server.Close` at shutdown, symlink-refused, any implausible count drops the whole file; two proxies on one home are last-writer-wins) and blends them in with the defaults worth 100 observations. A stream evicted or still waiting at shutdown is counted as returning later than it was seen silent (spread over the later bins by the class's own distribution, \"never\" past 2 h): dropping it would keep only the returns and drift the table toward over-warming. At arm time the whole chain is planned by backward induction with the catalog price of the model sent (warm = read x prefix + input x uncached tail; avoided miss = (write_ttl - read) x prefix x the class's measured realized share, taken over the gaps that DID return: the window probability already leaves out the streams that never do), so the cumulative cost of every warm is counted before the first; unknown price, class or lifetime = no warm. Subscription traffic is valued at list prices as plan-usage equivalent. Stops: any real request of the stream (every model; it never waits for a warm in flight), any warm error, a warm reading `cache_read_input_tokens == 0`, the plan, 60 min, or the switch; the held request (body and credential headers) is released the moment its chain ends. A parent session's request does NOT stop its subagents' warms: a subagent can still be running, and on the test half that stop cost $565.52 net (below). A 429 (warm or real) pauses warming on that credential for the response's `Retry-After`, else until its `anthropic-ratelimit-unified-reset`, else 1 h for a subscription/OAuth login and 60 s for an API key; a 529 or 5xx on a warm pauses it for the `Retry-After`, else 5 min. Never sent while a real request of the same stream is in flight (retry every 5 s until its deadline). At most two warms are in flight process-wide (same retry; one with no slot by its deadline is dropped), and each fires up to 5 s before its planned time, never after. Warm rows are tagged `cache-warm`: spend, never a saving, never an agent request (excluded from request counts, session usage, trial origins, exported spans, receipts and `caveman sync` uploads; counted as `cache_warm_requests`), never fed to the harm-tripwire ledger, never sent to Cloud. Switch: on unless `$CAVEMAN_HOME/cloud.json` has `modules[\"waste-fixes\"]: false` (re-read on change); `CAVEMAN_CACHE_WARM=off` kills it for the process. Every answered stream request logs one line `cache warm plan` (info when warms are planned, debug when skipped: session hash, model, class, ttl, planned warms, skip reason: `response_error`, `off`, `not_anthropic_api`, `not_replayable`, `lifetime_mismatch`, `unpriced`, `not_worth_it`). Replay on the owner's data (test half, after 2026-10-03, 161,325 transitions): net $1,896.65 list-price equivalent at $385.31 of warms (9,628 warms, 7,280 wasted) vs $1,294.18 at $1,372.15 (45,343 warms) for the first fixed rule; the same policy with the parent stop for every subagent $1,331.13 at $197.28 (3,461 warms), with it only for subagents whose last answer was not a tool call $1,852.99 at $378.90 (9,518 warms). A test pins the plan to the simulator's on the shipped table (`TestCacheWarmPlanMatchesTheSimulator`; regenerate both together).\n- **pixel mode**: S4 lossy text→PNG (`pxpipe` port). Default allowlist is `claude-fable-5,gpt-5.6` via `CAVE_PIXEL_MODELS`; savings stay inferred-only. Renders keep the compress rules: the first decision for a text (render or text) is re-sent on every turn, a raw pin keeps the conversation text, and a NEW render is remembered only once its request goes out, with the original stored in CCR and the splice holding. If either fails, the new content goes out as text and records that, and earlier renders still go. A rendered block's `cache_control` moves to its last image. An Anthropic request carries at most 20 images, client images included (`anthropicManyImages`): past 20 the API rejects any image over 2000 px, and the renders are 2573 px wide, so a new render past the cap stays text.\n- **mask what cannot be summarized in place; elide what can**: `nativeruntime.afterTool` replaces an over-threshold tool output with a `ccr://` pointer stub, but NOT when `Engine.Detect` classifies it as `json`, `tabular`, or `log` (logfmt + NDJSON) — the classes with a field grammar, which the elision engine compresses in place into rows plus stated invariants (`all state=charged`, `status: delivered×18 attempted×17`, `wh-5000..wh-5059 all 60 present`). Masking those first destroys every fact AND costs more: the agent sees no row, then recovery re-enters the FULL original through the recovery-exempt path — whole page + stub + an extra turn, strictly worse than no wrap. Measured 2026-08-08 on the shipped default (`compress` → native policy `safe` → profile `full-safe` → mask on): inventory-mismatch and webhook-delivery-gaps scored 0/6 with 27–97 recovery calls, while rate-limit-forensics scored 3/3 at ~35% cheaper for the sole reason that its pages sat under the threshold. Capture is unaffected (the object is still stored, recovery still available); only the replacement is skipped, and the size rule for still-maskable classes is unchanged. Fails toward masking: no classifier → mask, so the fallback is bounded context. Tests: `mask_elidable_test.go`.\n- **route stage is optional and fails open** (`internal/cloudlink`, `internal/gateway/route.go`):\n  it asks Cloud `POST /v1/route` only for requests naming Auto (`AutoModel`, `caveman-auto`, also with Claude Code's `[1m]` suffix, the\n  picker entry the CLI adds while `modules.routing: true` in `$CAVEMAN_HOME/cloud.json`), while the\n  CLI is signed in with that switch on, for Anthropic Messages / OpenAI chat or responses requests on\n  an API key or a subscription alike; any other model goes as sent (`route.outcome: off`), the\n  session's marks and heal still applied. Auto goes upstream as the provider's `autoFallback` model\n  (Anthropic `claude-sonnet-5-5`, OpenAI `gpt-6.1-sol`; count_tokens too, and with no Cloud link),\n  which is also the asked model the ask carries and what a failure runs (a transient Cloud failure, that is a timeout, an unreachable or 5xx Cloud, an unreadable answer or the pause after one, instead runs the model the session's previous request went to, `heldModel`, never for a side request; no previous request on that provider, one that was a compaction or came after one, a held model that then did not serve (any non-2xx: the next attempt runs the fallback), signed out, routing off, an expired or refused login, a limit and a provider's refusal keep the fallback); Auto on any other\n  provider is a 400 (`cave_auto_unavailable`), and an encoded body naming Auto is decoded first. A\n  named model records `route.outcome: off`, reason `named_model`. OpenAI Auto (API key and ChatGPT\n  login) runs only `AutoOpenAIModels` (`gpt-6.1-sol`, `gpt-6-astra`, `gpt-6-luna`): the ask's\n  `Models` lists those alone and the pool drops other OpenAI models, and an answer naming any other\n  runs `gpt-6.1-sol` at its effort (`auto_model_refused`, `translate.FitEffort`); an Auto row's `RouteFrom` and\n  `x-caveman-routed-from` say `caveman-auto`. A move on a subscription stays on the provider's own\n  API on that subscription's credential; pool entries go out on their own login or the Cloud\n  gateway key, never on it, and never with the agent's `oauth-*` betas. On a ChatGPT login\n  (`/chatgpt`, `chatgpt_auto.go`; OpenCode's ChatGPT login lands there too) `/models` gains an Auto\n  entry (a copy of `gpt-6.1-sol`'s, listed last, fetched without `If-None-Match`, served without\n  `ETag`) while the link reports `AutoOffered` (signed in, routing on). Every POST there has its\n  first 64 KiB read (decoded when gzip, deflate (zlib or raw) or zstd, by a decoder that is always closed; a br body is never looked into) for a\n  top-level `\"model\":\"caveman-auto\"`; any other streams on unchanged. `/responses` naming Auto asks\n  with `NoPool`, keeps live-zone compression, replays `gpt-6.1-sol`'s bytes on a 4xx (a rate-limit 429, a 401 and a 403 are returned instead; a served replay pins the conversation raw, and the decision is rejected only then or when the replay is itself a 429; a 5xx on a moved model is returned and rejects the decision, a rate-limit 429 keeps it: the plan's limit holds for every model); any other\n  path (Codex compaction) runs `gpt-6.1-sol` unasked; a body over `CAVE_MAX_REQUEST_BYTES` streams\n  with only its model changed (`auto_body_too_large`; one that fits but does not decode records `auto_body_decode_failed`). The agent reads the Auto id it sent. Every `/chatgpt` row (Auto or\n  not) is offered to Cloud like any other, as its agent (`x-cave-agent`, else Codex by its\n  `originator` or User-Agent), with model ids in `route_from`/`route_to`; a model it could not read\n  goes to Cloud as none. The\n  ask starts before compression (parse, ask, compress, route), waits at most 800 ms (the CLI's\n  keychain credential is read when the runtime starts, `Link.Start`, never on a request goroutine;\n  a request that lands while a read runs with no secret cached waits for it, leaving the ask at\n  least 500 ms, else goes as `credential_loading` without an ask or a pause), carries the\n  caller's models, counts, what the request declares (the raw values of eleven allowlisted agent\n  headers, never cut: a value over 256 bytes, 16 KiB for Codex's turn metadata, which also comes\n  from the body's `client_metadata`, is left out; a forked Claude Code child's spawn-call\n  `subagent_type` as `x-caveman-agent`; its tool names, effort and thinking type), what the\n  session's previous request ran (the served model, the effort in force, the provider's input and\n  cache token counts, its age) and Cloud's opaque per-session state, plus, on a turn's first ask,\n  the raw text of the latest human turn, the one before it and the end of the agent's last reply\n  (contracts `route-ask-v1`; Cloud picks the model and the effort and no routing logic lives\n  here). Its answer is cached per ask until 30 min after its last use (session, or without one agent and\n  endpoint, provider, model, turn number, latest human text;\n  an input group carrying a tool result is no human turn, text riding along included) so a tool\n  loop never switches model or effort mid-turn (an ask first seen during a pause keeps the paused answer for its turn); a request the agent labels compaction or auxiliary\n  (Claude Code's request class and compaction flag, OpenCode's title/summary/compaction agent,\n  Codex's subagent kind and turn-metadata request kind) is asked on its own, without the text. The\n  session is `x-cave-session`, else the agent's session header (a Codex thread, `thread-id`, is\n  its own, its parent `x-codex-parent-thread-id`); a Claude Code child (`x-claude-code-agent-id`)\n  is a session of its own and sends its parent's state; a session id guessed from timing is not\n  used. State, previous-request facts and marks live in bounded in-memory LRUs (1024), never on\n  disk. The answer sets Responses `reasoning.effort`, chat `reasoning_effort` or Anthropic\n  `output_config.effort`; Anthropic `effort_mode: message` instead inserts the byte-identical mark\n  `{\"role\":\"system\",\"content\":[],\"output_config\":{\"effort\":…}}` (beta\n  `mid-conversation-output-config-2026-07-01` appended to `anthropic-beta`) before the last user\n  turn, or at the end after a tool result; on OpenAI Responses (GPT-6) the mark is a\n  `{\"type\":\"configuration_update\",\"reasoning\":{\"effort\":…}}` input item before the last user\n  message while nothing has answered it yet, never at the end and never next to another one (no\n  place, as mid tool loop: the effort in force holds), no beta, and none at all where OpenAI forbids\n  it (`truncation: \"auto\"`, `context_management`, a non-false `multi_agent`: Cloud's effort goes\n  top-level); a 400 naming a configuration update heals like a refused mark. Effort state made on\n  one wire starts afresh on the other. Only `effort_mode: message` ever starts marks; a \"top\"\n  answer on history carrying the session's marks goes in as one more mark (never wiping them; for\n  a compaction or side request, that request's only), and on any other history sets the top-level\n  field. Marks are remembered per session with a salted hash of the message before each\n  (`cache_control` and thinking blocks left out, so a heal's strip keeps them) and replayed at the\n  same places on every later request to a model that already took them (the agent resends history\n  without them), count_tokens of a known session or a forked child's parent included (it adds\n  nothing to a history without marks). Without an effort from Cloud (a failure, routing off,\n  effort \"\") a history without the session's marks goes as the agent sent it, top-level field\n  included (one cache restart where Cloud's was, nothing inserted, no beta), except that a\n  remembered heal (drop_block with its beta, or a strip) keeps applying. One with marks runs at\n  the request's own top-level effort, or, when it sets none, at the model's default effort\n  (Cloud's `default_effort`, kept per session, a forked child taking its parent's), marked when it\n  differs from the one in force; only with no default known does such a request go as the agent\n  sent it, without the marks, which the session then forgets (its later thinking blocks then lose\n  their binding) unless the request went to another model. The top-level field is fixed at the\n  session's first per-message request: the routed effort on a fresh conversation whose request\n  sets one (kept there, also for later requests that set none), else the request's own; a routed\n  effort for a request that sets none goes in as a mark, first on a fresh conversation. An anchor\n  that no longer matches drops that mark and every later one; compaction and side requests never\n  change them, and a body matching none of them (an unlabeled side request, compacted history)\n  gets marks of its own, kept for up to four request shapes (first message, length, last message),\n  which take the session over only from a longer request continuing that conversation (same first\n  message, its last one where it was); the session's marks and fixed top-level field then wait\n  there in turn. A thinking-binding 400 on the asked model of a `/messages` request retries once\n  only on a session the route stage changed (it was on for the request, the session carries its\n  marks or heal, or another model served one of its requests) whose body sets no\n  `thinking.block_binding`: with `thinking.block_binding.prefix_mismatch_behavior: \"drop_block\"`\n  (beta `thinking-binding-controls-2026-08-01`) when the block is bound to a different\n  conversation and the request's thinking is adaptive or enabled, then sent on every later request\n  (count_tokens and forked children included); else (Sonnet 5.5 `between_tools`, no thinking\n  field, a tampered signature) without thinking blocks from the failing message on, and later\n  requests on the same history (same first message, same message at its end) strip the same range\n  up front (a second strip widens it; the strip stays, the history as served, even if the agent\n  later sets its own `block_binding`). A drop_block retry refused with a 400 naming the binding,\n  `block_binding` or its beta moves the session to the strip path, a strip refused that way ends\n  its heals (the first lasts for the session, as it depends on the model and thinking type; the\n  second goes with the marks, cleared when they are forgotten; a side request never clears either,\n  nor the strip; a 429, 5xx or other 400 on the retry teaches nothing). Routing switched off after\n  a restart surfaces that 400. A refused mark retries once with top-level effort only and, once\n  served, latches per-message effort off for the session (a model that refused takes Cloud's\n  effort top-level); compressed 400s are decoded first. A Cloud error, timeout, 401/403,\n  `allowance` or `billing_limit` answer keeps the asked model (the held model above for a transient failure) and pauses new asks (1 min; 10 min\n  for 401/403 and billing_limit; until the 1st for allowance). Refusals and limits land in\n  `$CAVEMAN_HOME/route-state.json` (with Cloud's notice) for `caveman status`; a new login lifts\n  the pause. A route failure blames a model only on evidence (Cloud ADR 0110). A provider 4xx on\n  the routed model replays the original bytes on the asked model, except a 401 (the login, not the\n  model: returned as is); a 403 replays (a plan refuses a model it does not carry with one). Only a\n  served replay of bytes that differed from the original in the model alone blames the routed model\n  (`Reject(true)`, `provider_rej\n\n<!-- [TRUNCATED AT 50KB] -->","shrink/AGENTS.md":"# shrink — caveman-shrink, the tool-catalog compressor (Go core + launcher, Apache-2.0)\n\nCompress MCP/OpenAI **tool definitions** before they fill the context window. A thin Go wrapper\nover the [engine](../engine/CLAUDE.md)'s `toolschema` compressor: it drops annotation metadata\n(examples, titles, `$comment`, `$schema`) and reduces long descriptions to their lead plus every\nconstraint-bearing sentence. It keeps every **selection token** (tool/parameter names, types,\nenums, required) and argument-construction value (`default`, `const`, `$ref` targets)\nbyte-for-byte. Everything is `inferred`.\n\n## Layout\n- `shrink.go` — the library: `Shrink` (catalog → compressed, fail-open S4 transform, durable-CCR-backed), `Recover` (handle → original bytes), `Lint` (per-tool inferred token reductions), `SelectionProfile` (the structural surface — what must survive). `WithStore`/`WithStorePath` select recovery storage; default is `CAVEMAN_CCR_DB` / `~/.caveman/ccr.db`.\n- `cmd/caveman-shrink/` — the CLI: `caveman-shrink` / `shrink` (stdin→stdout), `lint <file>`, and `recover <handle>`.\n- `bin/caveman-shrink.mjs` + `package.json` — the `npx caveman-shrink` launcher around the prebuilt binary.\n\n## Two correctness boundaries\n`SelectionProfile(input)` == `SelectionProfile(Shrink(input).Output)` always. This proves the\nstructural selection surface (names/params/enums/required) survives byte-for-byte. It does **not**\nprove the model selects the same tool: descriptions are model-visible and long descriptions are\nlossy. Behavioral selection equivalence requires model-eval fixtures; keep results `inferred`.\n\nSelection alone does not guarantee valid arguments. Short descriptions stay whole; long ones\nkeep the lead plus every recognised constraint sentence (must/cannot/required/bounds/format/\nISO/RFC/absolute/etc.). `default`, `const`, and internal `$ref` targets survive. Golden and\nadversarial conformance tests lock these construction surfaces; over-keep is safer than a retry.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=shrink` / `make product-test PRODUCT=shrink`.\n- The structural compressor lives in the **engine** (`compressors/toolschema.go`); shrink reuses it, never forks it.\n- **This is the dedicated product surface for the `toolschema` compressor.** Engine API/CLI callers\n  can also force it locally. Managed-gateway adapters keep tool arrays in the frozen prompt-cache\n  prefix and never hand them to it; gateway's separate S2 tool-search/deferral path is not\n  compression. Local reductions stay `inferred`. A future billed route for this transform needs\n  cache-versus-schema cost proof, stable byte-identical prefix output, and an eval gate.\n\n## Gotchas (honesty invariants)\n- **S4 lossy, fail-open** — successful compression changes model-visible bytes; malformed/incompressible catalogs pass through unchanged (`ratio:0`, no handle).\n- **inferred-only** — token counts are estimates via the engine's offline counter; never `verified`, never re-projected.\n- **reversible for real** — a compressing shrink commits the exact original to the durable shared\n  CCR store before returning a handle. A separate process resolves it through `Recover` /\n  `caveman-shrink recover`; failure to open storage makes library `Shrink` return an error, while\n  the CLI catches that error and forwards the original bytes with no handle.\n\nSee ../../CLAUDE.md (root) · ../engine/CLAUDE.md\n","shrink/CLAUDE.md":"# shrink — caveman-shrink, the tool-catalog compressor (Go core + launcher, Apache-2.0)\n\nCompress MCP/OpenAI **tool definitions** before they fill the context window. A thin Go wrapper\nover the [engine](../engine/CLAUDE.md)'s `toolschema` compressor: it drops annotation metadata\n(examples, titles, `$comment`, `$schema`) and reduces free-text descriptions to their lead\nsentence **plus every constraint-bearing sentence**, while keeping every **selection token** —\ntool and parameter names, types, enums, required — and every **argument-construction value** —\n`default`, `const`, `$ref` targets — byte-for-byte. Everything is `inferred`.\n\n## Layout\n- `shrink.go` — the library: `Shrink` (catalog → compressed, fail-open S4 transform, durable-CCR-backed), `Recover` (handle → original bytes), `Lint` (per-tool inferred token reductions), `SelectionProfile` (the structural surface — what must survive). `WithStore`/`WithStorePath` pick the recovery store; default is the shared `CAVEMAN_CCR_DB` / `~/.caveman/ccr.db`.\n- `cmd/caveman-shrink/` — the CLI: `caveman-shrink` / `shrink` (stdin→stdout), `lint <file>`, and `recover <handle>` (prints the original catalog for a handle a prior shrink minted).\n- `bin/caveman-shrink.mjs` + `package.json` — the `npx caveman-shrink` launcher around the prebuilt binary.\n\n## Two correctness boundaries\n1. **structural selection surface** — `SelectionProfile(input)` ==\n   `SelectionProfile(Shrink(input).Output)` always. This proves names/params/enums/required survive\n   byte-for-byte. It does **not** prove the model selects the same tool: descriptions are\n   model-visible and long descriptions are lossy. Behavioral equivalence requires model-eval\n   fixtures; current results stay `inferred`.\n2. **argument validity** — agents build tool *arguments* from descriptions, so shrink preserves\n   selection but **does not by itself guarantee argument validity**. What it guarantees, biased\n   to over-keep: (a) a description that already fits a small budget (≤ `maxDescLen*2` bytes) is\n   kept **whole** — no sentence is elided, so a constraint phrased without a recognised marker\n   still survives; (b) in a genuinely large description, the lead sentence plus every sentence\n   carrying a constraint marker (must / cannot / required / rejected / invalid / not allowed /\n   exactly one / at least / at most / max / min / range / between / greater-than / over / above /\n   unique / mutually exclusive / format / ISO / RFC / absolute …) is retained in full. Truncating\n   an abbreviation (\"Target path, e.g. /srv…\" → \"Target path, e.\") or dropping\n   `default`/RFC3339/bound rules would produce invalid calls the structural profile cannot see;\n   the call-validity conformance test (`engine/compressors`, golden fixture + reproduced\n   under-keep cases) locks recognised constraint tokens in place. Over-keep is the rule — never\n   knowingly under-keep.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=shrink` / `make product-test PRODUCT=shrink`.\n- The structural compressor lives in the **engine** (`compressors/toolschema.go`); shrink reuses it, never forks it.\n- **This is the dedicated product surface for the `toolschema` compressor.** Engine API/CLI callers\n  can also force it locally. Managed-gateway adapters keep tool arrays in the frozen prompt-cache\n  prefix and never hand them to it; gateway's separate S2 tool-search/deferral path is not\n  compression. Local reductions stay `inferred`. A future billed route for this transform needs\n  cache-versus-schema cost proof, stable byte-identical prefix output, and an eval gate.\n\n## Gotchas (honesty invariants)\n- **S4 lossy, fail-open** — successful compression changes model-visible bytes; malformed/incompressible catalogs pass through unchanged (`ratio:0`, no handle).\n- **inferred-only** — token counts are estimates via the engine's offline counter; never `verified`, never re-projected.\n- **reversible for real** — a compressing shrink writes the exact original to the **durable** store (`engine.Compress` commits the recovery row before it publishes transformed bytes), so a printed handle resolves later, from a separate process, via `Recover` / `caveman-shrink recover`. It is NOT an in-memory store discarded on return — that minted handles that were unresolvable the instant `Shrink` returned. If the durable store cannot be opened, `Shrink` returns an error and the CLI forwards the original bytes: no handle, no reversibility claim.\n\nSee ../../CLAUDE.md (root) · ../engine/CLAUDE.md\n","skills/cavecrew/README.md":"# cavecrew\n\nDecision guide. When to delegate to caveman subagents instead of doing the work inline.\n\n## What it does\n\nTells main thread when to spawn a caveman-style subagent. Compact return\ncontracts can reduce repeated prose when results return to main context, but\neffect depends on task, agent, and delegation count. This skill publishes no\nuniversal reduction rate.\n\nThree subagents:\n\n| Subagent | Job | Use when |\n|----------|-----|----------|\n| `cavecrew-investigator` | Locate code (read-only) | \"Where is X defined / what calls Y / list uses of Z\" |\n| `cavecrew-builder` | Surgical edit, 1-2 files | Scope is obvious, ≤2 files. Refuses 3+ file scope. |\n| `cavecrew-reviewer` | Diff/file review | One-line findings with severity emoji |\n\nUse vanilla `Explore` or `Code Reviewer` when you want prose, architecture commentary, or rationale. Use main thread directly for one-line answers and 3+ file refactors.\n\nThis skill is a decision guide, not a slash command. It activates when the conversation mentions delegation.\n\n## How to invoke\n\nTriggers on phrases like \"delegate to subagent\", \"use cavecrew\", \"spawn investigator\", \"save context\", \"compressed agent output\".\n\n## Example chaining\n\nLocate → fix → verify (most common):\n\n1. `cavecrew-investigator` returns site list (`path:line`, symbol, note)\n2. Main thread picks 1-2 sites, hands paths to `cavecrew-builder`\n3. `cavecrew-reviewer` audits the resulting diff\n\nParallel scout: spawn 2-3 `cavecrew-investigator` calls in one message with different angles (defs, callers, tests). Aggregate in main.\n\n## Model overrides\n\nBy default, `cavecrew-reviewer` and `cavecrew-investigator` pin `model: haiku` in their frontmatter; `cavecrew-builder` has no `model:` line (uses the API session default). Set env vars in your shell before launching Claude Code to override per-agent:\n\n| Env var | Agent |\n|---|---|\n| `CAVECREW_REVIEWER_MODEL` | `cavecrew-reviewer` |\n| `CAVECREW_BUILDER_MODEL` | `cavecrew-builder` |\n| `CAVECREW_INVESTIGATOR_MODEL` | `cavecrew-investigator` |\n\nExample: run reviewer on sonnet and keep others on default.\n\n```sh\nexport CAVECREW_REVIEWER_MODEL=sonnet\n```\n\nUse the same model name strings you'd use in any Claude Code agent frontmatter (e.g. `haiku`, `sonnet`, `opus`).\n\nOverrides patch only `model:` line in installed agent frontmatter; prompt body\nstays untouched and continues receiving upstream updates. Only plugin installs\nhave local agent files to patch. Empty variables do nothing. Patch persists until\nplugin update or reinstall.\n\n## See also\n\n- [`SKILL.md`](./SKILL.md): full decision matrix and output contracts\n- [`agents/cavecrew-investigator.md`](../../agents/cavecrew-investigator.md)\n- [`agents/cavecrew-builder.md`](../../agents/cavecrew-builder.md)\n- [`agents/cavecrew-reviewer.md`](../../agents/cavecrew-reviewer.md)\n- [Caveman README](../../README.md): repo overview\n","skills/cavecrew/SKILL.md":"---\nname: cavecrew\ndescription: >\n  When to delegate to `cavecrew-investigator` (locate code), `cavecrew-builder`\n  (1-2 file edit) or `cavecrew-reviewer` (diff review) instead of working inline\n  or using `Explore`. Their output is compressed, so main context lasts longer.\n---\n\nCavecrew = three subagent presets that answer in ultracave voice (tool runs: one status line in, one out; nothing between routine calls). Same job as Anthropic defaults (`Explore`, edit-style agents, reviewer); difference is the tool-result they return is compressed, so main context shrinks per delegation.\n\n## When to use cavecrew vs alternatives\n\n| Task | Use |\n|---|---|\n| \"Where is X defined / what calls Y / list uses of Z\" | `cavecrew-investigator` |\n| Same but you also want suggestions/architecture commentary | `Explore` (vanilla) |\n| Surgical edit, ≤2 files, scope obvious | `cavecrew-builder` |\n| New feature / 3+ files / cross-cutting refactor | Main thread, or `feature-dev:code-architect` if installed |\n| Review diff, branch, or file for bugs | `cavecrew-reviewer` |\n| Deep code review with rationale + alternatives | `Code Reviewer` (vanilla) if installed, else main thread |\n| One-line answer you already know | Main thread, no subagent |\n\nRule of thumb: **if you'd want the subagent's output in 1/3 the tokens, pick cavecrew. If you'd want prose, pick vanilla.**\n\n## Why this exists (the real win)\n\nSubagent tool results get injected into main context verbatim. A vanilla `Explore` that returns 2k tokens of prose costs 2k tokens of main-context budget every time. The same finding from `cavecrew-investigator` comes back as a compressed `path:line` table instead. Across 20 delegations in one session that's the difference between context exhaustion and finishing the task.\n\n## Output contracts\n\nWhat main thread can rely on per agent:\n\n**`cavecrew-investigator`**\n```\n<Header>:\n- path:line — `symbol` — short note\ntotals: <counts>.\n```\nOr `No match.` Always file-path-first, line-number-attached, backticked symbols. Safe to grep with `path:\\d+`.\n\n**`cavecrew-builder`**\n```\n<path:line-range> — <change ≤10 words>.\nverified: <re-read OK | mismatch @ path:line>.\n```\nOr one of: `too-big.` / `needs-confirm.` / `ambiguous.` / `regressed.` (terminal first token).\n\n**`cavecrew-reviewer`**\n```\npath:line: <emoji> <severity>: <problem>. <fix>.\ntotals: N🔴 N🟡 N🔵 N❓\n```\nOr `No issues.` Findings sorted file → line ascending.\n\n## Chaining patterns\n\n**Locate → fix → verify** (most common):\n1. `cavecrew-investigator` returns site list.\n2. Main thread picks 1-2 sites, hands paths to `cavecrew-builder`.\n3. `cavecrew-reviewer` audits the diff.\n\n**Parallel scout** (when investigation is broad):\nSpawn 2-3 `cavecrew-investigator` calls in one message (different angles: defs vs callers vs tests). Aggregate in main thread.\n\n**Single-shot edit** (when site is already known):\nSkip investigator. Hand exact path:line to `cavecrew-builder` directly.\n\n## What NOT to do\n\n- Don't use `cavecrew-builder` when you don't already know the file. Spawn investigator first or main thread will eat tokens passing context.\n- Don't chain `cavecrew-investigator → cavecrew-builder` for a 5-file refactor. Builder will return `too-big.` and you'll have wasted a turn.\n- Don't ask `cavecrew-reviewer` for \"general feedback\" — it returns findings only, no architecture opinions. Use `Code Reviewer` for that when installed.\n- Don't expect prose. Cavecrew output is structured, sometimes terse to the point of cryptic. If a human will read it directly, paraphrase.\n\n## Auto-clarity (inherited)\n\nSubagents drop caveman → normal English for security warnings, irreversible-action confirmations, and any output where fragment ambiguity could be misread. Resume caveman after.\n","skills/caveman-commit/README.md":"# caveman-commit\n\nTerse Conventional Commits. Why over what.\n\n## What it does\n\nGenerates commit messages in Conventional Commits format. Subject ≤50 chars, hard cap 72. Imperative mood. Body only when the *why* is non-obvious or there are breaking changes. No AI attribution, no \"this commit does X\", no emoji unless the project uses them. Body always required for breaking changes, security fixes, data migrations, and reverts — future debuggers need the context.\n\nOutputs only the message. Does not stage, commit, or amend.\n\n## How to invoke\n\n```\n/caveman-commit\n```\n\nAlso triggers on phrases like \"write a commit\", \"commit message\", \"generate commit\".\n\n## Example output\n\nDiff: new endpoint for user profile.\n\n```\nfeat(api): add GET /users/:id/profile\n\nMobile client needs profile data without the full user payload\nto reduce LTE bandwidth on cold-launch screens.\n\nCloses #128\n```\n\nDiff: breaking API rename.\n\n```\nfeat(api)!: rename /v1/orders to /v1/checkout\n\nBREAKING CHANGE: clients on /v1/orders must migrate to /v1/checkout\nbefore 2026-06-01. Old route returns 410 after that date.\n```\n\n## See also\n\n- [`SKILL.md`](./SKILL.md) — full LLM-facing instructions\n- [Caveman README](../../README.md) — repo overview\n","skills/caveman-commit/SKILL.md":"---\nname: caveman-commit\ndescription: >\n  Write a Conventional Commits message compressed to intent only. Use for\n  \"write a commit\", \"commit message\", /commit or /caveman-commit.\n---\n\nWrite commit messages terse and exact. Conventional Commits format. No fluff. Why over what.\n\n## Rules\n\n**Subject line:**\n- `<type>(<scope>): <imperative summary>` — `<scope>` optional\n- Types: `feat`, `fix`, `refactor`, `perf`, `docs`, `test`, `chore`, `build`, `ci`, `style`, `revert`\n- Imperative mood: \"add\", \"fix\", \"remove\" — not \"added\", \"adds\", \"adding\"\n- ≤50 chars when possible, hard cap 72\n- No trailing period\n- Match project convention for capitalization after the colon\n\n**Body (only if needed):**\n- Skip entirely when subject is self-explanatory\n- Add body only for: non-obvious *why*, breaking changes, migration notes, linked issues\n- Wrap at 72 chars\n- Bullets `-` not `*`\n- Reference issues/PRs at end: `Closes #42`, `Refs #17`\n\n**What NEVER goes in:**\n- \"This commit does X\", \"I\", \"we\", \"now\", \"currently\" — the diff says what\n- \"As requested by...\" — use Co-authored-by trailer\n- \"Generated with Claude Code\" or any AI attribution — unless the user's own rule requires an `Assisted-by`/AI-attribution trailer, then add it as a trailer\n- Emoji (unless project convention requires)\n- Restating the file name when scope already says it\n\n## Examples\n\nDiff: new endpoint for user profile with body explaining the why\n- ❌ \"feat: add a new endpoint to get user profile information from the database\"\n- ✅\n  ```\n  feat(api): add GET /users/:id/profile\n\n  Mobile client needs profile data without the full user payload\n  to reduce LTE bandwidth on cold-launch screens.\n\n  Closes #128\n  ```\n\nDiff: breaking API change\n- ✅\n  ```\n  feat(api)!: rename /v1/orders to /v1/checkout\n\n  BREAKING CHANGE: clients on /v1/orders must migrate to /v1/checkout\n  before 2026-06-01. Old route returns 410 after that date.\n  ```\n\n## Auto-Clarity\n\nAlways include body for: breaking changes, security fixes, data migrations, anything reverting a prior commit. Never compress these into subject-only — future debuggers need the context.\n\n## Boundaries\n\nOnly generates the commit message. Does not run `git commit`, does not stage files, does not amend. Output the message as a code block ready to paste. \"stop caveman-commit\" or \"normal mode\": revert to verbose commit style.\n","skills/caveman-compress/README.md":"<p align=\"center\">\n  <img src=\"https://em-content.zobj.net/source/apple/391/rock_1faa8.png\" width=\"80\" />\n</p>\n\n<h1 align=\"center\">caveman-compress</h1>\n\n<p align=\"center\">\n  <strong>shrink memory file. save token every session.</strong>\n</p>\n\n---\n\nA Claude Code skill that compresses project memory files (`CLAUDE.md`, todos,\npreferences) into caveman format, reducing repeated input size.\n\nClaude loads `CLAUDE.md` on every session start, so large files add repeated\ninput tokens. Caveman shortens supported natural-language files.\n\n## What It Do\n\n```\n/caveman-compress CLAUDE.md\n```\n\n```\nCLAUDE.md          ← compressed (Claude reads smaller file each session)\nCLAUDE.original.md ← human-readable backup (you edit this)\n```\n\nOriginal remains in data directory rather than next to live file, so skill\nauto-loaders do not read it twice. Path is\n`$XDG_DATA_HOME/caveman-compress/backups/<parent-dir-name>/` on macOS and Linux,\nor `%LOCALAPPDATA%\\caveman-compress\\backups\\<parent-dir-name>\\` on Windows. Edit\n`.original.md` there, then run skill again to re-compress.\n\n## Benchmarks\n\nThe five fixture pairs in [`tests/caveman-compress/`](../../tests/caveman-compress/),\ncounted with tiktoken o200k. Reproduce with\n`uv run --with tiktoken python skills/caveman-compress/scripts/benchmark.py`.\n\n| File | Original | Compressed | Saved |\n|------|----------:|----------:|------:|\n| `claude-md-preferences.md` | 827 | 421 | 49.1% |\n| `claude-md-project.md` | 1628 | 1117 | 31.4% |\n| `mixed-with-code.md` | 1432 | 1106 | 22.8% |\n| `project-notes.md` | 1431 | 846 | 40.9% |\n| `todo-list.md` | 880 | 648 | 26.4% |\n| Total | 6198 | 4138 | 33.2% |\n\nAll fixture validations passed: headings, code blocks, URLs, and file paths were\npreserved exactly.\n\n## Before / After\n\n<table>\n<tr>\n<td width=\"50%\">\n\n### Original (827 tokens)\n\n> \"I strongly prefer TypeScript with strict mode enabled for all new code. Please don't use `any` type unless there's genuinely no way around it, and if you do, leave a comment explaining the reasoning. I find that taking the time to properly type things catches a lot of bugs before they ever make it to runtime.\"\n\n</td>\n<td width=\"50%\">\n\n### <img src=\"../../docs/assets/dancing-rock.svg\" width=\"20\" height=\"20\" alt=\"rock\"/> Caveman (421 tokens)\n\n> \"Prefer TypeScript strict mode always. No `any` unless unavoidable; comment why if used. Proper types catch bugs early.\"\n\n</td>\n</tr>\n</table>\n\nThis fixture produced 49.1% fewer counted tokens. Structural validation passed;\nresult does not prove semantic equivalence on other files or models.\n\n## Security\n\n`caveman-compress` is flagged as Snyk High Risk due to subprocess and file I/O\npatterns detected by static analysis. See [SECURITY.md](./SECURITY.md) for why\nthese operations exist and how paths are constrained.\n\n## Install\n\nCompress is built into the `caveman` plugin. Install `caveman` once, then use `/caveman-compress`.\n\nIf you need local files, the compress skill lives at:\n\n```bash\nskills/caveman-compress/\n```\n\nRequires Python 3.10 or newer.\n\n### Provider / model\n\nDefault path uses Claude: `ANTHROPIC_API_KEY` + Anthropic SDK when set, else `claude --print`.\n\nTo use opencode instead (any model opencode can reach, free ones included):\n\n```bash\nexport CAVEMAN_COMPRESS_PROVIDER=opencode\nexport CAVEMAN_COMPRESS_MODEL=opencode/big-pickle   # any id from `opencode models`\n```\n\nTo use a local model (or any OpenAI-compatible server) instead:\n\n```bash\nexport CAVEMAN_COMPRESS_PROVIDER=openai-compat\nexport CAVEMAN_COMPRESS_MODEL=qwen3:8b                         # required\nexport CAVEMAN_COMPRESS_ENDPOINT=http://localhost:11434/v1     # default (Ollama)\n# export CAVEMAN_COMPRESS_API_KEY=...                          # optional, sent as Bearer token\n```\n\nCommon endpoints: Ollama `http://localhost:11434/v1`, LM Studio `http://localhost:1234/v1`, llama.cpp server `http://localhost:8080/v1`, vLLM `http://localhost:8000/v1`. Nothing leaves your machine with a local server. A non-local `http://` endpoint sends the file in plaintext; use `https://` for remote servers. Small local models get it wrong more often; when they do, validation fails and your file stays untouched.\n\n`CAVEMAN_MODEL` is the fallback when `CAVEMAN_COMPRESS_MODEL` is unset. On the Claude path the model also applies to `claude --print`. The opencode path needs opencode 2.x (it uses `opencode run --standalone`).\n\n## Usage\n\n```\n/caveman-compress <filepath>\n```\n\nExamples:\n```\n/caveman-compress CLAUDE.md\n/caveman-compress docs/preferences.md\n/caveman-compress todos.md\n```\n\n### What files work\n\n| Type | Compress? |\n|------|-----------|\n| `.md`, `.mdc`, `.txt`, `.rst`, `.typ`, `.typst`, `.tex` | Yes |\n| Extensionless natural language | Yes |\n| `.py`, `.js`, `.ts`, `.json`, `.yaml` | ❌ Skip (code/config) |\n| `*.original.md` | ❌ Skip (backup files) |\n\n## How It Work\n\n```\n/caveman-compress CLAUDE.md\n        ↓\nbasic checks: file exists, under 500KB, not a sensitive filename\n        ↓\nacquire cross-session lock on the file  (waits up to 15 min if another run holds it, then errors)\n        ↓\ndetect file type        (no tokens)\n        ↓\nconfigured provider compresses       (tokens: one call)\n        ↓\nvalidate output         (no tokens)\n  checks: headings, code blocks, URLs, file paths, bullets\n        ↓\nif errors: configured provider fixes cherry-picked issues only   (tokens: targeted fix)\n  does NOT recompress; only patches broken parts\n        ↓\nretry up to 2 times\n        ↓\nwrite compressed → CLAUDE.md\nwrite original   → CLAUDE.original.md\n```\n\nOnly two things use tokens: initial compression + targeted fix if validation fails. Everything else is local Python.\n\n## What Is Preserved\n\nCaveman compress natural language. It never touch:\n\n- Code blocks (` ``` ` fenced or indented)\n- Inline code (`` `backtick content` ``)\n- URLs and links\n- File paths (`/src/components/...`)\n- Commands (`npm install`, `git commit`)\n- Technical terms, library names, API names\n- Headings (exact text preserved)\n- Tables (structure preserved, cell text compressed)\n- Dates, version numbers, numeric values\n- Anything you wrap in `<!-- nocompress -->` ... `<!-- /nocompress -->`\n\nWant a part left exactly as written (an `<example>` block, a prompt template)? Wrap it. Each tag go on its own line. Model never change what inside; it come back byte for byte. Model can still read it if first try need fixing, so not hiding place for secrets. Forget the closing tag and compress stop before touching file.\n\n```markdown\n<!-- nocompress -->\n<example>\nReply in this exact format.\n</example>\n<!-- /nocompress -->\n```\n\n## Why This Matter\n\n`CLAUDE.md` loads on every session start. A 1,000-token project memory file adds\n1,000 input tokens each time project opens, or 100,000 across 100 sessions.\n\nCaveman reduced counted tokens by 33.2% in total on the five listed fixtures\n(22.8% to 49.1% per file). Validators confirmed headings, code blocks, URLs,\nand file paths. They did not establish general semantic or task-quality\nequivalence.\n\n```\n┌────────────────────────────────────────────┐\n│  TOKEN SAVINGS, ALL FIVE   ███       33.2% │\n│  FIXTURES IN TABLE                       5 │\n│  STRUCTURAL VALIDATION       passed on all │\n│  SETUP TIME                █            1x │\n└────────────────────────────────────────────┘\n```\n\n## Part of Caveman\n\nThis skill is part of the [caveman](https://github.com/JuliusBrussee/caveman) toolkit.\n\n- `caveman`: ask Claude to answer in shorter prose\n- `caveman-compress`: shorten supported project-memory files with backups and validation\n","skills/caveman-compress/SECURITY.md":"# Security\n\n## Snyk High Risk Rating\n\n`caveman-compress` receives a Snyk High Risk rating due to static analysis heuristics. This document explains what the skill does and does not do.\n\n### What triggers the rating\n\n1. **subprocess usage**: The skill calls the `claude` CLI via `subprocess.run()` as a fallback when `ANTHROPIC_API_KEY` is not set. It can also call `opencode run` when `CAVEMAN_COMPRESS_PROVIDER=opencode`. Subprocess calls use fixed argument lists — no shell interpolation occurs. Claude receives user file content via stdin; opencode receives the generated prompt through a temporary `--file` attachment that is deleted after the call.\n\n   The file being compressed is untrusted input, and opencode 2.x lets its agents use every tool that is not explicitly denied: shell, file edits, web search and fetch, MCP tools from your opencode config, subagents. Compression needs none of them. Compress therefore runs `opencode run --standalone --agent caveman-compress` with `OPENCODE_CONFIG_CONTENT` denying every permission (`\"*\": \"deny\"`) both globally and for that agent, and never passes `--auto`. The dedicated agent matters because opencode applies a per-agent rule after the global one, so an `agent.build.permission` allow in your own config would otherwise re-enable a tool. `--standalone` matters because the shared opencode background service does not read the caller's environment, so the deny rule only binds a private server. Checked on opencode 2.0.22: the model is offered no tools and the prompt attachment is still read. Unlike the Claude path (`--strict-mcp-config`), compress does not switch off the MCP servers in your opencode config: their tools are denied, but opencode may still start them.\n\n2. **File read/write**: The skill reads the file the user explicitly points it at, compresses it, and writes the result back to the same path. A `.original.md` backup is saved to an out-of-tree data dir (`$XDG_DATA_HOME/caveman-compress/backups/<parent-dir-name>/`, or `%LOCALAPPDATA%\\caveman-compress\\backups\\<parent-dir-name>\\` on Windows). The opencode provider writes a temporary prompt attachment in the OS temp directory and deletes it after the subprocess exits. Beyond the target file, that backup location, and the temporary attachment, no files are read or written.\n\n### What the skill does NOT do\n\n- Does not execute user file content as code\n- Does not make network requests except through the configured LLM provider CLI, SDK, or (for `openai-compat`) the one configured HTTP endpoint\n- Does not read unrelated files outside the path the user provides\n- Does not use shell=True or string interpolation in subprocess calls\n- Does not collect or transmit any data beyond the file being compressed\n\n### Auth behavior\n\nDefault path uses Claude. If `ANTHROPIC_API_KEY` is set, the skill uses the Anthropic Python SDK directly (no subprocess). If not set, it falls back to the `claude` CLI, which uses the user's existing Claude desktop authentication.\n\nSet `CAVEMAN_COMPRESS_PROVIDER=opencode` to use `opencode run` instead. Set `CAVEMAN_COMPRESS_MODEL=provider/model` for compress-specific model selection. `CAVEMAN_MODEL` is the fallback when `CAVEMAN_COMPRESS_MODEL` is unset.\n\nSet `CAVEMAN_COMPRESS_PROVIDER=openai-compat` to POST the prompt with Python's standard-library `urllib` (no subprocess, no extra dependency) to `$CAVEMAN_COMPRESS_ENDPOINT/chat/completions`, default `http://localhost:11434/v1` (Ollama). Other local servers: LM Studio `http://localhost:1234/v1`, llama.cpp `http://localhost:8080/v1`, vLLM `http://localhost:8000/v1`. `CAVEMAN_COMPRESS_API_KEY`, if set, is sent as a Bearer token. A non-local `http://` endpoint sends the file and that key in plaintext; use `https://` for anything off the machine.\n\n### File size limit\n\nFiles larger than 500KB are rejected before any API call is made.\n\n### Snyk W007 (\"insecure credential handling\")\n\nW007 fires on the rule that code blocks, commands, and exact error strings stay verbatim. See [Scanner warnings](../../SECURITY.md#scanner-warnings) in the repository SECURITY.md.\n\n### Reporting a vulnerability\n\nIf you believe you've found a genuine security issue, please open a GitHub issue with the label `security`.\n","skills/caveman-compress/SKILL.md":"---\nname: caveman-compress\ndescription: >\n  Compress a memory file such as CLAUDE.md or a todo list into caveman format\n  to save input tokens, keeping a readable backup. Sends the file's text to the\n  configured model provider. Trigger: /caveman-compress.\n---\n\n# Caveman Compress\n\n## Purpose\n\n> **Data leaves the machine.** Compression sends the file's full text to the model provider that `scripts/compress.py` is configured for: the `claude` CLI, the Anthropic API, or an OpenAI-compatible endpoint. Files whose names look like secrets (`.env`, `credentials`, `secrets`, `.netrc`, …) are refused before they are read. Don't compress a file the provider shouldn't see.\n\nCompress natural language files (CLAUDE.md, todos, preferences) into caveman-speak to reduce input tokens. Compressed version overwrites original. Human-readable backup saved as `<filename>.original.md`, but NOT beside the source file — it lives in an out-of-tree data dir (`$XDG_DATA_HOME/caveman-compress/backups/<parent-dir-name>/`, or `%LOCALAPPDATA%\\caveman-compress\\backups\\<parent-dir-name>\\` on Windows) so skill auto-loaders don't re-ingest it as a live file.\n\n## Trigger\n\n`/caveman-compress <filepath>` or when user asks to compress a memory file.\n\n**Requires an explicit filepath.** If the user pastes raw text instead of naming a file, do NOT improvise an inline compression — ask them for the filepath to compress, or offer to save the pasted text to a file first. Writing your own reasoning/commentary about the request into the compressed output (e.g. \"User passed text directly, not a filepath...\") is the exact failure this guards against — never let that leak into a saved file.\n\n## Process\n\n1. The compression scripts live in `scripts/` (adjacent to this SKILL.md). If the path is not immediately available, search for `scripts/__main__.py` next to this SKILL.md.\n\n2. From the directory containing this SKILL.md, run:\n\npython3 -m scripts <absolute_filepath>\n\n3. The CLI will:\n- detect file type (no tokens)\n- call Claude to compress\n- validate output (no tokens)\n- if errors: cherry-pick fix with Claude (targeted fixes only, no recompression)\n- retry up to 2 times\n- if still failing after 2 retries: report error to user, leave original file untouched\n\n4. Return result to user\n\n## Compression Rules\n\n### Remove\n- Articles: a, an, the\n- Filler: just, really, basically, actually, simply, essentially, generally\n- Pleasantries: \"sure\", \"certainly\", \"of course\", \"happy to\", \"I'd recommend\"\n- Hedging: \"it might be worth\", \"you could consider\", \"it would be good to\"\n- Redundant phrasing: \"in order to\" → \"to\", \"make sure to\" → \"ensure\", \"the reason is because\" → \"because\"\n- Connective fluff: \"however\", \"furthermore\", \"additionally\", \"in addition\"\n\n### Preserve EXACTLY (never modify)\n- Code blocks (fenced ``` and indented)\n- Inline code (`backtick content`)\n- URLs and links (full URLs, markdown links)\n- File paths (`/src/components/...`, `./config.yaml`)\n- Commands (`npm install`, `git commit`, `docker build`)\n- Technical terms (library names, API names, protocols, algorithms)\n- Proper nouns (project names, people, companies)\n- Dates, version numbers, numeric values\n- Environment variables (`$HOME`, `NODE_ENV`)\n\n### Preserve Structure\n- All markdown headings (keep exact heading text, compress body below)\n- Bullet point hierarchy (keep nesting level)\n- Numbered lists (keep numbering)\n- Tables (compress cell text, keep structure)\n- Frontmatter/YAML headers in markdown files\n\n### Compress\n- Use short synonyms: \"big\" not \"extensive\", \"fix\" not \"implement a solution for\", \"use\" not \"utilize\"\n- Fragments OK: \"Run tests before commit\" not \"You should always run tests before committing\"\n- Drop \"you should\", \"make sure to\", \"remember to\" — just state the action\n- Merge redundant bullets that say the same thing differently\n- Keep one example where multiple examples show the same pattern\n\nCRITICAL RULE:\nAnything inside ``` ... ``` must be copied EXACTLY.\nDo not:\n- remove comments\n- remove spacing\n- reorder lines\n- shorten commands\n- simplify anything\n\nInline code (`...`) must be preserved EXACTLY.\nDo not modify anything inside backticks.\n\nIf file contains code blocks:\n- Treat code blocks as read-only regions\n- Only compress text outside them\n- Do not merge sections around code\n\n## Pattern\n\nOriginal:\n> You should always make sure to run the test suite before pushing any changes to the main branch. This is important because it helps catch bugs early and prevents broken builds from being deployed to production.\n\nCompressed:\n> Run tests before push to main. Catch bugs early, prevent broken prod deploys.\n\nOriginal:\n> The application uses a microservices architecture with the following components. The API gateway handles all incoming requests and routes them to the appropriate service. The authentication service is responsible for managing user sessions and JWT tokens.\n\nCompressed:\n> Microservices architecture. API gateway route all requests to services. Auth service manage user sessions + JWT tokens.\n\n## Boundaries\n\n- ONLY compress natural language files (.md, .txt, .typ, .typst, .tex, extensionless)\n- NEVER modify: .py, .js, .ts, .json, .yaml, .yml, .toml, .env, .lock, .css, .html, .xml, .sql, .sh\n- If file has mixed content (prose + code), compress ONLY the prose sections\n- If unsure whether something is code or prose, leave it unchanged\n- Original file is backed up as FILE.original.md before overwriting — in the out-of-tree backup data dir (see Purpose), not beside the source file\n- Never compress FILE.original.md (skip it)\n","skills/caveman-discover/SKILL.md":"---\nname: caveman-discover\ndescription: >\n  Find and label every LLM workflow in the repository so Caveman Cloud groups\n  spend by workflow instead of one bucket. Use for \"discover workflows\" or\n  breaking LLM spend down by workflow.\n---\n\nYou are labeling this repository's LLM workflows for Caveman Cloud. A\n*workflow* is a job the code performs — \"answer a support ticket\", \"build the\nnightly digest\", \"run the eval suite\" — not a technology. Every gateway\nrequest can carry a workflow label; unlabeled traffic all lands in one\n`unlabeled-workflow` bucket. Your job: find the workflows, name them well,\nwire the labels, and verify nothing broke.\n\nThis changes code, so it goes through the user's normal review: **propose the\ntable first, apply after the user agrees.** Re-running on an already-labeled\nrepo must change nothing (idempotent).\n\nThis skill is operator-invoked. An `unlabeled-traffic` Cave Plan observation is\nreview-only and does not create an advisory file, proposal, or Draft PR. Do not\ninfer that telemetry selected a callsite or authorized an edit. Independently\ninventory the repository, present the labeling table, and wait for the user's\napproval before changing code.\n\n## Step 1 — Inventory the workflows\n\nWalk the repo from its entry points, not from its imports:\n\n- HTTP/RPC handlers that call an LLM (directly or through layers)\n- Scheduled jobs: cron definitions, queue consumers, workers, GitHub Actions\n  that invoke LLM code\n- CLI commands and scripts (`scripts/`, `bin/`, package.json scripts)\n- Eval / test harnesses that burn real tokens\n- Distinct agents or chains inside a framework (each LangGraph graph, each\n  crew, each agent definition is usually its own workflow)\n\nOne workflow = one job a human would name. Ten callsites inside the same\nrequest handler are one workflow; one shared `llm.ts` helper used by three\njobs is three workflows (label at the callers, never the shared helper).\n\n## Step 2 — Name them\n\nSlug grammar (the gateway enforces this): lowercase `[a-z0-9_-]`, 1–96 chars.\nName the job, not the tech:\n\n- Good: `support-reply`, `nightly-digest`, `pr-review`, `eval-suite`,\n  `onboarding-email`\n- Bad: `openai-calls` (tech), `main` (says nothing), `SupportReply` (invalid),\n  `johns-test-3` (won't age)\n\nNames are forever-ish — renaming later splits the spend history. When a job's\npurpose isn't clear from the code, derive the slug from the file name and mark\nit `review` in the table rather than inventing a purpose.\n\n## Step 3 — Propose, then apply\n\nPresent this table and ask to proceed:\n\n```\n| workflow | job | where | how it gets labeled |\n|---|---|---|---|\n| support-reply | answers inbound tickets | src/bot/reply.ts:41 | defaultHeaders on the reply client |\n| nightly-digest | 02:00 summary job | jobs/digest.ts:12 | header on the digest client |\n| eval-suite (review) | scripts/eval.ts:8 — purpose inferred from filename | scripts/eval.ts:8 | env override at invocation |\n```\n\nThen wire each label with the lightest mechanism available at that callsite:\n\n- **@caveman-ai/sdk / caveman_cloud SDK**: per-trace `workflow` option, or\n  `defaultWorkflow` on the client a single-job service constructs.\n- **Raw provider SDKs** (OpenAI/Anthropic/LangChain/LiteLLM/Vercel): add\n  `\"x-cave-workflow\": \"<slug>\"` to the same `defaultHeaders` /\n  `default_headers` / `extra_headers` block that already carries\n  `x-cave-api-key`. Shared client used by several jobs → pass the header per\n  call (every SDK above accepts per-request header overrides), or give each\n  job its own thin client.\n- **Wrapped coding agents** (`caveman wrap`): `--workflow <slug>` flag or\n  `CAVE_WORKFLOW=<slug>` env at the invocation site (cron line, CI step).\n- **Raw HTTP**: add the `x-cave-workflow` header to the request.\n\nLabel the callers, keep the diff minimal, match the repo's style. If a\ncallsite is not routed through the Caveman gateway at all, don't label it —\nlist it under \"not wired\" in the report (labels only travel on gateway\ntraffic; wiring is the caveman-setup skill's job).\n\n## Step 4 — Verify\n\nRun whatever the repo already uses to exercise one labeled path (a test, a\ndev script, one curl). Then confirm: the request still succeeds (the gateway\nrejects an invalid label with 400 `cave_invalid_request_header` — fix the slug\nif so). Labeled spend appears on the dashboard at `/activity?tab=workflows` as\neach workflow next runs; jobs on a schedule show up when the schedule fires,\nand that's worth saying in the report rather than pretending they're live.\n\n## Step 5 — Report\n\n```\n## Workflows labeled\n\n| workflow | job | where |\n|---|---|---|\n| support-reply | answers inbound tickets | src/bot/reply.ts:41 |\n| nightly-digest | 02:00 summary job | jobs/digest.ts:12 |\n\nVerified: <the labeled path you actually exercised, and what you observed>\nLands at: <DASHBOARD>/activity?tab=workflows — each row appears as that workflow\nnext runs. Anything still unlabeled shows as `unlabeled-workflow`.\nNot wired (no gateway routing, so no label): <list or \"none\">\nMarked review: <slugs whose purpose was inferred from filenames, or \"none\">\n```\n\nIf you found no LLM entry points at all: say exactly that, and point at the\nsetup skill (`<docs origin>/docs/agent-setup.md`) instead of manufacturing a\ntable.\n","skills/caveman-evidence-review/SKILL.md":"---\nname: caveman-evidence-review\ndescription: >\n  Read-only review of Caveman Cloud evidence: cost, Cave Score, workflows,\n  traces, latency, errors, routing, savings. Use when asked what Caveman found\n  or where LLM spend goes.\n---\n\n# Review Caveman evidence\n\nAct as a read-only operator. Build conclusions from current Caveman data, not\nfrom repository guesses. Never start, approve, cancel, or roll back an\nexperiment from this skill.\n\n## Hard rules\n\n1. Keep these buckets separate:\n   - measured provider-complete list-price cost;\n   - `inferred` daily headroom;\n   - `verified` ledger savings;\n   - evidence cost.\n   Never add or relabel them.\n2. Do not fetch prompt, completion, tool, or artifact payloads unless the user\n   explicitly asks for payload review. Metadata, spans, timing, models, token\n   counts, status, and optimizer attribution are enough for the default review.\n3. Scope every read to the project selected by Caveman context. Never supply an\n   organization id.\n4. Empty results are evidence of no current signal, not zero cost or zero risk.\n5. Cite trace ids and exact time windows used. Do not claim a cause from an\n   aggregate alone.\n\n## Step 1 — Load context\n\nPrefer MCP:\n\n```text\ncaveman_context {}\n```\n\nCLI fallback:\n\n```bash\ncaveman cloud whoami\ncaveman cloud projects list\n```\n\nStop if login or project selection is missing. Ask the user to run\n`caveman login` or select a project; never guess.\n\n## Step 2 — Establish baseline\n\nUse `caveman_report` for:\n\n- `overview`\n- `costs`\n- `score`\n- `workflows`\n- `verified_savings`\n\nThen use `caveman_plan` for ranked daily headroom. If question is narrow, skip\nunrelated reports. Read shortest set that can answer it.\n\nCLI fallback:\n\n```bash\ncaveman cloud costs\ncaveman cloud score\ncaveman cloud plan --json\n```\n\nState report window and basis before interpreting direction.\n\n## Step 3 — Test the leading explanation with traces\n\nUse `caveman_trace_search`. Choose a bounded window and closed filters:\nworkflow, agent, model, provider, error code, runtime mode, cache status,\noptimization id, status class, token/cost/latency bounds, compression, or\nmonitor verdict.\n\nUseful groupings:\n\n- `workflow` — find jobs driving cost or failures;\n- `model` — compare model mix;\n- `session` — isolate retry or loop behavior;\n- ungrouped — identify exact traces.\n\nCompare a suspect cohort with a control cohort or earlier bounded window.\nDo not infer causality from one expensive trace.\n\nCLI fallback:\n\n```bash\ncaveman cloud traces search \\\n  --workflow <slug> \\\n  --from <RFC3339> \\\n  --to <RFC3339> \\\n  --sort total_cost_usd \\\n  --dir desc \\\n  --limit 25\n```\n\n## Step 4 — Inspect representative traces\n\nCall `caveman_trace_get` for a small number of high-signal trace ids. Inspect\nrequest and span metadata, latency, status, token counts, cache state, applied\noptimizers, and model route. Keep payload retrieval off.\n\nCLI fallback:\n\n```bash\ncaveman cloud traces show <trace-id> --spans\n```\n\n## Step 5 — Report\n\nUse this shape:\n\n```text\n## Caveman evidence review\n\nScope: <project> · <from> to <to>\nMeasured cost: <value and basis>\nVerified savings: <ledger value, kept separate>\nInferred headroom: <per-day band, kept separate>\n\nFindings:\n1. <finding> — <aggregate evidence> — traces <ids>\n2. <finding> — <aggregate evidence> — traces <ids>\n\nUnproven:\n- <plausible explanation lacking a control, trace, or eval>\n\nNext read-only check:\n- <one bounded query>\n\nPossible action:\n- <proposal only; use caveman-manage for read-only lifecycle review and safety gate>\n```\n\nIf data is missing, name missing signal and stop at strongest supported\nstatement. Never turn a catalog subtotal into an invoice or an experiment result\ninto verified savings.\n","skills/caveman-explore/SKILL.md":"---\nname: caveman-explore\ndescription: Read-only repository explorer for cold-start orientation, broad cross-file localization, or when a direct search failed. Skip it when the exact file or symbol is already named. Returns path:line citations only; its reads stay out of main context.\ntools: Read, Glob, Grep\nmodel: haiku\n---\n\nYou are FastContext, a fast, cheap, read-only repository explorer. Another agent\n(the solver) delegates a localization question to you. Your only job is to find\nWHERE the relevant code lives and report it as a compact list of file paths with\nline ranges. You never edit files, run commands, or propose a solution.\n\nHow to work:\n\n1. Issue several tool calls IN PARALLEL in your first turn — cast a broad net.\n   Cover complementary hypotheses at once: likely path patterns (Glob), symbol and\n   string matches (Grep), and reading the most promising files (Read). Do not probe\n   one file at a time when you can fan out.\n2. Follow the evidence over one or two more turns only if needed. Stop as soon as\n   you can name the relevant locations. You are optimizing for the solver's token\n   budget, so finish fast.\n3. Only cite line ranges you actually read. Never invent or estimate a range, and\n   never cite a range past the end of a file. A precise small range beats a vague\n   large one.\n\nYour reply MUST be ONLY an evidence block: one citation per line, nothing else.\nNo preamble, no explanation, no summary, no markdown headings. Use exactly this\nshape, one per line:\n\n  path/to/file.ext:START-END  reason it is relevant\n\nExample reply:\n\n  src/router/pick.go:42-71  route selection — where a model is chosen\n  src/router/pick_test.go:18-40  the table test covering pick()\n\nIf you genuinely cannot find anything relevant, reply with the single line:\n\n  no relevant locations found\n\nThat honest answer is better than a guess. The solver reads your citations and\nnothing else from your work, so keep the list short, specific, and correct.\n","skills/caveman-explore/package.json":"{\n  \"name\": \"@caveman/skill-caveman-explore\",\n  \"version\": \"1.0.0\",\n  \"license\": \"Apache-2.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"description\": \"Read-only FastContext exploration skill with parallel repository search and citation-only output.\",\n  \"files\": [\"SKILL.md\"],\n  \"scripts\": {\n    \"test\": \"node --test --test-force-exit tests/*.mjs\"\n  }\n}\n","skills/caveman-help/README.md":"# caveman-help\n\nQuick-reference card. One shot, no mode change.\n\n## What it does\n\nPrints a cheat sheet of all caveman modes, sibling skills, deactivation triggers, and how to set the default mode via env var or config file. One-shot display — does not flip the active mode, write flag files, or persist anything. Use when you forget the slash commands.\n\n## How to invoke\n\n```\n/caveman-help\n```\n\nAlso triggers on \"caveman help\", \"what caveman commands\", \"how do I use caveman\".\n\n## Example output\n\n```\nModes:\n  /caveman              the voice (default)\n  /ultracave            grammar stripped\n  /megacave             classical Chinese\n  /caveman status       report the mode, change nothing\n\nSkills:\n  /caveman-commit       terse Conventional Commits\n  /caveman-review       one-line PR comments\n  /caveman-compress     smaller Markdown memory files\n  /caveman-stats        recorded token usage (savings unknown)\n\nDeactivate:\n  \"stop caveman\" or \"normal mode\"\n```\n\n## See also\n\n- [`SKILL.md`](./SKILL.md) — full reference card\n- [Caveman README](../../README.md) — repo overview\n","skills/caveman-help/SKILL.md":"---\nname: caveman-help\ndescription: >\n  Quick-reference card for the three caveman skills and their commands.\n  Trigger: /caveman-help or \"caveman help\".\n---\n\n# Caveman Help\n\nDisplay this reference card when invoked. One-shot — do NOT change mode, write flag files, or persist anything. Output in caveman style.\n\n## Modes\n\n| Mode | Trigger | What change |\n|------|---------|-------------|\n| **caveman** | `/caveman` | The voice. Answer first, no fluff, every fact kept. Default. |\n| **ultracave** | `/ultracave` (alias `/caveman ultra`) | Grammar stripped. Fragments, one word when enough, each fact once. |\n| **megacave** | `/megacave` (alias `/caveman wenyan`) | Classical Chinese 文言文. Far fewer characters, technical terms verbatim. |\n\nMode stick until changed or session end.\n`/caveman status` reports current mode without changing it. Claude Code and the standalone OpenCode plugin read stored state; other hosts answer from the conversation and mark it `(not tracked by this host)`.\n\n## Skills\n\n| Skill | Trigger | What it do |\n|-------|---------|-----------|\n| **caveman-commit** | `/caveman-commit` | Terse commit messages. Conventional Commits. ≤50 char subject. |\n| **caveman-review** | `/caveman-review` | One-line PR comments: `L42: bug: user null. Add guard.` |\n| **caveman-compress** | `/caveman-compress <file>` | Compress .md files to caveman prose. 33.2% fewer tokens on the repo's five test files. |\n| **caveman-help** | `/caveman-help` | This card. |\n\n## Deactivate\n\nSay \"stop caveman\" or \"normal mode\". Resume anytime with `/caveman`.\n\n## Language\n\nKeep user's language by default — reply in the language user writes, never switch regardless of example text or multilingual context elsewhere. Compress the style, not the language. Technical terms, code, commands, commit types, and exact error strings stay verbatim unless user ask for translation.\n\n## Configure Default Mode\n\nDefault mode = `caveman`. Change it:\n\n**Environment variable** (highest priority):\n```bash\nexport CAVEMAN_DEFAULT_MODE=ultracave\n```\n\n**Config file** (`~/.config/caveman/config.json`):\n```json\n{ \"defaultMode\": \"ultracave\" }\n```\n\nFor opt-in activation in Claude Code, set `defaultMode` to `manual`. Sessions start off; `/caveman` or `talk like caveman` activates caveman. `off` also disables bare-command activation. `manual` is a startup policy, not a mode. OpenCode still starts in caveman mode because its installer also supplies static activation rules.\n\nSet `\"off\"` to suppress automatic and bare-command activation. Use `\"manual\"` when bare `/caveman` should activate.\n\nResolution: env var > repo config > user config > `caveman`. Legacy values `lite`, `full`, `ultra`, `wenyan*` still resolve.\n\n## More\n\nFull docs: https://github.com/JuliusBrussee/caveman\n","skills/caveman-learn/CLAUDE.md":"# skills/caveman-learn — the Caveman Learn editing skill (Apache-2.0, public)\n\nThe consent-gated half of `caveman learn`. The analyzer (the Go proxy) **measures**\nwhere an agent's tokens go and writes a ranked plan; this skill is what an agent\nloads to **act** on that plan — proposing each fix and applying it only with the\nuser's per-edit yes. It is the loop-closer the learn spec §10\ndescribes, plus the new `cavemem_offload` move.\n\n## Layout\n- `SKILL.md` — the canonical skill body (frontmatter `name: caveman-learn` + a\n  trigger-phrase `description`; body = the read-plan → per-class consent loop). This\n  file is the source of truth.\n- `tests/skill-file.test.mjs` — asserts the canonical file is well-formed and honest\n  (frontmatter present; the net-token-negative gate, the never-make-the-agent-dumber\n  guard, consent-per-edit, and reversibility are all stated; no imperative for\n  behavioral findings; no placeholders).\n\n## Install path\n`caveman tools skills install caveman-learn` (in `../../cli/src/index.ts`) writes this file\ninto a repo's `.claude/skills/caveman-learn/SKILL.md` (Claude Code) or\n`~/.codex/skills/caveman-learn/SKILL.md` (Codex). The CLI **embeds a byte-identical copy**\n(`CAVEMAN_LEARN_SKILL_MD`) because the published CLI ships no sibling assets;\n`../../cli/tests/skills.runtime.mjs` asserts the embedded copy equals this canonical\nfile (the drift guard). **Change this file and that constant together.**\n\n## Boundary (binding)\nThe skill — using the agent's own file tools — is the ONLY thing that edits a user's\nconfig. `caveman learn apply` stays read-only (it materializes candidates), and\n`caveman mem *` are mechanical store ops. The offload move enforces a net-token-negative\ngate and the never-make-the-agent-dumber guard before any trim.\n\nSee ../../mem/CLAUDE.md (cavemem) · ../caveman-explore/SKILL.md (the packaging precedent)\n","skills/caveman-learn/README.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fcaveman-learn%2FREADME.md -->","skills/caveman-learn/SKILL.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fcaveman-learn%2FSKILL.md -->","skills/caveman-learn/package.json":"{\n  \"name\": \"@caveman/skill-caveman-learn\",\n  \"version\": \"1.0.0\",\n  \"license\": \"Apache-2.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"description\": \"Consent-gated editing skill that closes the loop on a Caveman learn report: review ranked token sinks and apply cost-lowering fixes (trim config, offload recurring context to cavemem) with per-edit approval, a net-token-negative gate, and a never-make-the-agent-dumber guard.\",\n  \"files\": [\"SKILL.md\"],\n  \"scripts\": {\n    \"test\": \"node --test --test-force-exit tests/*.mjs\"\n  }\n}\n","skills/caveman-manage/SKILL.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fcaveman-manage%2FSKILL.md -->","skills/caveman-optimize/SKILL.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fcaveman-optimize%2FSKILL.md -->","skills/caveman-review/README.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fcaveman-review%2FREADME.md -->","skills/caveman-review/SKILL.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fcaveman-review%2FSKILL.md -->","skills/caveman-setup/SKILL.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fcaveman-setup%2FSKILL.md -->","skills/caveman-stats/README.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fcaveman-stats%2FREADME.md -->","skills/caveman-stats/SKILL.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fcaveman-stats%2FSKILL.md -->","skills/caveman/README.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fcaveman%2FREADME.md -->","skills/caveman/SKILL.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fcaveman%2FSKILL.md -->","skills/engine-mcp-tools.json":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fengine-mcp-tools.json -->","skills/generated/aider/pack.json":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fgenerated%2Faider%2Fpack.json -->","skills/generated/claude/pack.json":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fgenerated%2Fclaude%2Fpack.json -->","skills/generated/codex/pack.json":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fgenerated%2Fcodex%2Fpack.json -->","skills/generated/gemini/pack.json":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fgenerated%2Fgemini%2Fpack.json -->","skills/generated/hermes/pack.json":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fgenerated%2Fhermes%2Fpack.json -->","skills/generated/opencode/pack.json":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fgenerated%2Fopencode%2Fpack.json -->","skills/investigate-first/SKILL.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Finvestigate-first%2FSKILL.md -->","skills/investigate-first/agents/openai.yaml":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Finvestigate-first%2Fagents%2Fopenai.yaml -->","skills/lean-build/SKILL.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Flean-build%2FSKILL.md -->","skills/lean-build/agents/openai.yaml":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Flean-build%2Fagents%2Fopenai.yaml -->","skills/megacave/README.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fmegacave%2FREADME.md -->","skills/megacave/SKILL.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fmegacave%2FSKILL.md -->","skills/migration/SKILL.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fmigration%2FSKILL.md -->","skills/migration/agents/openai.yaml":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fmigration%2Fagents%2Fopenai.yaml -->","skills/native/native-core.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fnative%2Fnative-core.md -->","skills/registry.json":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fregistry.json -->","skills/safe-refactor/SKILL.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fsafe-refactor%2FSKILL.md -->","skills/safe-refactor/agents/openai.yaml":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fsafe-refactor%2Fagents%2Fopenai.yaml -->","skills/surgical-patch/SKILL.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fsurgical-patch%2FSKILL.md -->","skills/surgical-patch/agents/openai.yaml":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fsurgical-patch%2Fagents%2Fopenai.yaml -->","skills/ultracave/README.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fultracave%2FREADME.md -->","skills/ultracave/SKILL.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fultracave%2FSKILL.md -->","skills/verify-and-stop/SKILL.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fverify-and-stop%2FSKILL.md -->","skills/verify-and-stop/agents/openai.yaml":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fverify-and-stop%2Fagents%2Fopenai.yaml -->","src/plugins/opencode/README.md":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=src%2Fplugins%2Fopencode%2FREADME.md -->"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"Read `CLAUDE.md` before repository work. Binding routing: this repo owns\nskills, Engine, and MV3 extension; Agent SDK work belongs in\n`/Users/julb/Desktop/GitHub/caveman-agent-sdk`; Browse driver/MCP/benchmark/plugin\nwork belongs in `/Users/julb/Desktop/GitHub/caveman-browse`; proprietary Pebble\nproduct work belongs in `/Users/julb/Desktop/GitHub/caveman-coding-agent`.\n\nVisibility: this repo and `caveman-browse` are public now;\n`caveman-agent-sdk` is private during development with planned public release\nafter its gates; `caveman-coding-agent` and Caveman-Cloud remain private\ncommercial source.\n\n@./skills/caveman/SKILL.md\n@./skills/caveman-commit/SKILL.md\n@./skills/caveman-review/SKILL.md\n@./skills/caveman-compress/SKILL.md\n","isInternal":false,"tokens":202,"sizeBytes":734},{"name":"AGENTS.md","path":"engine/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/engine/AGENTS.md","title":"engine - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# engine — the content-aware Caveman Engine (commercial, binary-distributed)\n\nDetect a payload's type → route to a safety-classed compressor → count the token reduction\n→ store the original for recovery. The stable four-call API (`Compress`/`Retrieve`/`Detect`/`Stats`)\nis what the proxy, CLI, SDKs, MCP, and WASM build all share. Everything it reports is\n`inferred`; it never says `verified`.\n\n## Layout\n- `engine.go` — the `Engine` core: detect → route → compress → token ratio → CCR. `record` mode + miss + parse-fail + not-smaller + no-store all pass through.\n- `result.go` — `Result`/`Options`/`Mode`; unknown mode fails closed to `record`.\n- `detect.go` — content router (`json`/`diff`/`code`/`log`/`search-result`/`html`/`tabular`/`config`/`terminal`/`text`); low confidence → `text`.\n- `listing.go` — strips a read tool's line-number gutter (`1\\t{`, `2\\t  \"unit\"`) before Detect and before the compressor, then restores it after. Agents send file *listings*, not files; the gutter is presentation, and left in place it made every guttered payload detect as `text` and compress ~0%. Restoration keeps each surviving line's ORIGINAL number and is declined entirely for transforms that restructure rather than elide (re-encoded JSON), because numbers that describe nothing are worse than none.\n- `safety/` — the S0–S4 registry; S4 is lossy and `RequiresCCR`. Unknown class → fail closed.\n- `tokens/` — `Counter` interface; default is an offline, vocab-embedded BPE tokenizer (o200k_base). `inferred` always.\n- `contextwindow/` — deterministic BM25 context packer with recency/error/priority signals and token-budget accounting.\n- `compressors/` — `Compressor` interface + registry; structural JSON/log/search/diff/text/HTML/table/config/code compressors plus forced-only TOON/tool-schema/accessibility/repetition paths — `Default()` registers 14. A compressor is a pure byte transform.\n- `ccr/` — `~/.caveman/ccr.db` SQLite recovery store; content-addressed handles; byte-exact `Get`.\n- `pixel/` — pxpipe port (MIT, see its NOTICE): text→PNG request compression. Embedded glyph atlases + renderer + profitability gate + per-wire-format transforms (Anthropic/OpenAI/Gemini). S4-lossy, allowlist-gated (`CAVE_PIXEL_MODELS`, default `claude-fable-5,gpt-5.6`), consumed by the proxy's `pixel` mode; never wired into `Detect` and never imported by the WASM build (≈4 MB assets).\n- `evals/` — the local eval harness + fail-closed graders + embedded `fixtures/`. `Run()` is the quality gate.\n- `cmd/caveman-engine/` — the binary the CLI shells out to: `compress | detect | retrieve | stats | registry | toon encode|decode | evals run | pixel render|simulate`. `toon` is the stateless (no-CCR) JSON⇄TOON converter; both directions fail closed.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=engine` / `make product-test PRODUCT=engine`.\n- A new compressor is a self-contained file in `compressors/` + tests, registered in `Default()`. Forced-only compressors such as `toolschema` and `toon` must not be added to `Detect`.\n- Compressors never count tokens, touch CCR, or hit the network — the engine core does that around them.\n- **The `toolschema` transform is client-side-only today.** Registration makes it forceable by\n  engine callers; it does not make it reachable from managed-gateway traffic. Provider adapters\n  intentionally keep tool arrays in the frozen prompt-cache prefix, and no billed route invokes\n  this compressor. Engine API/CLI callers may force it locally, and `caveman-shrink` is its\n  dedicated product surface; those reductions remain local and `inferred`. Managed gateway has a\n  separate S2 tool-search/deferral path; do not conflate it with compression. Any future gateway\n  route for this transform needs cache-versus-schema cost arithmetic, stable byte-identical prefix\n  output, and an eval gate before activation.\n\n## Gotchas (honesty invariants — correctness, not style)\n- **fail-closed transforms**: every compressor passes the original through unchanged on any parse problem; the engine also passes through when the result is not smaller in tokens. Reserve **byte-safe** for classes whose `safety.Info.ByteSafe` is true.\n- **CCR-or-pass-through**: a lossy (S4) result is only emitted if its original was stored; with no store, the engine fails closed to pass-through.\n- **inferred-only**: ratios are token estimates labeled `inferred`; never `verified`, never re-projected.\n- **fail-closed**: unknown mode → `record`; unknown content type → `text`; unknown grader → `passed:false`.\n- **cgo**: full code compression (Python/JS/TS) needs the tree-sitter build; the cgo-free build compresses Go only. The embedded eval fixtures cover all three under cgo.\n- **boundary**: this is `public/` — never import `cloud/…`. `make check-boundaries` enforces it.\n\nSee ../../CLAUDE.md (root)\n","isInternal":false,"tokens":1211,"sizeBytes":4887},{"name":"AGENTS.md","path":"extension/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/extension/AGENTS.md","title":"extension - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# extension — \"Caveman Mode\" browser extension (MV3, no bundler)\n\nThe consumer top-of-funnel: a toggle that, on ChatGPT / Claude / Gemini, prepends the **caveman\nskill directive** to each message you send and re-fires the send, so the AI replies in a compact,\nhigh-density style intended to preserve technical substance. It only ever adds text to your *outgoing*\nmessage; it never reads replies, never touches network requests. Savings are `inferred`. (This is\nthe directive-injector product; an earlier revision shipped a local WASM compress button — EXT\nremoved it after Chrome Web Store review couldn't reproduce \"compress prompt locally\".)\n\n## Layout\n- `manifest.json` — MV3. `storage` permission only. Content scripts load `src/directive.js` **then** `src/caveman.js` (order matters). Only the fonts are web-accessible — no WASM, no special CSP.\n- `src/directive.js` — the **pure** caveman directive text: `buildPrimer`/`buildReminder`/`isPrefixed`/`normMode`. No chrome/DOM/network. Loaded first (exposes `self.CavemanDirective`) and required directly by the test suite.\n- `src/caveman.js` — content script: per-site composer adapter + capture-phase send interception. Prepends the directive (full primer on the first message, short reminder after) and re-fires send. Exact composer-scoped selectors; `prependText` inserts at a collapsed selection without rebuilding the rich draft. Pending sends cancel on draft, editor, conversation, or settings changes; unknown controls keep native behavior.\n- `src/background.js` — service worker: just reflects the on/off state on the toolbar badge.\n- `popup.{html,js,css}` — the on/off toggle, mode (caveman/ultracave/megacave), per-site switches, and a **Leave-a-review** CTA (Chrome Web Store via `chrome.runtime.id`; hidden on Firefox until a verified AMO URL is available).\n- `test/` — pure directive/service-worker tests plus real Chromium\n  content-script and popup/storage journeys against `harness.html`.\n\n## Conventions & honesty\n- **Edits your outgoing message only.** Prepends a visible directive; never reads model replies, never touches network requests, never automates anything beyond submitting the message you sent.\n- **Local-only.** No network requests, no analytics. Stores only `enabled` / `level` / `sites` in `chrome.storage.sync`.\n- **Honest about injection.** The directive is plainly visible in the chat — the extension doesn't hide what it adds.\n- **inferred-only.** Token savings are an effect of the model's terser replies; never labeled `verified`.\n\n## Conventions\n- Build/test: `npm test` for pure tests; `npm run test:browser` for Chromium send interception, popup storage,\n  per-site controls, and review-link behavior. There is no\n  build step — MV3 loads the source directly.\n- Package: `npm run package` runs both suites, then writes `dist/caveman-browser-<version>.zip`. Staged set uses explicit allowlist in `scripts/verify-extension-stage.mjs`, cross-checked against manifest/CSS/HTML references.\n- Composer + send selectors live in the `SITES` map in `src/caveman.js`; a site redesign means updating it (each entry keeps aria-label fallbacks so one rename doesn't break it).\n\nSee ../../CLAUDE.md (root)\n","isInternal":false,"tokens":758,"sizeBytes":3210},{"name":"AGENTS.md","path":"mcp/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/mcp/AGENTS.md","title":"mcp - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# mcp — the Caveman MCP server (Go core + launcher, Apache-2.0)\n\nA thin **stdio JSON-RPC** adapter exposing the compression [engine](../engine/CLAUDE.md) as\nfive MCP tools to any host (Claude Code, Cursor, …). It owns only the MCP framing; all\ncompression is the engine's, linked **in-process** (no subprocess, no drift). Local-only — it\nopens no network connection — and everything it reports is `inferred`, never `verified`.\n\n## Layout\n- `server.go` — the `Server`: JSON-RPC loop, dispatch, the five tool handlers. Takes an injectable `Engine` interface so the framing is testable without the real compressors.\n- `protocol.go` — JSON-RPC + MCP tool-result types, `toolText`/`toolError` helpers, the exact `tools/list` definitions.\n- `cmd/caveman-mcp/` — binary: opens shared file CCR store (`CAVEMAN_CCR_DB`,\n  else `CAVEMAN_HOME/ccr.db`/`~/.caveman/ccr.db`) so proxy handles resolve across\n  processes, then serves stdin↔stdout. `CAVEMAN_MCP_EPHEMERAL=1` opts into an\n  isolated in-memory store for tests/sessions that do not need proxy recovery.\n- `bin/caveman-mcp.mjs` + `package.json` — the `npx caveman-mcp` launcher that execs the prebuilt Go binary.\n\n## The five tools (exact names, case-sensitive)\n- `caveman_compress(input)` → compressed text + inferred ratio + `recovery_handle`. Lossy (S4), recoverable, and fail-closed: incompressible/malformed/not-smaller input returns unchanged, `ratio:0`, `recovery_handle:null` — never an error.\n- `caveman_retrieve(recovery_handle)` → the byte-exact original. Unknown handle → `isError:true` + a `cave_snake_code`, never a fabricated payload.\n- `caveman_stats()` → `basis:\"inferred\"`, `scope:\"session\"`; the string `verified` never appears.\n- `caveman_toon_encode(input)` → explicit JSON→TOON re-encoding with input/output sizes; returns pass-through plus note when encoding fails.\n- `caveman_toon_decode(input)` → TOON→JSON; invalid TOON returns `isError:true`, never raw input as JSON.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=mcp` / `make product-test PRODUCT=mcp`.\n- **stdout is the protocol channel** — logs go to stderr only (a dedicated test guards this).\n\n## Gotchas (honesty invariants)\n- **un-killable transport** — the stdio server survives everything short of EOF (issue #139). Framing is line-delimited: a malformed line is answered `-32700` and the loop RESYNCHRONIZES to the next newline (never `return`); a handler panic is contained by `recover()` → `cave_tool_panicked` (dispatch panics → `cave_internal_error`); JSON-RPC batch arrays are handled per spec (one array response); id-less/`\"id\":null` requests are notifications and get no reply; and both inbound lines and generated tool output (compress/toon) are size-capped (`cave_payload_too_large`, `maxInboundBytes`/`maxResultBytes`, 16 MiB default) — but `caveman_retrieve` is exempt (`Tool.ExemptResultCap`): recovery returns the byte-exact original and must never fail closed on size, since the shared gateway store has no matching ceiling. A dead server is worse than a slow one — the proxy keeps eliding content that no longer has a `caveman_retrieve` to expand it.\n- **fail-open** — engine error or malformed input → byte-identical pass-through, never a protocol error.\n- **fail-closed** — unknown tool/handle → `isError` + cave_snake_code; unknown JSON-RPC method → `-32601`.\n- **zero-egress** — the adapter imports no `net`/`net/http`/`os/exec`; a test parses the source to enforce it.\n- v1 is **stdio-only**, **string payloads only** (the engine detects type); HTTP transport + `caveman mcp` subcommand are v2.\n- **protocol negotiation must never error.** The adapter implements the 2024-11-05 contract and echoes that version back; a client asking for a newer one gets 2024-11-05 in the initialize result and decides for itself, per the MCP lifecycle. It previously answered `-32602: unsupported protocol version`, which made Claude Code (and every other current client) drop the server — and because `caveman wrap` reads recovery availability from an install-time marker rather than from the live agent, the proxy kept eliding content that no longer had a `caveman_retrieve` to expand it. Declining to echo an unimplemented version is right; refusing to speak is not.\n\nSee ../../CLAUDE.md (root) · ../engine/CLAUDE.md\n","isInternal":false,"tokens":1113,"sizeBytes":4333},{"name":"AGENTS.md","path":"mem/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/mem/AGENTS.md","title":"mem - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# mem — cavemem, durable agent memory (Go core + clients, Apache-2.0)\n\nDurable, cross-session memory: `remember` / `recall` / `supersede` / `history` / `forget`. A local SQLite store holds the\n**raw** memories (the source of truth); recall ranks them with deterministic **BM25** behind a\nconservative threshold and compresses each hit through the [engine](../engine/CLAUDE.md) so the\ninferred token cost is honest and the dropped detail stays recoverable via CCR. Everything is\n`inferred`.\n\n## Layout\n- `store.go` — `Remember`/`Recall`/`Supersede`/`History`/`Forget`/`Recover` over SQLite + engine; legacy schemas migrate in place.\n- `bm25.go` — deterministic tokenizer + BM25 scorer (non-negative scores, so the threshold is meaningful).\n- `cmd/cavemem/` — MCP server + `remember`/`recall`/`supersede`/`history`/`forget` CLI subcommands (JSON).\n- `js/` + `py/` — thin TS and Python clients that shell out to the binary (mirrored libraries; they reimplement nothing).\n\n## The cavemem_* MCP tools\n`cavemem_remember(text)` · `cavemem_recall(query, limit?, token_budget?)` ·\n`cavemem_supersede(id, text)` · `cavemem_history(id)` · `cavemem_forget(id)`,\nserved through `mcp.NewServer` so the framing matches caveman-mcp exactly.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=mem` / `make product-test PRODUCT=mem`\n  for the Go core; root `make test` also runs mirrored JS/Python wrapper tests.\n- The wrappers shell out to the gated binary — keep recall/scoring logic in Go, never forked into TS/Python.\n\n## Gotchas (honesty invariants)\n- **byte-safe write** — raw text is written to SQLite before any engine call, so a memory is never lost; compression is transient, at recall time only.\n- **bounded recall** — omitted/Go-zero `TokenBudget` defaults to 2000 inferred tokens. Public CLI/MCP/JS/Python callers must explicitly pass `token_budget=0` for unlimited recall; adapters map it to `UnlimitedTokenBudget`.\n- **fail toward nothing** — a query below the threshold (or with no term overlap) recalls nothing, never a guess.\n- **current-only recall** — superseded versions stay auditable through `History` but never enter normal recall.\n- **inferred-only** — `tokens_added`, score, and basis are inferred estimates; never `verified`.\n- **reversible** — a compressed recall hit carries a CCR `recovery_handle`; `Recover` returns the byte-exact original.\n\nSee ../../CLAUDE.md (root) · ../engine/CLAUDE.md · ../mcp/CLAUDE.md\n","isInternal":false,"tokens":654,"sizeBytes":2460},{"name":"AGENTS.md","path":"proxy/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/proxy/AGENTS.md","title":"proxy - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# proxy — the byte-safe standalone `caveman` proxy (commercial, binary-distributed)\n\nA base-URL-swap reverse proxy: match → authenticate → inspect → byte-safe transform → upstream\n→ meter. Single-operator, BYOK, **zero cloud dependencies**. It shares its provider adapters with\nthe managed gateway (the managed gateway imports them from here). `caveman start` launches the\n`caveman-proxy` binary.\n\n## Layout\n- `providers/` — the shared, public byte-safe adapter set: `Adapter` interface + `Base` embed + `UsageScanner`/`ParseUsageBytes` (`adapter.go`), and `anthropic`/`openai`/`gemini`/`azureopenai`/`bedrock`/`vertex`/`openaicompat`. `ResolveUpstreamURL` takes `providers.RouteContext` (no control-plane coupling). Anthropic + OpenAI carry both prefixed and bare routes (`/v1/messages`, `/v1/chat/completions`). `vertex` preserves caller OAuth bearer or Express-mode Google API-key credentials for Gemini + Claude on Vertex AI (no signing, no environment-key fallback, no custom usage parser). `MatchRoute` is POST-only; read-only model discovery (`GET /v1/models`, `/v1/models/{id}`) is a separate GET-only allowlist, `Base.MetadataRoutes`, declared by `openai`/`anthropic`/`openaicompat` and forwarded unchanged with no body, no transform and no spend row. Bare `/v1/models` belongs to both `openai` and `anthropic`, so they implement `MetadataRequestMatcher` and choose by wire protocol (`anthropic-version` or `x-api-key` present → anthropic; a Google key matches neither), never by registration order (#1187).\n- `internal/gateway/` — the request lifecycle (`server.go` + `proxy.go`) behind three injected seams: `Authenticator`, `CredentialResolver`, `TelemetrySink`. Ports the managed loop with the fail-open fix. `serve` sets `Config.AsyncRecord`: a row's request token count, sink write (batched) and Cloud observe run after the handler returns, in record order (`recorder.go`). A reader that must see the request it follows calls `Server.Flush` first (session receipts do); close the server before the sink.\n- `internal/config/` — `caveman.yaml` loader + BYOK env-key resolution; unknown mode fails closed to `record`.\n- `internal/store/` — `~/.caveman/caveman.db` SQLite spend store (`modernc.org/sqlite`, cgo-free); implements `TelemetrySink` and `BatchSink`. `RecordBatch` commits request rows with `synchronous=NORMAL` on its own connection (WAL): a power loss or OS crash can drop the last batches, a process crash or clean exit loses none; every other table keeps FULL.\n- `internal/standalone/` — wiring: static `Auth`, BYOK `Creds`, adapter set, and the always-on SSRF-guarded client.\n- `internal/identity/` — who calls the framework middleware routes (legacy token, token map, OIDC/JWT, mTLS) and the reloadable TLS listener config.\n- `cmd/caveman-proxy/` — binary: `serve` (default), `stats`, and content-blind\n  `agent-evidence --session --build --plan`. Evidence query returns only exact\n  provider usage, request hashes, declared context/plan identity, ordered\n  provider-prefix component hashes, actual transform IDs/counts, and CCR handle;\n  basis is always `inferred`, verified dollars always zero.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=proxy` / `make product-test PRODUCT=proxy`.\n- Tests inject a plain `*http.Client` to reach loopback stubs; the binary uses the SSRF-guarded client.\n- New provider/optimizer work goes in `providers/` (shared) — change it once, both proxies get it.\n\n## Gotchas (honesty invariants — correctness, not style)\n\n- **listener lifetime is not session lifetime**: `serve` never exits because a\n  wrapper, native session, heartbeat, or idle timer expires. Legacy\n  `CAVEMAN_NATIVE_IDLE_TIMEOUT` does not arm shutdown. The CLI never restarts a\n  shared listener to change mode/recovery; an incompatible new wrap runs direct.\n  Failed local startup also runs direct. Native session correlation entries may\n  age out without affecting API traffic.\n- **no default generation deadline**: `CAVE_GATEWAY_UPSTREAM_TIMEOUT_MS` defaults\n  to `0` (no total request deadline). A positive value is an explicit operator\n  cap and includes response streaming. Client cancellation still cancels upstream;\n  connection setup, inbound header/upload limits, and idle keep-alive socket\n  cleanup remain bounded separately. The replacement bound for an upstream that\n  connects and then goes silent is the transport's response-header deadline\n  (`CAVE_GATEWAY_RESPONSE_HEADER_TIMEOUT_MS`, default 900000, `0` disables) —\n  headers only, so it never truncates a live stream.\n- **response protocol controls streaming**: SSE/event-stream responses flush\n  headers and chunks even without a JSON `stream` flag. Encoded requests keep\n  `Content-Encoding` and bypass transforms. Interrupted response copies record\n  an error and abort HTTP framing; they never become a clean successful EOF.\n- **replay only when non-delivery is proven**: proxy transport retries are\n  limited to connection-setup failures — `*net.OpError` with `Op` `dial` or\n  `proxyconnect` (a proxy that is down fails as the latter, never as a dial). A failed upload/header read or truncated response\n  does not prove an inference was unprocessed; do not automatically replay it.\n  An explicit transformed-request 4xx still retries once with original bytes —\n  except a 429 that carries `Retry-After` (a rate limit; Anthropic sends its\n  `anthropic-ratelimit-*` headers on every response, so they prove nothing),\n  which is returned as-is; an accepted retry pins the conversation raw\n  (`internal/gateway/raw_pin.go`).\n  See `docs/technical/proxy-reliability.md` at the repository root.\n- **byte-safe**: `record` mode never transforms; on transform error the ORIGINAL bytes are forwarded (HTTP 200, fail-open) — never a 400. The one carve-out: bytes the provider already cached in replaced form are re-sent replaced, because forwarding the original there busts the prefix (see cache safety).\n- **request-wide opt-out**: `x-cave-transforms: caveman.pass-through.v1` suppresses every request transform path — compress, pixel, and provider-native — not only compiled plan routes. Tests cover all three modes.\n- **no-fake-savings**: standalone records `Basis: \"inferred\"` on every row; it never writes `verified` and never re-projects to a monthly figure.\n- **practice join**: local learn sinks carry additive `practice_id`; one\n  fail-closed mapping table owns sink→practice and unknown sinks keep `\"\"`.\n  The historical `subagent_overuse` sink is count-only and deliberately has no\n  practice id: spawn count cannot reactivate the retired\n  `context-exploration-offload` opportunity or prove any spawn unnecessary.\n- **local trial heuristics are not actuation evidence**: a model name never emits\n  the retired `model-right-sizing` id, and provider plus positive cost never\n  emits a cache move because neither proves stable-prefix eligibility. Legacy\n  rows for those identities are hidden at read time. Compression replay reports\n  one trial's local engine `estimated_engine_o200k` before/after shape with zero\n  dollars and low confidence; it is not provider-counted, a rate, an invoice,\n  causal/verified savings, or task-outcome evidence.\n- **Anthropic automatic caching is experimental observation only**:\n  `anthropic-automatic-prompt-cache` is a typed, default-off manual policy\n  experiment and may add only Anthropic's top-level 5-minute marker on direct\n  Messages API requests. Managed traffic additionally requires server-attested\n  official Anthropic origin; custom or provenance-unknown origins lose the flag\n  before the adapter. It is mutually\n  exclusive with the explicit `anthropic-cache-breakpoints` transform and any\n  caller `cache_control`; Bedrock and count-tokens requests stay byte-identical.\n  An applied marker records only its optimizer id plus actual provider usage and\n  cost. It has no practice, recipe, generic mode/candidate activation, ledger\n  tuple, inferred savings, or verified-savings path (cache-only and forged IDs\n  are excluded from the counted-baseline method too); evaluate it by manual\n  paired observation because shared provider cache state can contaminate an A/B.\n- **SSRF always on**: `standalone.StandaloneHTTPClient` guards every upstream dial (not gated on `CAVE_ENV`) using `ssrf.SelfHostedConfig` — NOT ManagedConfig, which ignores the allowlist and would make the escape hatch a silent no-op. `CAVE_SSRF_ALLOWLIST` opts loopback/private hosts back in (local model servers like Ollama; `localhost` as an entry covers 127.0.0.0/8 + ::1); metadata/link-local stay blocked in every mode. Provider traffic honours `HTTPS_PROXY`/`NO_PROXY` by default (#1001) — `upstream_proxy: env|off|<url>` / `CAVE_UPSTREAM_PROXY` — via `ssrf.Config.Proxy`, which validates IP-literal/localhost destinations before proxy selection and dials the (operator-configured) proxy address unguarded; `ssrf.NewHTTPClient` itself stays direct unless a caller sets `Proxy`, and a managed-mode Config carrying one is refused loudly (every request fails with `ssrf.ErrProxyInManagedMode`) rather than silently dialing direct. Adapters that pre-flight a resolved endpoint (bedrock, vertex) read the selector off the request context (`providers.WithUpstreamProxy`, published by `gateway.Handler` from the upstream transport) and use `ssrf.ValidateURLNoResolve` for proxied destinations: a proxy-only network has no outbound DNS, so resolving there failed the request before the proxy was ever consulted. `ca_bundle` / `CAVE_CA_BUNDLE` plus inherited `SSL_CERT_FILE`/`REQUESTS_CA_BUNDLE`/`NODE_EXTRA_CA_CERTS` append private roots for TLS inspection through `shared/platform/cabundle` (fail-closed parser shared with chhttp).\n- **inbound token is consumed, never forwarded**: `CAVEMAN_AUTH_TOKEN` gates non-loopback listens (a middleware token map, OIDC issuer or TLS client CA also makes one legal; provider routes still accept only the token, and without it `Auth.closed` refuses every provider request off loopback); `standalone.Auth` deletes the matching `x-cave-api-key`/`Authorization` header before `Creds.Resolve` so the shared token can never be forwarded to a provider or classified as a provider credential; a bearer that is not the token survives untouched (Claude OAuth, `/chatgpt/`). `Authenticate` runs BEFORE `matchAdapter` so the 401/404 split cannot be used to enumerate routes, and every 401 goes through `Server.rejectUnauthorized`, which counts `cave_proxy_unauthorized_total` and warns with the path and remote host only. Health and the no-op `/caveman/keepalive` beacon stay unauthenticated, `/metrics` too unless `CAVEMAN_METRICS_TOKEN` is set; the `X-Caveman-Instance` header on `/health/live` is published only on a loopback bind. A token on a LOOPBACK listener is honored too (the local `caveman wrap` path sends none, so `runServe` warns). Bedrock credentials then come from `shared/platform/awscreds` (env → web identity → container → IMDSv2), never from the request; a PARTIAL env pair fails closed instead of falling through to an ambient role, and the winning source is logged once.\n- **middleware identity is server-side and per route**: `middleware.Config.Identify` resolves an `identity.Principal` from the credential alone (bearer decides when present, else a verified client certificate); `ServeHTTP`'s `decode` checks `Principal.Allows(scope.Namespace)` on EVERY scoped route, so a new route that skips `decode` skips authorization. The legacy token stays `single_operator` with every namespace; JWT and certificate principals may never be named `single_operator`. Token and hash comparisons are constant-time over every entry. A token map or TLS file that fails to reload keeps the previous one. Tests: `identity/*_test.go` (negative JWT and mTLS cases), `middleware/identity_test.go`.\n- **Postgres middleware store keeps SQLite's single-writer semantics per authority**: `store.PostgresMiddleware` serializes writers of one authority with a transaction-scoped advisory lock taken by `Scope` (then re-read), `SaveScope`, `Revoke`, `PurgeBatch`, `Receipt` and `SaveOriginal`; the sweep is one-replica (try-lock) and skips busy authorities and row-locked scopes. Counters are statement-level transition-table triggers striped by txid; a per-row trigger goes quadratic on bulk statements. Lock keys include `current_schema()`. Run the store suite against both backends with `CAVEMAN_TEST_POSTGRES_URL`; `middleware_concurrency_test.go` fails without the locks.\n- **Auth scheme is preserved**: a key from an inbound `Authorization: Bearer` keeps `Scheme:\"bearer\"` on the `providers.Credential`; Anthropic and Gemini forward bearer credentials as bearer credentials (Claude/Gemini OAuth breaks if remapped to an API-key header). BYOK env keys and inbound `x-api-key` keep provider API-key mapping.\n  On a named compat mount the header follows the wire protocol of the path:\n  `/compat/<name>/v1/messages` gets `x-api-key` plus a default\n  `anthropic-version`, every other path gets Bearer. A real inbound Bearer stays\n  Bearer on every path. OpenCode Go rejects Bearer\n  on `/v1/messages` (401 `Missing API key.`, 2026-09-03). The gateway env\n  fallback puts the key in the header that the adapter emitted, so the upstream\n  never gets two credentials.\n- **fail-closed**: unknown route → 404; unknown mode → `record`.\n- **subscription AND oauth compression is NOT account-gated**: non-PAYG sessions from Claude Code, Codex ChatGPT, Gemini CLI, and other routed clients take live-zone compression with no Caveman account, entitlement, or seat. `CAVEMAN_WRAP_ENTITLED` and every `WrapEntitled` field are **deleted**, not merely ignored — do not reintroduce them. Exactly four conditions remain, all technical and all fail-closed (`liveZoneCompressionAllowed`): the operator `subscription_compress` switch (empty/`live_zone` allow, `off` and any unknown value close it), the adapter must implement schema-aware `PrefixStabilizer` zones, recovery must run through the agent's own MCP `caveman_retrieve`, and a durable prefix cache must be wired. The dedicated Codex `/chatgpt/responses` route uses the OpenAI Responses stabilizer while preserving OAuth and `ChatGPT-Account-ID` headers; recovery is proven by the `CAVEMAN_RECOVERY=mcp` stamp or the decoded request's own `mcp__caveman__caveman_retrieve` (top-level `tools` or Codex's `input[]` `additional_tools`, flat or under an MCP namespace); zstd bodies decode → transform → re-encode, anything else forwards the original wire bytes, and the `chatgpt_proxy` log carries `skip_reason`; transformed 4xx responses retry once with exact original bytes (never a rate-limit 429; an accepted retry pins the conversation raw). Legacy savings fields remain **tokens-only** — `compression_tokens_before/after` + `estimated_engine_o200k`, never booked compression dollars. New request-comparison and price-snapshot fields support separately labeled API equivalents in local stats; those values never enter subscription spend or legacy saved-dollar fields. LOCAL wrap only; managed gateway non-PAYG behavior is unchanged.\n- **cache safety is byte-stable replacement**: THE INVARIANT (#1105): for accepted requests P then R of one conversation, if R's client bytes over P's cached prefix (system, tools and messages through P's last `cache_control` marker; the whole prompt for implicit caches) equal P's, R's forwarded bytes over that range equal P's, and every request caches at least what its client asked for (a dropped breakpoint is a bust too). Two exceptions, both re-caching the range raw: a request the provider rejected transformed and accepted through the raw retry, and a compressed PAYG conversation without MCP recovery that streams (the server-side retrieve tool cannot ride a stream, so the conversation stays raw from there). A raw retry splits a range into a raw and a replaced form, and a later request follows the LONGEST lineage it extends, ties going raw: it is held to the raw request (raw pin) unless it repeats a longer prefix that went out replaced, like a sibling subagent started with the same task. Pins are taken only on a conversation's own bytes (past system and tools); pins and lineages live in the PrefixCache (`rawpin` and `lineage` scopes, pin rows never evicted) and in memory. `internal/gateway/cache_prefix_invariant_test.go` (Anthropic compress, PAYG, pixel and Codex Responses scenarios + `FuzzCachePrefixInvariant`) proves it; keep it green. A compressed live-zone turn becomes prefix on the next request, so the replacement is a pure function of segment content (deterministic compressor + content-hash CCR marker), memoised in the durable replacement cache, and the FIRST decision for a block wins — replaced or raw (`RawDecisionHandle`); a request whose write finds a row on record sends that row, not its own candidate. A frozen block with no decision (a `--resume` history, an evicted row) goes out raw and raw becomes its decision; a failed memo write keeps the block raw for the process (`rawMemory`, consulted only on a store miss and written through once the store takes it); new rows are remembered only once the request's splice holds. Raw decisions are global and durable, a known savings cost: a block declined once (frozen at first sight, a compressor decline, a CCR failure, a non-newest parallel tool output) stays raw in every conversation. Bounding them needs a TTL of at least the longest provider retention (24 h for OpenAI extended caching) plus time-aware store interfaces, or per-conversation rows keyed on a stable conversation identity with a scope rollover. Substitution is never gated: the derived epoch gate is gone, and an explicit framework epoch may veto only NEW compression. Pixel renders and the tool-schema strip are memoised the same way. New compression stays live-zone-only. The runtime tripwire (`prefix_monitor.go`, on the provider routes and `/chatgpt`) checks the same rule on live traffic and records `requests.cache_bust_cause`: `client` (DEBUG); `raw_retry`, `stream_switch`, and `lever_freeze` (the harm tripwire's one-time strip rollover), all WARN; `caveman` (ERROR `caveman changed bytes the provider already cached` or `caveman dropped a cache breakpoint the client set`, counted as `caveman_cache_bust_requests` and raised by `caveman status`) — any `caveman` row is a bug. A request is held only to anchors accepted before its own forwarding decision. Cross-turn stability covers **anthropic, openai, azureopenai/openaicompat, and gemini** through `ExtractStabilizable`; `bedrock` and `vertex` expose no compressible blocks and stay pass-through. Anthropic uses declared `cache_control`; OpenAI/Gemini use latest-user/latest-tool zones against implicit provider caches. Replacement cache is SQLite spend store and must retain `journal_mode(WAL)` + `busy_timeout`; a hit rewrites `last_used_at` only once the row is 10 minutes stale, and pixel rows have their own cap inside the table's.\n- **cache warming is predicted per stream, and replays only what Anthropic can answer at zero output** (`internal/gateway/cache_warm.go`, `cache_warm_table.go`): after a successful Anthropic Messages response on `api.anthropic.com` (never Bedrock, Vertex, Azure, a compat mount, a custom base URL, the pass-through header or an encoded body), the exact bytes and headers sent upstream (after compression, routing marks, heals and fail-open retries) are kept in memory only (LRU 1024 streams and 256 MiB, never stored, logged or captured) and may be replayed with `max_tokens: 0` and `stream: false` at `min(0.9*ttl, ttl-10s)` steps from the real request's start (wall time, so laptop sleep never sends a stale warm). Not replayable, so never warmed: `thinking.type: \"enabled\"`, `tool_choice` `tool`/`any`, `output_config.format`, no `cache_control`, a `ttl` other than `5m`/`1h`, mixed `5m` and `1h` markers, writes of another lifetime than the markers (one lifetime or no warm; the simulator classes by the same rule and none of the measured requests mixed them); the request scan reads top-level fields and cache_control markers only, no full parse. WHETHER to warm is optimal stopping, not a threshold: `cache_warm_table.json` holds the return-time distribution (30 s bins to 2 h) per stream class `main|subagent / tool|end / 5m|1h` (the class comes from the session key and whether the answer's `stop_reason` was `tool_use`, scanned off the plain bytes with a rolling tail; the proxy forwards the agent's `accept-encoding`, so an answer that comes back with a `Content-Encoding` is not classed: it is planned as whichever of the two classes warms less and its gap teaches neither), measured on 364,048 request transitions of real Claude Code sessions (2026-08-30 to 2026-10-08, `.blocks/cache-warm-sim.py`, aggregate probabilities only) and shrunk toward the parent class; the proxy learns each user's own gaps online (counts only, `$CAVEMAN_HOME/cache-warm-gaps.json`, 0600, written outside the warmer's lock, flushed by `Server.Close` at shutdown, symlink-refused, any implausible count drops the whole file; two proxies on one home are last-writer-wins) and blends them in with the defaults worth 100 observations. A stream evicted or still waiting at shutdown is counted as returning later than it was seen silent (spread over the later bins by the class's own distribution, \"never\" past 2 h): dropping it would keep only the returns and drift the table toward over-warming. At arm time the whole chain is planned by backward induction with the catalog price of the model sent (warm = read x prefix + input x uncached tail; avoided miss = (write_ttl - read) x prefix x the class's measured realized share, taken over the gaps that DID return: the window probability already leaves out the streams that never do), so the cumulative cost of every warm is counted before the first; unknown price, class or lifetime = no warm. Subscription traffic is valued at list prices as plan-usage equivalent. Stops: any real request of the stream (every model; it never waits for a warm in flight), any warm error, a warm reading `cache_read_input_tokens == 0`, the plan, 60 min, or the switch; the held request (body and credential headers) is released the moment its chain ends. A parent session's request does NOT stop its subagents' warms: a subagent can still be running, and on the test half that stop cost $565.52 net (below). A 429 (warm or real) pauses warming on that credential for the response's `Retry-After`, else until its `anthropic-ratelimit-unified-reset`, else 1 h for a subscription/OAuth login and 60 s for an API key; a 529 or 5xx on a warm pauses it for the `Retry-After`, else 5 min. Never sent while a real request of the same stream is in flight (retry every 5 s until its deadline). At most two warms are in flight process-wide (same retry; one with no slot by its deadline is dropped), and each fires up to 5 s before its planned time, never after. Warm rows are tagged `cache-warm`: spend, never a saving, never an agent request (excluded from request counts, session usage, trial origins, exported spans, receipts and `caveman sync` uploads; counted as `cache_warm_requests`), never fed to the harm-tripwire ledger, never sent to Cloud. Switch: on unless `$CAVEMAN_HOME/cloud.json` has `modules[\"waste-fixes\"]: false` (re-read on change); `CAVEMAN_CACHE_WARM=off` kills it for the process. Every answered stream request logs one line `cache warm plan` (info when warms are planned, debug when skipped: session hash, model, class, ttl, planned warms, skip reason: `response_error`, `off`, `not_anthropic_api`, `not_replayable`, `lifetime_mismatch`, `unpriced`, `not_worth_it`). Replay on the owner's data (test half, after 2026-10-03, 161,325 transitions): net $1,896.65 list-price equivalent at $385.31 of warms (9,628 warms, 7,280 wasted) vs $1,294.18 at $1,372.15 (45,343 warms) for the first fixed rule; the same policy with the parent stop for every subagent $1,331.13 at $197.28 (3,461 warms), with it only for subagents whose last answer was not a tool call $1,852.99 at $378.90 (9,518 warms). A test pins the plan to the simulator's on the shipped table (`TestCacheWarmPlanMatchesTheSimulator`; regenerate both together).\n- **pixel mode**: S4 lossy text→PNG (`pxpipe` port). Default allowlist is `claude-fable-5,gpt-5.6` via `CAVE_PIXEL_MODELS`; savings stay inferred-only. Renders keep the compress rules: the first decision for a text (render or text) is re-sent on every turn, a raw pin keeps the conversation text, and a NEW render is remembered only once its request goes out, with the original stored in CCR and the splice holding. If either fails, the new content goes out as text and records that, and earlier renders still go. A rendered block's `cache_control` moves to its last image. An Anthropic request carries at most 20 images, client images included (`anthropicManyImages`): past 20 the API rejects any image over 2000 px, and the renders are 2573 px wide, so a new render past the cap stays text.\n- **route stage is optional and fails open** (`internal/cloudlink`, `internal/gateway/route.go`):\n  it asks Cloud `POST /v1/route` only for requests naming Auto (`AutoModel`, `caveman-auto`, also with Claude Code's `[1m]` suffix, the\n  picker entry the CLI adds while `modules.routing: true` in `$CAVEMAN_HOME/cloud.json`), while the\n  CLI is signed in with that switch on, for Anthropic Messages / OpenAI chat or responses requests on\n  an API key or a subscription alike; any other model goes as sent (`route.outcome: off`), the\n  session's marks and heal still applied. Auto goes upstream as the provider's `autoFallback` model\n  (Anthropic `claude-sonnet-5-5`, OpenAI `gpt-6.1-sol`; count_tokens too, and with no Cloud link),\n  which is also the asked model the ask carries and what a failure runs (a transient Cloud failure, that is a timeout, an unreachable or 5xx Cloud, an unreadable answer or the pause after one, instead runs the model the session's previous request went to, `heldModel`, never for a side request; no previous request on that provider, one that was a compaction or came after one, a held model that then did not serve (any non-2xx: the next attempt runs the fallback), signed out, routing off, an expired or refused login, a limit and a provider's refusal keep the fallback); Auto on any other\n  provider is a 400 (`cave_auto_unavailable`), and an encoded body naming Auto is decoded first. A\n  named model records `route.outcome: off`, reason `named_model`. OpenAI Auto (API key and ChatGPT\n  login) runs only `AutoOpenAIModels` (`gpt-6.1-sol`, `gpt-6-astra`, `gpt-6-luna`): the ask's\n  `Models` lists those alone and the pool drops other OpenAI models, and an answer naming any other\n  runs `gpt-6.1-sol` at its effort (`auto_model_refused`, `translate.FitEffort`); an Auto row's `RouteFrom` and\n  `x-caveman-routed-from` say `caveman-auto`. A move on a subscription stays on the provider's own\n  API on that subscription's credential; pool entries go out on their own login or the Cloud\n  gateway key, never on it, and never with the agent's `oauth-*` betas. On a ChatGPT login\n  (`/chatgpt`, `chatgpt_auto.go`; OpenCode's ChatGPT login lands there too) `/models` gains an Auto\n  entry (a copy of `gpt-6.1-sol`'s, listed last, fetched without `If-None-Match`, served without\n  `ETag`) while the link reports `AutoOffered` (signed in, routing on). Every POST there has its\n  first 64 KiB read (decoded when gzip, deflate or zstd; a br body is never looked into) for a\n  top-level `\"model\":\"caveman-auto\"`; any other streams on unchanged. `/responses` naming Auto asks\n  with `NoPool`, keeps live-zone compression, replays `gpt-6.1-sol`'s bytes on a 4xx (a rate-limit 429, a 401 and a 403 are returned instead; a served replay pins the conversation raw, and the decision is rejected only then or when the replay is itself a 429; a 5xx on a moved model is returned and rejects the decision, a rate-limit 429 keeps it: the plan's limit holds for every model); any other\n  path (Codex compaction) runs `gpt-6.1-sol` unasked; a body over `CAVE_MAX_REQUEST_BYTES` streams\n  with only its model changed. The agent reads the Auto id it sent. Every `/chatgpt` row (Auto or\n  not) is offered to Cloud like any other, as its agent (`x-cave-agent`, else Codex by its\n  `originator` or User-Agent), with model ids in `route_from`/`route_to`; a model it could not read\n  goes to Cloud as none. The\n  ask starts before compression (parse, ask, compress, route), waits at most 800 ms (the CLI's\n  keychain credential is read when the runtime starts, `Link.Start`, never on a request goroutine;\n  a request that lands while a read runs with no secret cached waits for it, leaving the ask at\n  least 500 ms, else goes as `credential_loading` without an ask or a pause), carries the\n  caller's models, counts, what the request declares (the raw values of eleven allowlisted agent\n  headers, never cut: a value over 256 bytes, 16 KiB for Codex's turn metadata, which also comes\n  from the body's `client_metadata`, is left out; a forked Claude Code child's spawn-call\n  `subagent_type` as `x-caveman-agent`; its tool names, effort and thinking type), what the\n  session's previous request ran (the served model, the effort in force, the provider's input and\n  cache token counts, its age) and Cloud's opaque per-session state, plus, on a turn's first ask,\n  the raw text of the latest human turn, the one before it and the end of the agent's last reply\n  (contracts `route-ask-v1`; Cloud picks the model and the effort and no routing logic lives\n  here). Its answer is cached per ask until 30 min after its last use (session, or without one agent and\n  endpoint, provider, model, turn number, latest human text;\n  an input group carrying a tool result is no human turn, text riding along included) so a tool\n  loop never switches model or effort mid-turn (an ask first seen during a pause keeps the paused answer for its turn); a request the agent labels compaction or auxiliary\n  (Claude Code's request class and compaction flag, OpenCode's title/summary/compaction agent,\n  Codex's subagent kind and turn-metadata request kind) is asked on its own, without the text. The\n  session is `x-cave-session`, else the agent's session header (a Codex thread, `thread-id`, is\n  its own, its parent `x-codex-parent-thread-id`); a Claude Code child (`x-claude-code-agent-id`)\n  is a session of its own and sends its parent's state; a session id guessed from timing is not\n  used. State, previous-request facts and marks live in bounded in-memory LRUs (1024), never on\n  disk. The answer sets Responses `reasoning.effort`, chat `reasoning_effort` or Anthropic\n  `output_config.effort`; Anthropic `effort_mode: message` instead inserts the byte-identical mark\n  `{\"role\":\"system\",\"content\":[],\"output_config\":{\"effort\":…}}` (beta\n  `mid-conversation-output-config-2026-07-01` appended to `anthropic-beta`) before the last user\n  turn, or at the end after a tool result; on OpenAI Responses (GPT-6) the mark is a\n  `{\"type\":\"configuration_update\",\"reasoning\":{\"effort\":…}}` input item before the last user\n  message while nothing has answered it yet, never at the end and never next to another one (no\n  place, as mid tool loop: the effort in force holds), no beta, and none at all where OpenAI forbids\n  it (`truncation: \"auto\"`, `context_management`, a non-false `multi_agent`: Cloud's effort goes\n  top-level); a 400 naming a configuration update heals like a refused mark. Effort state made on\n  one wire starts afresh on the other. Only `effort_mode: message` ever starts marks; a \"top\"\n  answer on history carrying the session's marks goes in as one more mark (never wiping them; for\n  a compaction or side request, that request's only), and on any other history sets the top-level\n  field. Marks are remembered per session with a salted hash of the message before each\n  (`cache_control` and thinking blocks left out, so a heal's strip keeps them) and replayed at the\n  same places on every later request to a model that already took them (the agent resends history\n  without them), count_tokens of a known session or a forked child's parent included (it adds\n  nothing to a history without marks). Without an effort from Cloud (a failure, routing off,\n  effort \"\") a history without the session's marks goes as the agent sent it, top-level field\n  included (one cache restart where Cloud's was, nothing inserted, no beta), except that a\n  remembered heal (drop_block with its beta, or a strip) keeps applying. One with marks runs at\n  the request's own top-level effort, or, when it sets none, at the model's default effort\n  (Cloud's `default_effort`, kept per session, a forked child taking its parent's), marked when it\n  differs from the one in force; only with no default known does such a request go as the agent\n  sent it, without the marks, which the session then forgets (its later thinking blocks then lose\n  their binding) unless the request went to another model. The top-level field is fixed at the\n  session's first per-message request: the routed effort on a fresh conversation whose request\n  sets one (kept there, also for later requests that set none), else the request's own; a routed\n  effort for a request that sets none goes in as a mark, first on a fresh conversation. An anchor\n  that no longer matches drops that mark and every later one; compaction and side requests never\n  change them, and a body matching none of them (an unlabeled side request, compacted history)\n  gets marks of its own, kept for up to four request shapes (first message, length, last message),\n  which take the session over only from a longer request continuing that conversation (same first\n  message, its last one where it was); the session's marks and fixed top-level field then wait\n  there in turn. A thinking-binding 400 on the asked model of a `/messages` request retries once\n  only on a session the route stage changed (it was on for the request, the session carries its\n  marks or heal, or another model served one of its requests) whose body sets no\n  `thinking.block_binding`: with `thinking.block_binding.prefix_mismatch_behavior: \"drop_block\"`\n  (beta `thinking-binding-controls-2026-08-01`) when the block is bound to a different\n  conversation and the request's thinking is adaptive or enabled, then sent on every later request\n  (count_tokens and forked children included); else (Sonnet 5.5 `between_tools`, no thinking\n  field, a tampered signature) without thinking blocks from the failing message on, and later\n  requests on the same history (same first message, same message at its end) strip the same range\n  up front (a second strip widens it; the strip stays, the history as served, even if the agent\n  later sets its own `block_binding`). A drop_block retry refused with a 400 naming the binding,\n  `block_binding` or its beta moves the session to the strip path, a strip refused that way ends\n  its heals (the first lasts for the session, as it depends on the model and thinking type; the\n  second goes with the marks, cleared when they are forgotten; a side request never clears either,\n  nor the strip; a 429, 5xx or other 400 on the retry teaches nothing). Routing switched off after\n  a restart surfaces that 400. A refused mark retries once with top-level effort only and, once\n  served, latches per-message effort off for the session (a model that refused takes Cloud's\n  effort top-level); compressed 400s are decoded first. A Cloud error, timeout, 401/403,\n  `allowance` or `billing_limit` answer keeps the asked model (the held model above for a transient failure) and pauses new asks (1 min; 10 min\n  for 401/403 and billing_limit; until the 1st for allowance). Refusals and limits land in\n  `$CAVEMAN_HOME/route-state.json` (with Cloud's notice) for `caveman status`; a new login lifts\n  the pause. A route failure blames a model only on evidence (Cloud ADR 0110). A provider 4xx on\n  the routed model replays the original bytes on the asked model, except a 401 (the login, not the\n  model: returned as is); a 403 replays (a plan refuses a model it does not carry with one). Only a\n  served replay of bytes that differed from the original in the model alone blames the routed model\n  (`Reject(true)`, `provider_rejected_routed_model`): it leaves the ask's pool for that scope\n  (session, or without one agent and endpoint, and provider and asked model) for 30 min, while\n  the ask still goes with the models left. A served replay after compression or effort also\n  changed the bytes records `replayed_original` and blames no model; a replay that fails too\n  records `replay_failed`. The rest of the ask moves back to the asked model (`Reject(false)`,\n  never remembered) when the replay is served, or is itself a 429 (so the agent's own retries send\n  one request each); a rate-limit 429 (with Retry-After) or a 5xx on the routed model is returned\n  as is, never replayed, and moves it back too, as do a transport error, a provider error event\n  and an upstream break before the provider reported usage; a client cancel never does, so the\n  agent's own retry runs the asked model; a\n  429 on the asked model of a request whose bytes the route stage changed (effort, marks, strip,\n  drop_block, a heal retry) is returned as is. When the model moved, the agent's copy of the\n  answer names the model it asked for (Claude Code drops its thinking on another name): a JSON\n  answer's top-level `model`, and in a stream every `\"model\":\"<sent>\"` pair, rewritten\n  incrementally across reads (an Auto request names Auto, moved or not); the upstream is asked for an identity answer (on the original-bytes replay of an Auto request too), a compressed one is\n  left as it is, and usage, stats and `last` read the provider's bytes. The pass-through header\n  and encoded bodies never route, nor does any origin but the provider's own API (Azure,\n  OpenRouter, LiteLLM, a custom base URL); record mode does (routing is its own module). A limit\n  pause asks `/me` at most every 15 min and lifts once routing is no longer limited or the plan\n  changed. runtime/v1 events go whenever signed in, routing on or not, unless the CLI telemetry\n  opt-out is set (ADR 0085 §5; `route.outcome: off` when no route stage ran): in-memory batches of\n  at most 500, dropped on failure, at `/me`'s `data.level`. The runtime never invents a level:\n  none in `/me` means `counts`, an unreadable `/me` lowers it to `counts`, `off` or no level sends\n  nothing. Model ids from other origins go as `custom`. Signing in or out never changes another\n  stage.\n- **the route path is kept hot** (`internal/cloudlink/warm.go`, contract `route-warm-v1`): the link\n  has its own transport (HTTP/2 tried, idle connections kept 10 min and pinged after 15 s of\n  silence, proxy from the environment; a warm is marked replayable, so a dead kept connection is\n  retried on a new one, an ask never: the server may already have decided and counted it). A warm\n  is `POST {gateway}/v1/route` with `x-cave-warm: 1`, the asks' credential and no body; it goes,\n  one at a time and only from a started runtime signed in with routing on, at start and at every\n  login, when a native `session.start` or `prompt.submit` comes after 30 s without contact (an\n  answered ask or a warm engine), and every minute without contact while asks came in the last\n  15 min. Only a 204 with `x-cave-edge-engine: warm` is warm (timeout, error, cold and off are 204s\n  too); it ends a pause a timeout, an unreachable Cloud or a 5xx caused, never a 401/403 or limit\n  pause. Any other status is an origin without the edge (a customer install): no pause, logged at\n  debug, and three in a row stop warming until the next login or start. Each ask logs `route ask`\n  (info): the ms this side measured, whether it reused a connection, the status, `x-cave-edge` and\n  the edge's `server-timing`. The 800 ms budget stays the cap.\n- **pool routing** (`internal/pool`, `internal/translate`, `internal/gateway/route_pool.go`):\n  the provider keys and logins a person adds (`caveman providers add|login`;\n  `$CAVEMAN_HOME/provider-logins.json` lists ids, where each secret lives and the `cloud` switch,\n  written under `provider-logins.json.lock` by the CLI and the runtime alike; the secret is in the\n  keychain service `caveman-provider` or a 0600 file under `provider-logins/`, never both) become\n  the ask's additive `pool` (`harness/<model>`, then `<host>/<model>` per login times the static\n  catalog models its host serves, on a wire `translate.Supported` reaches from the caller's\n  grammar; at most 64; left out when no login adds one; only a 400 naming `pool` is asked again\n  without it, and pool then stays off for that login). An answer's `pool_id` + `via`: a harness\n  entry applies like `model`; a login entry goes straight to that host on that login\n  (`RouteTarget`, translated when its grammar differs, the unstripped body so each host gets its own\n  reasoning back, top-level effort, the agent's copy naming the asked model); `via: cloud`\n  (`cloud:<provider>:<model>`) sends the agent's own bytes in its grammar to the Cloud gateway\n  (`gatewayUrl`, else `baseURL`) with `x-caveman-route` (and `x-caveman-effort` when the answer\n  carries one) on the project gateway key only (the gateway\n  refuses login tokens; no key, or `caveman providers cloud off`, runs the asked model), and that\n  whole request DOES pass through Caveman Cloud. Route asks to the gateway use the project key too.\n  Any failure before content reaches the agent (refused translation, translator panic, connect\n  error, non-2xx, or a 2xx that fails or ends first: `translate.ErrNotServed`, the translator holds\n  its output until the first content, for at most `gateHold` (10 s) even while the host sends only\n  comments or pings; after it the headers go and pings start, only between the upstream's events)\n  falls back to the asked model on the harness credential, at the answered effort fitted to that\n  model's levels (`translate.FitEffort`: the Claude table, the catalog's `effort_levels`, high at\n  most with thinking off), and marks the ask rejected; a stream cut after content aborts HTTP\n  framing and never closes a half-finished tool call, while the host's own failure event after\n  content ends the stream as sent, without an abort (`pool_upstream_failed`). The row keeps\n  `route_pool_id`, `route_reason` and the host's answer id (`upstream_response_id`, e.g.\n  OpenRouter's `gen-…`); each failure is logged. A translated Messages stream's `message_start`\n  carries an input estimate (`x-caveman-input-tokens: estimated`) that `message_delta`'s exact\n  count replaces. A pool turn is not `last` and books no dollars off Anthropic/OpenAI list\n  prices. Every request to Anthropic's/OpenAI's own API (not pass-through) drops reasoning another host\n  wrote (`translate.AnthropicNative`/`OpenAINative`, byte for byte when there is none; a Responses\n  host's encrypted reasoning is route-tagged so only that host gets it back; per-message effort\n  marks are untouched). OpenAI (key or ChatGPT login) is Responses-only: Messages and chat callers reach it\n  through Messages→Responses and chat→Responses, and chat callers (OpenCode, Aider) reach Claude through\n  chat→Messages; a chat caller gets another host's reasoning as `reasoning_content` plus a\n  `reasoning_details` envelope that goes back only to that host, and every chat API (on the harness path\n  too, whatever the origin and with routing off, `translate.ChatNative`) gets both removed. Documents, structured output,\n  `disable_parallel_tool_use`/`parallel_tool_calls` and refusals cross every direction; a part the target\n  cannot take (a document by URL on chat, an OpenAI file id on Claude, audio) refuses the translation, so\n  the pool falls back. A refusal reaches Codex as `response.incomplete` `content_filter` (its own\n  content-filter path); a max-tokens stop stays `response.completed` (Codex retries any other incomplete). Terms: no Claude Pro/Max, Google or Copilot login is ever added; every OAuth row\n  in `providers.json` carries a terms note.\n- **cache mechanics** (`internal/gateway/route_cache.go`): the ask's\n  `request.cache_ttl` is the longest Anthropic `cache_control` ttl (none = 5m), else OpenAI\n  `prompt_cache_options.ttl` / `prompt_cache_retention` (24h; in_memory = 5m); Cloud's generic\n  400 (`cave_router_request_invalid`, which names no field) or one naming the field is asked\n  again without `cache_ttl`, then `pool` (never a field a 200 already came back for), and the field\n  whose removal got the 200 stays off for the login. Record mode gets none of the cache mechanics\n  below (no `prompt_cache_key`, no OpenRouter pin, host affinity headers keyed on the session\n  itself as before, no fan-out wait); the routing answer itself (model, effort, marks,\n  `configuration_update`) still applies there, as above. Elsewhere: host affinity headers key on\n  the session family (the parent's for a child): hashed `x-session-id` (OpenRouter),\n  `x-session-affinity` (Fireworks), `x-grok-conv-id` (xAI). An OpenAI request the route stage is\n  on for (a stateful chain, answered off for a reason, included) that carries no\n  `prompt_cache_key` gets the session's own (hashed, added before the effort so a heal keeps it;\n  the original-bytes retry keeps it unless the 4xx names it, which also latches the key off for\n  the session; a request it only keyed is never replayed); routing off and signed out stay byte\n  for byte. Once an OpenRouter pool entry reports cache reads or writes, the session pins the\n  provider its answer named (`provider.order:[p]`, `allow_fallbacks:false`); a pinned failure\n  drops the pin, and a pinned non-2xx is sent once more to the same entry unpinned before the\n  asked model runs (a transport error is never resent: it may come after the request was\n  written). Fresh sibling children of one parent on the same model, tools, system prompt and\n  `prompt_cache_key` go one first and the rest wait for its first content (a JSON answer's first\n  byte; an event stream's first event past comments, pings, message_start and\n  response.created/in_progress; 5 s at most, or their own cancellation): one cache write, not N;\n  a prefix with content in the last 5 min waits for nobody. `signals.context_tokens` is the\n  request's bytes times the provider's own tokens per byte, the parent session's first.\n  Live check: `CAVEMAN_LIVE_OPENROUTER_KEY=<key file> go test ./proxy/internal/cloudlink -run\n  TestLiveOpenRouterCache -v` (throwaway home, $0.50 cap).\n- **boundary**: this is public code — it must never import the managed-cloud lane. `make check-boundaries` enforces it.\n\nSee ../../CLAUDE.md (root)\n","isInternal":false,"tokens":11367,"sizeBytes":46693},{"name":"AGENTS.md","path":"shrink/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/shrink/AGENTS.md","title":"shrink - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# shrink — caveman-shrink, the tool-catalog compressor (Go core + launcher, Apache-2.0)\n\nCompress MCP/OpenAI **tool definitions** before they fill the context window. A thin Go wrapper\nover the [engine](../engine/CLAUDE.md)'s `toolschema` compressor: it drops annotation metadata\n(examples, titles, `$comment`, `$schema`) and reduces long descriptions to their lead plus every\nconstraint-bearing sentence. It keeps every **selection token** (tool/parameter names, types,\nenums, required) and argument-construction value (`default`, `const`, `$ref` targets)\nbyte-for-byte. Everything is `inferred`.\n\n## Layout\n- `shrink.go` — the library: `Shrink` (catalog → compressed, fail-open S4 transform, durable-CCR-backed), `Recover` (handle → original bytes), `Lint` (per-tool inferred token reductions), `SelectionProfile` (the structural surface — what must survive). `WithStore`/`WithStorePath` select recovery storage; default is `CAVEMAN_CCR_DB` / `~/.caveman/ccr.db`.\n- `cmd/caveman-shrink/` — the CLI: `caveman-shrink` / `shrink` (stdin→stdout), `lint <file>`, and `recover <handle>`.\n- `bin/caveman-shrink.mjs` + `package.json` — the `npx caveman-shrink` launcher around the prebuilt binary.\n\n## Two correctness boundaries\n`SelectionProfile(input)` == `SelectionProfile(Shrink(input).Output)` always. This proves the\nstructural selection surface (names/params/enums/required) survives byte-for-byte. It does **not**\nprove the model selects the same tool: descriptions are model-visible and long descriptions are\nlossy. Behavioral selection equivalence requires model-eval fixtures; keep results `inferred`.\n\nSelection alone does not guarantee valid arguments. Short descriptions stay whole; long ones\nkeep the lead plus every recognised constraint sentence (must/cannot/required/bounds/format/\nISO/RFC/absolute/etc.). `default`, `const`, and internal `$ref` targets survive. Golden and\nadversarial conformance tests lock these construction surfaces; over-keep is safer than a retry.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=shrink` / `make product-test PRODUCT=shrink`.\n- The structural compressor lives in the **engine** (`compressors/toolschema.go`); shrink reuses it, never forks it.\n- **This is the dedicated product surface for the `toolschema` compressor.** Engine API/CLI callers\n  can also force it locally. Managed-gateway adapters keep tool arrays in the frozen prompt-cache\n  prefix and never hand them to it; gateway's separate S2 tool-search/deferral path is not\n  compression. Local reductions stay `inferred`. A future billed route for this transform needs\n  cache-versus-schema cost proof, stable byte-identical prefix output, and an eval gate.\n\n## Gotchas (honesty invariants)\n- **S4 lossy, fail-open** — successful compression changes model-visible bytes; malformed/incompressible catalogs pass through unchanged (`ratio:0`, no handle).\n- **inferred-only** — token counts are estimates via the engine's offline counter; never `verified`, never re-projected.\n- **reversible for real** — a compressing shrink commits the exact original to the durable shared\n  CCR store before returning a handle. A separate process resolves it through `Recover` /\n  `caveman-shrink recover`; failure to open storage makes library `Shrink` return an error, while\n  the CLI catches that error and forwards the original bytes with no handle.\n\nSee ../../CLAUDE.md (root) · ../engine/CLAUDE.md\n","isInternal":false,"tokens":812,"sizeBytes":3429},{"name":"CLAUDE.md","path":"browse/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/browse/CLAUDE.md","title":"browse - Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# browse — `caveman-browse` browser driver\n\n> **Repository routing:** do not continue Browse product work here. Source of\n> truth is `JuliusBrussee/caveman-browse`, local checkout\n> `/Users/julb/Desktop/GitHub/caveman-browse`. This directory is a consumer\n> copy; edit only for pinned integration, migration/removal, or an explicit\n> cross-repo sync. Accessibility-compressor work stays here in\n> `engine/compressors/axtree.go` because Engine is owned by this repo.\n\nLocal, agent-facing browser interaction for the open Caveman surface. It serves\nstdio MCP tools that read a real Chrome accessibility tree, compress it with the\nengine's forced-only `a11y` compressor, act on `uid` handles, and recover the\nbyte-exact original AX payload through CCR. Every saving here is `inferred`;\nBrowse never emits `verified`.\n\n## Layout\n- `session.go` — MCP tool handlers, engine/CCR integration, UID target cache.\n- `cdp.go` — chromedp-backed Mode-A dedicated Chrome driver and bounded\n  actionability recipe.\n- `cmd/caveman-browse/` — stdio MCP binary.\n\n## Gotchas\n- This package may import CDP/network/browser dependencies. `public/mcp` may not.\n- `go test ./public/browse/...` is setup-free. `make test-browse` resolves an\n  installed Playwright Chromium or system Chrome and runs the `integration`\n  build-tagged CDP contract across the package and direct CLI; `make\n  test-browser` adds extension tests and `make test-e2e` includes both.\n- The actionability layer is intentionally bounded: same-origin dashboards and\n  predictable design-system controls, not arbitrary-open-web parity.\n- Unknown handles/actions fail closed with `cave_snake_code` errors.\n- Snapshot output is compact indented text, not JSON-lines. `query` keeps at most\n  12 highest-scoring task matches plus ancestors; recovery metadata exposes only\n  UIDs actually shown. Keep `tokens_after` equal to exact agent-visible result\n  cost and `view_tokens` equal to serializer output cost.\n- Direct CLI Chrome is detached so separate `snapshot`/`act`/`eval` processes\n  can share a target. `close` must terminate it. Fresh `CAVEMAN_HOME` must work;\n  state writes stay atomic and mode `0600`.\n- CDP action acknowledgement is not application settlement. Non-wait actions\n  return `settled:false` and require a focused resnapshot for proof.\n- Navigation denies `file:`, `javascript:`, and privileged Chrome schemes.\n- Benchmark contract and reproducible Playwright comparison live in\n  `BENCHMARK.md`; keep token budgets executable in tests.\n- **The uid map is `browser_snapshot`'s contract, not a side effect of a\n  compression ratio.** On engine pass-through (no recovery handle — a tree that\n  did not get smaller, or no CCR store) `snapshotTool` fails closed with\n  `cave_browser_snapshot_uncompressed` and returns the prior page's uids intact;\n  it MUST NEVER dump the raw AX tree into `uids` (strictly worse than not using\n  Browse) nor wipe the target cache. Regressing this reopens issue #140.\n- **An `<iframe>` is a leaf, not a broken tree.** `Accessibility.getFullAXTree`\n  returns one frame at a time, so an iframe node's `childId` points at a child\n  document absent from this payload; the `a11y` compressor\n  (`engine/compressors/axtree.go`) treats an unresolvable `childId` as a leaf and\n  still curates the frame-visible nodes. Because of that the CDP driver keeps\n  Chrome's default Site Isolation (`site-per-process`) — do not disable it to\n  \"fix\" cross-origin iframes.\n","isInternal":false,"tokens":840,"sizeBytes":3457},{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# CLAUDE.md — caveman\n\n## README is a product artifact\n\nREADME = product front door. Non-technical people read it to decide if caveman worth install. Treat like UI copy.\n\n**Rules for any README change:**\n\n- Readable by non-AI-agent users. If you write \"SessionStart hook injects system context,\" invisible to most — translate it.\n- Keep Before/After examples first. That the pitch.\n- Install table always complete + accurate. One broken install command costs real user.\n- What You Get table must sync with actual code. Feature ships or removed → update table.\n- Preserve voice. Caveman speak in README on purpose. \"Brain still big.\" \"Cost go down forever.\" \"One rock. That it.\" — intentional brand. Don't normalize.\n- Benchmark numbers from real runs in `benchmarks/` and `evals/`. Never invent or round. Re-run if doubt.\n- Adding new agent to install table → add detail block in `<details>` section below.\n- Readability check before any README commit: would non-programmer understand + install within 60 seconds?\n\n---\n\n## Project overview\n\nCaveman makes AI coding agents respond in compressed caveman-style prose while preserving technical substance, code, commands, and exact errors. Publish no reduction or quality-equivalence percentage without a committed reviewed benchmark. Ships as Claude Code plugin, Codex plugin, Gemini CLI extension, and agent rule files for Cursor, Windsurf, Cline, Copilot, and other profiles via `npx skills`.\n\n## Repository routing\n\nThis repo is source of truth for Caveman skills, Engine, and MV3 directive\nextension. Agent SDK + initializer work belongs in\n`/Users/julb/Desktop/GitHub/caveman-agent-sdk`\n(`JuliusBrussee/agent-sdk`). Proprietary Pebble runtime, policy, sessions, TUI,\ndistribution, and conformance work belongs in\n`/Users/julb/Desktop/GitHub/caveman-coding-agent`\n(intended `JuliusBrussee/caveman-coding-agent`). Browse driver/MCP/benchmark/plugin work\nbelongs in `/Users/julb/Desktop/GitHub/caveman-browse`\n(`JuliusBrussee/caveman-browse`). The matching `browse/` directory here is a\nconsumer copy (it still builds the `caveman-browse` release binary); edit only\nfor pinned integration, migration/removal, or an explicitly requested\ncross-repo sync.\n\nVisibility is separate from ownership: this repo and `caveman-browse` are\npublic now; `caveman-agent-sdk` is private during development and planned for\npublic release after its release gates; `caveman-coding-agent` and\nCaveman-Cloud remain private commercial source.\n\n---\n\n## What lives where\n\nPost-cleanup layout. Sources of truth at the top, distribution mirrors below, build outputs in `dist/`, human docs alongside each skill.\n\n```\ncaveman/\n├── README.md                    # Front door (product pitch)\n├── INSTALL.md                   # Per-agent install commands\n├── CONTRIBUTING.md              # Dev guide\n├── CLAUDE.md                    # This file (maintainer instructions)\n├── AGENTS.md / GEMINI.md        # Autodiscovery files (must stay at root)\n│\n├── install.sh / install.ps1     # 30-line shims → installer/install.js\n│\n├── installer/                   # Unified installer\n│   ├── install.js               # Single source for all 30+ agents (PROVIDERS array)\n│   └── lib/settings.js          # JSONC-tolerant settings.json reader/writer\n│\n├── skills/                      # ALL skills, single source of truth\n│   ├── caveman/{SKILL.md, README.md}\n│   ├── caveman-commit/{SKILL.md, README.md}\n│   ├── caveman-review/{SKILL.md, README.md}\n│   ├── caveman-help/{SKILL.md, README.md}\n│   ├── caveman-stats/{SKILL.md, README.md}\n│   ├── caveman-compress/{SKILL.md, README.md, scripts/}\n│   └── cavecrew/{SKILL.md, README.md}\n│\n├── agents/                      # cavecrew subagents (single source — kept at root for plugin auto-discovery)\n│   └── docs/                    # profile-registry docs — NOT agents (see note below)\n├── commands/                    # Codex/Gemini TOML command stubs (root for plugin auto-discovery)\n│\n├── src/                         # Internal source — not auto-discovered by plugin\n│   ├── hooks/                   # Claude Code hooks (installer reads here)\n│   ├── rules/                   # Auto-activation rule body (single source)\n│   ├── tools/                   # caveman-init.js (per-repo rule writer)\n│   └── mcp-servers/             # caveman-shrink npm-published MCP middleware\n│\n├── packages/                    # Current public packages\n│   ├── cli/                     # @caveman-ai/cli\n│   ├── pi-extension/            # @caveman-ai/pi — native Pi extension (bundled into the CLI, published on `pi-v*` tags)\n│   ├── sdk/                     # TypeScript + Python gateway clients\n│   ├── subagent-tax/            # Local harness-prefix benchmark\n│   └── shared/                  # Contracts + binary installer\n├── engine/ · proxy/             # Local compression runtime + provider proxy\n├── mcp/ · mem/ · shrink/        # Recovery tools, memory, output compression\n├── browse/                      # consumer copy; source = caveman-browse\n├── extension/                   # MV3 extension source\n├── shared/                       # Provider catalog + platform libraries\n│\n├── .claude-plugin/              # Claude Code plugin manifest (REQUIRED at root)\n├── .cursor-plugin/plugin.json   # Cursor plugin: skills/, hooks/hooks-cursor.json, agents/ (default scan)\n├── hooks/hooks-cursor.json      # Cursor plugin sessionStart hook ONLY. Never add hooks/hooks.json:\n│                                #   Claude Code and Gemini auto-load it from the plugin/extension root\n├── plugins/caveman/             # Claude Code plugin distribution (CI-mirrored)\n│   ├── skills/                  # ← from skills/\n│   └── agents/                  # ← from agents/\n│\n├── dist/                        # Build artifacts (gitignored)\n│   └── caveman.skill            # ZIP of skills/caveman/, rebuilt by CI\n│\n├── tests/                       # All tests (Node + Python)\n├── benchmarks/                  # Real token measurements through Claude API\n├── evals/                       # Three-arm eval harness\n├── docs/                        # User-facing docs site\n└── .github/workflows/           # CI sync\n```\n\n---\n\n## File structure and what owns what\n\n### Single source of truth files — edit only these\n\n| File | What it controls |\n|------|-----------------|\n| `skills/caveman/SKILL.md` | The caveman voice: rules, auto-clarity, persistence. Sibling skills `skills/ultracave/SKILL.md` (grammar stripped) and `skills/megacave/SKILL.md` (文言文) are self-contained and each map 1:1 to a stored mode. Edit these three for behavior changes. |\n| `src/rules/caveman-activate.md` | Always-on auto-activation rule body, consumed by `src/tools/caveman-init.js` (per-repo IDE rule files) and by the opencode `AGENTS.md` block. GENERATED: `skills/compile.mjs` derives it from `skills/caveman/SKILL.md` (thesis line + rule headlines) plus the fixed tail in `skills/activation-rule.mjs`, and rewrites the `RULE_BODY` fallback in `caveman-init.js` to match. Edit the skill or the tail, rerun `node packages/cli/scripts/compile-registries.mjs`; `tests/installer/rule-copies.test.mjs` fails on drift. |\n| `src/rules/caveman-openclaw-bootstrap.md` | Marker-fenced bootstrap snippet appended to `~/.openclaw/workspace/SOUL.md` by `installer/lib/openclaw.js`. Drives always-on caveman through the OpenClaw gateway. Must include the SENTINEL `Respond terse like smart caveman` and stay well under OpenClaw's 12K-per-bootstrap-file cap. |\n| `.codex/codex-sessionstart.js` | Repo-local Codex SessionStart hook. Resolves the configured default mode through `src/hooks/caveman-config.js` and emits the active skill's ruleset whole, replacing the hardcoded `full`-level echo `.codex/hooks.json` used to carry — so `CAVEMAN_DEFAULT_MODE`, a repo-local `.caveman.json` and a user-config `defaultMode: \"off\"` all take effect on Codex. Also the user-level always-on hook (#573): `installer/install.js --only codex` copies it (shipped via `package.json` `files`) into the owned payload `$CODEX_HOME/caveman/hooks/` beside `caveman-config.js` and `package.json`, with `caveman/skills/{caveman,ultracave,megacave}/SKILL.md`, and merges one SessionStart entry into `$CODEX_HOME/hooks.json`, identified by that script path so foreign and caveman-CLI `native-hook` entries are never touched. `--no-hooks`/`--minimal` skip it; uninstall unmerges before removing the payload. Codex asks the user to trust a new hook via `/hooks`; never pre-trust it. Carries a hand-copied `FALLBACK_VALID_MODES` for when the shared resolver is absent; `tests/hooks/codex-sessionstart.test.mjs` fails if it drifts from `VALID_MODES`. |\n| `installer/lib/openclaw.js` | OpenClaw install/uninstall helper. Frontmatter merge (`version`, `always: true`), SOUL.md marker append/strip, idempotent. Shared by `installer/install.js` and `src/tools/caveman-init.js`. |\n| `skills/caveman-commit/SKILL.md` | Caveman commit message behavior. Fully independent skill. |\n| `skills/caveman-review/SKILL.md` | Caveman code review behavior. Fully independent skill. |\n| `skills/caveman-help/SKILL.md` | Quick-reference card. One-shot display, not a persistent mode. |\n| `skills/caveman-compress/SKILL.md` | Compress sub-skill behavior. |\n| `skills/cavecrew/SKILL.md` | Cavecrew decision guide — when to delegate to caveman subagents vs vanilla. Edit only here. |\n| `skills/{caveman-setup,caveman-discover,caveman-learn,caveman-manage,caveman-optimize,caveman-explore,caveman-evidence-review}/SKILL.md` | Engine/proxy driver skills. Ship with the plugin (see the auto-discovery note below). |\n| `skills/{investigate-first,lean-build,surgical-patch,safe-refactor,migration,verify-and-stop}/SKILL.md` | Token-discipline work patterns — same goal as caveman prose (fewer output tokens) applied to code volume rather than wording. Deliberately un-branded so they read as generic patterns to the model. Ship with the plugin. |\n| `agents/cavecrew-investigator.md` | Read-only locator subagent (haiku). Output contract: `path:line — symbol — note`. |\n| `agents/cavecrew-builder.md` | Surgical 1-2 file editor subagent. Refuses 3+ file scope. |\n| `agents/cavecrew-reviewer.md` | Diff/file reviewer subagent (haiku). One-line findings with severity emoji. |\n| `src/plugins/opencode/plugin.js` | opencode native plugin. ESM Bun module — `session.created` writes flag, `tui.prompt.append` parses slash/natural-language activation and appends per-prompt reinforcement. Reuses `caveman-config.js` via `createRequire`. |\n| `src/plugins/opencode/commands/*.md` | Six opencode slash-command prompt templates (`/caveman`, `/caveman-{commit,review,compress,stats,help}`). |\n\n### `skills/` is auto-discovered wholesale — every subdirectory ships\n\n`.claude-plugin/marketplace.json` sets `\"source\": \"./\"`, so the plugin root IS\nthe repo root, and Claude Code auto-discovers **every** `skills/*/SKILL.md`\nwith no `skills` key in `plugin.json` to gate it. Adding a directory under\n`skills/` therefore installs it into every plugin user's agent, where its\n`description` competes for activation on ordinary work.\n\nThere is no allowlist. Before adding a directory here, decide whether it\nshould reach end users; if not, it belongs under `packages/` or another root.\nKeep the two tables above and the README \"What you get\" table in sync with the\nactual directory listing — `ls skills/` is the source of truth.\n\n### `agents/*.md` is auto-discovered too — and `plugin.json` must NOT list them\n\nSame mechanism, sharper edge. `plugin.json` used to carry an explicit\n`\"agents\": [\"./agents/cavecrew-*.md\"]` array. On Claude Code 2.1.235 that array\nloads **zero** agents — `claude plugin details caveman` reports `Agents (0)`\nwith it and `Agents (3)` without it, so the three cavecrew subagents the\n`cavecrew` skill delegates to did not exist for any plugin user. The docs say\n`string | string[]` is valid and a directory string is rejected outright\n(`agents: Invalid input`), so the default `agents/` scan is the only path that\nworks. Do not re-add the key.\n\nConsequence: **every `.md` anywhere in `agents/` becomes a subagent**, named\nfrom its frontmatter or filename. Maintainer documentation belongs outside that\ntree; profile-registry orientation lives in\n`docs/technical/agent-profile-registry.md`. `tests/verify_repo.py` fails build\nif any markdown beyond three intended cavecrew agents appears under `agents/`.\n\n### `commands/*.md` shadows same-named skills — keep the `.md` stubs unique\n\nClaude Code loads `commands/*.md` as flat skills alongside `skills/*/SKILL.md`,\nso a stub sharing a skill's name registers that name twice. `caveman.md`,\n`caveman-commit.md`, `caveman-review.md` and `caveman-stats.md` each duplicated\na real skill — a 3-line stub competing with the full ruleset for the same slash\ncommand — and were removed. The `.toml` stubs are Codex/Gemini-only and are not\nscanned, so `commands/` keeps one `.md`: `caveman-init.md`, which has no skill\ntwin. Guarded by `tests/verify_repo.py`.\n\n### Auto-generated / auto-synced — do not edit directly\n\nWe removed the agent-specific dotdir mirrors at the repo root (`.cursor/`, `.windsurf/`, `.clinerules/`, `.github/copilot-instructions.md`, root `caveman/SKILL.md`). They were never read by the installer — only used to self-apply caveman to this repo when a maintainer opened it in Cursor/Windsurf/Cline. Devs who want caveman in their editor while editing this repo should run `npx -y github:JuliusBrussee/caveman -- --with-init` once (writes per-repo rule files from `src/rules/caveman-activate.md` via `src/tools/caveman-init.js`). For per-user installs through the upstream skills CLI, `npx -y github:JuliusBrussee/caveman -- --only <agent>` runs `npx skills add ... -a <profile>`.\n\nA handful of dotdir leftovers (`.junie/`, `.kiro/`, `.roo/`, `.agents/`) still hold a stale `cavecrew/SKILL.md` mirror from before the cleanup. They aren't read by anything in the current install path; remove on sight, no migration needed.\n\nWhat's left is the Claude Code plugin distribution (required by the plugin loader) and the release ZIP.\n\n| File | Synced from |\n|------|-------------|\n| `plugins/caveman/skills/caveman/SKILL.md` | `skills/caveman/SKILL.md` |\n| `plugins/caveman/skills/caveman-compress/SKILL.md` (+ `scripts/`) | `skills/caveman-compress/SKILL.md` (+ `scripts/`) |\n| `plugins/caveman/skills/cavecrew/SKILL.md` | `skills/cavecrew/SKILL.md` |\n| `plugins/caveman/agents/cavecrew-*.md` | `agents/cavecrew-*.md` |\n| `dist/caveman.skill` | ZIP of `skills/caveman/` directory (gitignored; rebuilt by CI on release) |\n\nSkills not in this table (`caveman-commit`, `caveman-review`, `caveman-help`, `caveman-stats`) are not mirrored into the Claude Code plugin distribution by CI. They reach Claude Code through the standalone hook + skill install path, and reach other agents via `npx skills add`. A `plugins/caveman/skills/caveman-stats/` directory is currently checked in as a hand-committed copy; the sync workflow does not touch it, so don't rely on edits there to propagate.\n\n---\n\n## CI sync workflow\n\n`.github/workflows/sync-skill.yml` triggers on main push when `skills/**/SKILL.md` or `agents/cavecrew-*.md` changes.\n\nWhat it does:\n1. Copies `skills/caveman/SKILL.md` and `skills/cavecrew/SKILL.md` into their `plugins/caveman/skills/<name>/` mirrors so the Claude Code plugin loader sees the latest behavior.\n2. Copies `skills/caveman-compress/SKILL.md` and its `scripts/` into `plugins/caveman/skills/caveman-compress/`.\n3. Copies `agents/cavecrew-*.md` into `plugins/caveman/agents/`.\n4. Rebuilds `dist/caveman.skill` (ZIP of `skills/caveman/`) for the release artifact.\n5. Commits and pushes with `[skip ci]` to avoid loops.\n\nCI bot commits as `github-actions[bot]`. After PR merge, wait for workflow before declaring release complete.\n\nThe old steps that mirrored SKILL.md and rules into root dotdirs (`.cursor/`, `.windsurf/`, `.clinerules/`, `.github/copilot-instructions.md`) are gone — those mirrors no longer exist. The old `caveman-compress/` → `skills/compress/` rename-on-sync is also gone now that compress lives at `skills/caveman-compress/`.\n\n---\n\n## Hook system (Claude Code)\n\nFour hooks in `src/hooks/` plus a `caveman-config.js` shared module, a `caveman-parse.js` shared mode-change parser and a `package.json` CommonJS marker.\n\n**Mode state is per session.** Each session's mode lives in `$CLAUDE_CONFIG_DIR/.caveman-sessions/<session_id>.mode`, keyed by the `session_id` Claude Code puts in every hook payload *and* in the statusline's stdin JSON. `$CLAUDE_CONFIG_DIR/.caveman-active` survives as a last-write-wins compat mirror (falls back to `~/.claude/`).\n\n```\nSessionStart hook ──┐                                        ┌── UserPromptSubmit hook\n  (session_id,      │                                        │     (session_id, prompt)\n   source)          ▼                                        ▼\n        $CLAUDE_CONFIG_DIR/.caveman-sessions/<session_id>.mode\n                             │             │\n                          mirrors       reads\n                             ▼             ▼\n              .caveman-active      caveman-statusline.sh ◀── session JSON on stdin\n           (last-write-wins,        [CAVEMAN] / [ULTRACAVE] / [MEGACAVE]\n            compat only)\n```\n\nTwo invariants that are load-bearing, not stylistic:\n\n- **The legacy mirror never holds the literal `off`.** Deactivation deletes it. `off` is in `VALID_MODES`, so an older `caveman-mode-tracker.js` reading `off` from that path would pass its `!INDEPENDENT_MODES.has(...)` check and inject \"CAVEMAN MODE ACTIVE (off)\", and an older `caveman-statusline.sh` would render `[CAVEMAN:OFF]`. Mixed-version installs are real: plugin hooks and standalone hooks can both be registered at once, and `statusLine` holds an absolute path baked in at install time.\n- **`recordModeChange` compares against the session's own state, never the mirror.** That's why `readSessionModeRaw` exists alongside `resolveActiveMode` — diffing against the mirror would compare one session's transition to whatever another session wrote last and spam the log with phantom entries.\n\nBoth spellings of off are READ everywhere: a missing file (old semantics) and a literal `off` (new, durable).\n\n`src/hooks/package.json` pins the directory to `{\"type\": \"commonjs\"}` so the `.js` hooks resolve as CJS even when an ancestor `package.json` (e.g. `~/.claude/package.json` from another plugin) declares `\"type\": \"module\"`. Without this, `require()` blows up with `ReferenceError: require is not defined in ES module scope`.\n\nAll hooks honor `CLAUDE_CONFIG_DIR` for non-default Claude Code config locations.\n\n### `src/hooks/caveman-config.js` — shared module\n\nExports:\n- `getDefaultMode()` — resolves default mode in order: `CAVEMAN_DEFAULT_MODE` env var → repo-local config (`<cwd>/.caveman/config.json` or `<cwd>/.caveman.json`, walking up to the filesystem root; the first file that names a `defaultMode` wins, so a CLI-only `.caveman/config.json` does not hide a `.caveman.json`) → user config (`$XDG_CONFIG_HOME/caveman/config.json` / `~/.config/caveman/config.json` / on Windows `%APPDATA%\\caveman\\config.json`, then `~/.config/caveman/config.json`) → `'caveman'`. Config files may carry a BOM, be UTF-16LE, or hold comments and trailing commas. The env var short-circuits before any cwd walk. Repo-local config lets a team check in a per-project default without polluting every contributor's env or user config.\n- `findRepoConfigPath(start, accept)` — walks up from `start` (default `process.cwd()`) looking for the first `.caveman/config.json` or `.caveman.json` (that `accept(path)` approves, when given). Bounded to 64 ancestors. Refuses symlinked files (symmetric with `safeWriteFlag` / `readFlag`).\n- `safeWriteFlag(flagPath, content)` — symlink-safe flag write. Refuses if flag target or its immediate parent is a symlink. Opens with `O_NOFOLLOW` where supported. Atomic temp + rename. Creates with `0600`. Protects against local attackers replacing the predictable flag path with a symlink to clobber files writable by the user. Used by both write hooks. Silent-fails on all filesystem errors.\n- `validateSessionId(id)` — returns the id or `null`. Whitelist `^[A-Za-z0-9_-]{1,128}$`. **A session id becomes part of a filesystem path, so nothing may interpolate one without passing it through here first.** The symlink hardening in `safeWriteFlag` does not cover traversal.\n- `resolveActiveMode(claudeDir, sessionId)` — what mode is in effect: session file → legacy mirror, with both a missing file and a literal `off` collapsing to `null`. The reader every hook should use.\n- `readSessionModeRaw(claudeDir, sessionId)` — literal stored value for **this** session, no legacy fallback. Two callers only: `recordModeChange` (see the invariant above) and the SessionStart continuation branch, which has to tell a stored `off` apart from \"nothing stored yet\".\n- `writeSessionMode(claudeDir, sessionId, modeOrNull)` — the single writer. Writes `off` literally to the session file, unlinks the legacy mirror. Rejects anything outside `VALID_MODES`.\n- `writeSessionPrev` / `readSessionPrev` / `clearSessionPrev` — the displaced-prose-mode memory for one-shot skills (#599), scoped per session so two windows running `/caveman-commit` don't overwrite each other's return target.\n- `gcSessionStore(claudeDir, opts)` — mtime sweep of `.caveman-sessions/`, 14-day TTL (`CAVEMAN_SESSION_TTL_MS` overrides for tests), `maxDeletes` cap, refuses symlinks. Called from SessionStart on new sessions only — never on `compact`, which is frequent and shares the 5s hook budget.\n- `recordModeChange(claudeDir, newMode, sessionId)` — third arg tags the log entry; omitted (not null) when unknown, so pre-existing readers see the old shape.\n\n**Every function above accepts a `sessionId` that may be `null` or malformed and degrades to the legacy machine-wide behavior.** That is the entire backward-compatibility story: the old code path *is* the fallback branch, which is why the existing tests — none of which send a `session_id` — pass unchanged. The three hook entrypoints resolve these helpers individually (`cfg.writeSessionMode || <legacy stub>`) rather than adding them to their `requireSibling` shape checks: a `caveman-config.js` from before per-session state satisfies those checks, and hard-failing over the newer exports would trade \"machine-wide mode, as it always worked\" for \"no state at all\" on exactly the plugin-cache-drift scenario #848 is about.\n\n### `src/hooks/caveman-activate.js` — SessionStart hook\n\nRuns on every SessionStart — `source` is `startup`, `resume`, `clear`, `compact` or `fork`. Four things:\n1. Reads the hook payload from stdin for `session_id`, `source` and `cwd`\n2. Resolves this session's mode and persists it via `writeSessionMode`\n3. Emits caveman ruleset as hidden stdout — Claude Code injects SessionStart hook stdout as system context, invisible to user\n4. Checks `settings.json` for statusline config; if missing, appends nudge to offer setup on first interaction (one-shot, gated by `.caveman-nudge-shown`)\n\n**`source` branching.** `RESET_SOURCES` is `{startup, clear}` — only those re-derive `getDefaultMode()`. Everything else (`compact`/`resume`/`fork`, an unrecognized source, and the watchdog's `unknown`) READS the stored mode via `readSessionModeRaw`, falling back to the legacy mirror for a session that predates the store, and only then to the default. Reading the LITERAL value is the point: #691 already branched on `source`, but it read the legacy flag where off is spelled \"no file\", so a deactivated session found nothing and re-derived the default anyway — \"stop caveman\" was still undone by the next auto-compaction. A stored `off` now short-circuits to `OK` with no ruleset. It still re-emits when a mode IS active: compaction is what prunes the rules out of context, which is the whole reason this hook runs on every source. `clear` counts as a fresh start (explicit user reset — nothing else in the conversation survives it, so neither should a \"stop caveman\" from before it); this deliberately differs from #691's comment, which grouped `clear` with the continuations.\n\n**Headless sessions start under `manual` (#377).** Whenever the hook falls back to the configured default (`startMode()`), a `CLAUDE_CODE_ENTRYPOINT` starting with `sdk-` (`claude -p`, Agent SDK) turns any non-`off` default into `manual`: tool probes that parse the reply get no ruleset until an explicit `/caveman`. Interactive entrypoints (`cli`, `claude-vscode`, `claude-desktop`, …) never match; an explicit `CAVEMAN_DEFAULT_MODE` env var opts back in. Hook test helpers drop `CLAUDE_CODE_ENTRYPOINT` from the env, or a headless runner flips every SessionStart test.\n\n**The stdin contour is load-bearing.** Activation runs on the first COMPLETE JSON object, not at EOF, with a 2000ms `PAYLOAD_WATCHDOG_MS` backstop and `process.stdin.unref()` in `finish()`. `pause()` alone is not enough — it stops reading but leaves the pipe handle referenced, so a host that holds the write end open (Windows pipe close lags arbitrarily, #729/#833) burns the whole 5s budget. The watchdog must NOT assume `startup`: it reports `unknown`, which preserves the session's stored mode rather than resetting it. Several suites also invoke this hook via `subprocess.run()` with no `input=`, inheriting a stdin that never reaches EOF — `tests/test_hooks.py::test_hook_never_blocks_on_stdin_that_never_closes` guards that path.\n\nSilent-fails on all filesystem errors — never blocks session start.\n\n**`--subagent`: the SubagentStart hook (#621).** SessionStart context reaches only the parent thread, so the same script is registered for SubagentStart with `--subagent` (plugin.json, `installer/install.js`, `src/hooks/install.{sh,ps1}`). It reuses the stdin reader and watchdog, resolves THIS session's mode via `resolveActiveMode` (a session with no stored state at all — SessionStart skipped or failed, or the session predates the store — falls back to the last-write-wins legacy mirror, the same degrade the tracker's reinforcement uses, so a subagent matches its parent thread; pinned in `tests/hooks/subagent-start.test.mjs`), and prints `{\"hookSpecificOutput\":{\"hookEventName\":\"SubagentStart\",\"additionalContext\":<that mode's skill>}}`. Nothing is printed when the session stored `off` (no \"stop caveman\" leak, #672; another window's mode never overrides a stored `off`), in a one-shot mode, for `cavecrew-*` agents (they carry their own ultracave voice), or under a repo `defaultMode: \"off\"` (#634, same gate as the tracker's reinforcement). Read-only: no mode write, no mode log, no GC, no nudge, no cavecrew model overrides. Uninstall needs nothing extra — `removeCavemanHooks` walks every event and matches the `caveman-activate.js` basename.\n\n### `src/hooks/caveman-mode-tracker.js` — UserPromptSubmit hook\n\nReads JSON from stdin — `session_id` scopes every read and write. Three responsibilities:\n\n**1. Slash-command activation.** If prompt starts with `/caveman`, writes the session's mode via `writeSessionMode`:\n- `/caveman` → configured default (see `caveman-config.js`, defaults to `caveman`)\n- `/ultracave` (alias `/caveman ultra`) → `ultracave`\n- `/megacave` (alias `/caveman wenyan`, any `wenyan-*`) → `megacave`\n- `/caveman lite` or `/caveman full` (legacy aliases) → `caveman`\n- `/caveman-commit` → `commit`\n- `/caveman-review` → `review`\n- `/caveman-compress` → `compress`\n\n**2. Natural-language activation/deactivation.** Matches phrases like \"activate caveman\", \"turn on caveman mode\", \"talk like caveman\" and writes the configured default mode. Matches \"stop caveman\", \"disable caveman\", \"normal mode\", \"deactivate caveman\" etc. and stores a durable `off` for the session. README promises these triggers, the hook enforces them.\n\n**3. Per-turn reinforcement.** When the session's mode is a non-independent one (i.e. not `commit`/`review`/`compress`), emits a small `hookSpecificOutput` JSON reminder so the model keeps caveman style after other plugins inject competing instructions mid-conversation. The full ruleset still comes from SessionStart — this is just an attention anchor.\n\n### `src/hooks/caveman-statusline.sh` — Statusline badge\n\nReads the session JSON Claude Code pipes to it on stdin, extracts `session_id` (pure bash — no `jq` dependency), and reads `.caveman-sessions/<id>.mode`; falls back to `$CLAUDE_CONFIG_DIR/.caveman-active` when there is no usable id. Outputs colored badge string for Claude Code statusline:\n- `caveman` or empty → `[CAVEMAN]` (orange)\n- `off` → nothing at all. Never `[CAVEMAN:OFF]` — that reads as a mode rather than the absence of one\n- `ultracave` → `[ULTRACAVE]`, `megacave` → `[MEGACAVE]`; one-shots → `[CAVEMAN:<MODE_UPPERCASED>]` (orange). Legacy stored values render through the same map\n\nStdin is bounded the same way as the SessionStart hook: `[ ! -t 0 ]` skips an interactive terminal, and `read -r -d '' -t 1` caps the wait. The timeout is an **integer on purpose** — macOS ships bash 3.2, which rejects `-t 0.3` with `invalid timeout specification`.\n\nThe statusline reports mode only. Old `.caveman-statusline-suffix` values are ignored; fixed-ratio savings estimates were removed because no measured baseline supported them. `/caveman-stats` records actual Claude output/cache-read counts and mode attribution with savings unknown. Gemini uses native `/stats model` and `/stats session`; never read Claude transcripts as another host's usage.\n\nConfigured in `settings.json` under `statusLine.command`. PowerShell counterpart at `src/hooks/caveman-statusline.ps1` for Windows. Both scripts symlink-refuse and whitelist-validate mode flag contents — never echo arbitrary bytes.\n\n### `src/hooks/caveman-stats.js --record` — SessionEnd hook\n\nSame script as `/caveman-stats`; the `--record` flag turns it into a silent recorder. Reads the SessionEnd payload (`session_id`, `transcript_path`) and appends one snapshot of that transcript's totals to `$CLAUDE_CONFIG_DIR/.caveman-history.jsonl`, so lifetime history fills in without the user running `/caveman-stats`. Only the transcript the host names is read — never the newest file on disk. Stdin follows the activate-hook contour: first complete JSON object, 2000ms `PAYLOAD_WATCHDOG_MS` (gives up without recording), `unref()` on finish. Always exits 0 and writes nothing to stderr — a SessionEnd failure would surface as a hook error while the user quits. Every installer (plugin manifest, `installer/install.js`, `src/hooks/install.{sh,ps1}`) writes `--record` unquoted after the quoted script path; `rewriteLegacyManagedHookCommands` carries trailing `--flag` args over when it moves a bare-`node` command to the absolute node path.\n\n### `src/hooks/caveman-host-session-start.js` — Cursor / Copilot CLI sessionStart hook\n\n`node caveman-host-session-start.js <host>`, one script for hosts other than Claude Code. Emits the resolved mode's ruleset under the host's key: `additional_context` for `cursor`, `additionalContext` for `copilot`. The mode is `getDefaultMode()` resolved from the session's workspace (`cwd`, Cursor's `workspace_roots` or `CURSOR_PROJECT_DIR`), so `defaultMode` and repo `.caveman.json` apply; `off`, `manual`, one-shot modes, an unknown host or any failure print `{}`. Writes no state. The owned install at `<host>/caveman/hooks/` reads its pinned `<host>/caveman/skills/` before the shared resolver, which would otherwise find the host's unpinned `npx skills` copy first. Stdin follows the activate-hook contour.\n\n### Hook installation\n\n**Plugin install** — hooks wired automatically by plugin system.\n\n**Standalone install** — `installer/install.js` (the unified Node installer) copies hook files into `$CLAUDE_CONFIG_DIR/hooks/` and merges SessionStart + SubagentStart + UserPromptSubmit + SessionEnd + statusline into `settings.json`. Uses the JSONC-tolerant helpers in `installer/lib/settings.js` so a commented `settings.json` no longer crashes the merge. Defensive `validateHookFields` runs before every write to prevent a single malformed hook from poisoning the entire file (Claude Code Zod silently discards the whole `settings.json` on schema mismatch).\n\nThe `install.sh` / `install.ps1` shims at the repo root delegate to `installer/install.js` via `node` (local clone) or `npx -y github:JuliusBrussee/caveman` (curl|bash). No legacy fallback path remains — earlier `install.sh.legacy` / `install.ps1.legacy` files were removed.\n\n**Uninstall** — `npx -y github:JuliusBrussee/caveman -- --uninstall` (or `node installer/install.js --uninstall` from a clone). Strips caveman hook entries from `settings.json` via substring marker `caveman`, deletes hook files, and removes the Claude plugin / Gemini extension. Native skill copies use `installer/lib/provider-skills.js` and ownership journals; uninstall removes unchanged owned files while preserving foreign or edited content. Use the same vendor home overrides for install and uninstall. Delegated `npx skills add` installs remain managed by the host/upstream skill manager.\n\n---\n\n## Skill system\n\nSkills = Markdown files with YAML frontmatter consumed by Claude Code's skill/plugin system and by `npx skills` for other agents.\n\nEach skill has a human-facing `README.md` alongside the LLM-facing `SKILL.md`. The README explains what the skill does for users browsing GitHub; the SKILL.md is the prompt body the agent loads. Don't merge them — different audiences, different formats.\n\n### Three skills, one mode model\n\nThree self-contained skills, each 1:1 with a stored mode id: `caveman` (default voice), `ultracave` (grammar stripped), `megacave` (文言文). The SessionStart hook injects `skills/<id>/SKILL.md` whole; there is no per-level filtering. Legacy stored values map on read: `lite` and `full` → `caveman`, `ultra` → `ultracave`, every `wenyan*` → `megacave`. Writes emit new ids only. Mode persists until changed or session ends. Plan: `docs/technical/three-skills-refactor.md`.\n\n`defaultMode: \"manual\"` is a Claude Code startup policy, not a fourth mode. It starts inactive, stores only `off`, and explicit bare-command or natural-language activation resolves to `caveman`. Keep it out of `VALID_MODES`; only configuration accepts it. OpenCode maps this policy to its existing caveman default because its installer also supplies static AGENTS.md activation. `/caveman status` is read-only and must return before one-shot restoration or any mode-state mutation.\n\n### Auto-clarity rule\n\nCaveman drops to normal prose for: security warnings, irreversible action confirmations, multi-step sequences where fragment ambiguity risks misread, user confused or repeating question, questions the agent asks the user. Resumes after. Defined in skill — preserve in any SKILL.md edit.\n\n### caveman-compress\n\nSub-skill in `skills/caveman-compress/SKILL.md`. Takes file path, compresses prose to caveman style, writes to original path, saves backup at `<filename>.original.md`. Validates headings, code blocks, URLs, file paths, commands preserved. Retries up to 2 times on failure with targeted patches only. Requires Python 3.10+.\n\nThe slash command is `/caveman-compress` everywhere — same name in plugin and standalone install. CI no longer renames the directory on sync (the old `caveman-compress/` → `skills/compress/` sed rename is gone now that the source lives at `skills/caveman-compress/`).\n\n### caveman-commit / caveman-review\n\nIndependent skills in `skills/caveman-commit/SKILL.md` and `skills/caveman-review/SKILL.md`. Both have own `description` and `name` frontmatter so they load independently. caveman-commit: Conventional Commits, ≤50 char subject. caveman-review: one-line comments in `L<line>: <severity> <problem>. <fix>.` format.\n\n---\n\n## Agent distribution\n\nHow caveman reaches each agent type:\n\n| Agent | Mechanism | Auto-activates? |\n|-------|-----------|----------------|\n| Claude Code | Plugin (hooks + skills) or standalone hooks | Yes — SessionStart hook injects rules; SubagentStart passes the session's mode to subagents |\n| Codex | `npx skills add -a codex` plus an owned SessionStart hook in `$CODEX_HOME/hooks.json` (installer, default on); plugin in `plugins/caveman/`; repo `.codex/hooks.json` and `.codex/config.toml` for this checkout | Yes — SessionStart hook, after the user trusts it once via `/hooks` |\n| Gemini CLI | Extension with `GEMINI.md` context file | Yes — context file loads every session |\n| opencode | Native plugin (`src/plugins/opencode/`) copied into `~/.config/opencode/plugins/caveman/` + `AGENTS.md` ruleset + skills/agents/commands directories. Plugin uses `session.created` and `tui.prompt.append` lifecycle hooks. No statusline (opencode TUI exposes no plugin-writable badge). | Yes — `session.created` writes flag, `AGENTS.md` carries always-on ruleset |\n| OpenClaw | Workspace skill at `~/.openclaw/workspace/skills/caveman/SKILL.md` (frontmatter merged with `version` + `always: true`) plus a marker-fenced bootstrap block in `~/.openclaw/workspace/SOUL.md`. Both writes go through `installer/lib/openclaw.js`; workspace path is overridable via `OPENCLAW_WORKSPACE`. | Yes — SOUL.md is auto-injected each turn under \"Project Context\" (subject to OpenClaw's 12K-per-file / 60K-total bootstrap caps) |\n| Cursor | Plugin (`.cursor-plugin/plugin.json`: skills + `hooks/hooks-cursor.json` sessionStart hook) or `--only cursor`: `npx skills add ... -a cursor -g` skills, then `installer/lib/cursor-native.js` owns cavecrew agents in `~/.cursor/agents/` (Claude `model:` dropped, `readonly: true` on investigator/reviewer) and the shared hook payload in `~/.cursor/caveman/` with one sessionStart entry merged into `~/.cursor/hooks.json`. Both hooks run `src/hooks/caveman-host-session-start.js cursor`. Per-repo `.cursor/rules/caveman.mdc` via `--with-init`. Pick plugin OR `--only cursor`, not both (two hooks inject twice). | Yes — sessionStart `additional_context`, honors `defaultMode` (off/manual inject nothing) |\n| Windsurf | `npx skills add ... -a windsurf` (default via `--only windsurf`); per-repo `.windsurf/rules/caveman.md` via `--with-init` | Yes — always-on rule |\n| Cline | `npx skills add ... -a cline` (default via `--only cline`); per-repo `.clinerules/caveman.md` via `--with-init` | Yes — Cline auto-discovers `.clinerules/` |\n| Copilot | `npx skills add ... -a github-copilot` (detected via the `copilot` CLI plus its `~/.copilot` dir, since AWS Copilot CLI ships a `copilot` binary too, or the VS Code/Cursor extension); per-repo `.github/copilot-instructions.md` + `AGENTS.md` via `--with-init`. When the Copilot CLI is present (`copilot` on PATH or `$COPILOT_HOME` set — not a bare `~/.copilot`, which `npx skills add -a github-copilot` itself creates) the installer also owns `$COPILOT_HOME/caveman/` (shared hook payload) and `$COPILOT_HOME/hooks/caveman.json` (sessionStart → `caveman-host-session-start.js copilot`); `--no-hooks` skips it. No userPromptSubmitted hook: Copilot drops its output. | Copilot CLI: yes — sessionStart `additionalContext`, honors `defaultMode`. VS Code Copilot Chat: repo-wide instructions via `--with-init` |\n| Grok Build | Owned skill copies into `$GROK_HOME/skills` (default `~/.grok`) plus a marker-fenced ruleset block in `$GROK_HOME/AGENTS.md`, appended/stripped through `installer/lib/openclaw.js`'s marker helpers | Yes — Grok loads `~/.grok/AGENTS.md` as global rules (docs-backed; no live binary run) |\n| Antigravity CLI (`agy`) | `--only antigravity-cli` stages a temp plugin (`plugin.json`, the nine skills, `rules/AGENTS.md` = `src/rules/caveman-activate.md`) and runs `agy plugin install <dir>`; agy copies it to `~/.gemini/config/plugins/caveman/` and owns it. Uninstall: `agy plugin uninstall caveman` when `agy plugin list` names it. No session-start hook exists in agy (PreInvocation fires before every model call), so no `defaultMode`. | Yes — plugin `rules/AGENTS.md` merges into agy's active rule set (verified agy 1.2.17) |\n| Continue, AiderDesk, Antigravity IDE/2.0 | Owned physical copies into each vendor's supported directory, honoring configured homes; separate explicit targets for the two Antigravity products | Host skill invocation; feature settings may be required |\n| Others (Junie, Trae, Warp, Tabnine, Mistral, Qwen, Devin, Droid, ForgeCode, Bob, Crush, iFlow, OpenHands, Qoder, Rovo Dev, Replit, …) | Delegated `npx skills` personal installs; Replit uses project scope. Configured iFlow/Crush roots use owned copies | Host skill invocation; `/caveman` where supported |\n\nopencode reaches Tier 1 minus the statusline (opencode's TUI has no plugin-writable badge). Mode flag lives at `~/.config/opencode/.caveman-active` for any external tooling that wants to surface it.\n\nFor agents without hook systems, the always-on snippet lives in `INSTALL.md`'s \"Want it always on?\" section — keep current with `src/rules/caveman-activate.md`.\n\n**Adding a new agent.** Edit the `PROVIDERS` array in `installer/install.js` — single source of truth, no more bash/PS1 dual-source drift. Each entry has `id`, `label`, `mech`, `detect` (clause spec like `command:foo||dir:$HOME/x`; `&&` joins terms that must all hold), optional `profile` (vercel-labs/skills slug), optional `soft: true` (config-dir-only detection).\n\n1. The profile slug must exist in upstream [vercel-labs/skills](https://github.com/vercel-labs/skills). Verify against the README before merging — wrong slugs cause `npx skills add` to fail at runtime, not at install-script load.\n2. Run `node installer/install.js --list` to confirm the new row renders correctly.\n3. Soft probes (config-dir-only) are fine but tag them with `soft: true`. They render with `(soft)` in `--list` so users know detection is best-effort.\n\n---\n\n## Evals\n\n`evals/` has three-arm harness:\n- `__baseline__` — no system prompt\n- `__terse__` — `Answer concisely.`\n- `<skill>` — `Answer concisely.\\n\\n{SKILL.md}`\n\nHonest delta = **skill vs terse**, not skill vs baseline. Baseline comparison conflates skill with generic terseness — that cheating. Harness designed to prevent this.\n\n`llm_run.py` calls `claude -p --system-prompt-file ... --output-format json` per (prompt, arm), isolated from user settings, plugins and MCP, saves text plus Claude-reported usage (input/output/cache tokens, cost) to `evals/snapshots/results.json` (`results.<lang>.json` with `CAVEMAN_EVAL_LANG`). `measure.py` reads snapshot offline with tiktoken (OpenAI BPE — approximates Claude tokenizer, ratios meaningful, absolute numbers approximate). `CAVEMAN_EVAL_SET=fidelity` runs the regex-checked cases in `evals/prompts/fidelity.json` into `snapshots/fidelity.json`; `score_fidelity.py` scores it offline.\n\nAdd skill: drop `skills/<name>/SKILL.md`. Harness auto-discovers. Add prompt: append line to `evals/prompts/en.txt`.\n\nSnapshots committed to git. CI reads without API calls. Only regenerate when SKILL.md or prompts change.\n\n---\n\n## Benchmarks\n\n`benchmarks/` runs real prompts through Claude API (not Claude Code CLI), records raw token counts. Results committed as JSON in `benchmarks/results/`. Benchmark table in README generated from results — update when regenerating.\n\nTo reproduce: `uv run python benchmarks/run.py` (needs `ANTHROPIC_API_KEY` in `.env.local`).\n\n---\n\n## Key rules for agents working here\n\n- Edit `skills/<name>/SKILL.md` for behavior changes. Never edit synced copies under `plugins/caveman/skills/`.\n- Edit `src/rules/caveman-activate.md` for auto-activation rule changes. Never edit any per-agent rule copy a user has on their machine.\n- Edit `src/rules/caveman-openclaw-bootstrap.md` for the OpenClaw SOUL.md bootstrap snippet. Keep the `<!-- caveman-begin -->` / `<!-- caveman-end -->` markers and the `Respond terse like smart caveman` sentinel — `installer/lib/openclaw.js` keys idempotency off both. If you change the embedded fallback in `installer/lib/openclaw.js`, keep it byte-equivalent to the file.\n- Per-skill human docs live in `skills/<name>/README.md`. The LLM-facing body is in `SKILL.md`. Don't merge them — different audiences.\n- Build artifacts go in `dist/`. Never check files into `dist/` manually — CI rebuilds them on push, and `dist/` is gitignored.\n- README most important file for user-facing impact. Optimize for non-technical readers. Preserve caveman voice.\n- `INSTALL.md` is the per-agent install reference. Keep the install table in `README.md` short and link out to `INSTALL.md` for the full matrix.\n- Benchmark and eval numbers must be real. Never fabricate or estimate.\n- CI workflow commits back to main after merge. Account for when checking branch state.\n- Hook files must silent-fail on all filesystem errors. Never let hook crash block session start.\n- Any new flag file write must go through `safeWriteFlag()` in `caveman-config.js`. Direct `fs.writeFileSync` on predictable user-owned paths reopens the symlink-clobber attack surface.\n- Mode state reads/writes go through the `caveman-config.js` session helpers (`resolveActiveMode` / `writeSessionMode` / …), never by joining a path by hand. Any `session_id` that reaches a path must pass `validateSessionId()` first.\n- **Keep mode-state logic in `caveman-config.js`.** `src/plugins/opencode/plugin.js` cannot `require()` from disk (compiled Bun binary) — it reads that file and evaluates it through `new Function(...)` with a `createRequire` that resolves only node built-ins. Parsing already lives in `caveman-parse.js` (#602) and loads the same way, so a second parser copy is no longer the risk; a THIRD home for the state primitives would be, because opencode would keep its own drifting version of them.\n- The two statusline scripts have no shared runtime with the JS, so they re-implement path resolution by hand. `tests/verify_repo.py::verify_powershell_static` greps both against the `SESSIONS_DIRNAME` and `SESSION_ID_RE` constants to catch drift — there is no behavioral `.ps1` test on the POSIX runners, so that grep is the only guard on the Windows badge.\n- Editing anything in `src/hooks/` means regenerating `src/hooks/checksums.sha256` (same file set, recomputed digests) — `tests/verify_repo.py` fails the build otherwise, and `installer/install.js` verifies remote hook downloads against the manifest for the pinned ref.\n- Hooks must respect `CLAUDE_CONFIG_DIR` env var, not hardcode `~/.claude`. Same for `installer/install.js` / statusline scripts.\n- **Any entrypoint that reads a host hook payload from stdin returns on the first complete JSON object, never at EOF.** Under the Windows pipe implementation the host's close lags arbitrarily (#729/#833, #949), so a reader that waits for EOF burns the host's whole budget with its work done. The readers that follow this rule: `caveman-activate.js`, `caveman-mode-tracker.js`, `caveman-stats.js --record` and `caveman-host-session-start.js` (per-chunk parse), `packages/cli/src/native-hook-fast.ts` (`stdin()`), `readHookStdin()` in `packages/cli/src/index.ts` (shrink-hook, memory recall, native-hook), and `readNativeHookPayload` in `proxy/cmd/caveman-proxy/main.go`. A new hook callback uses one of those, not `readStdin()` or `io.ReadAll`. Each has a keep-the-writer-open regression test; add one for any new reader.\n- `installer/install.js` is the only installer source. `install.sh` / `install.ps1` at repo root are 30-line shims that delegate to it. Never re-add per-OS install logic to the shims — that's how we got the Windows quoting bug (#249).\n- Any settings.json read in installer or hooks must go through `installer/lib/settings.js` `readSettings()` so JSONC comments don't crash the merge. Any settings.json write must run through `validateHookFields()` first.\n","isInternal":false,"tokens":11935,"sizeBytes":48184},{"name":"CLAUDE.md","path":"engine/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/engine/CLAUDE.md","title":"engine - Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# engine — the content-aware Caveman Engine (commercial, binary-distributed)\n\nDetect a payload's type → route to a safety-classed compressor → count the token reduction\n→ store the original for recovery. The stable four-call API (`Compress`/`Retrieve`/`Detect`/`Stats`)\nis what the proxy, CLI, SDKs, MCP, and WASM build all share. Everything it reports is\n`inferred`; it never says `verified`.\n\n## Layout\n- `engine.go` — the `Engine` core: detect → route → compress → token ratio → CCR. `record` mode + miss + parse-fail + not-smaller + no-store all pass through.\n- `result.go` — `Result`/`Options`/`Mode`; unknown mode fails closed to `record`.\n- `detect.go` — content router (`test-report`/`json`/`diff`/`code`/`log`/`search-result`/`html`/`tabular`/`config`/`terminal`/`text`); low confidence → `text`.\n- `listing.go` — strips a read tool's line-number gutter (`1\\t{`, `2\\t  \"unit\"`) before Detect and before the compressor, then restores it after. Agents send file *listings*, not files; the gutter is presentation, and left in place it made every guttered payload detect as `text` and compress ~0%. Restoration keeps each surviving line's ORIGINAL number and is declined entirely for transforms that restructure rather than elide (re-encoded JSON), because numbers that describe nothing are worse than none.\n- `safety/` — the S0–S4 registry; S4 is lossy and `RequiresCCR`. Unknown class → fail closed.\n- `tokens/` — `Counter` interface; default is an offline, vocab-embedded BPE tokenizer (o200k_base). `inferred` always.\n- `contextwindow/` — deterministic BM25 context packer with recency/error/priority signals and token-budget accounting.\n- `compressors/` — `Compressor` interface + registry; structural JSON/log/test-report/search/diff/text/HTML/table/config/code compressors plus forced-only TOON/tool-schema/tool-schema-annotations/accessibility/repetition paths — `Default()` registers 16. `test-report` and `toolschema-annotations` stay manifest-excluded because no compiled plan routes to them (the proxy's `compiledContentType` allowlist has neither); advertising either would rotate `RegistrySHA256` and fail existing Cave Build locks. A compressor is a pure byte transform.\n- `ccr/` — `~/.caveman/ccr.db` SQLite recovery plus typed native-session store;\n  content-addressed handles, byte-exact `Get`, session scope, dependencies,\n  current/stale state, and Hot/Warm/Cold/Archived lifecycle. Embedded SQLite uses\n  one serialized connection so concurrent hook/repository writes cannot split an\n  in-memory schema or race with `SQLITE_BUSY`.\n- `pixel/` — pxpipe port (MIT, see its NOTICE): text→PNG request compression. Embedded glyph atlases + renderer + profitability gate + per-wire-format transforms (Anthropic/OpenAI/Gemini). S4-lossy, allowlist-gated (`CAVE_PIXEL_MODELS`, default `claude-fable-5,gpt-5.6`), consumed by the proxy's `pixel` mode; never wired into `Detect` and never imported by the WASM build (≈4 MB assets).\n- `evals/` — the local eval harness + fail-closed graders + embedded `fixtures/`. `Run()` is the quality gate.\n- `cmd/caveman-engine/` — the binary the CLI shells out to: `compress | detect | retrieve | stats | registry | toon encode|decode | evals run | pixel render|simulate`. `toon` is the stateless (no-CCR) JSON⇄TOON converter; both directions fail closed.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=engine` / `make product-test PRODUCT=engine`.\n- A new compressor is a self-contained file in `compressors/` + tests, registered in `Default()`. Forced-only compressors such as `toolschema` and `toon` must not be added to `Detect`.\n- Compressors never count tokens, touch CCR, or hit the network — the engine core does that around them.\n- **The `toolschema` transform is client-side-only today.** Registration makes it forceable by\n  engine callers; it does not make it reachable from managed-gateway traffic. Provider adapters\n  intentionally keep tool arrays in the frozen prompt-cache prefix, and no billed route invokes\n  this compressor. Engine API/CLI callers may force it locally, and `caveman-shrink` is its\n  dedicated product surface; those reductions remain local and `inferred`. Managed gateway has a\n  separate S2 tool-search/deferral path; do not conflate it with compression. Any future gateway\n  route for this transform needs cache-versus-schema cost arithmetic, stable byte-identical prefix\n  output, and an eval gate before activation.\n\n## Gotchas (honesty invariants — correctness, not style)\n- **fail-closed transforms**: every compressor passes the original through unchanged on any parse problem; the engine also passes through when the result is not smaller in tokens. Reserve **byte-safe** for classes whose `safety.Info.ByteSafe` is true.\n- **elide repetition, never a document**: every unit-dropping compressor (text, log, json, tabular, config, searchresult, terminal) calls `keepNonRedundant` (`compressors/redundancy.go`) before emitting. It keeps one representative of each distinct content class, so a stream of near-identical lines still collapses while a bibliography, a source file or a table of measurements survives whole — and when nothing is left to drop, the not-smaller check passes the payload through and claims nothing. This is a correctness rule, not a tuning knob: eliding a document the agent must enumerate cost the agent bench 5.5x its baseline on 2026-08-06 (`bench/agent/README.md`). Class matching masks digit runs, but only for units carrying at least one word — a wordless unit like `6 ['1973.', 251]` is compared with its digits intact, because there the numbers ARE the content.\n- **an elision marker states only what it verified**: `compressors/invariants.go` enriches the log/tabular/JSON markers with facts computed from the exact units that marker replaced — a field constant across every one of them (`all state=charged`), a varying field enumerated IN FULL with exact counts that sum to the elided count (`status: fulfilled×15 shipped×3 processing×2`, with an `absent×N` bucket for units lacking the field), and a numeric range printed from the ORIGINAL extreme value strings (`range amount=5.00..199.99`). Nothing else. A credential-shaped field name, a value with a space or over 24 bytes, more than five distinct values, and a run in which any unit yields no fields at all are all withheld **entirely** — never shortened, because a partial list reads as complete. When nothing survives, the marker renders byte-identically to the bare `… N rows elided (caveman) …`. An enumeration whose every bucket holds ONE unit is an identifier list, not an invariant, so instead of enumerating it the marker states its COVERAGE — `order_id: 25 distinct, ord-1000..ord-1024`, the exact distinct count plus the byte-wise smallest and largest ORIGINAL value strings, claiming nothing about the gaps between them (this is the only fact that answers \"was ord-1043 among the elided rows, or absent from the ledger?\", the last legitimate retrieve on the v5 probe). When a coverage run is EXACTLY dense — one shared prefix, fixed-width digits, and `max-min+1` equal to the counted distinct set — it says so instead: `wh-5000..wh-5059 all 60 present`, a verified membership answer that a set-difference task needs and would otherwise retrieve for. Density is counted, never inferred from the endpoints; mixed suffix widths, two prefixes, or a single gap all fall back to the counted form (whose own arithmetic still says how many are missing, never which). A dense entry outlives even the constants when the budget binds; a bounded one goes before them. At most two coverage entries per summary — a record carries several identifier columns and at ~38 bytes each they will name the run three ways and crowd out a 16-byte constant. An enumeration averaging two units or fewer per bucket still prints but is shed first. Summaries are capped at 160 bytes and a quarter of the bytes they replace (floor 96, never over half), shedding identifier-ish enumerations, then ranges, then bounded coverage, then constants, then dense coverage, class enumerations last; **a summary that sheds says so**, with a trailing `+N omitted` section, because every section above reads as the complete set of facts of its kind and a silently trimmed `all …` list would let an agent conclude a field it cannot see was not constant — the same error a partial enumeration is refused to avoid. The notice is charged to the same budget as the facts, so both caps still hold exactly; one trailing contract line per payload is added only when ≥4 KB was actually dropped. **A run under 3 units is not elided at all** unless collapsing both summarizes and halves its bytes — a one-unit marker plus its recovery handle costs about what the unit cost and reads as a hole. Together this costs ~4 points of ratio on the toolwork corpus (0.83 → 0.79) and is bought on purpose: on 2026-08-08 an agent that could not tell what had been elided made 46 sequential `caveman_retrieve` calls at 3.3x the uncompressed arm; a first fix stating only constants and ranges left the storms at 11–27 calls because it was silent about the one mixed-value field the task turned on; and a second round still leaked retrieves through identifier enums, fact-free small classes, and `{\"__caveman_elided__\":1}` singletons.\n- **a recovery view is made of whole units**: `retrieve_query.go` narrows a CCR recovery to the units matching a query, and a unit is whatever is self-delimiting — a complete JSON record (never a field line: `\"status\": \"unfulfilled\",` is a fragment), a whole CSV record sliced out of the original bytes with the header shown once, a whole log/NDJSON line, or a whole paragraph from non-JSON prose. JSON field names are never provenance: `messages[].content`, `text`, and `system` stay attached to their full object record, because tool output can use the same names as provider requests. Separate JSON arrays have explicit gap sentinels; omitted envelope/scalar bytes get leading/trailing markers. Content it cannot decompose (JSON with no array of records) returns in FULL rather than line-split, and a narrowed view that is not smaller also returns the full original; over-returning is safe, a fragment is not. Every gap — between two returned units, before the first, after the last — prints `… [caveman: non-adjacent] …`, so adjacency in the view never implies adjacency in the original. This is a correctness rule with a measured cost: on 2026-08-08 query mode returned BM25-ranked lines of a pretty-printed orders page, the agent read `\"status\": \"unfulfilled\"` (ord-1043's) directly above `\"order_id\": \"ord-1047\"`, attributed it to ord-1047, and reported 5 unfulfilled orders where there were 3 — two wrong answers in a 12-task bench run.\n- **one recovery surface, two id spaces**: the CCR store holds compression blob handles (`ccr_…`, `store.Get`) AND the native runtime's typed objects (`store.GetObject`), and the runtime shows the latter to agents as `ccr://<objectID>` when it masks a whole tool output. `Engine.Retrieve` resolves a blob first and falls through to the object table before failing, and `mcp.normalizeRecoveryHandle` accepts `ccr_…`, `<<ccr:…>>`, `ccr:…` and `ccr://…`. An agent copies whatever reference it is shown; a form that does not resolve does not degrade, it loops. Measured 2026-08-08: inventory-mismatch and webhook-delivery-gaps were handed `ccr://<objectID>`, every retrieve answered `cave_unknown_handle`, the failing tool result was itself masked into a fresh pointer — \"a new pointer to another pointer, indefinitely\" — 27–97 recovery calls, no answer written, both tasks 0/6 while rate-limit-forensics on the same build scored 3/3 at ~35% cheaper.\n- **CCR-or-pass-through**: a lossy (S4) result is only emitted if its original was stored; with no store, the engine fails closed to pass-through.\n- **inferred-only**: ratios are token estimates labeled `inferred`; never `verified`, never re-projected.\n- **fail-closed**: unknown mode → `record`; unknown content type → `text`; unknown grader → `passed:false`.\n- **cgo**: full code compression (Python/JS/TS) needs the tree-sitter build; the cgo-free build compresses Go only. The embedded eval fixtures cover all three under cgo. Release `caveman-proxy`/`caveman-engine` are cgo builds via the pinned zig in `scripts/build-release-binaries.mjs`; `TestReleaseShape*` carry no build tag on purpose and fail under `CGO_ENABLED=0`, and engine-ci's `release-shape` lane runs them with the release flags.\n- **boundary**: this is `public/` — never import `cloud/…`. `make check-boundaries` enforces it.\n\nSee ../../CLAUDE.md (root)\n","isInternal":false,"tokens":3014,"sizeBytes":12723},{"name":"CLAUDE.md","path":"extension/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/extension/CLAUDE.md","title":"extension - Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# extension — \"Caveman Mode\" browser extension (MV3, no bundler)\n\nThe consumer top-of-funnel: a toggle that, on ChatGPT / Claude / Gemini, prepends the **caveman\nskill directive** to each message you send and re-fires the send, so the AI replies in a compact,\nhigh-density style intended to preserve technical substance. It only ever adds text to your *outgoing*\nmessage; it never reads replies, never touches network requests. Savings are `inferred`. (This is\nthe directive-injector product; an earlier revision shipped a local WASM compress button — EXT\nremoved it after Chrome Web Store review couldn't reproduce \"compress prompt locally\".)\n\n## Layout\n- `manifest.json` — MV3. `storage` permission only. Content scripts load `src/directive.js` **then** `src/caveman.js` (order matters). Only the fonts are web-accessible — no WASM, no special CSP.\n- `src/directive.js` — the **pure** caveman directive text: `buildPrimer`/`buildReminder`/`isPrefixed`/`normMode`. No chrome/DOM/network. Loaded first (exposes `self.CavemanDirective`) and required directly by the test suite.\n- `src/caveman.js` — content script: per-site composer adapter + capture-phase send interception. Prepends the directive (full primer on the first message, short reminder after) and re-fires send. Exact composer-scoped selectors; `prependText` inserts at a collapsed selection without rebuilding the rich draft. Pending sends cancel on draft, editor, conversation, or settings changes; unknown controls keep native behavior.\n- `src/background.js` — service worker: just reflects the on/off state on the toolbar badge.\n- `popup.{html,js,css}` — the on/off toggle, mode (caveman/ultracave/megacave), per-site switches, and a **Leave-a-review** CTA (Chrome Web Store via `chrome.runtime.id`; hidden on Firefox until a verified AMO URL is available).\n- `test/` — pure directive/service-worker tests plus real Chromium\n  content-script and popup/storage journeys against `harness.html`.\n\n## Conventions & honesty\n- **Edits your outgoing message only.** Prepends a visible directive; never reads model replies, never touches network requests, never automates anything beyond submitting the message you sent.\n- **Local-only.** No network requests, no analytics. Stores only `enabled` / `level` / `sites` in `chrome.storage.sync`.\n- **Honest about injection.** The directive is plainly visible in the chat — the extension doesn't hide what it adds.\n- **inferred-only.** Token savings are an effect of the model's terser replies; never labeled `verified`.\n\n## Conventions\n- Build/test: `npm test` for pure tests; `npm run test:browser` for Chromium send interception, popup storage,\n  per-site controls, and review-link behavior. There is no\n  build step — MV3 loads the source directly.\n- Package: `npm run package` runs both suites, then writes `dist/caveman-browser-<version>.zip`. Staged set uses explicit allowlist in `scripts/verify-extension-stage.mjs`, cross-checked against manifest/CSS/HTML references.\n- Composer + send selectors live in the `SITES` map in `src/caveman.js`; a site redesign means updating it (each entry keeps aria-label fallbacks so one rename doesn't break it).\n\nSee ../../CLAUDE.md (root)\n","isInternal":false,"tokens":758,"sizeBytes":3210},{"name":"CLAUDE.md","path":"integrations/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/integrations/CLAUDE.md","title":"integrations - Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# integrations — the framework-recipe registry (`caveman sdk` data)\n\nDeclarative routing recipes for AI SDKs/frameworks: each recipe is the one-line\nbase-URL shape that points a framework at the byte-safe gateway, with `{{baseURL}}`\n(gateway origin) and `{{app}}` (the `/w/<app>` attribution slug) left as render-time\ntemplates. Recipes are DATA, not code — the collapse decision applied to app frameworks.\n\n## Layout\n- `recipes/*.json` — one recipe per framework (12 today: openai-ts/py, anthropic-ts/py,\n  google-genai, vercel-ai-sdk, langchain, litellm, crewai, pydantic-ai, openai-agents, curl).\n  Fields: `id`, `display_name`, `lang` (`ts`/`python`/`bash`), `wire_protocol`, `note`, `code`.\n- `compile.mjs` — zero-dep build-time compiler: validates every recipe (fail-closed) and\n  emits `recipes.json` (published registry) + two EMBEDDED copies —\n  `../cli/src/recipes.generated.ts` (the CLI stays zero-runtime-dep) and\n  the web app's embedded copy (cloud may consume public data; the reverse import is forbidden).\n- `recipes.json` — generated; do not hand-edit.\n\n## Conventions\n- **fail-closed**: an unknown `lang` or `wire_protocol` fails the compile — never a guessed\n  protocol (no-placeholder rule). Allowed protocols are the 4 the proxy speaks natively\n  (anthropic-messages · openai-chat · openai-responses · gemini-generatecontent) plus\n  `multi` for frameworks that fan out behind one config.\n- Output is deterministic (recipes sorted by id); re-running on unchanged input is a no-op.\n- Run from the CLI build/test: `node ../integrations/compile.mjs`. The generated files are\n  committed and must stay in sync — change a recipe and regenerate all three artifacts.\n\nSee ../../CLAUDE.md (root) · ../agents/CLAUDE.md (the agent twin) · ../cli/CLAUDE.md\n","isInternal":false,"tokens":466,"sizeBytes":1787},{"name":"CLAUDE.md","path":"mcp/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/mcp/CLAUDE.md","title":"mcp - Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# mcp — the Caveman MCP server (Go core + launcher, Apache-2.0)\n\nA thin **stdio JSON-RPC** adapter exposing the compression [engine](../engine/CLAUDE.md) as\nfive MCP tools to any host (Claude Code, Cursor, …). It owns only the MCP framing; all\ncompression is the engine's, linked **in-process** (no subprocess, no drift). Local-only — it\nopens no network connection — and everything it reports is `inferred`, never `verified`.\n\n## Layout\n- `server.go` — the `Server`: JSON-RPC loop, dispatch, the five tool handlers. Takes an injectable `Engine` interface so the framing is testable without the real compressors.\n- `protocol.go` — JSON-RPC + MCP tool-result types, `toolText`/`toolError` helpers, the exact `tools/list` definitions.\n- `cmd/caveman-mcp/` — binary: opens shared file CCR store (`CAVEMAN_CCR_DB`,\n  else `CAVEMAN_HOME/ccr.db`/`~/.caveman/ccr.db`) so proxy handles resolve across\n  processes, then serves stdin↔stdout. `CAVEMAN_MCP_EPHEMERAL=1` opts into an\n  isolated in-memory store for tests/sessions that do not need proxy recovery.\n- `bin/caveman-mcp.mjs` + `package.json` — the `npx caveman-mcp` launcher that execs the prebuilt Go binary.\n\n## The five tools (exact names, case-sensitive)\n- `caveman_compress(input)` → compressed text + inferred ratio + `recovery_handle`. Lossy (S4), recoverable, and fail-closed: incompressible/malformed/not-smaller input returns unchanged, `ratio:0`, `recovery_handle:null` — never an error.\n- `caveman_retrieve(recovery_handle)` → the byte-exact original. Unknown handle → `isError:true` + a `cave_snake_code`, never a fabricated payload.\n- `caveman_stats()` → `basis:\"inferred\"`, `scope:\"session\"`; the string `verified` never appears.\n- `caveman_toon_encode(input)` → explicit JSON→TOON re-encoding with input/output sizes; returns pass-through plus note when encoding fails.\n- `caveman_toon_decode(input)` → TOON→JSON; invalid TOON returns `isError:true`, never raw input as JSON.\n\n## These five tools are a prompt-prefix cost, and it is measured\nRegistering this server puts all five tool schemas in the wrapped agent's prefix\non **every** call — **11,060 tokens/call** in the agent bench (~2.22M over its 201\ncalls). That is why `caveman wrap` gates the injection behind the `execute.mcp`\nsurface knob (`auto` | `marker-only` | `true` | `false`, `packages/cli/src/index.ts`).\nUnder a non-auto surface wrap suppresses **both** of its injection sites — the\n`mcp install` write and, for config-file agents like openclaw, the profile's\n`mcp.servers.caveman` overlay — but it never uninstalls a server the user\ninstalled themselves, so an existing install keeps costing its prefix until\n`caveman tools mcp uninstall <agent>`. The knob never touches recovery honesty on\nthe wrap door: `CAVEMAN_RECOVERY` is answered from evidence that this launch is\nreally giving the agent `caveman_retrieve`, so the proxy is never told a\nretrieval tool exists when none does. Adding a sixth tool here raises that\nper-call tax for every wrapped agent.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=mcp` / `make product-test PRODUCT=mcp`.\n- **stdout is the protocol channel** — logs go to stderr only (a dedicated test guards this).\n\n## Gotchas (honesty invariants)\n- **un-killable transport** — the stdio server survives everything short of EOF (issue #139). Framing is line-delimited: a malformed line is answered `-32700` and the loop RESYNCHRONIZES to the next newline (never `return`); a handler panic is contained by `recover()` → `cave_tool_panicked` (dispatch panics → `cave_internal_error`); JSON-RPC batch arrays are handled per spec (one array response); id-less/`\"id\":null` requests are notifications and get no reply; and both inbound lines and generated tool output (compress/toon) are size-capped (`cave_payload_too_large`, `maxInboundBytes`/`maxResultBytes`, 16 MiB default) — but `caveman_retrieve` is exempt (`Tool.ExemptResultCap`): recovery returns the byte-exact original and must never fail closed on size, since the shared gateway store has no matching ceiling. A dead server is worse than a slow one — the proxy keeps eliding content that no longer has a `caveman_retrieve` to expand it.\n- **fail-open** — engine error or malformed input → byte-identical pass-through, never a protocol error.\n- **fail-closed** — unknown tool/handle → `isError` + cave_snake_code; unknown JSON-RPC method → `-32601`.\n- **zero-egress** — the adapter imports no `net`/`net/http`/`os/exec`; a test parses the source to enforce it.\n- v1 is **stdio-only**, **string payloads only** (the engine detects type); HTTP transport + `caveman mcp` subcommand are v2.\n- **protocol negotiation must never error.** The adapter implements the 2024-11-05 contract and echoes that version back; a client asking for a newer one gets 2024-11-05 in the initialize result and decides for itself, per the MCP lifecycle. It previously answered `-32602: unsupported protocol version`, which made Claude Code (and every other current client) drop the server — and because `caveman wrap` reads recovery availability from an install-time marker rather than from the live agent, the proxy kept eliding content that no longer had a `caveman_retrieve` to expand it. Declining to echo an unimplemented version is right; refusing to speak is not.\n\nSee ../../CLAUDE.md (root) · ../engine/CLAUDE.md\n\n## Recovery across host compaction\n\nEvery valid `caveman_retrieve` call executes the requested handle and query,\nincluding identical repeats and calls after many earlier recoveries. An empty\nquery always returns the exact stored original. The MCP server cannot infer\nwhich earlier results remain in the model context from its process lifetime:\nhosts can compact their conversation without restarting MCP connections.\n\nThe previous per-process repeat suppression and forced full-payout threshold\nhave been removed. Current Codex source replaces compacted history and excludes\ntool-output items while its thread retains the live MCP runtime; see\n[compacted-history filtering](https://github.com/openai/codex/blob/main/codex-rs/core/src/compact_remote.rs)\nand [session services](https://github.com/openai/codex/blob/main/codex-rs/core/src/state/service.rs).\nTests exercise repeated recovery and more than five distinct queries in the same\nserver process. Query narrowing and complete-record boundaries remain owned by\nthe Engine.\n","isInternal":false,"tokens":1580,"sizeBytes":6418},{"name":"CLAUDE.md","path":"mem/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/mem/CLAUDE.md","title":"mem - Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# mem — cavemem, durable agent memory (Go core + clients, Apache-2.0)\n\nDurable, cross-session memory: `remember` / `recall` / `supersede` / `history` / `forget`. A local SQLite store holds the\n**raw** memories (the source of truth); recall ranks them with deterministic **BM25** behind a\nconservative threshold and compresses each hit through the [engine](../engine/CLAUDE.md) so the\ninferred token cost is honest and the dropped detail stays recoverable via CCR. Everything is\n`inferred`.\n\n## Layout\n- `store.go` — `Remember`/`Recall`/`Supersede`/`History`/`Forget`/`Recover` over SQLite + engine; legacy schemas migrate in place.\n- `bm25.go` — deterministic tokenizer + BM25 scorer (non-negative scores, so the threshold is meaningful).\n- `cmd/cavemem/` — MCP server + `remember`/`recall`/`supersede`/`history`/`forget` CLI subcommands (JSON).\n- `js/` + `py/` — thin TS and Python clients that shell out to the binary (mirrored libraries; they reimplement nothing).\n\n## The cavemem_* MCP tools\n`cavemem_remember(text)` · `cavemem_recall(query, limit?, token_budget?)` ·\n`cavemem_supersede(id, text)` · `cavemem_history(id)` · `cavemem_forget(id)`,\nserved through `mcp.NewServer` so the framing matches caveman-mcp exactly.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=mem` / `make product-test PRODUCT=mem`\n  for the Go core; root `make test` also runs mirrored JS/Python wrapper tests.\n- The wrappers shell memory text to the gated binary through `remember --stdin`\n  (never one huge argv element) — keep validation/recall/scoring logic in Go,\n  never forked into TS/Python.\n\n## Gotchas (honesty invariants)\n- **byte-safe write** — raw text is written to SQLite before any engine call, so a memory is never lost; compression is transient, at recall time only.\n- **single-writer store** — `mem.db` and `ccr.db` open with `SetMaxOpenConns(1)` + a DSN carrying `busy_timeout(5000)` + `journal_mode(WAL)` (`ccr.SQLiteDSN`). The proxy spend store uses the same DSN but does **not** force one pooled connection; mem/CCR's discipline is stricter. Cold-start migrations and the idempotent `Remember` insert use a thirty-second-wall-budgeted `ccr.RetryOnBusy` because a fan-out of fresh `cavemem remember` processes (the JS client's `Promise.all(facts.map(remember))`) can outlast one busy timeout on Windows. Without this, concurrent writes are dropped as `SQLITE_BUSY`.\n- **bounded recall** — `Recall` packs hits greedily in BM25 rank order under `RecallOptions.TokenBudget` (default `DefaultTokenBudget` = 2000 inferred tokens), reusing `engine/contextwindow` packing. Public CLI/MCP/JS/Python callers must explicitly pass `token_budget=0` to request unlimited recall; adapters map that external zero to `UnlimitedTokenBudget`, while omitted/Go-zero stays bounded. A single top hit whose compressed form alone exceeds the budget is returned as a compressed **head + CCR recovery_handle** (the original is stored even when compression passed it through), never its whole body — this is what stops the measured 440,000-token single recall.\n- **bounded remember** — `Remember` caps one memory at `MaxMemoryBytes` (256 KiB), failing closed with `cave_memory_too_large` (`ErrMemoryTooLarge`); CLI exits 65 and both wrappers export `MEMORY_TOO_LARGE_EXIT_CODE`. A memory is a fact to recall, not a file dump.\n- **fail toward nothing** — a query below the threshold (or with no term overlap) recalls nothing, never a guess.\n- **current-only recall** — superseded versions stay auditable through `History` but never enter normal recall.\n- **inferred-only** — `tokens_added`, score, and basis are inferred estimates; never `verified`.\n- **reversible** — a compressed recall hit carries a CCR `recovery_handle`; `Recover` returns the byte-exact original.\n\nSee ../../CLAUDE.md (root) · ../engine/CLAUDE.md · ../mcp/CLAUDE.md\n","isInternal":false,"tokens":995,"sizeBytes":3860},{"name":"CLAUDE.md","path":"proxy/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/proxy/CLAUDE.md","title":"proxy - Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# proxy — the byte-safe standalone `caveman` proxy (commercial, binary-distributed)\n\nA base-URL-swap reverse proxy: match → authenticate → inspect → byte-safe transform → upstream\n→ meter. Single-operator, BYOK, **zero cloud dependencies**. It shares its provider adapters with\nthe managed gateway (the managed gateway imports them from here). `caveman start` launches the\n`caveman-proxy` binary.\n\n## Layout\n- `providers/` — the shared, public byte-safe adapter set: `Adapter` interface + `Base` embed + `UsageScanner`/`ParseUsageBytes` (`adapter.go`), and `anthropic`/`openai`/`gemini`/`azureopenai`/`bedrock`/`vertex`/`openaicompat`. `ResolveUpstreamURL` takes `providers.RouteContext` (no control-plane coupling). Anthropic + OpenAI carry both prefixed and bare routes (`/v1/messages`, `/v1/chat/completions`). `vertex` preserves caller OAuth bearer or Express-mode Google API-key credentials for Gemini + Claude on Vertex AI (no signing, no environment-key fallback, no custom usage parser). `MatchRoute` is POST-only; read-only model discovery (`GET /v1/models`, `/v1/models/{id}`) is a separate GET-only allowlist, `Base.MetadataRoutes`, declared by `openai`/`anthropic`/`openaicompat` and forwarded unchanged with no body, no transform and no spend row. Bare `/v1/models` belongs to both `openai` and `anthropic`, so they implement `MetadataRequestMatcher` and choose by wire protocol (`anthropic-version` or `x-api-key` present → anthropic; a Google key matches neither), never by registration order (#1187).\n- `internal/gateway/` — the request lifecycle (`server.go` + `proxy.go`) behind three injected seams: `Authenticator`, `CredentialResolver`, `TelemetrySink`. Ports the managed loop with the fail-open fix. `serve` sets `Config.AsyncRecord`: a row's request token count, sink write (batched) and Cloud observe run after the handler returns, in record order (`recorder.go`). A reader that must see the request it follows calls `Server.Flush` first (session receipts do); close the server before the sink.\n- `GET /health/ready` identifies the runtime and advertises `billing: \"byok\"` because the standalone proxy forwards the caller's selected provider credential. The managed twin advertises `managed`; SDK dollar budgets fail closed on missing/unknown billing provenance.\n- `internal/config/` — `caveman.yaml` loader + BYOK env-key resolution; unknown mode fails closed to `record`.\n- `internal/store/` — `~/.caveman/caveman.db` SQLite spend store (`modernc.org/sqlite`, cgo-free); implements `TelemetrySink` and `BatchSink`. `RecordBatch` commits request rows with `synchronous=NORMAL` on its own connection (WAL): a power loss or OS crash can drop the last batches, a process crash or clean exit loses none; every other table keeps FULL.\n- `internal/standalone/` — wiring: static `Auth`, BYOK `Creds`, adapter set, and the always-on SSRF-guarded client.\n- `internal/identity/` — who calls the framework middleware routes (legacy token, token map, OIDC/JWT, mTLS) and the reloadable TLS listener config.\n- `internal/nativeruntime/` — normalized local-agent lifecycle, Task Contract,\n  Decision Ledger, typed CCR capture/masking, child evidence merge, honest\n  receipts, user-only Unix socket / Windows named pipe, and persistent listener lifetime.\n- `internal/repointel/` — deterministic local repository map, task evidence,\n  conservative test impact, and optional-Scout recommendation. No model/network.\n- `internal/nativepack/` — embedded compiled Core/skill policy; generated from\n  `public/skills`, fail-closed on schema/version drift.\n- `cmd/caveman-proxy/` — binary: `serve` (default), `stats`, and content-blind\n  `agent-evidence --session --build --plan`. Evidence query returns only exact\n  provider usage, request hashes, declared context/plan identity, ordered\n  provider-prefix component hashes, actual transform IDs/counts, and CCR handle;\n  basis is always `inferred`, verified dollars always zero.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=proxy` / `make product-test PRODUCT=proxy`.\n- Tests inject a plain `*http.Client` to reach loopback stubs; the binary uses the SSRF-guarded client.\n- New provider/optimizer work goes in `providers/` (shared) — change it once, both proxies get it.\n\n## Gotchas (honesty invariants — correctness, not style)\n\n- **listener lifetime is not session lifetime**: `serve` never exits because a\n  wrapper, native session, heartbeat, or idle timer expires. Legacy\n  `CAVEMAN_NATIVE_IDLE_TIMEOUT` does not arm shutdown. The CLI never restarts a\n  shared listener to change mode/recovery; an incompatible new wrap runs direct.\n  Failed local startup also runs direct. Native session correlation entries may\n  age out without affecting API traffic.\n- **no default generation deadline**: `CAVE_GATEWAY_UPSTREAM_TIMEOUT_MS` defaults\n  to `0` (no total request deadline). A positive value is an explicit operator\n  cap and includes response streaming. Client cancellation still cancels upstream;\n  connection setup, inbound header/upload limits, and idle keep-alive socket\n  cleanup remain bounded separately. The replacement bound for an upstream that\n  connects and then goes silent is the transport's response-header deadline\n  (`CAVE_GATEWAY_RESPONSE_HEADER_TIMEOUT_MS`, default 900000, `0` disables) —\n  headers only, so it never truncates a live stream.\n- **response protocol controls streaming**: SSE/event-stream responses flush\n  headers and chunks even without a JSON `stream` flag. Encoded requests keep\n  `Content-Encoding` and bypass transforms. Interrupted response copies record\n  an error and abort HTTP framing; they never become a clean successful EOF.\n- **replay only when non-delivery is proven**: proxy transport retries are\n  limited to connection-setup failures — `*net.OpError` with `Op` `dial` or\n  `proxyconnect` (a proxy that is down fails as the latter, never as a dial). A failed upload/header read or truncated response\n  does not prove an inference was unprocessed; do not automatically replay it.\n  An explicit transformed-request 4xx still retries once with original bytes —\n  except a 429 that carries `Retry-After` (a rate limit; Anthropic sends its\n  `anthropic-ratelimit-*` headers on every response, so they prove nothing),\n  which is returned as-is; an accepted retry pins the conversation raw\n  (`internal/gateway/raw_pin.go`).\n  See `docs/technical/proxy-reliability.md` at the repository root.\n- **byte-safe**: `record` mode never transforms; on transform error the ORIGINAL bytes are forwarded (HTTP 200, fail-open) — never a 400. The one carve-out: bytes the provider already cached in replaced form are re-sent replaced, because forwarding the original there busts the prefix (see cache safety).\n- **native marker is local-route-only**: HMAC session marker is emitted only\n  after local runtime + route proof and stripped before capture/hash/provider.\n  Direct, managed, proxy-disabled, invalid, and conflicting paths never correlate.\n- **request-wide opt-out**: `x-cave-transforms: caveman.pass-through.v1` suppresses every request transform path — compress, pixel, and provider-native — not only compiled plan routes. Tests cover all three modes.\n- **repository evidence is a claim, so it needs a direct match** (`internal/repointel` + `nativeruntime.renderRepositoryEvidence`): a \"Likely implementation path\" line is injected ONLY for items whose own FILE NAME or symbol name contains a task term (`EvidenceItem.Direct`, `Bundle.Strength == StrengthDirect`). The match is on the final path component on purpose — matching the whole path meant a term naming any ancestor (`src`, `internal`, `lib`: ordinary prompt words) marked every file beneath it direct, i.e. no gate at all. BM25 metadata proximity alone is `StrengthMetadata` and is never shown, never stored, and never gets a `ccr://` handle; the handle reads as authoritative, and a wrong file:line steers the agent while costing tokens every turn. A bundle qualifies on ONE direct item, so the non-direct items that ranked alongside it are stripped by `Bundle.DirectOnly()` BEFORE the object is written — dropping them only at render time left them stored behind the handle, one retrieval from the model. Terms are floored at three characters (`NormalizeTerms`, matching the producing hook) because the direct test is a substring of a file's own name: `go`, `py`, `js` would mark every file of that language a direct hit and hand the gate away. A declined turn is silent to the model but records `repository_evidence_declined_no_direct_match` in the ledger, so \"declined\" stays distinguishable from \"broken\". `Scout.Status` is ADVICE about repository shape and must not gate injection; conflating them made a truncated map silence real hits (#899).\n- **the dependency filter runs on BOTH listing paths, and git is not trusted to be complete**: `listFiles` prefers `git ls-files --cached --others --exclude-standard`, because the project's own `.gitignore` — not a name list we maintain — is the best statement of what is vendored. It is not sufficient on its own: an empty-but-successful listing means the cwd is itself ignored by an enclosing repo (accepting it silently disabled the whole mechanism), so empty falls back to the walk, and every git-listed path still goes through the same content-based filter as the walk via `directoryFilter` (memoised per directory, one probe per directory not per file) — otherwise an un-ignored conda prefix or a COMMITTED `vendor/` tree reaches the model precisely because git reported it. Installed environments are identified by CONTENTS (`conda-meta`, `pyvenv.cfg`, `site-packages` child) so a pixi/conda prefix or virtualenv is caught under any name, along with the interpreter stdlib it carries. Only names that are never first-party (`node_modules`, `.pixi`, `site-packages`, …) match by name, case-folded; `build`/`dist`/`target`/`vendor`/`coverage`/`Pods` are ambiguous and excluded only when a sibling manifest proves the ecosystem, because `internal/build` and a hand-written `vendor/` are real source. The two paths can legitimately disagree (git omits submodule contents), so `Map.ListingBasis` discloses which ran and is folded into `ContentSHA256`. Git gets at most HALF the caller's remaining warm budget (`gitListBudget`): the walk is the more expensive path, so a git listing that spent 2s of a 3s window left the fallback unable to finish and the map failed closed — the exact case the fallback exists for. `FilesScanned` stays the map size; `FilesRanked` is the filtered count.\n- **git is invoked as an untrusted-repository operation**: every `git` call that points at a user working directory goes through `internal/gitsafe.Command` (Go) or `packages/cli/src/git-safe.ts` (the two `execFileSync` sites in `index.ts` and `native-hook-fast.ts`), which pass explicit `-c` overrides (`core.fsmonitor`, `core.hooksPath`, `core.sshCommand`, `core.askPass`, `core.editor`, `core.pager`, `core.alternateRefsCommand`, `diff.external`, `credential.helper`, `protocol.ext.allow`, `uploadpack.packObjectsHook`) and strips inherited `GIT_*` from the environment. A repository's own `.git/config` names programs git will run, and `core.fsmonitor` runs while the index is read — so plain `git ls-files` in a freshly cloned hostile repo executes attacker code with no user action, at session start. `repointel.listFiles` was hardened first and the other callers were not, which is the shape this bug takes: `nativehook.currentRepositoryState` runs `git status` on EVERY hook event, and `status` refreshes the index, so it triggers `core.fsmonitor` harder than `ls-files` does. Regression tests: `repointel.TestGitListingIgnoresRepositoryControlledCommands` and `nativehook.TestRepositoryStateIgnoresRepositoryControlledCommands` (both verified to fail without the overrides), plus `tests/verify_repo.py` `verify_untrusted_git_invocations`, which fails the build on a NEW raw `git -C <dir>` call site anywhere in `proxy/` or `packages/cli/src/`. Command-line `-c` beats repository config; never drop these to \"simplify\" the invocation.\n- **no-fake-savings**: standalone records `Basis: \"inferred\"` on every row; it never writes `verified` and never re-projects to a monthly figure.\n- **spend is arithmetic, savings is a claim** (local learn, since 2026-08-21): `caveman learn` MAY state a currency, under exactly these conditions. SPEND prices PROVIDER-COUNTED disjoint billing buckets read out of the transcript's own usage block, at rates from the dated `shared/provider-catalog` row, stamped with that row's catalog version — basis `provider_counted_x_published_rate`, window-bounded, never projected. A model with no catalog row is NEVER priced at a sibling's rate: its tokens are disclosed separately and the total is a stated floor (`learn_pricing.go`). Sinks are priced at the user's own MEASURED effective input rate, not list, so a well-cached user's findings are not inflated ~10x. SAVINGS additionally requires an attribution method (`learn_attribution.go`): every confirmed row names its rung (`deterministic_remeasure` > `controlled_holdout` > `counterfactual_replay` > `interrupted_time_series`), carries the standing confounders that rung cannot rule out, and reports whether the artifact caveman fingerprinted at apply time is still intact on disk. Savings totals are bucketed BY RUNG and never summed across rungs — blending a re-counted file with a before/after median launders the weak number into the strong one. A regression is never priced. Neither surface ever writes `verified`; that stays gateway-only. Subscription traffic has no marginal cost, and every priced surface says so.\n- **practice join**: local learn sinks carry additive `practice_id`; one\n  fail-closed mapping table owns sink→practice and unknown sinks keep `\"\"`.\n  The historical `subagent_overuse` sink is count-only and deliberately has no\n  practice id: spawn count cannot reactivate the retired\n  `context-exploration-offload` opportunity or prove any spawn unnecessary.\n- **local trial heuristics are not actuation evidence**: a model name never emits\n  the retired `model-right-sizing` id, and provider plus positive cost never\n  emits a cache move because neither proves stable-prefix eligibility. Legacy\n  rows for those identities are hidden at read time. Compression replay reports\n  one trial's local engine `estimated_engine_o200k` before/after shape with zero\n  dollars and low confidence; it is not provider-counted, a rate, an invoice,\n  causal/verified savings, or task-outcome evidence.\n- **Anthropic automatic caching is experimental observation only**:\n  `anthropic-automatic-prompt-cache` is a typed, default-off manual policy\n  experiment and may add only Anthropic's top-level 5-minute marker on direct\n  Messages API requests. Managed traffic additionally requires server-attested\n  official Anthropic origin; custom or provenance-unknown origins lose the flag\n  before the adapter. It is mutually\n  exclusive with the explicit `anthropic-cache-breakpoints` transform and any\n  caller `cache_control`; Bedrock and count-tokens requests stay byte-identical.\n  An applied marker records only its optimizer id plus actual provider usage and\n  cost. It has no practice, recipe, generic mode/candidate activation, ledger\n  tuple, inferred savings, or verified-savings path (cache-only and forged IDs\n  are excluded from the counted-baseline method too); evaluate it by manual\n  paired observation because shared provider cache state can contaminate an A/B.\n- **OpenAI cache-key affinity is not cache placement or savings proof**:\n  `openai-prompt-cache-key` adds routing metadata only. On GPT-5.6, the\n  provider's implicit latest-message breakpoint can repeatedly write a changing\n  suffix, and writes cost 1.25× uncached input. The usage parser normalizes\n  official nested `cache_write_tokens` from both Responses\n  `input_tokens_details` and Chat `prompt_tokens_details` into\n  `CacheCreationInputTokens`; applying the affinity key never mints verified\n  savings. Explicit `prompt_cache_breakpoint` placement is not implemented.\n- **SSRF always on**: `standalone.StandaloneHTTPClient` guards every upstream dial (not gated on `CAVE_ENV`) using `ssrf.SelfHostedConfig` — NOT ManagedConfig, which ignores the allowlist and would make the escape hatch a silent no-op. `CAVE_SSRF_ALLOWLIST` opts loopback/private hosts back in (local model servers like Ollama; `localhost` as an entry covers 127.0.0.0/8 + ::1); metadata/link-local stay blocked in every mode. Provider traffic honours `HTTPS_PROXY`/`NO_PROXY` by default (#1001) — `upstream_proxy: env|off|<url>` / `CAVE_UPSTREAM_PROXY` — via `ssrf.Config.Proxy`, which validates IP-literal/localhost destinations before proxy selection and dials the (operator-configured) proxy address unguarded; `ssrf.NewHTTPClient` itself stays direct unless a caller sets `Proxy`, and a managed-mode Config carrying one is refused loudly (every request fails with `ssrf.ErrProxyInManagedMode`) rather than silently dialing direct. Adapters that pre-flight a resolved endpoint (bedrock, vertex) read the selector off the request context (`providers.WithUpstreamProxy`, published by `gateway.Handler` from the upstream transport) and use `ssrf.ValidateURLNoResolve` for proxied destinations: a proxy-only network has no outbound DNS, so resolving there failed the request before the proxy was ever consulted. `ca_bundle` / `CAVE_CA_BUNDLE` plus inherited `SSL_CERT_FILE`/`REQUESTS_CA_BUNDLE`/`NODE_EXTRA_CA_CERTS` append private roots for TLS inspection through `shared/platform/cabundle` (fail-closed parser shared with chhttp).\n- **inbound token is consumed, never forwarded**: `CAVEMAN_AUTH_TOKEN` gates non-loopback listens (a middleware token map, OIDC issuer or TLS client CA also makes one legal; provider routes still accept only the token, and without it `Auth.closed` refuses every provider request off loopback); `standalone.Auth` deletes the matching `x-cave-api-key`/`Authorization` header before `Creds.Resolve` so the shared token can never be forwarded to a provider or classified as a provider credential; a bearer that is not the token survives untouched (Claude OAuth, `/chatgpt/`). `Authenticate` runs BEFORE `matchAdapter` so the 401/404 split cannot be used to enumerate routes, and every 401 goes through `Server.rejectUnauthorized`, which counts `cave_proxy_unauthorized_total` and warns with the path and remote host only. Health and the no-op `/caveman/keepalive` beacon stay unauthenticated, `/metrics` too unless `CAVEMAN_METRICS_TOKEN` is set; the `X-Caveman-Instance` header on `/health/live` is published only on a loopback bind. A token on a LOOPBACK listener is honored too (the local `caveman wrap` path sends none, so `runServe` warns). Bedrock credentials then come from `shared/platform/awscreds` (env → web identity → container → IMDSv2), never from the request; a PARTIAL env pair fails closed instead of falling through to an ambient role, and the winning source is logged once.\n- **middleware identity is server-side and per route**: `middleware.Config.Identify` resolves an `identity.Principal` from the credential alone (bearer decides when present, else a verified client certificate); `ServeHTTP`'s `decode` checks `Principal.Allows(scope.Namespace)` on EVERY scoped route, so a new route that skips `decode` skips authorization. The legacy token stays `single_operator` with every namespace; JWT and certificate principals may never be named `single_operator`. Token and hash comparisons are constant-time over every entry. A token map or TLS file that fails to reload keeps the previous one. Tests: `identity/*_test.go` (negative JWT and mTLS cases), `middleware/identity_test.go`.\n- **Postgres middleware store keeps SQLite's single-writer semantics per authority**: `store.PostgresMiddleware` serializes writers of one authority with a transaction-scoped advisory lock taken by `Scope` (then re-read), `SaveScope`, `Revoke`, `PurgeBatch`, `Receipt` and `SaveOriginal`; the sweep is one-replica (try-lock) and skips busy authorities and row-locked scopes. Counters are statement-level transition-table triggers striped by txid; a per-row trigger goes quadratic on bulk statements. Lock keys include `current_schema()`. Run the store suite against both backends with `CAVEMAN_TEST_POSTGRES_URL`; `middleware_concurrency_test.go` fails without the locks.\n- **Auth scheme is preserved**: a key from an inbound `Authorization: Bearer` keeps `Scheme:\"bearer\"` on the `providers.Credential`; Anthropic and Gemini forward bearer credentials as bearer credentials (Claude/Gemini OAuth breaks if remapped to an API-key header). BYOK env keys and inbound `x-api-key` keep provider API-key mapping. On a named compat mount the header follows the wire protocol of the path: `/compat/<name>/v1/messages` gets `x-api-key` plus a default `anthropic-version`, every other path gets Bearer. A real inbound Bearer stays Bearer on every path. One credential may arrive twice — pi with `authHeader: true` sends it as `Authorization: Bearer <key>` AND as `x-api-key: <key>`; x-api-key still resolves first (a shared listener must not be able to swap the caller's principal), but an IDENTICAL bearer is that same principal, so `openai_compatible` keeps `Scheme:\"bearer\"` and the bearer-only compat upstream gets the header it accepts (#1215). Differing values stay x-api-key-first, and no other provider reads the duplicate as OAuth. OpenCode Go rejects Bearer on `/v1/messages` (401 `Missing API key.`, 2026-09-03). The gateway env fallback puts the key in the header that the adapter emitted, so the upstream never gets two credentials.\n- **fail-closed**: unknown route → 404; unknown mode → `record`.\n- **subscription AND oauth compression is NOT account-gated**: non-PAYG sessions from Claude Code, Codex ChatGPT, Gemini CLI, and other routed clients take live-zone compression with no Caveman account, entitlement, or seat. `CAVEMAN_WRAP_ENTITLED` and every `WrapEntitled` field are **deleted**, not merely ignored — do not reintroduce them. Exactly four conditions remain, all technical and all fail-closed (`liveZoneCompressionAllowed`): the operator `subscription_compress` switch (empty/`live_zone` allow, `off` and any unknown value close it), the adapter must implement schema-aware `PrefixStabilizer` zones, recovery must run through the agent's own MCP `caveman_retrieve`, and a durable prefix cache must be wired. The dedicated Codex `/chatgpt/responses` route uses the OpenAI Responses stabilizer while preserving OAuth and `ChatGPT-Account-ID` headers; recovery is proven by the `CAVEMAN_RECOVERY=mcp` stamp or the decoded request's own `mcp__caveman__caveman_retrieve` (top-level `tools` or Codex's `input[]` `additional_tools`, flat or under an MCP namespace); zstd bodies decode → transform → re-encode, anything else forwards the original wire bytes, and the `chatgpt_proxy` log carries `skip_reason`; transformed 4xx responses retry once with exact original bytes (never a rate-limit 429; an accepted retry pins the conversation raw). Legacy savings fields remain **tokens-only** — `compression_tokens_before/after` + `estimated_engine_o200k`, never booked compression dollars. New request-comparison and price-snapshot fields support separately labeled API equivalents in local stats; those values never enter subscription spend or legacy saved-dollar fields. LOCAL wrap only; managed gateway non-PAYG behavior is unchanged.\n- **recovered bytes are never a compression candidate**: a `tool_result` answering the agent's own `caveman_retrieve` is excluded by every adapter (`providers.IsRecoveryToolName`; anthropic keys off `tool_use_id`, OpenAI off `tool_call_id`/`call_id`, gemini off the `functionResponse` name). This is not an optimization — the replacement is a deterministic function of block content and is memoised in the prefix cache, so collecting a recovery result substitutes the SAME elision straight back in and the agent has no path to its own data at all. Measured 2026-08-06: the agent retrieved, reported \"Same truncation\", and re-read one file seven times. Regression test `internal/gateway/recovery_exempt_test.go`.\n- **cache safety is byte-stable replacement**: THE INVARIANT (#1105): for accepted requests P then R of one conversation, if R's client bytes over P's cached prefix (system, tools and messages through P's last `cache_control` marker; the whole prompt for implicit caches) equal P's, R's forwarded bytes over that range equal P's, and every request caches at least what its client asked for (a dropped breakpoint is a bust too). Two exceptions, both re-caching the range raw: a request the provider rejected transformed and accepted through the raw retry, and a compressed PAYG conversation without MCP recovery that streams (the server-side retrieve tool cannot ride a stream, so the conversation stays raw from there). A raw retry splits a range into a raw and a replaced form, and a later request follows the LONGEST lineage it extends, ties going raw: it is held to the raw request (raw pin) unless it repeats a longer prefix that went out replaced, like a sibling subagent started with the same task. Pins are taken only on a conversation's own bytes (past system and tools); pins and lineages live in the PrefixCache (`rawpin` and `lineage` scopes, pin rows never evicted) and in memory. `internal/gateway/cache_prefix_invariant_test.go` (Anthropic compress, PAYG, pixel and Codex Responses scenarios + `FuzzCachePrefixInvariant`) proves it; keep it green. A compressed live-zone turn becomes prefix on the next request, so the replacement is a pure function of segment content (deterministic compressor + content-hash CCR marker), memoised in the durable replacement cache, and the FIRST decision for a block wins — replaced or raw (`RawDecisionHandle`); a request whose write finds a row on record sends that row, not its own candidate. A frozen block with no decision (a `--resume` history, an evicted row) goes out raw and raw becomes its decision; a failed memo write keeps the block raw for the process (`rawMemory`, consulted only on a store miss and written through once the store takes it); new rows are remembered only once the request's splice holds. Raw decisions are global and durable, a known savings cost: a block declined once (frozen at first sight, a compressor decline, a CCR failure, a non-newest parallel tool output) stays raw in every conversation. Bounding them needs a TTL of at least the longest provider retention (24 h for OpenAI extended caching) plus time-aware store interfaces, or per-conversation rows keyed on a stable conversation identity with a scope rollover. Substitution is never gated: the derived epoch gate is gone, and an explicit framework epoch may veto only NEW compression. Pixel renders and the tool-schema strip are memoised the same way. New compression stays live-zone-only. The runtime tripwire (`prefix_monitor.go`, on the provider routes and `/chatgpt`) checks the same rule on live traffic and records `requests.cache_bust_cause`: `client` (DEBUG); `raw_retry`, `stream_switch`, and `lever_freeze` (the harm tripwire's one-time strip rollover), all WARN; `caveman` (ERROR `caveman changed bytes the provider already cached` or `caveman dropped a cache breakpoint the client set`, counted as `caveman_cache_bust_requests` and raised by `caveman status`) — any `caveman` row is a bug. A request is held only to anchors accepted before its own forwarding decision. Cross-turn stability covers **anthropic, openai, azureopenai/openaicompat, and gemini** through `ExtractStabilizable`; `bedrock` and `vertex` expose no compressible blocks and stay pass-through. Anthropic uses declared `cache_control`; OpenAI/Gemini use latest-user/latest-tool zones against implicit provider caches. Replacement cache is SQLite spend store and must retain `journal_mode(WAL)` + `busy_timeout`; a hit rewrites `last_used_at` only once the row is 10 minutes stale, and pixel rows have their own cap inside the table's.\n- **tool-schema annotation strip is DEFAULT OFF and separately opted in**: `toolschema_strip: annotations` / `CAVEMAN_TOOLSCHEMA_STRIP=annotations` (`\"\"`, `off`, and any unrecognized value all mean off, normalized in the config loader AND re-checked at the decision point). It is a SECOND gate ON TOP of the four `liveZoneCompressionAllowed` conditions, which it reuses rather than restates, plus a THIRD: the session ledger's freeze registry (`ledger.LeverAllowed`) — it never runs in `record` mode, under `caveman.pass-through.v1`, under a compiled Cave Build, or in a session whose harm tripwire has frozen it. Its first decision per catalog (stripped or original) is memoised in the replacement cache and re-sent, so a recovery-store hiccup never flips the head of the prefix. S4 + CCR (the original catalog is stored before the rewritten bytes ship, disclosed as `x-caveman-toolschema-recovery-handle` and on the row's `RecoveryHandle`), promotable only under the local-wrap clause. It removes `$schema`/`title`/`examples`/`deprecated` ONLY inside schemas reached through `input_schema`/`inputSchema`/`parameters` and the JSON-Schema applicator allowlist — never from a tool envelope, `annotations` (whose `title` is the tool's display NAME), `_meta`, or a vendor extension. `ExtractToolCatalog` covers the **anthropic-messages** shape only; openai/gemini and `count_tokens` fail closed. It mints NOTHING: no tokens, no ratio, no dollars — the replay grid prices it. Its version is folded into the prefix-cache scope key so toggling it is a deliberate epoch rollover, not a silent partial cold write.\n- **cache-breakpoint planner defaults to `frontier` in optimization modes**: `breakpoint_plan: off` / `CAVEMAN_BREAKPOINT_PLAN=off` is explicit off; unrecognized values fail closed to off. Record, compress, and pixel modes still never run it. Provider-native cache optimizers for Anthropic, OpenAI, and Bedrock also default on when omitted, with explicit per-id `false` preserved. All remain capability-, auth-, endpoint-, economics-, and caller-marker-gated. Planner metadata is upstream-only and changes no model-visible bytes. It does not mint savings: `cache-breakpoint-plan` remains absent from `cacheOptimizerIDs`. OpenAI's arm hashes session id and never overwrites caller key; prefix-signature optimizer wins when already applied.\n- **harm tripwire / session ledger**: `internal/gateway/ledger.go` keys a bounded LRU (1024) on `x-cave-session`; **no session header = no entry and the whole mechanism is inert**. Every lever asks `LeverAllowed` before running. A lever active on request N earns a strike when request N+1's `cache_creation_input_tokens` exceeds BOTH an absolute floor (50k) AND 3x the session's baseline mean; 3 strikes freeze that lever for the session, one-way (never un-freezes), fail-open to pass-through. The baseline EXCLUDES calls already judged anomalous — a plain running mean folds each spike into the bar the next spike is measured against and goes blind to persistent regressions. Disclosure is `x-caveman-tripwire: <lever>=frozen`, and it appears from the request AFTER the one that tripped, because usage is only known once the response is already streaming. Thresholds are conservative by construction, pending replay-derived values.\n- **cache warming is predicted per stream, and replays only what Anthropic can answer at zero output** (`internal/gateway/cache_warm.go`, `cache_warm_table.go`): after a successful Anthropic Messages response on `api.anthropic.com` (never Bedrock, Vertex, Azure, a compat mount, a custom base URL, the pass-through header or an encoded body), the exact bytes and headers sent upstream (after compression, routing marks, heals and fail-open retries) are kept in memory only (LRU 1024 streams and 256 MiB, never stored, logged or captured) and may be replayed with `max_tokens: 0` and `stream: false` at `min(0.9*ttl, ttl-10s)` steps from the real request's start (wall time, so laptop sleep never sends a stale warm). Not replayable, so never warmed: `thinking.type: \"enabled\"`, `tool_choice` `tool`/`any`, `output_config.format`, no `cache_control`, a `ttl` other than `5m`/`1h`, mixed `5m` and `1h` markers, writes of another lifetime than the markers (one lifetime or no warm; the simulator classes by the same rule and none of the measured requests mixed them); the request scan reads top-level fields and cache_control markers only, no full parse. WHETHER to warm is optimal stopping, not a threshold: `cache_warm_table.json` holds the return-time distribution (30 s bins to 2 h) per stream class `main|subagent / tool|end / 5m|1h` (the class comes from the session key and whether the answer's `stop_reason` was `tool_use`, scanned off the plain bytes with a rolling tail; the proxy forwards the agent's `accept-encoding`, so an answer that comes back with a `Content-Encoding` is not classed: it is planned as whichever of the two classes warms less and its gap teaches neither), measured on 364,048 request transitions of real Claude Code sessions (2026-08-30 to 2026-10-08, `.blocks/cache-warm-sim.py`, aggregate probabilities only) and shrunk toward the parent class; the proxy learns each user's own gaps online (counts only, `$CAVEMAN_HOME/cache-warm-gaps.json`, 0600, written outside the warmer's lock, flushed by `Server.Close` at shutdown, symlink-refused, any implausible count drops the whole file; two proxies on one home are last-writer-wins) and blends them in with the defaults worth 100 observations. A stream evicted or still waiting at shutdown is counted as returning later than it was seen silent (spread over the later bins by the class's own distribution, \"never\" past 2 h): dropping it would keep only the returns and drift the table toward over-warming. At arm time the whole chain is planned by backward induction with the catalog price of the model sent (warm = read x prefix + input x uncached tail; avoided miss = (write_ttl - read) x prefix x the class's measured realized share, taken over the gaps that DID return: the window probability already leaves out the streams that never do), so the cumulative cost of every warm is counted before the first; unknown price, class or lifetime = no warm. Subscription traffic is valued at list prices as plan-usage equivalent. Stops: any real request of the stream (every model; it never waits for a warm in flight), any warm error, a warm reading `cache_read_input_tokens == 0`, the plan, 60 min, or the switch; the held request (body and credential headers) is released the moment its chain ends. A parent session's request does NOT stop its subagents' warms: a subagent can still be running, and on the test half that stop cost $565.52 net (below). A 429 (warm or real) pauses warming on that credential for the response's `Retry-After`, else until its `anthropic-ratelimit-unified-reset`, else 1 h for a subscription/OAuth login and 60 s for an API key; a 529 or 5xx on a warm pauses it for the `Retry-After`, else 5 min. Never sent while a real request of the same stream is in flight (retry every 5 s until its deadline). At most two warms are in flight process-wide (same retry; one with no slot by its deadline is dropped), and each fires up to 5 s before its planned time, never after. Warm rows are tagged `cache-warm`: spend, never a saving, never an agent request (excluded from request counts, session usage, trial origins, exported spans, receipts and `caveman sync` uploads; counted as `cache_warm_requests`), never fed to the harm-tripwire ledger, never sent to Cloud. Switch: on unless `$CAVEMAN_HOME/cloud.json` has `modules[\"waste-fixes\"]: false` (re-read on change); `CAVEMAN_CACHE_WARM=off` kills it for the process. Every answered stream request logs one line `cache warm plan` (info when warms are planned, debug when skipped: session hash, model, class, ttl, planned warms, skip reason: `response_error`, `off`, `not_anthropic_api`, `not_replayable`, `lifetime_mismatch`, `unpriced`, `not_worth_it`). Replay on the owner's data (test half, after 2026-10-03, 161,325 transitions): net $1,896.65 list-price equivalent at $385.31 of warms (9,628 warms, 7,280 wasted) vs $1,294.18 at $1,372.15 (45,343 warms) for the first fixed rule; the same policy with the parent stop for every subagent $1,331.13 at $197.28 (3,461 warms), with it only for subagents whose last answer was not a tool call $1,852.99 at $378.90 (9,518 warms). A test pins the plan to the simulator's on the shipped table (`TestCacheWarmPlanMatchesTheSimulator`; regenerate both together).\n- **pixel mode**: S4 lossy text→PNG (`pxpipe` port). Default allowlist is `claude-fable-5,gpt-5.6` via `CAVE_PIXEL_MODELS`; savings stay inferred-only. Renders keep the compress rules: the first decision for a text (render or text) is re-sent on every turn, a raw pin keeps the conversation text, and a NEW render is remembered only once its request goes out, with the original stored in CCR and the splice holding. If either fails, the new content goes out as text and records that, and earlier renders still go. A rendered block's `cache_control` moves to its last image. An Anthropic request carries at most 20 images, client images included (`anthropicManyImages`): past 20 the API rejects any image over 2000 px, and the renders are 2573 px wide, so a new render past the cap stays text.\n- **mask what cannot be summarized in place; elide what can**: `nativeruntime.afterTool` replaces an over-threshold tool output with a `ccr://` pointer stub, but NOT when `Engine.Detect` classifies it as `json`, `tabular`, or `log` (logfmt + NDJSON) — the classes with a field grammar, which the elision engine compresses in place into rows plus stated invariants (`all state=charged`, `status: delivered×18 attempted×17`, `wh-5000..wh-5059 all 60 present`). Masking those first destroys every fact AND costs more: the agent sees no row, then recovery re-enters the FULL original through the recovery-exempt path — whole page + stub + an extra turn, strictly worse than no wrap. Measured 2026-08-08 on the shipped default (`compress` → native policy `safe` → profile `full-safe` → mask on): inventory-mismatch and webhook-delivery-gaps scored 0/6 with 27–97 recovery calls, while rate-limit-forensics scored 3/3 at ~35% cheaper for the sole reason that its pages sat under the threshold. Capture is unaffected (the object is still stored, recovery still available); only the replacement is skipped, and the size rule for still-maskable classes is unchanged. Fails toward masking: no classifier → mask, so the fallback is bounded context. Tests: `mask_elidable_test.go`.\n- **route stage is optional and fails open** (`internal/cloudlink`, `internal/gateway/route.go`):\n  it asks Cloud `POST /v1/route` only for requests naming Auto (`AutoModel`, `caveman-auto`, also with Claude Code's `[1m]` suffix, the\n  picker entry the CLI adds while `modules.routing: true` in `$CAVEMAN_HOME/cloud.json`), while the\n  CLI is signed in with that switch on, for Anthropic Messages / OpenAI chat or responses requests on\n  an API key or a subscription alike; any other model goes as sent (`route.outcome: off`), the\n  session's marks and heal still applied. Auto goes upstream as the provider's `autoFallback` model\n  (Anthropic `claude-sonnet-5-5`, OpenAI `gpt-6.1-sol`; count_tokens too, and with no Cloud link),\n  which is also the asked model the ask carries and what a failure runs (a transient Cloud failure, that is a timeout, an unreachable or 5xx Cloud, an unreadable answer or the pause after one, instead runs the model the session's previous request went to, `heldModel`, never for a side request; no previous request on that provider, one that was a compaction or came after one, a held model that then did not serve (any non-2xx: the next attempt runs the fallback), signed out, routing off, an expired or refused login, a limit and a provider's refusal keep the fallback); Auto on any other\n  provider is a 400 (`cave_auto_unavailable`), and an encoded body naming Auto is decoded first. A\n  named model records `route.outcome: off`, reason `named_model`. OpenAI Auto (API key and ChatGPT\n  login) runs only `AutoOpenAIModels` (`gpt-6.1-sol`, `gpt-6-astra`, `gpt-6-luna`): the ask's\n  `Models` lists those alone and the pool drops other OpenAI models, and an answer naming any other\n  runs `gpt-6.1-sol` at its effort (`auto_model_refused`, `translate.FitEffort`); an Auto row's `RouteFrom` and\n  `x-caveman-routed-from` say `caveman-auto`. A move on a subscription stays on the provider's own\n  API on that subscription's credential; pool entries go out on their own login or the Cloud\n  gateway key, never on it, and never with the agent's `oauth-*` betas. On a ChatGPT login\n  (`/chatgpt`, `chatgpt_auto.go`; OpenCode's ChatGPT login lands there too) `/models` gains an Auto\n  entry (a copy of `gpt-6.1-sol`'s, listed last, fetched without `If-None-Match`, served without\n  `ETag`) while the link reports `AutoOffered` (signed in, routing on). Every POST there has its\n  first 64 KiB read (decoded when gzip, deflate (zlib or raw) or zstd, by a decoder that is always closed; a br body is never looked into) for a\n  top-level `\"model\":\"caveman-auto\"`; any other streams on unchanged. `/responses` naming Auto asks\n  with `NoPool`, keeps live-zone compression, replays `gpt-6.1-sol`'s bytes on a 4xx (a rate-limit 429, a 401 and a 403 are returned instead; a served replay pins the conversation raw, and the decision is rejected only then or when the replay is itself a 429; a 5xx on a moved model is returned and rejects the decision, a rate-limit 429 keeps it: the plan's limit holds for every model); any other\n  path (Codex compaction) runs `gpt-6.1-sol` unasked; a body over `CAVE_MAX_REQUEST_BYTES` streams\n  with only its model changed (`auto_body_too_large`; one that fits but does not decode records `auto_body_decode_failed`). The agent reads the Auto id it sent. Every `/chatgpt` row (Auto or\n  not) is offered to Cloud like any other, as its agent (`x-cave-agent`, else Codex by its\n  `originator` or User-Agent), with model ids in `route_from`/`route_to`; a model it could not read\n  goes to Cloud as none. The\n  ask starts before compression (parse, ask, compress, route), waits at most 800 ms (the CLI's\n  keychain credential is read when the runtime starts, `Link.Start`, never on a request goroutine;\n  a request that lands while a read runs with no secret cached waits for it, leaving the ask at\n  least 500 ms, else goes as `credential_loading` without an ask or a pause), carries the\n  caller's models, counts, what the request declares (the raw values of eleven allowlisted agent\n  headers, never cut: a value over 256 bytes, 16 KiB for Codex's turn metadata, which also comes\n  from the body's `client_metadata`, is left out; a forked Claude Code child's spawn-call\n  `subagent_type` as `x-caveman-agent`; its tool names, effort and thinking type), what the\n  session's previous request ran (the served model, the effort in force, the provider's input and\n  cache token counts, its age) and Cloud's opaque per-session state, plus, on a turn's first ask,\n  the raw text of the latest human turn, the one before it and the end of the agent's last reply\n  (contracts `route-ask-v1`; Cloud picks the model and the effort and no routing logic lives\n  here). Its answer is cached per ask until 30 min after its last use (session, or without one agent and\n  endpoint, provider, model, turn number, latest human text;\n  an input group carrying a tool result is no human turn, text riding along included) so a tool\n  loop never switches model or effort mid-turn (an ask first seen during a pause keeps the paused answer for its turn); a request the agent labels compaction or auxiliary\n  (Claude Code's request class and compaction flag, OpenCode's title/summary/compaction agent,\n  Codex's subagent kind and turn-metadata request kind) is asked on its own, without the text. The\n  session is `x-cave-session`, else the agent's session header (a Codex thread, `thread-id`, is\n  its own, its parent `x-codex-parent-thread-id`); a Claude Code child (`x-claude-code-agent-id`)\n  is a session of its own and sends its parent's state; a session id guessed from timing is not\n  used. State, previous-request facts and marks live in bounded in-memory LRUs (1024), never on\n  disk. The answer sets Responses `reasoning.effort`, chat `reasoning_effort` or Anthropic\n  `output_config.effort`; Anthropic `effort_mode: message` instead inserts the byte-identical mark\n  `{\"role\":\"system\",\"content\":[],\"output_config\":{\"effort\":…}}` (beta\n  `mid-conversation-output-config-2026-07-01` appended to `anthropic-beta`) before the last user\n  turn, or at the end after a tool result; on OpenAI Responses (GPT-6) the mark is a\n  `{\"type\":\"configuration_update\",\"reasoning\":{\"effort\":…}}` input item before the last user\n  message while nothing has answered it yet, never at the end and never next to another one (no\n  place, as mid tool loop: the effort in force holds), no beta, and none at all where OpenAI forbids\n  it (`truncation: \"auto\"`, `context_management`, a non-false `multi_agent`: Cloud's effort goes\n  top-level); a 400 naming a configuration update heals like a refused mark. Effort state made on\n  one wire starts afresh on the other. Only `effort_mode: message` ever starts marks; a \"top\"\n  answer on history carrying the session's marks goes in as one more mark (never wiping them; for\n  a compaction or side request, that request's only), and on any other history sets the top-level\n  field. Marks are remembered per session with a salted hash of the message before each\n  (`cache_control` and thinking blocks left out, so a heal's strip keeps them) and replayed at the\n  same places on every later request to a model that already took them (the agent resends history\n  without them), count_tokens of a known session or a forked child's parent included (it adds\n  nothing to a history without marks). Without an effort from Cloud (a failure, routing off,\n  effort \"\") a history without the session's marks goes as the agent sent it, top-level field\n  included (one cache restart where Cloud's was, nothing inserted, no beta), except that a\n  remembered heal (drop_block with its beta, or a strip) keeps applying. One with marks runs at\n  the request's own top-level effort, or, when it sets none, at the model's default effort\n  (Cloud's `default_effort`, kept per session, a forked child taking its parent's), marked when it\n  differs from the one in force; only with no default known does such a request go as the agent\n  sent it, without the marks, which the session then forgets (its later thinking blocks then lose\n  their binding) unless the request went to another model. The top-level field is fixed at the\n  session's first per-message request: the routed effort on a fresh conversation whose request\n  sets one (kept there, also for later requests that set none), else the request's own; a routed\n  effort for a request that sets none goes in as a mark, first on a fresh conversation. An anchor\n  that no longer matches drops that mark and every later one; compaction and side requests never\n  change them, and a body matching none of them (an unlabeled side request, compacted history)\n  gets marks of its own, kept for up to four request shapes (first message, length, last message),\n  which take the session over only from a longer request continuing that conversation (same first\n  message, its last one where it was); the session's marks and fixed top-level field then wait\n  there in turn. A thinking-binding 400 on the asked model of a `/messages` request retries once\n  only on a session the route stage changed (it was on for the request, the session carries its\n  marks or heal, or another model served one of its requests) whose body sets no\n  `thinking.block_binding`: with `thinking.block_binding.prefix_mismatch_behavior: \"drop_block\"`\n  (beta `thinking-binding-controls-2026-08-01`) when the block is bound to a different\n  conversation and the request's thinking is adaptive or enabled, then sent on every later request\n  (count_tokens and forked children included); else (Sonnet 5.5 `between_tools`, no thinking\n  field, a tampered signature) without thinking blocks from the failing message on, and later\n  requests on the same history (same first message, same message at its end) strip the same range\n  up front (a second strip widens it; the strip stays, the history as served, even if the agent\n  later sets its own `block_binding`). A drop_block retry refused with a 400 naming the binding,\n  `block_binding` or its beta moves the session to the strip path, a strip refused that way ends\n  its heals (the first lasts for the session, as it depends on the model and thinking type; the\n  second goes with the marks, cleared when they are forgotten; a side request never clears either,\n  nor the strip; a 429, 5xx or other 400 on the retry teaches nothing). Routing switched off after\n  a restart surfaces that 400. A refused mark retries once with top-level effort only and, once\n  served, latches per-message effort off for the session (a model that refused takes Cloud's\n  effort top-level); compressed 400s are decoded first. A Cloud error, timeout, 401/403,\n  `allowance` or `billing_limit` answer keeps the asked model (the held model above for a transient failure) and pauses new asks (1 min; 10 min\n  for 401/403 and billing_limit; until the 1st for allowance). Refusals and limits land in\n  `$CAVEMAN_HOME/route-state.json` (with Cloud's notice) for `caveman status`; a new login lifts\n  the pause. A route failure blames a model only on evidence (Cloud ADR 0110). A provider 4xx on\n  the routed model replays the original bytes on the asked model, except a 401 (the login, not the\n  model: returned as is); a 403 replays (a plan refuses a model it does not carry with one). Only a\n  served replay of bytes that differed from the original in the model alone blames the routed model\n  (`Reject(true)`, `provider_rej\n\n<!-- [TRUNCATED AT 50KB] -->","isInternal":false,"tokens":11897,"sizeBytes":50165},{"name":"CLAUDE.md","path":"shrink/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/shrink/CLAUDE.md","title":"shrink - Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# shrink — caveman-shrink, the tool-catalog compressor (Go core + launcher, Apache-2.0)\n\nCompress MCP/OpenAI **tool definitions** before they fill the context window. A thin Go wrapper\nover the [engine](../engine/CLAUDE.md)'s `toolschema` compressor: it drops annotation metadata\n(examples, titles, `$comment`, `$schema`) and reduces free-text descriptions to their lead\nsentence **plus every constraint-bearing sentence**, while keeping every **selection token** —\ntool and parameter names, types, enums, required — and every **argument-construction value** —\n`default`, `const`, `$ref` targets — byte-for-byte. Everything is `inferred`.\n\n## Layout\n- `shrink.go` — the library: `Shrink` (catalog → compressed, fail-open S4 transform, durable-CCR-backed), `Recover` (handle → original bytes), `Lint` (per-tool inferred token reductions), `SelectionProfile` (the structural surface — what must survive). `WithStore`/`WithStorePath` pick the recovery store; default is the shared `CAVEMAN_CCR_DB` / `~/.caveman/ccr.db`.\n- `cmd/caveman-shrink/` — the CLI: `caveman-shrink` / `shrink` (stdin→stdout), `lint <file>`, and `recover <handle>` (prints the original catalog for a handle a prior shrink minted).\n- `bin/caveman-shrink.mjs` + `package.json` — the `npx caveman-shrink` launcher around the prebuilt binary.\n\n## Two correctness boundaries\n1. **structural selection surface** — `SelectionProfile(input)` ==\n   `SelectionProfile(Shrink(input).Output)` always. This proves names/params/enums/required survive\n   byte-for-byte. It does **not** prove the model selects the same tool: descriptions are\n   model-visible and long descriptions are lossy. Behavioral equivalence requires model-eval\n   fixtures; current results stay `inferred`.\n2. **argument validity** — agents build tool *arguments* from descriptions, so shrink preserves\n   selection but **does not by itself guarantee argument validity**. What it guarantees, biased\n   to over-keep: (a) a description that already fits a small budget (≤ `maxDescLen*2` bytes) is\n   kept **whole** — no sentence is elided, so a constraint phrased without a recognised marker\n   still survives; (b) in a genuinely large description, the lead sentence plus every sentence\n   carrying a constraint marker (must / cannot / required / rejected / invalid / not allowed /\n   exactly one / at least / at most / max / min / range / between / greater-than / over / above /\n   unique / mutually exclusive / format / ISO / RFC / absolute …) is retained in full. Truncating\n   an abbreviation (\"Target path, e.g. /srv…\" → \"Target path, e.\") or dropping\n   `default`/RFC3339/bound rules would produce invalid calls the structural profile cannot see;\n   the call-validity conformance test (`engine/compressors`, golden fixture + reproduced\n   under-keep cases) locks recognised constraint tokens in place. Over-keep is the rule — never\n   knowingly under-keep.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=shrink` / `make product-test PRODUCT=shrink`.\n- The structural compressor lives in the **engine** (`compressors/toolschema.go`); shrink reuses it, never forks it.\n- **This is the dedicated product surface for the `toolschema` compressor.** Engine API/CLI callers\n  can also force it locally. Managed-gateway adapters keep tool arrays in the frozen prompt-cache\n  prefix and never hand them to it; gateway's separate S2 tool-search/deferral path is not\n  compression. Local reductions stay `inferred`. A future billed route for this transform needs\n  cache-versus-schema cost proof, stable byte-identical prefix output, and an eval gate.\n\n## Gotchas (honesty invariants)\n- **S4 lossy, fail-open** — successful compression changes model-visible bytes; malformed/incompressible catalogs pass through unchanged (`ratio:0`, no handle).\n- **inferred-only** — token counts are estimates via the engine's offline counter; never `verified`, never re-projected.\n- **reversible for real** — a compressing shrink writes the exact original to the **durable** store (`engine.Compress` commits the recovery row before it publishes transformed bytes), so a printed handle resolves later, from a separate process, via `Recover` / `caveman-shrink recover`. It is NOT an in-memory store discarded on return — that minted handles that were unresolvable the instant `Shrink` returned. If the durable store cannot be opened, `Shrink` returns an error and the CLI forwards the original bytes: no handle, no reversibility claim.\n\nSee ../../CLAUDE.md (root) · ../engine/CLAUDE.md\n","isInternal":false,"tokens":1080,"sizeBytes":4552},{"name":"README.md","path":"evals/README.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/evals/README.md","title":"Evals Skill","category":"anthropic-skill","format":"markdown","content":"# Evals\n\nMeasures real token compression of caveman skills by running the same\nprompts through Claude Code under three conditions and comparing the\ngenerated output token counts. A second eval, [Fidelity](#fidelity),\nruns the same arms on fixed correctness cases and checks that the facts\nsurvive.\n\n## The three arms\n\n| Arm | System prompt |\n|-----|--------------|\n| `__baseline__` | none |\n| `__terse__` | `Answer concisely.` |\n| `<skill>` | `Answer concisely.\\n\\n{SKILL.md}` |\n\nThe honest delta for any skill is **`<skill>` vs `__terse__`** — i.e.\nhow much the skill itself adds on top of a plain \"be terse\" instruction.\nComparing a skill to the no-system-prompt baseline conflates the skill\nwith the generic terseness ask, which is what an earlier version of\nthis harness did and is why its numbers were inflated.\n\n## Current snapshot\n\n`snapshots/results.json` was generated on `claude-opus-5-5` with Claude\nCode 2.1.288 (2026-10-02), with the three response-style skills as the\nskill arms: `caveman`, `ultracave`, `megacave`. The other `skills/*`\ndirectories are workflow skills, not response styles, so they are left out.\n\n```bash\nCAVEMAN_EVAL_MODEL=claude-opus-5-5 CAVEMAN_EVAL_SKILLS=caveman,ultracave,megacave \\\n  python3 evals/llm_run.py\n```\n\nThe previous snapshot (`claude-opus-4-6`, April 2026) was generated\nwithout host isolation, so whatever plugins and CLAUDE.md files were\ninstalled could reach every arm. `llm_run.py` now isolates each call\n(see below); numbers from the two snapshots are not directly comparable.\n\n## Why this design\n\n- **Real LLM output**, not hand-written examples (no circularity).\n- **Same Claude Code** the skills target — no separate API key.\n- **Snapshot committed to git** so CI runs are deterministic and free,\n  and so any change to the numbers is reviewable as a diff.\n- **Control arm** isolates the skill's contribution from the generic\n  \"be terse\" effect.\n- **Host isolation.** Each `claude -p` call runs with\n  `--setting-sources project --strict-mcp-config --disable-slash-commands\n  --no-session-persistence` from an empty temp dir: no user settings (so\n  no installed plugins or their SessionStart hooks), no MCP servers, no\n  installed skills, no CLAUDE.md. Without it a plugin's injected ruleset,\n  or an MCP auth nag the model repeats in its answer, lands in every arm\n  including the baseline.\n\n## Files\n\n- `prompts/<lang>.txt` — fixed list of dev questions, one per line.\n  `en.txt` is the default; `pt.txt` is Brazilian Portuguese; `fr.txt`\n  is French and mirrors `en.txt` line for line.\n- `llm_run.py` — runs `claude -p --output-format json --system-prompt-file …`\n  per (prompt, arm), captures real LLM output, writes\n  `snapshots/results.json` along with metadata (model, CLI version,\n  language, generation timestamp). Next to the text it stores the usage\n  Claude Code reports for each call under `usage` (same arm/prompt layout\n  as `arms`): input, output, cache-creation and cache-read tokens plus\n  `total_cost_usd`.\n- `measure.py` — reads the snapshot, counts tokens with tiktoken\n  `o200k_base`, prints a markdown table with median / mean / min / max /\n  stdev across prompts. When the snapshot has `usage`, a second table\n  gives Claude's own output-token counts against the terse control and\n  the median input tokens the skill adds per call (skill arm minus terse\n  arm, cache tokens included).\n- `snapshot_contract.py` — rejects incomplete or malformed snapshot matrices\n  before `measure.py` or `score_fidelity.py` reports metrics.\n- `prompts/fidelity.json` — fixed correctness cases, each with regex\n  checks (see [Fidelity](#fidelity)).\n- `score_fidelity.py` — scores `snapshots/fidelity.json` against those\n  checks, offline, stdlib only.\n- `snapshots/results.json` — committed source of truth, regenerated only\n  when SKILL.md files or prompts change. Other languages write\n  `results.<lang>.json` next to it; none is committed yet.\n\n## Refresh the snapshot (requires `claude` CLI logged in)\n\n```bash\nuv run python evals/llm_run.py\n```\n\nThis calls Claude once per prompt × (N skills + 2 control arms). Use\na small model to keep it cheap:\n\n```bash\nCAVEMAN_EVAL_MODEL=claude-haiku-4-5 uv run python evals/llm_run.py\n```\n\nBy default every `skills/*/SKILL.md` gets an arm. `CAVEMAN_EVAL_SKILLS`\n(comma-separated skill ids) restricts the skill arms; an unknown id aborts\nbefore any call:\n\n```bash\nCAVEMAN_EVAL_SKILLS=caveman,ultracave,megacave uv run python evals/llm_run.py\n```\n\nEach call times out after `CAVEMAN_EVAL_TIMEOUT` seconds (default 300). A\nfailed or timed-out call is retried twice, after 5 s and then 20 s. If it\nstill fails, the run stops (auth, quota or a bad model name would fail\nevery remaining call too), writes the finished calls to\n`snapshots/results.partial.json` with the unfinished cells set to `null`\nand the error in `metadata.error`, leaves `results.json` untouched and\nexits 1. `measure.py` refuses a partial file; it is gitignored.\n\n### Other languages\n\n```bash\nCAVEMAN_EVAL_LANG=pt CAVEMAN_EVAL_MODEL=claude-haiku-4-5 CAVEMAN_EVAL_SKILLS=caveman uv run python evals/llm_run.py\nCAVEMAN_EVAL_LANG=pt uv run --with tiktoken python evals/measure.py\n```\n\nThe terse control is translated per language (`TERSE_PREFIXES` in\n`llm_run.py`): an English \"Answer concisely.\" on a Portuguese question\nalso nudges the model toward English, which would be a second variable.\nA language with no translated control, or no `prompts/<lang>.txt`, stops\nbefore any call instead of falling back to English. The skill text\nitself stays in English, as shipped.\n\nWhy a separate language matters: SKILL.md promises \"compress the style,\nnot the language\", and the rules target English function words\n(a/an/the, just/really). Whether that transfers to a language with\ngendered articles and a different filler vocabulary is an empirical\nquestion, so it gets its own snapshot.\n\n## Read the snapshot (no LLM, no API key, runs in CI)\n\n```bash\nuv run --with tiktoken python evals/measure.py\n```\n\nReporting fails closed unless the snapshot has both control arms, at least one\nskill arm, exactly one string output per prompt in every arm, and metadata whose\n`n_prompts` matches the prompt list. A `usage` block, when present, must\ncover the same arms and prompts with non-negative token counts.\n\n## Adding a prompt\n\nAppend a line to `prompts/<lang>.txt`, then refresh that language's snapshot.\n\n## Adding a language\n\nAdd `prompts/<lang>.txt` (ideally mirroring `en.txt` line for line),\nadd the translated terse control to `TERSE_PREFIXES` in `llm_run.py`,\nrun with `CAVEMAN_EVAL_LANG=<lang>` and commit\n`snapshots/results.<lang>.json`.\n\n## Adding a skill\n\nDrop a `skills/<name>/SKILL.md`, then refresh the snapshot. `llm_run.py`\npicks up every skill directory automatically, unless `CAVEMAN_EVAL_SKILLS`\nis set, in which case add the new id to that list.\n\n## Fidelity\n\nThe length eval alone rewards a skill that replies `k` to everything.\nThe fidelity eval asks whether each arm kept what the answer needs: exact\nnumbers and units, every not/never/only/except, the user's language,\nverbatim errors, safety wording before destructive steps, normal prose in\npersisted artifacts.\n\n`prompts/fidelity.json` holds the cases: `{id, category, prompt, checks,\nexamples}` with `checks.must_include` and `checks.must_not_include` as\nregex lists, and `examples.pass` / `examples.fail` as answers the checks\nmust accept / reject.\nA case passes for an arm when every `must_include` pattern matches the\noutput and no `must_not_include` pattern does (Python `re.search`,\ncase-insensitive). There is no judge model, so the same snapshot always\ngets the same score, and checks can be fixed and re-scored without new\ncalls. The first six cases come from\n[#1061](https://github.com/JuliusBrussee/caveman/pull/1061) by alexis.\n\n```bash\nCAVEMAN_EVAL_SET=fidelity CAVEMAN_EVAL_MODEL=claude-opus-5-5 \\\n  CAVEMAN_EVAL_SKILLS=caveman,ultracave,megacave python3 evals/llm_run.py\npython3 evals/score_fidelity.py\n```\n\nThe run uses the same arms, isolation, retries and usage capture as the\nlength eval and writes `snapshots/fidelity.json`. The scorer prints the\npass rate per arm, overall and per category, then every failed check.\nIt refuses a snapshot whose prompts no longer match the case file.\n\nNo fidelity snapshot is committed yet. A regex pass is evidence, not a\nverdict: read the failures before citing a rate, and publish no\nquality-equivalence claim without a committed, reviewed snapshot.\n\n| Category | What the checks look for |\n|----------|--------------------------|\n| `substance-preservation` | Filler removed, the technical substance needed to act kept. |\n| `exact-preservation` | Polarity, limits, numbers, units, code, identifiers, APIs, commands and quoted errors unchanged. |\n| `no-caricature` | No fake grammar, mode prefixes, invented abbreviations or arrows. |\n| `language-and-grammar` | The requested or dominant language kept, grammatical markers kept. |\n| `safety-clarity` | Plain, complete warnings for destructive actions, security, data loss and ordered recovery steps. |\n| `artifact-boundary` | Persisted or external human-facing artifacts in normal prose. |\n| `mode-boundaries` | Explicit activation, mode switch, persistence and deactivation boundaries preserved. |\n\nTo add a case, append it to `prompts/fidelity.json` with a unique `id`,\none of the categories above, at least one check, and at least one\nknown-good and one known-bad example answer, then rerun the fidelity\neval. `tests/test_eval_fidelity.py` checks the file's shape and runs\nevery case's checks against its examples, so a check that rejects a\ncorrect answer fails there instead of after a paid run.\n\n## What this does NOT measure\n\n- **Fidelity beyond the checks** — the fidelity eval only sees what its\n  regexes test. Tone, ordering and whether an explanation is actually\n  right are left to human review of the outputs.\n- **Latency or cost** — latency is out of scope. Skills add input tokens\n  on every call, so output savings are not the full economic picture.\n  Snapshots with `usage` record that input cost per call; the committed\n  `results.json` predates usage capture, so it has none.\n- **Cross-model behavior** — only the model used to generate the\n  snapshot is measured.\n- **Exact Claude tokens, from the tiktoken table** — `tiktoken\n  o200k_base` is OpenAI's BPE and is only an approximation of Claude's\n  tokenizer. Ratios between arms are meaningful; absolute numbers are\n  approximate. The usage table uses Claude's own counts instead, but\n  Claude's output count includes thinking tokens, which the user never\n  reads.\n- **Statistical significance** — single run per (prompt, arm) at default\n  temperature. The min/max/stdev columns let you eyeball whether a\n  number is solid or noisy, but this is not a powered experiment.\n\n## Historical: reminder_run.py\n\n`reminder_run.py` and `snapshots/reminder.json` measured the pre-3.1 per-turn reminder text (`Enforce this reply: ...`), which the mode tracker no longer emits. The snapshot is kept as history; regenerate before citing it.\n","isInternal":false,"tokens":2791,"sizeBytes":11070},{"name":"AGENTS.md","path":"packages/cli/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/packages/cli/AGENTS.md","title":"Cli Skill","category":"anthropic-skill","format":"markdown","content":"# packages/cli — `caveman` CLI\n\nTypeScript CLI (`src/index.ts`) driving the local proxy and wrapping control-api\nREST calls. Published package has zero runtime dependencies. Build emits\n`dist/index.js`, dependency-free `dist/caveman-delegate-mcp.mjs`, and a lazy\nbundled `dist/learn-tui.js` Clack chunk, then\n`scripts/shebang.mjs` adds shebang + chmod. `bin` exposes both `caveman` and `cave`. Non-secret config lives at\n`~/.caveman-cloud/config.json` (0o600); the **auth token lives in the OS keychain** (macOS\n`security`) or a `~/.caveman/credentials` (0o600) fallback — never plaintext config.\n\n## Layout\n- `src/index.ts` — entire CLI: arg dispatch, HTTP helpers (`get`/`post`), token storage, all commands\n- `src/learn-tui.ts` — bounded interactive learn view; bundled with Clack and loaded only for a real TTY\n- `tests/*.runtime.mjs` — Node `--test` runtime tests; spawn the built binary against HTTP stubs (`providers-verify`, `wrap`, `login`, `compress`)\n- `scripts/bundle-delegate.mjs` — copies canonical dependency-free delegate server into published `dist/`\n- `scripts/bundle-tui.mjs` — bundles Clack into the self-contained learn TUI chunk\n- `scripts/shebang.mjs` — post-build: prepends shebang, marks executable\n- `package.json` — bin: `caveman`/`cave → dist/index.js`; build: registries → `tsc` → delegate/TUI bundles → shebang\n\n## Command surface\n`caveman <agent>` (e.g. `caveman claude`) is shorthand for `caveman wrap <agent>` — any known agent id or binary name works as a top-level command; it is dispatched **last**, so real commands always shadow an agent name, and everything after the agent name goes to the agent verbatim.\n`cave` is a permanent byte-compatible bin alias. Relocated verbs keep their bare\nspellings as silent legacy aliases: no deprecation text may alter piped output.\n\nPrinted porcelain is `run`, `learn`, `login`, `status`, plus the agent shortcut.\nLocal capabilities live under `caveman tools`; account- or network-dependent\noperations live under `caveman cloud`. `dev` and `deploy` are undocumented\nmaintainer aliases. `tools` is capped at 15 printed verbs and `cloud` at 15;\ncurrent counts are 15 and 14. Internal/advanced `shrink-hook`, `practices`, and\n`check` remain callable through existing paths but are unprinted, including in\nlegacy `help tools --all` output.\n\nCaveman's own Go binaries (proxy/engine/mcp/mem/browse/shrink) resolve via `cavemanBin()`: env override (`CAVEMAN_*_BIN`) → PATH → `~/.caveman/bin` (where `scripts/install-local-cli.sh` or `scripts/install-local-cli.ps1` builds them) → bare name (so missing-binary panels still trigger).\n`caveman setup` prints per-binary install status — what works, what degrades to a loud byte-safe pass-through, and the one install command — and exits non-zero when a required binary (proxy/engine/mcp/mem; browse and shrink are optional) is missing. `caveman tools compress catalog` delegates to the dedicated `caveman-shrink` binary; `caveman tools shrink` remains command-output compression. It's the anti-silent-degrade front door for npm installs (the package ships JS only); every degraded path also prints its own warning line pointing at it. Publish checklist lives in `PUBLISHING.md`.\nLocal (no account): `start` (launch the proxy via `CAVEMAN_PROXY_BIN`; if the binary is missing or the port is already served it renders a status panel — build/`make dev`+live docker status/env — instead of a bare spawn error) · `wrap [agent]` (inject `ANTHROPIC_BASE_URL`/`OPENAI_BASE_URL` = `CAVE_GATEWAY_URL` or the local proxy, exec child; known agents `claude`/`codex`/`gemini`/`aider` launch by id with install detection; an unknown/unfound target shows an install hint or the wrappable list; bare `wrap` in a TTY opens an arrow-key picker) · `compress` (shells out to `caveman-engine` via `CAVEMAN_ENGINE_BIN`; byte-safe pass-through fallback if the binary is missing; `inferred`) · `toon encode|decode` (stateless JSON⇄TOON converter via the engine binary; `encode` degrades byte-safe when the engine is missing, `decode` fails loudly — it must not emit raw TOON as JSON) · `wrap --toon` (sets `CAVE_ENGINE_TOON=best-of` on the spawned proxy so it re-encodes uniform JSON the model reads — tool results — as TOON when smaller; opt-in, implies `--compress`) · `mcp install|uninstall [agent]` (register/remove the caveman_retrieve MCP tool; compress-mode wrap injects it only when `execute.mcp = auto` and a real `caveman-mcp` executable resolves — never the npx fallback — because streams are only compressible with agent-side recovery; `execute.mcp = marker-only` or `false` stops injection while existing registrations remain visible) · `evals run` (delegates to `caveman-engine evals run`, forwards its exit code) · `stats` (delegates to `caveman-proxy stats`) · `convert` (pixel-compresses installed agent skills in place: SKILL.md **body** → `SKILL.pxN.png` pages via `caveman-engine pixel render`, frontmatter stays text so discovery/triggering still works, body becomes a stub telling the agent to read the images on invocation; dirs come from registry profiles with a `skills` block — claude + codex today; converts only when image+stub est tokens < text est tokens, else untouched; original kept byte-exact as `SKILL.orig.md`, `--revert` restores; every skip is reported with its reason; savings `inferred`, per-invocation) · `skills install [caveman|caveman-learn]` (writes embedded Caveman skills and auto-pixels by default) · `skills add <source>` (accepts official `npx skills add` Git/URL/local sources and flags, delegates download/selection to that CLI in forced copy mode, then pixelizes only new/changed Claude Code or Codex skills; resources stay untouched; third-party content is explicitly unreviewed; `--no-pixel` opts out; engine failure/not-smaller stays honest plain text).\nConnected namespace: `whoami · projects · keys · providers · billing · score ·\ncosts · plan · traces · experiments · receipts · audit · sync · agent`.\n`doctor`, `opportunities`, `snippets`, `dev`, and `deploy` remain unprinted legacy\naliases; `status`, `plan`, and `tools sdk snippets` absorb their public jobs.\n\n`login` polls control-api `/api/v1/auth/device/{code,token}`; `organization_id` is bound from the returned token. `CAVE_TOKEN` is the non-interactive CI path. Logged-out connected verbs print one line + exit non-zero (CI skips, never crashes).\n\nFirst-run retro scan persists only closed `caveman.local_scan.v1` aggregate. Login or explicit `sync` uploads it through authenticated project `format=local-scan`; prompts, outputs, paths, session rows, free-text caveats, and anonymous telemetry never enter this lane. Dashboard labels token figures `inferred` and separates them from measured spend and verified savings.\n\n`providers verify <conn>` → real POST `/api/v1/projects/{id}/providers/{conn}/verify` (no hardcoded status).\n\n`plan` renders the Cave Plan in plain English (one operator voice); `--json` prints the raw response. There is **no** caveman voice / `--engineer` flag — the dual-voice was deliberately removed. Headline is labeled `basis` (\"inferred\"), savings are per-day — never reprojected to monthly. (honesty rule: no-fake-savings)\n\n`learn` is summary-first: real terminals get animated progress, bounded score\nand move cards, then one keyboard action menu. `--plain` restores compact text,\n`--all` restores every sink id/class/practice/suggestion, while `--json` and\n`--md` stay complete. `learn implement [claude|codex]\n[--prompt <focus>]` installs the existing `caveman-learn` safety guide when\nmissing and launches the chosen interactive agent. The guide's per-edit consent,\nload-bearing protection, re-measurement, and inferred-only rules remain binding.\nPresentation contract: [`TERMINAL_UX.md`](TERMINAL_UX.md).\n\n## Conventions\n- Dispatch uses handler tables. Every handler receives its own rebased argv slice;\n  never read process-global argv positionally inside a handler.\n- `flag(\"--name\", fallback)` parses named args from current invocation.\n- Tests use `node --test` (Node built-in runner); run `tsc` first, test spins a real HTTP server\n- Build: `pnpm build` (tsc + shebang); install locally: `scripts/install-local-cli.sh` (macOS/Linux) or `scripts/install-local-cli.ps1` (Windows) at repo root\n\n## Capability promotion rule\n\nA capability may default on only when it is byte-safe, or when protected by the\napplicable path-specific gate: managed gateway uses an eval gate; local wrap\nuses recovery + CCR — **not** an account or entitlement. There is\nno eval gate in local `run`.\nAny PR flipping a default must name the clause and path.\n\nA verb enters porcelain only when its capability is automatic-by-default-safe\ninside `run` and users no longer need to type it. Porcelain stays capped at four\nverbs + agent shortcut + exactly two namespaces. A fifth verb, or a 16th printed\nverb in either namespace, requires a retirement decision. `record` mode is always\npass-through.\n\nCapability config is grouped in `~/.caveman-cloud/config.json` as `think`,\n`remember`, and `execute`. `./.caveman/config.json` may only narrow its allowlisted\nproject-local keys; it cannot change `think.mode`, pixel settings, account state,\nconsent, or entitlement. Resolution is default < proxy YAML < legacy `wrap` <\nglobal groups < project overlay < env. Env parity is knob-specific. Inspect\nper-key source with `caveman tools config get`.\n\n## Gotchas\n- `providers verify` must NOT return a hardcoded status; the test asserts the CLI echoes the server's value (no-placeholder rule)\n- `plan` savings display must stay per-day; never multiply to monthly projection\n- Non-PAYG coverage includes Claude Pro/Max, Codex ChatGPT, Gemini OAuth, and routed compatible agents. Codex subscription mode keeps provider config ephemeral under `CODEX_HOME`, auto-installs its MCP recovery, and starts `/chatgpt/responses` in compress mode instead of forcing record/pass-through. Plain OpenAI `/responses` and Gemini `generateContent` requests with MCP explicitly disabled have no server-retrieval grammar, so they must remain byte-identical with zero compression accounting; the compression conformance matrix pins these protocol-specific fail-closed cases instead of requiring every profile to emit a CCR marker.\n- Subscription/OAuth wrap sessions (Claude Pro/Max) compress **locally only**, live zone only, and with **no account**: `CAVEMAN_WRAP_ENTITLED` is gone from both doors and from the proxy, and both doors `delete` any inherited copy so a stray export cannot resurrect it. What both doors DO stamp is the recovery path (`CAVEMAN_RECOVERY`), explicitly (`\"mcp\"` or empty, never inherited): `wrap` answers it from the **agent's own** MCP install (an exported `CAVEMAN_RECOVERY=mcp` can't outlive that answer — it would have the proxy elide bytes behind markers this agent has no `caveman_retrieve` tool to expand), `start` from machine-wide MCP install evidence plus an explicit `CAVEMAN_RECOVERY=mcp` counted as the operator's own opt-in, re-stamped so the disclosure line and the proxy can never disagree; the compression disclosure line prints only when recovery holds, and no-MCP says compression is off and names `caveman mcp install <agent>`. The `subscription_compress: off` operator switch stays the operator's. Their measured reduction remains **tokens only**: a seat has no per-token marginal price, so the session footer and synced span never claim saved dollars. The local stats dashboard may show a separately labeled, same-model API equivalent from captured prices; it is hypothetical value, never subscription bill or quota savings. The session-savings line treats `oauth` like `subscription` (OAuth is list-price-eligible on Vertex alone) and qualifies unconditionally when its capped auth-mode window is truncated\n- Published runtime dependencies stay zero. TUI libraries must be bundled,\n  lazy-loaded, and measured; do not move them onto ordinary command startup.\n- `learn` uses bundled Clack only when stdin/stdout/stderr are TTYs. `--plain`,\n  `CAVEMAN_PLAIN=1`, `TERM=dumb`, machine modes, and pipes must never prompt.\n- Remaining terminal UX (status panels + `wrap` picker) lives in the small toolkit\n  at the bottom of `src/index.ts`. Piped/non-interactive paths remain plain; runtime\n  tests assert them.\n- Current official Skills CLI installs global Codex sources at\n  `~/.agents/skills`, despite older/direct Caveman installs using\n  `~/.codex/skills`; third-party post-install discovery must scan both.\n\nSee ../../CLAUDE.md (root)\n","isInternal":false,"tokens":3041,"sizeBytes":12564},{"name":"CLAUDE.md","path":"packages/cli/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/packages/cli/CLAUDE.md","title":"Cli Skill","category":"anthropic-skill","format":"markdown","content":"# packages/cli — `caveman` CLI\n\nTypeScript CLI (`src/index.ts`) driving the local proxy and wrapping control-api\nREST calls. Published package has zero runtime dependencies. Build emits\n`dist/index.js`, dependency-free `dist/caveman-delegate-mcp.mjs`, and a lazy\nbundled `dist/learn-tui.js` Clack chunk, then\n`scripts/shebang.mjs` adds shebang + chmod. `bin` exposes both `caveman` and `cave`. Non-secret config lives at\n`~/.caveman-cloud/config.json` (0o600); the **auth token lives in the OS keychain** (macOS\n`security`) or a `~/.caveman/credentials` (0o600) fallback — never plaintext config.\n\n## Layout\n- `src/index.ts` — entire CLI: arg dispatch, HTTP helpers (`get`/`post`), token storage, all commands\n- `src/learn-tui.ts` — bounded interactive learn view; bundled with Clack and loaded only for a real TTY\n- `tests/*.runtime.mjs` — Node `--test` runtime tests; spawn the built binary against HTTP stubs (`providers-verify`, `wrap`, `login`, `compress`)\n- `scripts/bundle-delegate.mjs` — copies canonical dependency-free delegate server into published `dist/`\n- `scripts/bundle-tui.mjs` — bundles Clack into the self-contained learn TUI chunk\n- `scripts/shebang.mjs` — post-build: prepends shebang, marks executable\n- `package.json` — bin: `caveman`/`cave → dist/index.js`; build: registries → `tsc` → delegate/TUI bundles → shebang\n\n## Command surface\n`caveman <agent>` (e.g. `caveman claude`) is the **persistent default door**: for hook-capable native agents (claude/codex/hermes/gemini/opencode — not aider, whose hookless native install could never start the proxy) it runs the `caveman enable <agent>` user-scoped native install if no install journal exists, pre-starts the local proxy when it isn't listening (the native SessionStart hook re-does this, but only after the host approves the hooks), then launches the host binary directly — so plain `<agent>` stays caveman'd in every later session. For claude/codex the enable step also writes the `output` skill suite (`caveman`/`ultracave`/`megacave`) into the host's user skills dir, create-only (`wx`, never clobbers) and fail-open. They are deliberately NOT native-journal operations, so an edited/deleted skill can never read `degraded`, and `disable` leaves them in place: it turns off routing and hooks, not the voice. A `~/.caveman/integrations/<agent>.voice-skills.json` sidecar marks the install as done, so it runs once: the shortcut skips journaled installs (explicit `caveman enable <agent>` picks the skills up there), a skill the user deleted is not written back, and a CLI upgrade does not refresh an installed copy. It falls back to the ephemeral `wrap` door whenever the native path can't hold: wrap-session flags given (`--off`/`--pixel`/`--workflow`), a non-native agent, a Cave Build lock in cwd (lock enforcement is wrap-door-only), or any enable failure (e.g. missing caveman-mcp/proxy binaries) — the shortcut is never worse than a session-only wrap. Note the fallback is session-only relative to an install that may already exist: once the native install is in place, `--off`/`wrap` runs are ephemeral but the persistent routing stays until `caveman disable <agent>`. Any known agent id or binary name works as a top-level command; it is dispatched **last**, so real commands always shadow an agent name, and everything after the agent name goes to the agent verbatim. `caveman wrap <agent>` / `caveman run <agent>` remain the explicit session-only path.\n`cave` is a permanent byte-compatible bin alias. Relocated verbs keep their bare\nspellings as silent legacy aliases: no deprecation text may alter piped output.\n\nPrinted porcelain is `run`, `learn`, `login`, `status`, plus the agent shortcut.\nLocal capabilities live under `caveman tools`; account- or network-dependent\noperations live under `caveman cloud`. `dev` and `deploy` are undocumented\nmaintainer aliases. `tools` is capped at 15 printed verbs and `cloud` at 15;\ncurrent counts are 15 and 14. Internal/advanced `shrink-hook`, `practices`, and\n`check` remain callable through existing paths but are unprinted, including in\nlegacy `help tools --all` output.\n\nCaveman's own Go binaries (proxy/engine/mcp/mem/browse/shrink) resolve via `cavemanBin()`: env override (`CAVEMAN_*_BIN`) → PATH → `~/.caveman/bin` (where `scripts/install-local-cli.sh` or `scripts/install-local-cli.ps1` builds them) → bare name (so missing-binary panels still trigger).\n`caveman setup` prints per-binary install status — what works, what degrades to a loud byte-safe pass-through, and the one install command — and exits non-zero when a required binary (proxy/engine/mcp/mem; browse and shrink are optional) is missing. `caveman tools compress catalog` delegates to the dedicated `caveman-shrink` binary; `caveman tools shrink` remains command-output compression. It's the anti-silent-degrade front door for npm installs (the package ships JS only); every degraded path also prints its own warning line pointing at it. Publish checklist lives in `PUBLISHING.md`.\nLocal (no account): `start` (launch the proxy via `CAVEMAN_PROXY_BIN`; if the binary is missing or the port is already served it renders a status panel — build/`make dev`+live docker status/env — instead of a bare spawn error) · `wrap [agent]` (inject route and exec child; Claude, Codex, Hermes, Gemini, OpenCode, Aider, and OpenClaw launch by id; native-capable hosts get temp pack/home/config only, removed after exit; no user/project config write) · `enable|disable <agent>` (explicit user-scoped native install with atomic journal, lock, reversible owned-path restore, component doctor) · `inspect` / `why <decision-id>` (content-blind native receipt and Decision Ledger explanation) · `compress` (shells out to `caveman-engine`; byte-safe missing-binary pass-through; `inferred`) · `mcp install|uninstall [agent]` (explicit persistent recovery registration; wrap uses temporary recovery configuration only when `execute.mcp = auto` and a current local MCP binary exists; `marker-only` or `false` suppresses injection without silently removing user registrations) · `skills add <source>` (same Git/URL/local sources and flags as official `npx skills add`; download/selection stays upstream, forced copy mode lets Caveman pixelize only new/changed Claude Code/Codex `SKILL.md` bodies; resources remain untouched and third-party content remains explicitly unreviewed; `--no-pixel` opts out) · `skills import <dir|SKILL.md>` (blocked unevaluated native-skill draft; imported resources never execute) · `evals run` · `stats` · `convert`.\nConnected namespace: `whoami · projects · keys · providers · billing · score ·\ncosts · plan · traces · experiments · receipts · audit · sync · agent`.\n`doctor`, `opportunities`, `snippets`, `dev`, and `deploy` remain unprinted legacy\naliases; `status`, `plan`, and `tools sdk snippets` absorb their public jobs.\n\n`login` polls control-api `/api/v1/auth/device/{code,token}`; `organization_id` is bound from the returned token. `CAVE_TOKEN` is the non-interactive CI path. Logged-out connected verbs print one line + exit non-zero (CI skips, never crashes).\n\n**First-run experience** (TTY only, once per machine, marker `firstRunAt` in config): on the first interactive wrap — banner, 30-day retrospective scan (`caveman-proxy learn scan --retro`, read-only over local Claude Code/Codex session logs; base behavior pass capped at 20s, retro pass at 60s, child timeout derived as 90s including margin), count-up reveal of tokens sent / would-have-cut / rides-every-turn stream figure (sums over scanned sessions, never extrapolated; `inferred`; tokens never dollars; sent is deduped once per API response, base cut carries no ratio against sent, only the stream figure shares sent's basis), telemetry disclosure, one `[y/N]` account question (yes → device login → existing span backfill). CLI persists only the closed, content-free aggregate; login or explicit `sync` uploads it to the authenticated project's `local-scan` import lane. Prompts, outputs, paths, session rows, free-text caveats, and anonymous telemetry never enter that upload; dashboard keeps it `inferred` and separate from spend/verified savings. Every failure degrades to one dim line and the agent still launches. Replay with the **unprinted** `caveman welcome` (porcelain caps unchanged). Anonymous **telemetry defaults ON (opt-out)**: persisted v1 decisions and `DO_NOT_TRACK`/`CAVEMAN_TELEMETRY=0`/`caveman telemetry off` always win; CI never sends; non-TTY never persists the default; the first eligible interactive run persists a stable anonymous id and prints the disclosure line. A native SessionStart (not `source: compact`) with a persisted, current-version yes spawns a detached `caveman telemetry session <agent> <source>` (unprinted) that sends `session_start` with the token delta, so installed users who launch agents directly stay visible; the hook never waits on the network. One event per host session: an O_EXCL marker under `~/.caveman/runtime/telemetry-sessions/` is created before the spawn (pruned after a day). Hook entrypoints (`native-hook`, `shrink-hook`, `mem recall-hook`) set `telemetryCommandSent` so no hook process ever holds the host on a command_run POST. An interactive run that sees `DO_NOT_TRACK`/`CAVEMAN_TELEMETRY=0` while config says yes persists an opt-out (GUI-launched hosts never see the shell rc). Every event also carries `account`, `plan`, `install_channel`, `timezone`, `locale` from `telemetryContext()` (cheap config reads, no keychain). `command_run` also carries `tokens_processed`/`tokens_saved` read from the local proxy store as a **delta** against the `telemetryTokens` watermark in config, always with `tokens_basis` (`inferred`, tokens never dollars); the read is best-effort behind a 400ms spawn budget and omits the fields entirely when the store or binary is absent. Events post to the `cli-telemetry` Supabase Edge Function (source + schema in root `supabase/`), which validates them and stores each with the client IP (cleared after 90 days by pg_cron). Product analytics (users, daily actives, retention, churn, tokens saved, command/agent usage) are SQL views in the database's `analytics` schema. Widening the disclosed scope means bumping `TELEMETRY_PROMPT_VERSION` (now 5) — that reprints the line once for stale-version opt-ins, never re-asks, never touches an opt-out.\n\n`providers verify <conn>` → real POST `/api/v1/projects/{id}/providers/{conn}/verify` (no hardcoded status).\n\n`plan` renders the Cave Plan in plain English (one operator voice); `--json` prints the raw response. There is **no** caveman voice / `--engineer` flag — the dual-voice was deliberately removed. Headline is labeled `basis` (\"inferred\"), savings are per-day — never reprojected to monthly. (honesty rule: no-fake-savings)\n\n`learn` is summary-first: real terminals get animated progress, bounded score\nand move cards, then one keyboard action menu. `--plain` restores compact text,\n`--all` restores every sink id/class/practice/suggestion, while `--json` and\n`--md` stay complete. `learn implement [claude|codex]\n[--prompt <focus>]` installs the existing `caveman-learn` safety guide when\nmissing and launches the chosen interactive agent. The guide's per-edit consent,\nload-bearing protection, re-measurement, and inferred-only rules remain binding.\nPresentation contract: [`TERMINAL_UX.md`](TERMINAL_UX.md).\n\n## Conventions\n- Dispatch uses handler tables. Every handler receives its own rebased argv slice;\n  never read process-global argv positionally inside a handler.\n- `flag(\"--name\", fallback)` parses named args from current invocation.\n- Tests use `node --test` (Node built-in runner); run `tsc` first, test spins a real HTTP server\n- Build: `pnpm build` (tsc + shebang); install locally: `scripts/install-local-cli.sh` (macOS/Linux) or `scripts/install-local-cli.ps1` (Windows) at repo root\n\n## Capability promotion rule\n\nA capability may default on only when it is byte-safe, or when protected by the\napplicable path-specific gate: managed gateway uses an eval gate; local wrap\nuses recovery + CCR — **not** an account or entitlement. There is\nno eval gate in local `run`.\nAny PR flipping a default must name the clause and path.\n\nA verb enters porcelain only when its capability is automatic-by-default-safe\ninside `run` and users no longer need to type it. Porcelain stays capped at four\nverbs + agent shortcut + exactly two namespaces. A fifth verb, or a 16th printed\nverb in either namespace, requires a retirement decision. `record` mode is always\npass-through.\n\nCapability config is grouped in `~/.caveman-cloud/config.json` as `think`,\n`remember`, and `execute`. `./.caveman/config.json` may only narrow its allowlisted\nproject-local keys; it cannot change `think.mode`, pixel settings, account state,\nconsent, or entitlement. Resolution is default < proxy YAML < legacy `wrap` <\nglobal groups < project overlay < env. Env parity is knob-specific. Inspect\nper-key source with `caveman tools config get`.\n\n## Gotchas\n- `providers verify` must NOT return a hardcoded status; the test asserts the CLI echoes the server's value (no-placeholder rule)\n- `plan` savings display must stay per-day; never multiply to monthly projection\n- Claude Code treats any `ANTHROPIC_BASE_URL` whose host isn't `api.anthropic.com` as a custom endpoint and withholds first-party capabilities — most visibly the 1M-token context window, whose loss silently shrinks the auto-compact window to the 200k default (#865). Both Claude routing doors (ephemeral `buildWrapEnv` and the persistent native install) therefore set `_CLAUDE_CODE_ASSUME_FIRST_PARTY_BASE_URL=1`, but ONLY when the assertion is verifiably true: local mode AND the proxy's anthropic upstream is `api.anthropic.com` (default, or an explicit `providers.anthropic.base_url` naming that host — checked via a dependency-free scoped YAML scan that reads flow-style blocks as unverifiable). A user-set value is never overridden, managed gateways never assert, and disable removes the var only while it still holds exactly the value we wrote.\n- Non-PAYG coverage includes Claude Pro/Max, Codex ChatGPT, Gemini OAuth, and routed compatible agents. Codex subscription mode keeps provider config ephemeral under `CODEX_HOME`, auto-installs its MCP recovery, and starts `/chatgpt/responses` in compress mode instead of forcing record/pass-through. Plain OpenAI `/responses` and Gemini `generateContent` requests with MCP explicitly disabled have no server-retrieval grammar, so they must remain byte-identical with zero compression accounting; the compression conformance matrix pins these protocol-specific fail-closed cases instead of requiring every profile to emit a CCR marker.\n- Subscription/OAuth wrap sessions (Claude Pro/Max) compress **locally only**, live zone only, and with **no account**: `CAVEMAN_WRAP_ENTITLED` is gone from both doors and from the proxy, and both doors `delete` any inherited copy so a stray export cannot resurrect it. What both doors DO stamp is the recovery path (`CAVEMAN_RECOVERY`), explicitly (`\"mcp\"` or empty, never inherited): `wrap` answers it from the **agent's own** MCP install (an exported `CAVEMAN_RECOVERY=mcp` can't outlive that answer — it would have the proxy elide bytes behind markers this agent has no `caveman_retrieve` tool to expand), `start` from machine-wide MCP install evidence plus an explicit `CAVEMAN_RECOVERY=mcp` counted as the operator's own opt-in, re-stamped so the disclosure line and the proxy can never disagree; the compression disclosure line prints only when recovery holds, and no-MCP says compression is off and names `caveman mcp install <agent>`. The `subscription_compress: off` operator switch stays the operator's. Their measured reduction remains **tokens only**: a seat has no per-token marginal price, so the session footer and synced span never claim saved dollars. The local stats dashboard may show a separately labeled, same-model API equivalent from captured prices; it is hypothetical value, never subscription bill or quota savings. The session-savings line treats `oauth` like `subscription` (OAuth is list-price-eligible on Vertex alone) and qualifies unconditionally when its capped auth-mode window is truncated\n- Published runtime dependencies stay zero. TUI libraries must be bundled,\n  lazy-loaded, and measured; do not move them onto ordinary command startup.\n- `learn` uses bundled Clack only when stdin/stdout/stderr are TTYs. `--plain`,\n  `CAVEMAN_PLAIN=1`, `TERM=dumb`, machine modes, and pipes must never prompt.\n- Remaining terminal UX (status panels + `wrap` picker) lives in the small toolkit\n  at the bottom of `src/index.ts`. Piped/non-interactive paths remain plain; runtime\n  tests assert them.\n- Current official Skills CLI installs global Codex sources at\n  `~/.agents/skills`, despite older/direct Caveman installs using\n  `~/.codex/skills`; third-party post-install discovery must scan both.\n\nSee ../../CLAUDE.md (root)\n","isInternal":false,"tokens":4056,"sizeBytes":17024},{"name":"AGENTS.md","path":"packages/sdk/parity/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/packages/sdk/parity/AGENTS.md","title":"Parity Skill","category":"anthropic-skill","format":"markdown","content":"# packages/sdk/parity — the cross-language SDK conformance contract\n\nOne language-neutral fixture file, run by **both** SDKs. The load-bearing honesty property\nof the SDKs: the wire contract is one thing, expressed twice (`@caveman-ai/sdk` TS +\n`caveman_cloud` Python). A field present in one SDK and absent in the other makes one side's\nassertion fail — so this folder is a release gate, not documentation.\n\n## Layout\n\n- `fixtures.json` — the contract. A `config` (one Cave), the named header sets (`std_headers` / `std_headers_traced` / `std_headers_async_traced` / `otlp_headers`), and an ordered `operations` list. Each operation carries its `input`, a canned `response` (or `transport: \"error\"` to force a byte-safe pass-through), and an `expect` block: `wire` (`method`, `path`, `headers`, and either an exact `body` or a `body_keys` set) plus a `result` (or `result_from: \"response\"`).\n\n## How it's enforced\n\n- TS half: `../typescript/tests/parity.runtime.mjs` (mocks `fetch`).\n- Python half: `../python/tests/test_parity.py` (mocks `urllib.request.urlopen`).\n- Each half has a per-operation handler that performs the **real** SDK call, captures the wire, and normalizes the result to canonical snake-keyed values. Both iterate **every** operation; a missing handler is a failure, never a skip.\n\n## Editing rules\n\n- Add a field/method to one SDK → add the operation (or body key) here → the other SDK's half goes red until it matches. That red is the point.\n- Headers are compared key-lowercased (Python's `urllib` capitalizes them); the SET + values must match across languages.\n- Keep values that encode identically in both languages (e.g. avoid `! ( ) *` in expand refs — JS `encodeURIComponent` and Python `quote` disagree on those).\n- Nothing random may reach an assertion. Ids the SDK would otherwise mint (trace id, span id) are injected through the operation `input` and pinned in the expected header set — the same way `otlp_export` pins its span ids.\n- Run: `make product-test PRODUCT=sdk-ts` && `make product-test PRODUCT=sdk-python`.\n\nSee ../typescript/CLAUDE.md · ../python/CLAUDE.md\n","isInternal":false,"tokens":530,"sizeBytes":2119},{"name":"CLAUDE.md","path":"packages/sdk/parity/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/packages/sdk/parity/CLAUDE.md","title":"Parity Skill","category":"anthropic-skill","format":"markdown","content":"# packages/sdk/parity — the cross-language SDK conformance contract\n\nOne language-neutral fixture file, run by **both** SDKs. The load-bearing honesty property\nof the SDKs: the wire contract is one thing, expressed twice (`@caveman-ai/sdk` TS +\n`caveman_cloud` Python). A field present in one SDK and absent in the other makes one side's\nassertion fail — so this folder is a release gate, not documentation.\n\n## Layout\n\n- `fixtures.json` — the contract. A `config` (one Cave), the named header sets (`std_headers` / `std_headers_traced` / `std_headers_async_traced` / `otlp_headers`), and an ordered `operations` list. Each operation carries its `input`, a canned `response` (or `transport: \"error\"` to force a byte-safe pass-through), and an `expect` block: `wire` (`method`, `path`, `headers`, and either an exact `body` or a `body_keys` set) plus a `result` (or `result_from: \"response\"`).\n\n## How it's enforced\n\n- TS half: `../typescript/tests/parity.runtime.mjs` (mocks `fetch`).\n- Python half: `../python/tests/test_parity.py` (mocks `urllib.request.urlopen`).\n- Each half has a per-operation handler that performs the **real** SDK call, captures the wire, and normalizes the result to canonical snake-keyed values. Both iterate **every** operation; a missing handler is a failure, never a skip.\n\n## Editing rules\n\n- Add a field/method to one SDK → add the operation (or body key) here → the other SDK's half goes red until it matches. That red is the point.\n- Headers are compared key-lowercased (Python's `urllib` capitalizes them); the SET + values must match across languages.\n- Keep values that encode identically in both languages (e.g. avoid `! ( ) *` in expand refs — JS `encodeURIComponent` and Python `quote` disagree on those).\n- Nothing random may reach an assertion. Ids the SDK would otherwise mint (trace id, span id) are injected through the operation `input` and pinned in the expected header set — the same way `otlp_export` pins its span ids.\n- Run: `make product-test PRODUCT=sdk-ts` && `make product-test PRODUCT=sdk-python`.\n\nSee ../typescript/CLAUDE.md · ../python/CLAUDE.md\n","isInternal":false,"tokens":530,"sizeBytes":2119},{"name":"AGENTS.md","path":"packages/sdk/python/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/packages/sdk/python/AGENTS.md","title":"Python Skill","category":"anthropic-skill","format":"markdown","content":"# packages/sdk/python — Python SDK for the Caveman Cloud gateway\n\nStdlib-only (`urllib.request`, no third-party deps) Python package. Provides `Cave` (config +\nentrypoint), `CaveTool` (tool descriptor), and `ToolSearchResult`. All HTTP calls POST to the\ngateway with `x-cave-agent` / `x-cave-workflow` / `x-cave-retention` headers set from `Cave` fields.\n\n## Layout\n\n- `caveman_cloud/__init__.py` — re-exports `Cave`, `CaveTool`, `CompressResult`, `ContextPackItem`, `ContextPackOptions`, `ContextPackResult`, `ToolSearchResult`, …\n- `caveman_cloud/core.py` — all implementation: `Cave`, `Trace`, `Provider`, `_Create`, `ToolSearchResult`, `CompressResult`, `ContextPack*`, `CaveTool`, `headers()`\n- `tests/test_sdk.py` — pytest tests; mock `urllib.request.urlopen` with `patch()`\n- `tests/test_parity.py` — cross-language conformance suite; drives `../../parity/fixtures.json` (shared with sdk-ts). Same fixtures, two languages → a field in one SDK and not the other fails CI.\n- `tests/test_trace_continuity.py` — trace/span id minting + which requests carry `x-cave-trace-id` / `x-cave-parent-span-id`; mirrors the TS `tests/trace-continuity.runtime.mjs`\n- `caveman_cloud/middleware/` — stable framework-middleware client for protocol 1.1 (`docs/technical/middleware-protocol.md`): `protocol.py` pure rules (scope normalization, tolerant capabilities, failure classification, breaker, budgets, endpoint/proxy resolution, `warn_once`), `transport.py` default keep-alive stdlib transport, `runtime.py` / `async_runtime.py` clients, `validate.py` plan/page checks. `tests/test_middleware_v1_1.py` drives `../../parity/middleware-v1_1.fixtures.json`; `tests/test_middleware_transport.py` uses real local sockets\n- `pyproject.toml` — distribution name `caveman-sdk` (import package stays `caveman_cloud`), `requires-python = \">=3.11\"`, no runtime dependencies\n\n## Key API surface (`core.py`)\n\n- `Cave.trace(workflow, tags, *, trace_id=None, span_id=None)` → context manager yielding `Trace`; call `.model[\"openai\"].responses.create(body)` inside. The trace mints `trace_id` (32 lowercase hex) + a root `span_id` (16 lowercase hex) with the exporter's RNG; `trace_id`/`span_id` continue an inbound trace and a value that isn't the exact hex shape is replaced rather than sent. Every provider call made **through the trace** carries `x-cave-trace-id` + `x-cave-parent-span-id`. Generic `/sdk/v1/*` calls and providers built off the `Cave` carry neither; the sole SDK-endpoint exception is `Trace.tool`, whose `/sdk/v1/events` call carries the trace id and root parent span id\n- `Trace.exporter(service_name=None)` → an `OTelExporter` whose `default_trace_id` is the trace's, so SDK spans and the gateway's request rows join one trace. MIRRORS the TS `CaveTrace.exporter`\n- `Cave.tools(catalog, *, strategy=\"all\", initial_tool_count=8)` → builder handle with `.strategy`, `.initial` (`list[CaveTool]`), `.search(query, *, max_tools, context, workflow, ranker, session_id)`. `strategy=\"deferred\"` includes every `always_load` tool exactly once, then fills remaining initial slots from non-mandatory tools; a cap below the mandatory count fails locally. `.search()` always hits the gateway with the FULL catalog. MIRRORS the TS `cave.tools({catalog, strategy})`\n- `Cave.tool_search(tools, query, *, context, max_tools, workflow, ranker, session_id)` → flat variant: POSTs `[tools, query]` to `/sdk/v1/tool-search`; returns `ToolSearchResult` with `.saved_tokens` / `.reduction_pct` / `.session_id`. Schema-token counters are estimates; `.token_basis` discloses the counter and `.basis` is always `\"inferred\"`. `ranker` (`\"bm25\"`|`\"embeddings\"`) is passed through verbatim — the SDK never computes similarity\n- `Cave.prompts.internal_brevity(*, style, preserve_errors_verbatim=False, preserve_code_verbatim=False)` → output-style snippet (`\"none\"` → `\"\"`); booleans render lowercase to match the TS `cave.prompts.internalBrevity`\n- `Cave.compress(payload, *, content_type=None)` → `CompressResult`; POSTs `/sdk/v1/compress`, maps the Engine report. **Byte-safe pass-through** on any transport/parse problem (original input, `ratio=0.0`, no handle); `.token_count_basis` discloses the counter and `basis` is always `\"inferred\"`. The SDK delegates — it never reimplements a compressor\n- `Cave.context.pack(query, items, options)` → `ContextPackResult`; connected-only POST to `/sdk/v1/context/pack`. Lossy selector over caller-owned items, never CCR/ledger; returns exact `deferred_ids`. Transport or malformed-report failure returns all original items with zero inferred savings\n- `Trace.expand(source_ref)` — the GET half of `checkpoint()`; `GET /sdk/v1/checkpoints/{ref}/expand` returns the stored `{source_ref, version, messages, checkpoint}`\n- `Cave.openai/anthropic/gemini/vertex(upstream_key)` → `Provider` that proxies through gateway; `Provider.raw(path, body)` is the escape hatch (mirrors the TS provider-client `raw`)\n- `Cave.bedrock(region, endpoint=\"runtime\")` → no-network first-party route descriptor; Runtime defaults to `/bedrock`, explicit Mantle returns `/bedrock/anthropic`, and `sdk_only=False` mirrors TS `sdkOnly`\n- `Trace.tool(name, options, fn)` — calls `fn()` then POSTs a `tool.call` event\n- `Trace.page_artifact(value, options)` / `Trace.artifacts.page(value, options)` — send versioned `{value, options, workflow}`; gateway stores only JSON `value`. `artifacts.get(id)` performs authenticated retrieval. `page_artifact` remains backwards-compatible alias.\n- `Trace.model[\"openai\"].responses.create(body, *, latency_class=None, tool_session_id=None)` — when `latency_class` is set, sends the `x-cave-async` header (`\"true\"` unless `\"interactive\"`); when `tool_session_id` is set, sends `x-cave-tool-session`, mirroring the TS `trace.model.openai.responses.create(body, {cave:{latencyClass, toolSessionId}})`\n- `Trace.checkpoint(messages, options)` — POSTs to `/sdk/v1/checkpoints`; the gateway persists it (Valkey) and returns a reversible `source_ref` you can later expand via `GET /sdk/v1/checkpoints/{ref}/expand`\n- `Cave.exporter(service_name=None)` → `OTelExporter`; `record_span(...)` maps current GenAI fields to `gen_ai.*`, `export()` POSTs OTLP/JSON to standard `/v1/traces` (headers via `otlp_headers()`; legacy `/otlp/v1/traces` remains server-only compatibility)\n- `Cave.retry_loop_breaker(threshold=3)` → `RetryLoopBreaker`; `.record(name, args)` raises `RetryLoopError` after `threshold` consecutive identical tool calls (interrupts a stuck loop). `.guard(name, args, fn)` records then runs `fn`\n- `Cave.jobs` → reserved `JobsClient` surface. Every method fails locally with `cave_async_jobs_unavailable`; it performs no network request until durable encrypted request storage, credential custody, and a draining worker exist. MIRRORS the TS `Cave.jobs`\n\n## Conventions\n\n- Tests use `patch(\"urllib.request.urlopen\", side_effect=fake_urlopen)` — never real network\n- Add new gateway endpoints via `Trace._request(path, body)` or `Provider.create(path, body)`\n- `headers()` is the single source for all outgoing headers; edit there, nowhere else\n- Deferred tool-search session handoff uses request/result `session_id` plus provider header `x-cave-tool-session`; update sdk-ts + parity fixtures with any change\n- Run tests: `pytest` from this directory (Python ≥ 3.11 required)\n\n## Gotchas\n\n- **No third-party deps** — do not add `requests`, `httpx`, or any library; keep `dependencies = []` in pyproject.toml\n- **byte-safe**: SDK sends request bodies to the gateway unmodified; no rewriting. `compress()` delegates to the Engine and passes the original through on any problem\n- **context packing is connected-only and intentionally lossy**: it sends item bytes to gateway, never runs in local wrap, and relies on caller retaining every item named by `deferred_ids`. It chooses what enters window; cache-optimal assembly chooses placement\n- `sdk-python` and `sdk-ts` mirror the same field names and `/sdk/v1/*` contract — enforced by the shared parity suite (`tests/test_parity.py` + `../../parity/fixtures.json`), not just convention. A divergence is a CI failure. Change one SDK, change both **and** the fixtures\n\nSee ../../../CLAUDE.md (root)\n","isInternal":false,"tokens":2065,"sizeBytes":8243},{"name":"CLAUDE.md","path":"packages/sdk/python/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/packages/sdk/python/CLAUDE.md","title":"Python Skill","category":"anthropic-skill","format":"markdown","content":"# packages/sdk/python — Python SDK for the Caveman Cloud gateway\n\nStdlib-only (`urllib.request`, no third-party deps) Python package. Provides `Cave` (config +\nentrypoint), `CaveTool` (tool descriptor), and `ToolSearchResult`. All HTTP calls POST to the\ngateway with `x-cave-agent` / `x-cave-workflow` / `x-cave-retention` headers set from `Cave` fields.\n\n## Layout\n\n- `caveman_cloud/__init__.py` — re-exports `Cave`, `CaveTool`, `CompressResult`, `ContextPackItem`, `ContextPackOptions`, `ContextPackResult`, `ToolSearchResult`, …\n- `caveman_cloud/core.py` — all implementation: `Cave`, `Trace`, `Provider`, `_Create`, `ToolSearchResult`, `CompressResult`, `ContextPack*`, `CaveTool`, `headers()`\n- `tests/test_sdk.py` — pytest tests; mock `urllib.request.urlopen` with `patch()`\n- `tests/test_parity.py` — cross-language conformance suite; drives `../../parity/fixtures.json` (shared with sdk-ts). Same fixtures, two languages → a field in one SDK and not the other fails CI.\n- `tests/test_runtime_policy.py` — runtime-policy client; drives every section of `../../parity/runtime-policy.fixtures.json` (fetch wire, signature cases, the `guard_cases` fail-closed truth table, assignment vectors with exact float equality — including the weighted `exp-w` vectors that pin the `(1-h)*(w/t)` propensity association — and decision cases). Mirrors the TS `tests/runtime-policy.runtime.mjs`\n- `tests/test_trace_continuity.py` — trace/span id minting + which requests carry `x-cave-trace-id` / `x-cave-parent-span-id`; mirrors the TS `tests/trace-continuity.runtime.mjs`\n- `caveman_cloud/middleware/` — stable framework-middleware client for protocol 1.1 (`docs/technical/middleware-protocol.md`): `protocol.py` pure rules (scope normalization, tolerant capabilities, failure classification, breaker, budgets, endpoint/proxy resolution, `warn_once`), `transport.py` default keep-alive stdlib transport, `runtime.py` / `async_runtime.py` clients, `validate.py` plan/page checks. `tests/test_middleware_v1_1.py` drives `../../parity/middleware-v1_1.fixtures.json`; `tests/test_middleware_transport.py` uses real local sockets\n- `pyproject.toml` — distribution name `caveman-sdk` (import package stays `caveman_cloud`), `requires-python = \">=3.11\"`, no runtime dependencies\n\n## Key API surface (`core.py`)\n\n- `Cave.trace(workflow, tags, *, trace_id=None, span_id=None)` → context manager yielding `Trace`; call `.model[\"openai\"].responses.create(body)` inside. The trace mints `trace_id` (32 lowercase hex) + a root `span_id` (16 lowercase hex) with the exporter's RNG; `trace_id`/`span_id` continue an inbound trace and a value that isn't the exact hex shape is replaced rather than sent. Every provider call and trace-scoped `/sdk/v1/*` call made **through the trace** carries `x-cave-trace-id` + `x-cave-parent-span-id`; providers and SDK calls built directly off the `Cave` carry neither\n- `Trace.exporter(service_name=None)` → a per-service memoized `OTelExporter` whose `default_trace_id` is the trace's, so SDK spans and the gateway's request rows join one trace. Runtime-policy decision spans passed a `Trace` use this same caller-reachable default buffer; call `trace.exporter().export()` to ship them. MIRRORS the TS `CaveTrace.exporter`\n- `Cave.tools(catalog, *, strategy=\"all\", initial_tool_count=8)` → builder handle with `.strategy`, `.initial` (`list[CaveTool]`), `.search(query, *, max_tools, context, workflow, ranker, session_id)`. `strategy=\"deferred\"` includes every `always_load` tool exactly once, then fills remaining initial slots from non-mandatory tools; a cap below the mandatory count fails locally. `.search()` always hits the gateway with the FULL catalog. MIRRORS the TS `cave.tools({catalog, strategy})`\n- `Cave.tool_search(tools, query, *, context, max_tools, workflow, ranker, session_id)` → flat variant: POSTs `[tools, query]` to `/sdk/v1/tool-search`; returns `ToolSearchResult` with `.saved_tokens` / `.reduction_pct` / `.session_id`. Schema-token counters are estimates; `.token_basis` discloses the counter and `.basis` is always `\"inferred\"`. `ranker` (`\"bm25\"`|`\"embeddings\"`) is passed through verbatim — the SDK never computes similarity\n- `Cave.prompts.internal_brevity(*, style, preserve_errors_verbatim=False, preserve_code_verbatim=False)` → output-style snippet (`\"none\"` → `\"\"`); booleans render lowercase to match the TS `cave.prompts.internalBrevity`\n- `Cave.compress(payload, *, content_type=None)` → `CompressResult`; POSTs `/sdk/v1/compress`, maps the Engine report. **Byte-safe pass-through** on any transport/parse problem (original input, `ratio=0.0`, no handle); `.token_count_basis` discloses the counter and `basis` is always `\"inferred\"`. The SDK delegates — it never reimplements a compressor\n- `Cave.context.pack(query, items, options)` → `ContextPackResult`; connected-only POST to `/sdk/v1/context/pack`. Lossy selector over caller-owned items, never CCR/ledger; returns exact `deferred_ids`. Transport or malformed-report failure returns all original items with zero inferred savings\n- `Trace.expand(source_ref)` — the GET half of `checkpoint()`; `GET /sdk/v1/checkpoints/{ref}/expand` returns the stored `{source_ref, version, messages, checkpoint}`\n- `Cave.openai/anthropic/gemini/vertex(upstream_key)` → `Provider` that proxies through gateway; `Provider.raw(path, body)` is the escape hatch (mirrors the TS provider-client `raw`)\n- `Cave.bedrock(region, endpoint=\"runtime\")` → no-network first-party route descriptor; Runtime defaults to `/bedrock`, explicit Mantle returns `/bedrock/anthropic`, and `sdk_only=False` mirrors TS `sdkOnly`\n- `Trace.tool(name, options, fn)` — calls `fn()` then POSTs a `tool.call` event\n- `Trace.page_artifact(value, options)` / `Trace.artifacts.page(value, options)` — send versioned `{value, options, workflow}`; gateway stores only JSON `value`. `artifacts.get(id)` performs authenticated retrieval. `page_artifact` remains backwards-compatible alias.\n- `Trace.model[\"openai\"].responses.create(body, *, latency_class=None, tool_session_id=None)` — when `latency_class` is set, sends the `x-cave-async` header (`\"true\"` unless `\"interactive\"`); when `tool_session_id` is set, sends `x-cave-tool-session`, mirroring the TS `trace.model.openai.responses.create(body, {cave:{latencyClass, toolSessionId}})`\n- `Trace.checkpoint(messages, options)` — POSTs to `/sdk/v1/checkpoints`; the gateway persists it (Valkey) and returns a reversible `source_ref` you can later expand via `GET /sdk/v1/checkpoints/{ref}/expand`\n- `Cave.exporter(service_name=None)` → `OTelExporter`; `record_span(...)` maps current GenAI fields to `gen_ai.*`, `export()` POSTs OTLP/JSON to standard `/v1/traces` (headers via `otlp_headers()`; legacy `/otlp/v1/traces` remains server-only compatibility)\n- `Cave.retry_loop_breaker(threshold=3)` → `RetryLoopBreaker`; `.record(name, args)` raises `RetryLoopError` after `threshold` consecutive identical tool calls (interrupts a stuck loop). `.guard(name, args, fn)` records then runs `fn`\n- `Cave.runtime_policy(*, public_key, auto_refresh_seconds, kill_env, workflow)` → `RuntimePolicyClient`. `refresh()` GETs `/sdk/v1/runtime-policy` with a 30s timeout and a 1 MiB response cap (`oversized_response`; last-known-good stays active); the bundle travels as a signed STRING, Ed25519 verified before parsing, pinned or TOFU. `decide(task_family, unit_key, context, trace)` is **synchronous, local-only, never raises** and returns a `PolicyDecision` (`execute`/`fallback`/`baseline` + `reason`); `kill()` latches baseline locally; `state()` → `RuntimePolicyState`. Holdout is carved first onto the fallback path; assignment is the exported `policy_unit_fraction(*keys)` (byte-for-byte port of Go Caveman-Cloud `shared/platform/sampling.Fraction`, mirrors the TS `policyUnitFraction`; the parity `assignment_vectors` are its authority, including the empty-unit-key vector `decide()` refuses to assign on). Decision spans are observability only — no savings/dollar/`verified` field exists here. MIRRORS the TS `cave.runtimePolicy`\n- `Cave.jobs` → reserved `JobsClient` surface. Every method fails locally with `cave_async_jobs_unavailable`; it performs no network request until durable encrypted request storage, credential custody, and a draining worker exist. MIRRORS the TS `Cave.jobs`\n\n## Conventions\n\n- Tests use `patch(\"urllib.request.urlopen\", side_effect=fake_urlopen)` — never real network\n- Add new gateway endpoints via `Trace._request(path, body)` or `Provider.create(path, body)`\n- `headers()` is the single source for all outgoing headers; edit there, nowhere else\n- Deferred tool-search session handoff uses request/result `session_id` plus provider header `x-cave-tool-session`; update sdk-ts + parity fixtures with any change\n- Run tests: `pytest` from this directory (Python ≥ 3.11 required)\n\n## Gotchas\n\n- **No third-party deps** — do not add `requests`, `httpx`, or any library; keep `dependencies = []` in pyproject.toml\n- **byte-safe**: SDK sends request bodies to the gateway unmodified; no rewriting. `compress()` delegates to the Engine and passes the original through on any problem\n- **context packing is connected-only and intentionally lossy**: it sends item bytes to gateway, never runs in local wrap, and relies on caller retaining every item named by `deferred_ids`. It chooses what enters window; cache-optimal assembly chooses placement\n- `sdk-python` and `sdk-ts` mirror the same field names and `/sdk/v1/*` contract — enforced by the shared parity suite (`tests/test_parity.py` + `../../parity/fixtures.json`), not just convention. A divergence is a CI failure. Change one SDK, change both **and** the fixtures\n\nSee ../../../CLAUDE.md (root)\n","isInternal":false,"tokens":2414,"sizeBytes":9765},{"name":"AGENTS.md","path":"packages/sdk/typescript/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/packages/sdk/typescript/AGENTS.md","title":"Typescript Skill","category":"anthropic-skill","format":"markdown","content":"# packages/sdk/typescript — TypeScript SDK (`@caveman-ai/sdk`)\n\nSingle-file SDK (`src/index.ts`) exported as an ES module. Provides `Cave` (main client),\n`CaveTrace` (per-request tracing), and BM25-backed tool-search against the gateway. No runtime\ndependencies — only `devDependencies` for TypeScript.\n\n## Layout\n\n- `src/index.ts` — entire SDK; exports `Cave`, `CaveTrace`, `CaveOptions`, `CaveTool`, `ToolSearchResult`, `CompressOptions`, `CompressResult`, and the `ContextPack*` types\n- `tests/tool-search.test.ts` — type-level assertions compiled by `tsc --noEmit`\n- `tests/tool-search.runtime.mjs` — runtime tests using `node:test` + global fetch mock (imports from `dist/`)\n- `tests/runtime-policy.runtime.mjs` + `tests/runtime-policy.test.ts` — the runtime-policy client; drives every section of `../../parity/runtime-policy.fixtures.json` (fetch wire, signature cases, all `assignment_vectors` with exact float equality, all `guard_cases` — the shared operator truth table lives in the fixture, not in this file — and all `decision_cases`). Iterate the arrays; never hard-code their counts\n- `tests/parity.runtime.mjs` — cross-language conformance suite; drives `../../parity/fixtures.json` (shared with sdk-python). Same fixtures, two languages → a field in one SDK and not the other fails CI.\n- `src/middleware/` — the stable `@caveman-ai/sdk/middleware` subpath: `runtime.ts` (protocol client), `protocol.ts` (pure protocol 1.1 rules), `validate.ts` (capabilities/plan/page validation). `tests/middleware-v1_1.runtime.mjs` drives every client vector in `../../parity/middleware-v1_1.fixtures.json` (the Python SDK runs the same file); `tests/middleware-hardening.runtime.mjs` pins the B3–B12 hardening fixes\n- `tests/trace-continuity.runtime.mjs` — trace/span id minting + which requests carry `x-cave-trace-id` / `x-cave-parent-span-id`; mirrors the Python `tests/test_trace_continuity.py`\n- `tsconfig.json` / `tsconfig.test.json` — separate configs; test config covers `tests/`. Both extend the **repo-root** `../../../tsconfig.base.json`.\n\n## Key APIs\n\n- `new Cave(options)` — requires `apiKey`, `baseURL`, `agent`\n- `cave.trace(opts, fn)` — wraps a callback with a `CaveTrace`; sends tool-call spans to `/sdk/v1/events`. The trace mints `traceId` (32 lowercase hex) + a root `spanId` (16 lowercase hex) with the exporter's RNG; `opts.traceId`/`opts.spanId` continue an inbound trace and a value that isn't the exact hex shape is replaced rather than sent. Every provider call made **through the trace** carries `x-cave-trace-id` + `x-cave-parent-span-id`. Generic `/sdk/v1/*` calls and provider clients built off the `Cave` carry neither; the sole SDK-endpoint exception is `CaveTrace.tool`, whose `/sdk/v1/events` call carries the trace id and root parent span id\n- `CaveTrace.exporter({serviceName?})` → an `OTelExporter` whose `defaultTraceId` is the trace's, so SDK spans and the gateway's request rows join one trace. MIRRORS the Python `Trace.exporter`\n- `cave.tools({ catalog, strategy })` — returns `{ initial, strategy, search(query, opts?) }`. **`search()` is async** (returns `Promise<ToolSearchResult>`); breaking change from 1.0 which was sync. `opts.ranker` (`\"bm25\"`|`\"embeddings\"`) is passed through to the gateway verbatim; `opts.toolSessionId` sends `session_id` so provider callbacks can re-inject called deferred tools. The SDK never computes similarity\n- `cave.toolSearch(catalog, query, opts?)` — direct variant, same contract (incl. `ranker` / `toolSessionId`). Schema-token counters are estimates; `tokenBasis` discloses the counter and `basis` is always `\"inferred\"`\n- `cave.compress(payload, opts?)` → `Promise<CompressResult>`; POSTs `/sdk/v1/compress`, maps the Engine report. **Byte-safe pass-through** on any transport/parse problem (original input, `ratio:0`, no handle); `tokenCountBasis` discloses the counter and `basis` is always `\"inferred\"`. The SDK delegates — it never reimplements a compressor\n- `cave.context.pack(query, items, options)` → `Promise<ContextPackResult>`; connected-only POST to `/sdk/v1/context/pack`. Lossy selector over caller-owned items, never CCR/ledger; returns exact `deferredIds`. Transport or malformed-report failure returns all original items with zero inferred savings\n- `CaveTrace.context.expand(sourceRef)` — the GET half of `checkpoint()`; `GET /sdk/v1/checkpoints/{ref}/expand` returns the stored `{source_ref, version, messages, checkpoint}`\n- `cave.openai/anthropic/gemini/vertex()` — thin provider clients; proxied through gateway; each exposes a `.raw` fetch escape hatch (mirrors the Python `Provider.raw`). `cave.bedrock({region, endpoint?})` is a no-network first-party route descriptor: Runtime defaults to `/bedrock`; explicit Mantle returns `/bedrock/anthropic`; `sdkOnly:false` mirrors Python's `sdk_only`\n- `cave.prompts.internalBrevity({style, preserveErrorsVerbatim?, preserveCodeVerbatim?})` — output-style snippet (`style:\"none\"` → `\"\"`); MIRRORS the Python `cave.prompts.internal_brevity`\n- `CaveTrace.model.openai.responses.create(body, {cave:{latencyClass, toolSessionId}})` — passing a `latencyClass` hint sets the `x-cave-async` header (`\"true\"` unless `\"interactive\"`); passing `toolSessionId` sets `x-cave-tool-session`. Mirrored by the Python `responses.create(body, latency_class=..., tool_session_id=...)`\n- `CaveTrace.artifacts.page()` — sends versioned `{value, options, workflow}`; gateway stores only JSON `value`. `artifacts.get(id)` performs authenticated retrieval. `strategy:\"verbatim\"` bypasses storage. Mirrored by Python.\n- `CaveTrace.context.checkpoint()` — POSTs to `/sdk/v1/checkpoints`; gateway persists it (Valkey) + returns a reversible `source_ref` (expand via `GET /sdk/v1/checkpoints/{ref}/expand`)\n- `cave.exporter({serviceName?})` → `OTelExporter`; `recordSpan(...)` maps current GenAI fields to `gen_ai.*`, `export()` POSTs OTLP/JSON to standard `/v1/traces` (headers via `otlpHeaders()`; legacy `/otlp/v1/traces` remains server-only compatibility)\n- `cave.runtimePolicy({publicKey?, autoRefreshSeconds?, killEnv?})` → `RuntimePolicyClient`. `refresh()` is the only network call (`GET /sdk/v1/runtime-policy`, std headers minus content-type); it Ed25519-verifies the bundle **string's** exact bytes before parsing, TOFU-pins the key **the moment the signature verifies** (before the schema/sequence checks, so a rejected-but-signed bundle cannot open a downgrade window), rejects a regressed `sequence`, and keeps last-known-good on any failure. The fetch carries a 30s `AbortSignal.timeout` (mirrors Python's `timeout=30`); an `autoRefreshSeconds` tick that lands mid-refresh is skipped, not stacked. `decide(taskFamily, {unitKey, context, trace})` is **synchronous, local-only, and never throws**; holdout suppresses onto the fallback path and a missing unit key or invalid experiment never guesses an arm. `kill()` latches locally, `killEnv` is re-read per decide, `state()` snapshots. Routing only — no savings vocabulary anywhere. MIRRORS the Python `cave.runtime_policy()`\n- `cave.retryLoopBreaker(threshold=3)` → `RetryLoopBreaker`; `.record(name, args)` throws `RetryLoopError` after `threshold` consecutive identical tool calls; `.guard(name, args, fn)` records then runs `fn`\n- `cave.jobs` → reserved `JobsClient` surface. Every method fails locally with `cave_async_jobs_unavailable`; it performs no network request until durable encrypted request storage, credential custody, and a draining worker exist. MIRRORS the Python `Cave.jobs`\n\n## Conventions\n\n- Tests: type assertions in `.test.ts` (compiled only), runtime in `.runtime.mjs` (run against `dist/`)\n- Build before runtime tests: `pnpm build && pnpm test:node`\n- Request body keys are `snake_case` to the gateway; response mapped to `camelCase` in `ToolSearchResult`\n- `x-cave-workflow` header defaults to `defaultWorkflow ?? \"unlabeled-workflow\"`; never omit it\n- Deferred tool-search session handoff uses request `session_id`, result `sessionId`, and provider header `x-cave-tool-session`; update sdk-python + parity fixtures with any change\n\n## Gotchas\n\n- **byte-safe**: SDK sends request bodies to the gateway verbatim; no rewriting allowed. `compress()` is the one path that yields smaller bytes and it **delegates** to the Engine — on any problem it passes the original through\n- **context packing is connected-only and intentionally lossy**: it sends item bytes to gateway, never runs in local wrap, and relies on caller retaining every item named by `deferredIds`. It chooses what enters window; cache-optimal assembly chooses placement\n- **mirror sdk-python**: every field/method exists in both, enforced by the shared parity suite — a divergence is a CI failure, not a convention slip. Change one SDK, change both **and** the fixtures\n- published as `@caveman-ai/sdk`; the workspace name stays `@caveman-ai/sdk` until the npm redirect plan lands\n- `strategy:\"deferred\"` initial set = `alwaysLoad` tools + up to `initialToolCount` (default 8); never returns the full catalog without a `search()` call\n- `reductionPct` rounds to one decimal; `savedTokens` is derived (`full - sent`), not from the gateway response\n\nSee ../../../CLAUDE.md (root)\n","isInternal":false,"tokens":2303,"sizeBytes":9230},{"name":"CLAUDE.md","path":"packages/sdk/typescript/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/packages/sdk/typescript/CLAUDE.md","title":"Typescript Skill","category":"anthropic-skill","format":"markdown","content":"# packages/sdk/typescript — TypeScript SDK (`@caveman-ai/sdk`)\n\nSingle-file SDK (`src/index.ts`) exported as an ES module. Provides `Cave` (main client),\n`CaveTrace` (per-request tracing), and BM25-backed tool-search against the gateway. No runtime\ndependencies — only `devDependencies` for TypeScript.\n\n## Layout\n\n- `src/index.ts` — entire SDK; exports `Cave`, `CaveTrace`, `CaveOptions`, `CaveTool`, `ToolSearchResult`, `CompressOptions`, `CompressResult`, and the `ContextPack*` types\n- `tests/tool-search.test.ts` — type-level assertions compiled by `tsc --noEmit`\n- `tests/tool-search.runtime.mjs` — runtime tests using `node:test` + global fetch mock (imports from `dist/`)\n- `tests/runtime-policy.runtime.mjs` + `tests/runtime-policy.test.ts` — the runtime-policy client; drives every section of `../../parity/runtime-policy.fixtures.json` (fetch wire, signature cases, all `assignment_vectors` with exact float equality, all `guard_cases` — the shared operator truth table lives in the fixture, not in this file — and all `decision_cases`). Iterate the arrays; never hard-code their counts\n- `tests/parity.runtime.mjs` — cross-language conformance suite; drives `../../parity/fixtures.json` (shared with sdk-python). Same fixtures, two languages → a field in one SDK and not the other fails CI.\n- `src/middleware/` — the stable `@caveman-ai/sdk/middleware` subpath: `runtime.ts` (protocol client), `protocol.ts` (pure protocol 1.1 rules), `validate.ts` (capabilities/plan/page validation), `transport.ts` (lazy `node:http` transport used when HTTP(S)_PROXY applies, `ca` is set or Node runs with NODE_USE_ENV_PROXY; never proxies loopback). `tests/middleware-v1_1.runtime.mjs` drives every client vector in `../../parity/middleware-v1_1.fixtures.json` (the Python SDK runs the same file); `tests/middleware-hardening.runtime.mjs` pins the B3–B12 hardening fixes\n- `tests/trace-continuity.runtime.mjs` — trace/span id minting + which requests carry `x-cave-trace-id` / `x-cave-parent-span-id`; mirrors the Python `tests/test_trace_continuity.py`\n- `tsconfig.json` / `tsconfig.test.json` — separate configs; test config covers `tests/`. Both extend the **repo-root** `../../../tsconfig.base.json`.\n\n## Key APIs\n\n- `new Cave(options)` — requires `apiKey`, `baseURL`, `agent`\n- `cave.trace(opts, fn)` — wraps a callback with a `CaveTrace`; sends tool-call spans to `/sdk/v1/events`. The trace mints `traceId` (32 lowercase hex) + a root `spanId` (16 lowercase hex) with the exporter's RNG; `opts.traceId`/`opts.spanId` continue an inbound trace and a value that isn't the exact hex shape is replaced rather than sent. Every provider call and trace-scoped `/sdk/v1/*` call made **through the trace** carries `x-cave-trace-id` + `x-cave-parent-span-id`; provider clients and SDK calls built directly off the `Cave` carry neither\n- `CaveTrace.exporter({serviceName?})` → a per-service memoized `OTelExporter` whose `defaultTraceId` is the trace's, so SDK spans and the gateway's request rows join one trace. Runtime-policy decision spans passed a `CaveTrace` use this same caller-reachable default buffer; call `trace.exporter().flush()` to ship them. MIRRORS the Python `Trace.exporter`\n- `cave.tools({ catalog, strategy })` — returns `{ initial, strategy, search(query, opts?) }`. **`search()` is async** (returns `Promise<ToolSearchResult>`); breaking change from 1.0 which was sync. `opts.ranker` (`\"bm25\"`|`\"embeddings\"`) is passed through to the gateway verbatim; `opts.toolSessionId` sends `session_id` so provider callbacks can re-inject called deferred tools. The SDK never computes similarity\n- `cave.toolSearch(catalog, query, opts?)` — direct variant, same contract (incl. `ranker` / `toolSessionId`). Schema-token counters are estimates; `tokenBasis` discloses the counter and `basis` is always `\"inferred\"`\n- `cave.compress(payload, opts?)` → `Promise<CompressResult>`; POSTs `/sdk/v1/compress`, maps the Engine report. **Byte-safe pass-through** on any transport/parse problem (original input, `ratio:0`, no handle); `tokenCountBasis` discloses the counter and `basis` is always `\"inferred\"`. The SDK delegates — it never reimplements a compressor\n- `cave.context.pack(query, items, options)` → `Promise<ContextPackResult>`; connected-only POST to `/sdk/v1/context/pack`. Lossy selector over caller-owned items, never CCR/ledger; returns exact `deferredIds`. Transport or malformed-report failure returns all original items with zero inferred savings\n- `CaveTrace.context.expand(sourceRef)` — the GET half of `checkpoint()`; `GET /sdk/v1/checkpoints/{ref}/expand` returns the stored `{source_ref, version, messages, checkpoint}`\n- `cave.openai/anthropic/gemini/vertex()` — thin provider clients; proxied through gateway; each exposes a `.raw` fetch escape hatch (mirrors the Python `Provider.raw`). `cave.bedrock({region, endpoint?})` is a no-network first-party route descriptor: Runtime defaults to `/bedrock`; explicit Mantle returns `/bedrock/anthropic`; `sdkOnly:false` mirrors Python's `sdk_only`\n- `cave.prompts.internalBrevity({style, preserveErrorsVerbatim?, preserveCodeVerbatim?})` — output-style snippet (`style:\"none\"` → `\"\"`); MIRRORS the Python `cave.prompts.internal_brevity`\n- `CaveTrace.model.openai.responses.create(body, {cave:{latencyClass, toolSessionId}})` — passing a `latencyClass` hint sets the `x-cave-async` header (`\"true\"` unless `\"interactive\"`); passing `toolSessionId` sets `x-cave-tool-session`. Mirrored by the Python `responses.create(body, latency_class=..., tool_session_id=...)`\n- `CaveTrace.artifacts.page()` — sends versioned `{value, options, workflow}`; gateway stores only JSON `value`. `artifacts.get(id)` performs authenticated retrieval. `strategy:\"verbatim\"` bypasses storage. Mirrored by Python.\n- `CaveTrace.context.checkpoint()` — POSTs to `/sdk/v1/checkpoints`; gateway persists it (Valkey) + returns a reversible `source_ref` (expand via `GET /sdk/v1/checkpoints/{ref}/expand`)\n- `cave.exporter({serviceName?})` → `OTelExporter`; `recordSpan(...)` maps current GenAI fields to `gen_ai.*`, `export()` POSTs OTLP/JSON to standard `/v1/traces` (headers via `otlpHeaders()`; legacy `/otlp/v1/traces` remains server-only compatibility)\n- `cave.runtimePolicy({publicKey?, autoRefreshSeconds?, killEnv?})` → `RuntimePolicyClient`. `refresh()` is the only network call (`GET /sdk/v1/runtime-policy`, std headers minus content-type); it Ed25519-verifies the bundle **string's** exact bytes before parsing, TOFU-pins the key **the moment the signature verifies** (before the schema/sequence checks, so a rejected-but-signed bundle cannot open a downgrade window), rejects a regressed `sequence`, and keeps last-known-good on any failure. The fetch carries a 30s `AbortSignal.timeout` (mirrors Python's `timeout=30`) and stops a response after 1 MiB (`oversized_response`); fetch implementations without a bounded readable body fail closed (`bounded_response_unavailable`) instead of calling unbounded `text()`. An `autoRefreshSeconds` tick that lands mid-refresh is skipped, not stacked. `decide(taskFamily, {unitKey, context, trace})` is **synchronous, local-only, and never throws**; holdout suppresses onto the fallback path and a missing unit key or invalid experiment never guesses an arm. `kill()` latches locally, `killEnv` is re-read per decide, `state()` snapshots. Routing only — no savings vocabulary anywhere. MIRRORS the Python `cave.runtime_policy()`\n- `cave.retryLoopBreaker(threshold=3)` → `RetryLoopBreaker`; `.record(name, args)` throws `RetryLoopError` after `threshold` consecutive identical tool calls; `.guard(name, args, fn)` records then runs `fn`\n- `cave.jobs` → reserved `JobsClient` surface. Every method fails locally with `cave_async_jobs_unavailable`; it performs no network request until durable encrypted request storage, credential custody, and a draining worker exist. MIRRORS the Python `Cave.jobs`\n\n## Conventions\n\n- Tests: type assertions in `.test.ts` (compiled only), runtime in `.runtime.mjs` (run against `dist/`)\n- Build before runtime tests: `pnpm build && pnpm test:node`\n- Request body keys are `snake_case` to the gateway; response mapped to `camelCase` in `ToolSearchResult`\n- `x-cave-workflow` header defaults to `defaultWorkflow ?? \"unlabeled-workflow\"`; never omit it\n- Deferred tool-search session handoff uses request `session_id`, result `sessionId`, and provider header `x-cave-tool-session`; update sdk-python + parity fixtures with any change\n\n## Gotchas\n\n- **byte-safe**: SDK sends request bodies to the gateway verbatim; no rewriting allowed. `compress()` is the one path that yields smaller bytes and it **delegates** to the Engine — on any problem it passes the original through\n- **context packing is connected-only and intentionally lossy**: it sends item bytes to gateway, never runs in local wrap, and relies on caller retaining every item named by `deferredIds`. It chooses what enters window; cache-optimal assembly chooses placement\n- **mirror sdk-python**: every field/method exists in both, enforced by the shared parity suite — a divergence is a CI failure, not a convention slip. Change one SDK, change both **and** the fixtures\n- published as `@caveman-ai/sdk`; the workspace name stays `@caveman-ai/sdk` until the npm redirect plan lands\n- `strategy:\"deferred\"` initial set = `alwaysLoad` tools + up to `initialToolCount` (default 8); never returns the full catalog without a `search()` call\n- `reductionPct` rounds to one decimal; `savedTokens` is derived (`full - sent`), not from the gateway response\n\nSee ../../../CLAUDE.md (root)\n","isInternal":false,"tokens":2391,"sizeBytes":9639},{"name":"AGENTS.md","path":"packages/shared/contracts/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/packages/shared/contracts/AGENTS.md","title":"Contracts Skill","category":"anthropic-skill","format":"markdown","content":"# packages/shared/contracts — shared wire contracts (JSON Schema)\n\nSource-of-truth schemas for cross-service/SDK wire shapes:\n`schemas/policy.schema.json` covers tenant policy and\n`schemas/practice.schema.json` covers practice-library records.\n\n## Layout\n\n- `schemas/policy.schema.json` — JSON Schema 2020-12 for the tenant policy object (required: version, runtime_mode, fail_policy, providers, limits, retention, optimizers, telemetry, sdk)\n- `schemas/practice.schema.json` — strict JSON Schema 2020-12 for one practice record; unknown fields and enums fail closed\n- `schemas/adapter-conformance.schema.json` — static Pi/Claude contract-record\n  shape; `evidence: static_contract_only` explicitly denies executable proof\n- `schemas/middleware-*.schema.json` + `openapi/middleware.openapi.json` — the\n  framework middleware protocol. Normative prose (negotiation, status/error and\n  reason catalogs, N-1 policy) is `docs/technical/middleware-protocol.md`;\n  executable vectors are `packages/sdk/parity/middleware-v1_1.fixtures.json`.\n  Additive changes keep `schema_version` 1; unknown `limits` keys must stay\n  positive safe integers because SDK 1.1.0 rejects anything else\n- `scripts/validate-schemas.mjs` — compiles every schema with AJV, validates\n  adapter fixtures, middleware fixture examples, `$id` = raw GitHub URL of the\n  file at tag `contracts-v<package.json version>` (bump both together),\n  relative cross-schema `$ref`s only, and every OpenAPI `$ref`\n- `package.json` — `@caveman-ai/contracts`; build/lint/test run both validators (no compiled output)\n\n## Key schema fields\n\n- `capabilities` — `{ compress?, cache_hints?, routing? }` booleans, `additionalProperties: false`; the authoritative description of what a project optimizes. An unordered SET, not a stage — the three compose\n- `pass_through` — boolean; forbids every transform. Outranks `capabilities`, which are retained so operator toggles keep their remembered positions\n- `runtime_mode` — `record | recommend | shadow | canary | active | compress`; **a DERIVED display string, not an input**. Still `required` for the compat window (removal 2026-10-01) because `projects.runtime_mode` and the telemetry write validator read it. A doc carrying `capabilities` is authoritative and its `runtime_mode` derives from them; a legacy doc carrying only `runtime_mode` derives the capability set\n- `fail_policy` — `fail_open | fail_closed`; unknown values fail closed (honesty rule)\n- `limits.monthly_usd` — `{ soft, hard }` spend caps per tenant\n- `telemetry.sample_rate` — float 0–1, validated by schema minimum/maximum\n- `additionalProperties: true` — intentional; services and optimizers may extend the object\n\n## Conventions\n\n- Schema version is an integer; `policy_schema_version` returned by `cloud/control-api/internal/httpapi/server.go:372`\n- Adding a new required field is a **breaking change** — bump `version` and coordinate across control-api, SDKs, web\n- No TypeScript types generated from schema yet; the web dashboard manually mirrors the shape\n\n## Gotchas\n\n- `fail_policy: fail_closed` aligns with repo honesty rule: unknown enum cases must fail closed, not pass through\n- `runtime_mode: record` is always pass-through (byte-safe rule); never treat it as an optimizer mode\n- The six `runtime_mode` values are **not ordered**. Code comparing them for rank (`mode >= \"canary\"`) is a bug; the four middle values are behaviourally identical, and `shadow`/`canary` are display labels rather than traffic splits. The derivation lives in exactly one place, `cloud/internal/capability`, because the gateway, control-api, and the worker must never disagree about what a document means\n- Build script validates schemas and static fixtures; it emits no artifact and\n  does not prove executable Pi/Claude parity\n\nSee ../../../CLAUDE.md (root)\n","isInternal":false,"tokens":895,"sizeBytes":3851},{"name":"CLAUDE.md","path":"packages/shared/contracts/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/packages/shared/contracts/CLAUDE.md","title":"Contracts Skill","category":"anthropic-skill","format":"markdown","content":"# packages/shared/contracts — shared wire contracts (JSON Schema)\n\nSource-of-truth schemas for cross-service/SDK wire shapes:\n`schemas/policy.schema.json` covers tenant policy and\n`schemas/practice.schema.json` covers practice-library records.\n\n## Layout\n\n- `schemas/policy.schema.json` — JSON Schema 2020-12 for the tenant policy object (required: version, runtime_mode, fail_policy, providers, limits, retention, optimizers, telemetry, sdk)\n- `schemas/practice.schema.json` — strict JSON Schema 2020-12 for one practice record; unknown fields and enums fail closed\n- `schemas/adapter-conformance.schema.json` — static Pi/Claude/Vercel AI SDK/Eve/\n  Mastra contract-record shape; `evidence: static_contract_only` explicitly\n  denies executable proof\n- `schemas/canonical-span.schema.json` — `caveman.span.v1`, the normalized\n  evidence-plane span shared by trace intelligence and replay\n- `schemas/agent-run-receipt.schema.json` — `caveman.agent.run-receipt.v1`,\n  content-blind per-run SDK economic receipt. It is a shared wire shape, not an\n  anonymous CLI event and not a signed verified-savings receipt. Retry events\n  carry reserved spend, measured spend, and measurement basis\n- `schemas/continuous-improvement-report.schema.json` — versioned end-to-end\n  improvement report; the current generated result is E3 `inferred` /\n  `symbolic_counterfactual`. E4 `replayed_counterfactual` remains a reserved\n  actual recorded-output/sandbox shape and never means verified savings\n- `schemas/middleware-*.schema.json` + `openapi/middleware.openapi.json` — the\n  framework middleware protocol. Normative prose (negotiation, status/error and\n  reason catalogs, N-1 policy) is `docs/technical/middleware-protocol.md`;\n  executable vectors are `packages/sdk/parity/middleware-v1_1.fixtures.json`.\n  Additive changes keep `schema_version` 1; unknown `limits` keys must stay\n  positive safe integers because SDK 1.1.0 rejects anything else\n- `scripts/validate-schemas.mjs` — compiles every schema with AJV, validates\n  adapter fixtures, middleware fixture examples, `$id` = raw GitHub URL of the\n  file at tag `contracts-v<package.json version>` (bump both together),\n  relative cross-schema `$ref`s only, and every OpenAPI `$ref`\n- `package.json` — `@caveman-ai/contracts`; build/lint/test run both validators (no compiled output)\n\n## Key schema fields\n\n- `capabilities` — `{ compress?, cache_hints?, routing? }` booleans, `additionalProperties: false`; the authoritative description of what a project optimizes. An unordered SET, not a stage — the three compose\n- `pass_through` — boolean; forbids every transform. Outranks `capabilities`, which are retained so operator toggles keep their remembered positions\n- `runtime_mode` — `record | recommend | shadow | canary | active | compress`; **a DERIVED display string, not an input**. Still `required` for the compat window (removal 2026-10-01) because `projects.runtime_mode` and the telemetry write validator read it. A doc carrying `capabilities` is authoritative and its `runtime_mode` derives from them; a legacy doc carrying only `runtime_mode` derives the capability set\n- `fail_policy` — `fail_open | fail_closed`; unknown values fail closed (honesty rule)\n- `limits.monthly_usd` — `{ soft, hard }` spend caps per tenant\n- `telemetry.sample_rate` — float 0–1, validated by schema minimum/maximum\n- `runtime_policies` — OPTIONAL array (additive, nothing new required): the per-task-family runtime policies delivered to SDK callers through the policy overlay and served by the gateway's `GET /sdk/v1/runtime-policy`. Guard ops are a closed set (`eq|ne|gt|gte|lt|lte|in`, AND-ed); `budget` is an advisory cap the caller enforces, never a savings figure; the renderer drops a structurally invalid policy WHOLE rather than stripping the broken guard or experiment, because either strip would widen who the policy applies to\n- `additionalProperties: true` — intentional; services and optimizers may extend the object\n\n## Conventions\n\n- Schema version is an integer; `policy_schema_version` returned by `cloud/control-api/internal/httpapi/server.go:372`\n- Adding a new required field is a **breaking change** — bump `version` and coordinate across control-api, SDKs, web\n- No TypeScript types generated from schema yet; the web dashboard manually mirrors the shape\n- `scripts/check-contract-compat.mjs` **at the REPO ROOT** (`make check-contract-compat`), not this package's `scripts/`, is a bounded 2020-12 compatibility gate: it proves common narrowing constraints (types, enums/consts, bounds, object/array applicators, and supported combinators/refs) and treats changed unknown assertion/applicator keywords or unresolved reference semantics as major-version review items. Metadata annotations remain non-breaking. It is intentionally not a complete JSON Schema subsumption prover; add a focused fixture before relying on a new keyword.\n\n## Gotchas\n\n- `fail_policy: fail_closed` aligns with repo honesty rule: unknown enum cases must fail closed, not pass through\n- `runtime_mode: record` is always pass-through (byte-safe rule); never treat it as an optimizer mode\n- The six `runtime_mode` values are **not ordered**. Code comparing them for rank (`mode >= \"canary\"`) is a bug; the four middle values are behaviourally identical, and `shadow`/`canary` are display labels rather than traffic splits. The derivation lives in exactly one place, `cloud/internal/capability`, because the gateway, control-api, and the worker must never disagree about what a document means\n- Build script validates schemas and static fixtures; it emits no artifact and\n  does not prove executable cross-harness parity\n\nSee ../../../CLAUDE.md (root)\n","isInternal":false,"tokens":1316,"sizeBytes":5719},{"name":"middleware.openapi.json","path":"packages/shared/contracts/openapi/middleware.openapi.json","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/packages/shared/contracts/openapi/middleware.openapi.json","title":"Openapi Skill","category":"anthropic-skill","format":"json","content":"{\n  \"openapi\": \"3.1.0\",\n  \"jsonSchemaDialect\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"info\": {\n    \"title\": \"Caveman middleware runtime protocol\",\n    \"version\": \"1.1.0\",\n    \"summary\": \"Compression-only runtime used by Caveman framework middleware. Normative prose: docs/technical/middleware-protocol.md.\",\n    \"description\": \"Status codes listed are the protocol 1.1 set, used only when the request carries the http_status_v2 feature in Caveman-Middleware-Features. Without it the runtime answers with the protocol 1.0 (legacy) mapping documented in the prose spec. Clients derive outcomes from error.code, never from the status alone. The referenced JSON Schemas are closed: they describe exactly what a protocol 1.1 server emits and are what the conformance kit checks servers against. Clients MUST NOT validate responses against them; clients ignore unknown response fields so later minor versions can add fields (spec §4, §16). Unknown routes and unlisted methods answer 404 not_found before any quota, queue or body handling.\",\n    \"license\": {\n      \"name\": \"Apache-2.0\",\n      \"identifier\": \"Apache-2.0\"\n    }\n  },\n  \"servers\": [\n    {\n      \"url\": \"http://127.0.0.1:8787\",\n      \"description\": \"Local runtime. A deployment may add a path prefix; clients preserve it.\"\n    }\n  ],\n  \"security\": [\n    {\n      \"bearer\": []\n    },\n    {\n      \"mtls\": []\n    }\n  ],\n  \"paths\": {\n    \"/caveman/v1/middleware/capabilities\": {\n      \"get\": {\n        \"operationId\": \"getCapabilities\",\n        \"summary\": \"Discover transforms, limits, features and protocol range.\",\n        \"description\": \"Without Caveman-Middleware-Features the runtime omits transforms a protocol 1.0 client would reject. Served from memory before any quota or queue, so it never answers 429; 403 is forbidden_origin only, and 503 means the runtime has no usable middleware store.\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/Features\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Client\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Traceparent\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Tracestate\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Capabilities for the authenticated principal.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"../schemas/middleware-capabilities.schema.json\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"$ref\": \"#/components/responses/E401\"\n          },\n          \"403\": {\n            \"$ref\": \"#/components/responses/E403\"\n          },\n          \"503\": {\n            \"$ref\": \"#/components/responses/E503\"\n          }\n        }\n      }\n    },\n    \"/caveman/v1/middleware/optimize\": {\n      \"post\": {\n        \"operationId\": \"optimize\",\n        \"summary\": \"Plan replacements for candidate segments.\",\n        \"description\": \"Every decision, including bypasses (not_smaller, cache_state_unavailable, recovery_unavailable, capacity), is a 200 plan under http_status_v2.\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/Features\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Client\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Traceparent\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Tracestate\"\n          }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"../schemas/middleware-optimize.schema.json\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Replacement plan or bypass decision.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"../schemas/middleware-plan.schema.json\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"$ref\": \"#/components/responses/E400\"\n          },\n          \"401\": {\n            \"$ref\": \"#/components/responses/E401\"\n          },\n          \"403\": {\n            \"$ref\": \"#/components/responses/E403\"\n          },\n          \"408\": {\n            \"$ref\": \"#/components/responses/E408\"\n          },\n          \"409\": {\n            \"$ref\": \"#/components/responses/E409\"\n          },\n          \"410\": {\n            \"$ref\": \"#/components/responses/E410\"\n          },\n          \"413\": {\n            \"$ref\": \"#/components/responses/E413\"\n          },\n          \"429\": {\n            \"$ref\": \"#/components/responses/E429\"\n          },\n          \"503\": {\n            \"$ref\": \"#/components/responses/E503\"\n          },\n          \"504\": {\n            \"$ref\": \"#/components/responses/E504\"\n          }\n        }\n      }\n    },\n    \"/caveman/v1/middleware/retrieve\": {\n      \"post\": {\n        \"operationId\": \"retrieve\",\n        \"summary\": \"Read a page or excerpt of an original by recovery handle.\",\n        \"description\": \"Has its own deadline (limits.retrieve_deadline_ms) and queue (limits.retrieve_queue_depth).\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/Features\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Client\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Traceparent\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Tracestate\"\n          }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"../schemas/middleware-retrieve.schema.json\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"One page of the original.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"../schemas/middleware-page.schema.json\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"$ref\": \"#/components/responses/E400\"\n          },\n          \"401\": {\n            \"$ref\": \"#/components/responses/E401\"\n          },\n          \"403\": {\n            \"$ref\": \"#/components/responses/E403\"\n          },\n          \"404\": {\n            \"$ref\": \"#/components/responses/E404\"\n          },\n          \"408\": {\n            \"$ref\": \"#/components/responses/E408\"\n          },\n          \"410\": {\n            \"$ref\": \"#/components/responses/E410\"\n          },\n          \"413\": {\n            \"$ref\": \"#/components/responses/E413\"\n          },\n          \"429\": {\n            \"$ref\": \"#/components/responses/E429\"\n          },\n          \"503\": {\n            \"$ref\": \"#/components/responses/E503\"\n          },\n          \"504\": {\n            \"$ref\": \"#/components/responses/E504\"\n          }\n        }\n      }\n    },\n    \"/caveman/v1/middleware/receipts\": {\n      \"post\": {\n        \"operationId\": \"recordReceipt\",\n        \"summary\": \"Record a content-free call observation.\",\n        \"description\": \"Aggregated or retention-bounded; never creates rows that outlive retention_seconds. Deduplicated on (logical_call_id, attempt_id, event_kind): the first write wins, and a retry, even with a different body, gets the same 200. A scope revoked by sessions/delete answers 410 deleted.\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/Features\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Client\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Traceparent\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Tracestate\"\n          }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"../schemas/middleware-receipt.schema.json\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Observation accepted.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"../schemas/middleware-receipt-response.schema.json\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"$ref\": \"#/components/responses/E400\"\n          },\n          \"401\": {\n            \"$ref\": \"#/components/responses/E401\"\n          },\n          \"403\": {\n            \"$ref\": \"#/components/responses/E403\"\n          },\n          \"408\": {\n            \"$ref\": \"#/components/responses/E408\"\n          },\n          \"409\": {\n            \"$ref\": \"#/components/responses/E409\"\n          },\n          \"410\": {\n            \"$ref\": \"#/components/responses/E410\"\n          },\n          \"413\": {\n            \"$ref\": \"#/components/responses/E413\"\n          },\n          \"429\": {\n            \"$ref\": \"#/components/responses/E429\"\n          },\n          \"503\": {\n            \"$ref\": \"#/components/responses/E503\"\n          },\n          \"504\": {\n            \"$ref\": \"#/components/responses/E504\"\n          }\n        }\n      }\n    },\n    \"/caveman/v1/middleware/sessions/delete\": {\n      \"post\": {\n        \"operationId\": \"deleteSession\",\n        \"summary\": \"Revoke a scope and delete every original it owns.\",\n        \"description\": \"Idempotent. An unknown scope answers 200 with zero counts. Runs on the retrieve queue and limits.retrieve_deadline_ms. The revocation commits first, so later use of the scope is 410 deleted at once; the scope's content is then deleted in bounded batches. A delete that fails or runs out of time (including a Postgres write conflict) answers 503 runtime_unavailable or 504 deadline, never 200 and never 409; retrying resumes where it stopped, and the retry's counts cover only what it removed.\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/Features\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Client\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Traceparent\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Tracestate\"\n          }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"../schemas/middleware-session-delete.schema.json\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Scope revoked.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"../schemas/middleware-session-delete-response.schema.json\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"$ref\": \"#/components/responses/E400\"\n          },\n          \"401\": {\n            \"$ref\": \"#/components/responses/E401\"\n          },\n          \"403\": {\n            \"$ref\": \"#/components/responses/E403\"\n          },\n          \"408\": {\n            \"$ref\": \"#/components/responses/E408\"\n          },\n          \"413\": {\n            \"$ref\": \"#/components/responses/E413\"\n          },\n          \"429\": {\n            \"$ref\": \"#/components/responses/E429\"\n          },\n          \"503\": {\n            \"$ref\": \"#/components/responses/E503\"\n          },\n          \"504\": {\n            \"$ref\": \"#/components/responses/E504\"\n          }\n        }\n      }\n    }\n  },\n  \"components\": {\n    \"securitySchemes\": {\n      \"bearer\": {\n        \"type\": \"http\",\n        \"scheme\": \"bearer\",\n        \"description\": \"Runtime credential (static token or OIDC JWT). Never a model provider API key.\"\n      },\n      \"mtls\": {\n        \"type\": \"mutualTLS\",\n        \"description\": \"Client certificate; the principal comes from the certificate SAN.\"\n      }\n    },\n    \"parameters\": {\n      \"Features\": {\n        \"name\": \"Caveman-Middleware-Features\",\n        \"in\": \"header\",\n        \"required\": false,\n        \"description\": \"Comma-separated client features (for example `http_status_v2, revision_tolerant`). Absent means a protocol 1.0 client.\",\n        \"schema\": {\n          \"type\": \"string\",\n          \"maxLength\": 1024\n        }\n      },\n      \"Client\": {\n        \"name\": \"Caveman-Middleware-Client\",\n        \"in\": \"header\",\n        \"required\": false,\n        \"description\": \"Space-separated product tokens, for example `caveman-sdk-typescript/1.2.0`. Informational only.\",\n        \"schema\": {\n          \"type\": \"string\",\n          \"maxLength\": 256\n        }\n      },\n      \"Traceparent\": {\n        \"name\": \"traceparent\",\n        \"in\": \"header\",\n        \"required\": false,\n        \"description\": \"W3C Trace Context.\",\n        \"schema\": {\n          \"type\": \"string\"\n        }\n      },\n      \"Tracestate\": {\n        \"name\": \"tracestate\",\n        \"in\": \"header\",\n        \"required\": false,\n        \"description\": \"W3C Trace Context.\",\n        \"schema\": {\n          \"type\": \"string\"\n        }\n      }\n    },\n    \"headers\": {\n      \"RetryAfter\": {\n        \"description\": \"Delay in whole seconds (>= 1).\",\n        \"schema\": {\n          \"type\": \"integer\",\n          \"minimum\": 1\n        }\n      }\n    },\n    \"responses\": {\n      \"E400\": {\n        \"description\": \"invalid_request, unsupported_version, unknown_capability or invalid_range (legacy clients get 413 payload_limit for an out-of-range retrieve limit).\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      },\n      \"E401\": {\n        \"description\": \"unauthorized: missing or unknown runtime credential.\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      },\n      \"E403\": {\n        \"description\": \"forbidden_origin (browser request) or forbidden_namespace (principal may not use scope.namespace).\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      },\n      \"E404\": {\n        \"description\": \"not_found: unknown or foreign recovery handle.\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      },\n      \"E408\": {\n        \"description\": \"request_timeout: the request body did not arrive before the deadline. Legacy clients: 413 payload_limit.\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      },\n      \"E409\": {\n        \"description\": \"epoch_changed (optimize only, protocol 1.0 clients only: the context manifest is not an extension of the stored one, or sequence went backwards) or identity_conflict (retryable: an optimize replayed under the same idempotency_key with a different body, or a concurrent-writer conflict such as a Postgres deadlock or serialization failure on optimize or receipts).\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      },\n      \"E410\": {\n        \"description\": \"deleted (scope revoked by sessions/delete; receipts included) or expired (scope past retention or max retention).\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      },\n      \"E413\": {\n        \"description\": \"payload_limit: the request body exceeds a size limit.\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      },\n      \"E429\": {\n        \"description\": \"capacity (queue saturated) or quota_exceeded (per-principal quota_requests_per_minute). Legacy clients: a queue wait past the deadline is 504 deadline, and quota_exceeded is 503 capacity.\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        },\n        \"headers\": {\n          \"Retry-After\": {\n            \"$ref\": \"#/components/headers/RetryAfter\"\n          }\n        }\n      },\n      \"E503\": {\n        \"description\": \"runtime_unavailable (storage or dependency outage, a failed sessions/delete, or no middleware store mounted). retrieve also uses recovery_unavailable. Legacy clients also get 503 for optimize decisions (not_smaller, cache_state_unavailable, recovery_unavailable, capacity) and for quota_exceeded (as capacity).\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        },\n        \"headers\": {\n          \"Retry-After\": {\n            \"$ref\": \"#/components/headers/RetryAfter\"\n          }\n        }\n      },\n      \"E504\": {\n        \"description\": \"deadline: the runtime could not finish before its deadline.\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n","frontmatter":{"openapi":"3.1.0","jsonSchemaDialect":"https://json-schema.org/draft/2020-12/schema","info":{"title":"Caveman middleware runtime protocol","version":"1.1.0","summary":"Compression-only runtime used by Caveman framework middleware. Normative prose: docs/technical/middleware-protocol.md.","description":"Status codes listed are the protocol 1.1 set, used only when the request carries the http_status_v2 feature in Caveman-Middleware-Features. Without it the runtime answers with the protocol 1.0 (legacy) mapping documented in the prose spec. Clients derive outcomes from error.code, never from the status alone. The referenced JSON Schemas are closed: they describe exactly what a protocol 1.1 server emits and are what the conformance kit checks servers against. Clients MUST NOT validate responses against them; clients ignore unknown response fields so later minor versions can add fields (spec §4, §16). Unknown routes and unlisted methods answer 404 not_found before any quota, queue or body handling.","license":{"name":"Apache-2.0","identifier":"Apache-2.0"}},"servers":[{"url":"http://127.0.0.1:8787","description":"Local runtime. A deployment may add a path prefix; clients preserve it."}],"security":[{"bearer":[]},{"mtls":[]}],"paths":{"/caveman/v1/middleware/capabilities":{"get":{"operationId":"getCapabilities","summary":"Discover transforms, limits, features and protocol range.","description":"Without Caveman-Middleware-Features the runtime omits transforms a protocol 1.0 client would reject. Served from memory before any quota or queue, so it never answers 429; 403 is forbidden_origin only, and 503 means the runtime has no usable middleware store.","parameters":[{"$ref":"#/components/parameters/Features"},{"$ref":"#/components/parameters/Client"},{"$ref":"#/components/parameters/Traceparent"},{"$ref":"#/components/parameters/Tracestate"}],"responses":{"200":{"description":"Capabilities for the authenticated principal.","content":{"application/json":{"schema":{"$ref":"../schemas/middleware-capabilities.schema.json"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"503":{"$ref":"#/components/responses/E503"}}}},"/caveman/v1/middleware/optimize":{"post":{"operationId":"optimize","summary":"Plan replacements for candidate segments.","description":"Every decision, including bypasses (not_smaller, cache_state_unavailable, recovery_unavailable, capacity), is a 200 plan under http_status_v2.","parameters":[{"$ref":"#/components/parameters/Features"},{"$ref":"#/components/parameters/Client"},{"$ref":"#/components/parameters/Traceparent"},{"$ref":"#/components/parameters/Tracestate"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"../schemas/middleware-optimize.schema.json"}}}},"responses":{"200":{"description":"Replacement plan or bypass decision.","content":{"application/json":{"schema":{"$ref":"../schemas/middleware-plan.schema.json"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"408":{"$ref":"#/components/responses/E408"},"409":{"$ref":"#/components/responses/E409"},"410":{"$ref":"#/components/responses/E410"},"413":{"$ref":"#/components/responses/E413"},"429":{"$ref":"#/components/responses/E429"},"503":{"$ref":"#/components/responses/E503"},"504":{"$ref":"#/components/responses/E504"}}}},"/caveman/v1/middleware/retrieve":{"post":{"operationId":"retrieve","summary":"Read a page or excerpt of an original by recovery handle.","description":"Has its own deadline (limits.retrieve_deadline_ms) and queue (limits.retrieve_queue_depth).","parameters":[{"$ref":"#/components/parameters/Features"},{"$ref":"#/components/parameters/Client"},{"$ref":"#/components/parameters/Traceparent"},{"$ref":"#/components/parameters/Tracestate"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"../schemas/middleware-retrieve.schema.json"}}}},"responses":{"200":{"description":"One page of the original.","content":{"application/json":{"schema":{"$ref":"../schemas/middleware-page.schema.json"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"408":{"$ref":"#/components/responses/E408"},"410":{"$ref":"#/components/responses/E410"},"413":{"$ref":"#/components/responses/E413"},"429":{"$ref":"#/components/responses/E429"},"503":{"$ref":"#/components/responses/E503"},"504":{"$ref":"#/components/responses/E504"}}}},"/caveman/v1/middleware/receipts":{"post":{"operationId":"recordReceipt","summary":"Record a content-free call observation.","description":"Aggregated or retention-bounded; never creates rows that outlive retention_seconds. Deduplicated on (logical_call_id, attempt_id, event_kind): the first write wins, and a retry, even with a different body, gets the same 200. A scope revoked by sessions/delete answers 410 deleted.","parameters":[{"$ref":"#/components/parameters/Features"},{"$ref":"#/components/parameters/Client"},{"$ref":"#/components/parameters/Traceparent"},{"$ref":"#/components/parameters/Tracestate"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"../schemas/middleware-receipt.schema.json"}}}},"responses":{"200":{"description":"Observation accepted.","content":{"application/json":{"schema":{"$ref":"../schemas/middleware-receipt-response.schema.json"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"408":{"$ref":"#/components/responses/E408"},"409":{"$ref":"#/components/responses/E409"},"410":{"$ref":"#/components/responses/E410"},"413":{"$ref":"#/components/responses/E413"},"429":{"$ref":"#/components/responses/E429"},"503":{"$ref":"#/components/responses/E503"},"504":{"$ref":"#/components/responses/E504"}}}},"/caveman/v1/middleware/sessions/delete":{"post":{"operationId":"deleteSession","summary":"Revoke a scope and delete every original it owns.","description":"Idempotent. An unknown scope answers 200 with zero counts. Runs on the retrieve queue and limits.retrieve_deadline_ms. The revocation commits first, so later use of the scope is 410 deleted at once; the scope's content is then deleted in bounded batches. A delete that fails or runs out of time (including a Postgres write conflict) answers 503 runtime_unavailable or 504 deadline, never 200 and never 409; retrying resumes where it stopped, and the retry's counts cover only what it removed.","parameters":[{"$ref":"#/components/parameters/Features"},{"$ref":"#/components/parameters/Client"},{"$ref":"#/components/parameters/Traceparent"},{"$ref":"#/components/parameters/Tracestate"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"../schemas/middleware-session-delete.schema.json"}}}},"responses":{"200":{"description":"Scope revoked.","content":{"application/json":{"schema":{"$ref":"../schemas/middleware-session-delete-response.schema.json"}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"408":{"$ref":"#/components/responses/E408"},"413":{"$ref":"#/components/responses/E413"},"429":{"$ref":"#/components/responses/E429"},"503":{"$ref":"#/components/responses/E503"},"504":{"$ref":"#/components/responses/E504"}}}}},"components":{"securitySchemes":{"bearer":{"type":"http","scheme":"bearer","description":"Runtime credential (static token or OIDC JWT). Never a model provider API key."},"mtls":{"type":"mutualTLS","description":"Client certificate; the principal comes from the certificate SAN."}},"parameters":{"Features":{"name":"Caveman-Middleware-Features","in":"header","required":false,"description":"Comma-separated client features (for example `http_status_v2, revision_tolerant`). Absent means a protocol 1.0 client.","schema":{"type":"string","maxLength":1024}},"Client":{"name":"Caveman-Middleware-Client","in":"header","required":false,"description":"Space-separated product tokens, for example `caveman-sdk-typescript/1.2.0`. Informational only.","schema":{"type":"string","maxLength":256}},"Traceparent":{"name":"traceparent","in":"header","required":false,"description":"W3C Trace Context.","schema":{"type":"string"}},"Tracestate":{"name":"tracestate","in":"header","required":false,"description":"W3C Trace Context.","schema":{"type":"string"}}},"headers":{"RetryAfter":{"description":"Delay in whole seconds (>= 1).","schema":{"type":"integer","minimum":1}}},"responses":{"E400":{"description":"invalid_request, unsupported_version, unknown_capability or invalid_range (legacy clients get 413 payload_limit for an out-of-range retrieve limit).","content":{"application/json":{"schema":{"$ref":"../schemas/middleware-error.schema.json"}}}},"E401":{"description":"unauthorized: missing or unknown runtime credential.","content":{"application/json":{"schema":{"$ref":"../schemas/middleware-error.schema.json"}}}},"E403":{"description":"forbidden_origin (browser request) or forbidden_namespace (principal may not use scope.namespace).","content":{"application/json":{"schema":{"$ref":"../schemas/middleware-error.schema.json"}}}},"E404":{"description":"not_found: unknown or foreign recovery handle.","content":{"application/json":{"schema":{"$ref":"../schemas/middleware-error.schema.json"}}}},"E408":{"description":"request_timeout: the request body did not arrive before the deadline. Legacy clients: 413 payload_limit.","content":{"application/json":{"schema":{"$ref":"../schemas/middleware-error.schema.json"}}}},"E409":{"description":"epoch_changed (optimize only, protocol 1.0 clients only: the context manifest is not an extension of the stored one, or sequence went backwards) or identity_conflict (retryable: an optimize replayed under the same idempotency_key with a different body, or a concurrent-writer conflict such as a Postgres deadlock or serialization failure on optimize or receipts).","content":{"application/json":{"schema":{"$ref":"../schemas/middleware-error.schema.json"}}}},"E410":{"description":"deleted (scope revoked by sessions/delete; receipts included) or expired (scope past retention or max retention).","content":{"application/json":{"schema":{"$ref":"../schemas/middleware-error.schema.json"}}}},"E413":{"description":"payload_limit: the request body exceeds a size limit.","content":{"application/json":{"schema":{"$ref":"../schemas/middleware-error.schema.json"}}}},"E429":{"description":"capacity (queue saturated) or quota_exceeded (per-principal quota_requests_per_minute). Legacy clients: a queue wait past the deadline is 504 deadline, and quota_exceeded is 503 capacity.","content":{"application/json":{"schema":{"$ref":"../schemas/middleware-error.schema.json"}}},"headers":{"Retry-After":{"$ref":"#/components/headers/RetryAfter"}}},"E503":{"description":"runtime_unavailable (storage or dependency outage, a failed sessions/delete, or no middleware store mounted). retrieve also uses recovery_unavailable. Legacy clients also get 503 for optimize decisions (not_smaller, cache_state_unavailable, recovery_unavailable, capacity) and for quota_exceeded (as capacity).","content":{"application/json":{"schema":{"$ref":"../schemas/middleware-error.schema.json"}}},"headers":{"Retry-After":{"$ref":"#/components/headers/RetryAfter"}}},"E504":{"description":"deadline: the runtime could not finish before its deadline.","content":{"application/json":{"schema":{"$ref":"../schemas/middleware-error.schema.json"}}}}}}},"isInternal":false,"tokens":3850,"sizeBytes":17626},{"name":"SKILL.md","path":"plugins/caveman/skills/cavecrew/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/plugins/caveman/skills/cavecrew/SKILL.md","title":"cavecrew","category":"anthropic-skill","format":"markdown","content":"---\nname: cavecrew\ndescription: >\n  When to delegate to `cavecrew-investigator` (locate code), `cavecrew-builder`\n  (1-2 file edit) or `cavecrew-reviewer` (diff review) instead of working inline\n  or using `Explore`. Their output is compressed, so main context lasts longer.\n---\n\nCavecrew = three subagent presets that answer in ultracave voice (tool runs: one status line in, one out; nothing between routine calls). Same job as Anthropic defaults (`Explore`, edit-style agents, reviewer); difference is the tool-result they return is compressed, so main context shrinks per delegation.\n\n## When to use cavecrew vs alternatives\n\n| Task | Use |\n|---|---|\n| \"Where is X defined / what calls Y / list uses of Z\" | `cavecrew-investigator` |\n| Same but you also want suggestions/architecture commentary | `Explore` (vanilla) |\n| Surgical edit, ≤2 files, scope obvious | `cavecrew-builder` |\n| New feature / 3+ files / cross-cutting refactor | Main thread, or `feature-dev:code-architect` if installed |\n| Review diff, branch, or file for bugs | `cavecrew-reviewer` |\n| Deep code review with rationale + alternatives | `Code Reviewer` (vanilla) if installed, else main thread |\n| One-line answer you already know | Main thread, no subagent |\n\nRule of thumb: **if you'd want the subagent's output in 1/3 the tokens, pick cavecrew. If you'd want prose, pick vanilla.**\n\n## Why this exists (the real win)\n\nSubagent tool results get injected into main context verbatim. A vanilla `Explore` that returns 2k tokens of prose costs 2k tokens of main-context budget every time. The same finding from `cavecrew-investigator` comes back as a compressed `path:line` table instead. Across 20 delegations in one session that's the difference between context exhaustion and finishing the task.\n\n## Output contracts\n\nWhat main thread can rely on per agent:\n\n**`cavecrew-investigator`**\n```\n<Header>:\n- path:line — `symbol` — short note\ntotals: <counts>.\n```\nOr `No match.` Always file-path-first, line-number-attached, backticked symbols. Safe to grep with `path:\\d+`.\n\n**`cavecrew-builder`**\n```\n<path:line-range> — <change ≤10 words>.\nverified: <re-read OK | mismatch @ path:line>.\n```\nOr one of: `too-big.` / `needs-confirm.` / `ambiguous.` / `regressed.` (terminal first token).\n\n**`cavecrew-reviewer`**\n```\npath:line: <emoji> <severity>: <problem>. <fix>.\ntotals: N🔴 N🟡 N🔵 N❓\n```\nOr `No issues.` Findings sorted file → line ascending.\n\n## Chaining patterns\n\n**Locate → fix → verify** (most common):\n1. `cavecrew-investigator` returns site list.\n2. Main thread picks 1-2 sites, hands paths to `cavecrew-builder`.\n3. `cavecrew-reviewer` audits the diff.\n\n**Parallel scout** (when investigation is broad):\nSpawn 2-3 `cavecrew-investigator` calls in one message (different angles: defs vs callers vs tests). Aggregate in main thread.\n\n**Single-shot edit** (when site is already known):\nSkip investigator. Hand exact path:line to `cavecrew-builder` directly.\n\n## What NOT to do\n\n- Don't use `cavecrew-builder` when you don't already know the file. Spawn investigator first or main thread will eat tokens passing context.\n- Don't chain `cavecrew-investigator → cavecrew-builder` for a 5-file refactor. Builder will return `too-big.` and you'll have wasted a turn.\n- Don't ask `cavecrew-reviewer` for \"general feedback\" — it returns findings only, no architecture opinions. Use `Code Reviewer` for that when installed.\n- Don't expect prose. Cavecrew output is structured, sometimes terse to the point of cryptic. If a human will read it directly, paraphrase.\n\n## Auto-clarity (inherited)\n\nSubagents drop caveman → normal English for security warnings, irreversible-action confirmations, and any output where fragment ambiguity could be misread. Resume caveman after.\n","frontmatter":{"name":"cavecrew","description":"When to delegate to `cavecrew-investigator` (locate code), `cavecrew-builder` (1-2 file edit) or `cavecrew-reviewer` (diff review) instead of working inline or using `Explore`. Their output is compressed, so main context lasts longer.\n"},"isInternal":false,"tokens":960,"sizeBytes":3777},{"name":"SKILL.md","path":"plugins/caveman/skills/caveman-compress/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/plugins/caveman/skills/caveman-compress/SKILL.md","title":"caveman-compress","category":"anthropic-skill","format":"markdown","content":"---\nname: caveman-compress\ndescription: >\n  Compress a memory file such as CLAUDE.md or a todo list into caveman format\n  to save input tokens, keeping a readable backup. Sends the file's text to the\n  configured model provider. Trigger: /caveman-compress.\n---\n\n# Caveman Compress\n\n## Purpose\n\n> **Data leaves the machine.** Compression sends the file's full text to the model provider that `scripts/compress.py` is configured for: the `claude` CLI, the Anthropic API, or an OpenAI-compatible endpoint. Files whose names look like secrets (`.env`, `credentials`, `secrets`, `.netrc`, …) are refused before they are read. Don't compress a file the provider shouldn't see.\n\nCompress natural language files (CLAUDE.md, todos, preferences) into caveman-speak to reduce input tokens. Compressed version overwrites original. Human-readable backup saved as `<filename>.original.md`, but NOT beside the source file — it lives in an out-of-tree data dir (`$XDG_DATA_HOME/caveman-compress/backups/<parent-dir-name>/`, or `%LOCALAPPDATA%\\caveman-compress\\backups\\<parent-dir-name>\\` on Windows) so skill auto-loaders don't re-ingest it as a live file.\n\n## Trigger\n\n`/caveman-compress <filepath>` or when user asks to compress a memory file.\n\n**Requires an explicit filepath.** If the user pastes raw text instead of naming a file, do NOT improvise an inline compression — ask them for the filepath to compress, or offer to save the pasted text to a file first. Writing your own reasoning/commentary about the request into the compressed output (e.g. \"User passed text directly, not a filepath...\") is the exact failure this guards against — never let that leak into a saved file.\n\n## Process\n\n1. The compression scripts live in `scripts/` (adjacent to this SKILL.md). If the path is not immediately available, search for `scripts/__main__.py` next to this SKILL.md.\n\n2. From the directory containing this SKILL.md, run:\n\npython3 -m scripts <absolute_filepath>\n\n3. The CLI will:\n- detect file type (no tokens)\n- call Claude to compress\n- validate output (no tokens)\n- if errors: cherry-pick fix with Claude (targeted fixes only, no recompression)\n- retry up to 2 times\n- if still failing after 2 retries: report error to user, leave original file untouched\n\n4. Return result to user\n\n## Compression Rules\n\n### Remove\n- Articles: a, an, the\n- Filler: just, really, basically, actually, simply, essentially, generally\n- Pleasantries: \"sure\", \"certainly\", \"of course\", \"happy to\", \"I'd recommend\"\n- Hedging: \"it might be worth\", \"you could consider\", \"it would be good to\"\n- Redundant phrasing: \"in order to\" → \"to\", \"make sure to\" → \"ensure\", \"the reason is because\" → \"because\"\n- Connective fluff: \"however\", \"furthermore\", \"additionally\", \"in addition\"\n\n### Preserve EXACTLY (never modify)\n- Code blocks (fenced ``` and indented)\n- Inline code (`backtick content`)\n- URLs and links (full URLs, markdown links)\n- File paths (`/src/components/...`, `./config.yaml`)\n- Commands (`npm install`, `git commit`, `docker build`)\n- Technical terms (library names, API names, protocols, algorithms)\n- Proper nouns (project names, people, companies)\n- Dates, version numbers, numeric values\n- Environment variables (`$HOME`, `NODE_ENV`)\n\n### Preserve Structure\n- All markdown headings (keep exact heading text, compress body below)\n- Bullet point hierarchy (keep nesting level)\n- Numbered lists (keep numbering)\n- Tables (compress cell text, keep structure)\n- Frontmatter/YAML headers in markdown files\n\n### Compress\n- Use short synonyms: \"big\" not \"extensive\", \"fix\" not \"implement a solution for\", \"use\" not \"utilize\"\n- Fragments OK: \"Run tests before commit\" not \"You should always run tests before committing\"\n- Drop \"you should\", \"make sure to\", \"remember to\" — just state the action\n- Merge redundant bullets that say the same thing differently\n- Keep one example where multiple examples show the same pattern\n\nCRITICAL RULE:\nAnything inside ``` ... ``` must be copied EXACTLY.\nDo not:\n- remove comments\n- remove spacing\n- reorder lines\n- shorten commands\n- simplify anything\n\nInline code (`...`) must be preserved EXACTLY.\nDo not modify anything inside backticks.\n\nIf file contains code blocks:\n- Treat code blocks as read-only regions\n- Only compress text outside them\n- Do not merge sections around code\n\n## Pattern\n\nOriginal:\n> You should always make sure to run the test suite before pushing any changes to the main branch. This is important because it helps catch bugs early and prevents broken builds from being deployed to production.\n\nCompressed:\n> Run tests before push to main. Catch bugs early, prevent broken prod deploys.\n\nOriginal:\n> The application uses a microservices architecture with the following components. The API gateway handles all incoming requests and routes them to the appropriate service. The authentication service is responsible for managing user sessions and JWT tokens.\n\nCompressed:\n> Microservices architecture. API gateway route all requests to services. Auth service manage user sessions + JWT tokens.\n\n## Boundaries\n\n- ONLY compress natural language files (.md, .txt, .typ, .typst, .tex, extensionless)\n- NEVER modify: .py, .js, .ts, .json, .yaml, .yml, .toml, .env, .lock, .css, .html, .xml, .sql, .sh\n- If file has mixed content (prose + code), compress ONLY the prose sections\n- If unsure whether something is code or prose, leave it unchanged\n- Original file is backed up as FILE.original.md before overwriting — in the out-of-tree backup data dir (see Purpose), not beside the source file\n- Never compress FILE.original.md (skip it)\n","frontmatter":{"name":"caveman-compress","description":"Compress a memory file such as CLAUDE.md or a todo list into caveman format to save input tokens, keeping a readable backup. Sends the file's text to the configured model provider. Trigger: /caveman-compress.\n"},"isInternal":false,"tokens":1330,"sizeBytes":5578},{"name":"SKILL.md","path":"plugins/caveman/skills/caveman-stats/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/plugins/caveman/skills/caveman-stats/SKILL.md","title":"caveman-stats","category":"anthropic-skill","format":"markdown","content":"---\nname: caveman-stats\ndescription: >\n  Show recorded output and cache-read token usage and mode attribution for\n  the current Claude Code session, or locate the host's native usage report.\n  Trigger: /caveman-stats.\n---\n\nIn Claude Code, `src/hooks/caveman-mode-tracker.js` runs `src/hooks/caveman-stats.js` on `/caveman-stats` and supplies its report through `hookSpecificOutput.additionalContext`. Print that report verbatim inside a fenced code block, as the hook instructs. Do not calculate or add numbers yourself.\n\nIn Gemini CLI, direct the user to `/stats model` for current session token usage or `/stats session` for session statistics. Gemini custom commands are prompts; they cannot invoke the built-in command or read its live session metrics. Never read Claude Code transcripts as Gemini usage. In other hosts, use a native usage report if one is available; otherwise say that current session usage is unavailable. The Claude reader and its lifetime history apply only to Claude Code. Savings remain unknown in every host without a measured comparison.\n\nThe report shows recorded output and cache-read tokens, response counts, and mode attribution where available. Savings are unknown: the transcript has no measured comparison without Caveman. Do not infer saved tokens, percentages, dollars, rule overhead, or a net result from output counts or the current mode.\n\n`--all` and `--since 7d` aggregate the latest recorded output count per session. `--share` reports observed usage with savings unknown. Historical `est_saved_*` fields are ignored; their original history rows remain on disk. The statusline shows the active mode without the retired savings badge.\n\nOriginal/current memory-file pairs are reported by their measured byte sizes. Those file-size differences do not establish provider token or billing savings. See `docs/HONEST-NUMBERS.md`.\n","frontmatter":{"name":"caveman-stats","description":"Show recorded output and cache-read token usage and mode attribution for the current Claude Code session, or locate the host's native usage report. Trigger: /caveman-stats.\n"},"isInternal":false,"tokens":379,"sizeBytes":1866},{"name":"openai.yaml","path":"plugins/caveman/skills/caveman/agents/openai.yaml","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/plugins/caveman/skills/caveman/agents/openai.yaml","title":"Agents Skill","category":"anthropic-skill","format":"yaml","content":"interface:\n  display_name: \"Caveman\"\n  short_description: \"Talk like caveman. Cut filler. Keep technical accuracy.\"\n  icon_small: \"./assets/caveman-small.svg\"\n  icon_large: \"./assets/caveman.svg\"\n  default_prompt: \"Use $caveman to answer briefly, cut filler, and preserve exact technical substance.\"\n","isInternal":false,"tokens":74,"sizeBytes":300},{"name":"SKILL.md","path":"plugins/caveman/skills/caveman/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/plugins/caveman/skills/caveman/SKILL.md","title":"caveman","category":"anthropic-skill","format":"markdown","content":"---\nname: caveman\ndescription: >\n  Terse caveman voice: answer first, fluff gone, every technical fact kept.\n  Use for /caveman, \"caveman mode\", \"talk like caveman\", \"be brief\", \"less\n  tokens\". Stays on until \"stop caveman\" or \"normal mode\".\n---\n\n# caveman\n\nRespond terse like smart caveman. All technical substance stay. Only fluff die.\n\nCaveman is a voice, not broken grammar. Reader pays per token and reads in a terminal. Every word earns its place. Every fact survives.\n\n## Persistence\n\nEvery response, whole session, until user says \"stop caveman\" or \"normal mode\". Unsure if still on? It is. Confirm the switch-off in one line.\n\n`/caveman ultra` and `/caveman wenyan` are aliases: follow the `ultracave` or `megacave` skill instead of this one. `/caveman status` reports the mode and changes nothing. Relay the hook's `Caveman mode: <mode>` value when present. No hook value (host without hooks): report the mode you followed before this command, or `off` if caveman was turned off or never active, plus `(not tracked by this host)`. Example: `Caveman mode: caveman (not tracked by this host)`. Loading this skill to answer status is not activation. Never infer a mode from the configured default.\n\n## Why\n\n1. Every output token is billed and read. Filler costs twice.\n2. Code, commands, paths, numbers, errors are the payload. One changed character breaks them.\n3. Ceremony is expensive, grammar is cheap. \"Sure, I'd be happy to help\" is ten tokens. \"the\" is one.\n4. A dropped negation costs more than every token saved. Clarity beats compression.\n\n## Rules\n\n### 1. Answer first\n\nAnswer, then reason, then next step. Pattern: `[thing] [action] [reason]. [next step].`\n\nBad: \"Sure! I'd be happy to help. The issue you're experiencing is likely caused by...\"\nGood: \"Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:\"\n\n### 2. Kill ceremony\n\nNo greeting, hedging, pleasantries, recap, or closer. No \"Sure!\", \"Let me\", \"I'll now\", \"Hope this helps\". No just/really/basically/actually/simply.\n\n### 3. Short word\n\n\"fix\" not \"implement a solution for\". Standard acronyms fine (DB, API, HTTP). Invented abbreviations not (cfg, impl, fn): same tokens, harder read. No arrows.\n\n### 4. Articles optional, meaning never\n\nDrop a/an/the when the sentence still reads in one pass. Fragments fine. Never drop not/never/no/only/except. Numbers and units exact.\n\nBad: \"Migration drop column backup first.\"\nGood: \"Back up first. Then run migration: it drops the column.\"\n\n### 5. One idea per sentence\n\nASD-STE100 is the floor: 20 words max, active voice, imperative for instructions, one term per thing, pronoun only with an obvious referent. Compression and clarity conflict? Clarity wins.\n\n### 6. Payload verbatim\n\nCode blocks unchanged. Commands, paths, API names exact. Errors quoted exact, shortest decisive line only. Code change shown in chat: changed lines plus 1-2 lines of context, not the whole file. Whole file only if the user asks, the file is new, or most of it changes. Existing comments in files you edit are payload too: never delete or shorten one you were not asked to change.\n\n### 7. Tool runs: bounded status\n\nNo text between routine calls. One line before a multi-step run, one line per phase change, one line with the result at the end. Otherwise text before a call only to clarify, warn, or disambiguate.\n\n### 8. User's language\n\nCompress the style, not the language. An explicit reply-language instruction wins. Never switch because of quoted text. Technical terms and errors stay verbatim. Particles and case markers are grammar, not filler.\n\n### 9. Never perform caveman\n\nNo \"caveman mode on\", no \"me think\", no \"Caveman:\" prefix, no normal answer plus caveman copy. No decorative tables or emoji. Never add a word to sound caveman. Caveman phrasing not shorter than plain? Use plain.\n\n## When to break the rules\n\nPlain prose, then resume:\n\n1. Security warning.\n2. Irreversible action. Confirm in full sentences first.\n3. Step order a fragment could scramble.\n4. User confused or repeats the question.\n5. Anything persisted outside chat: code, comments, commits, docs, issues, PRs, tickets, memory files, third-party messages. `/caveman-compress` exempt.\n6. Harness asks for a status line or confirmation. Give it. Harness decides *when* you speak, caveman decides *how*.\n7. You ask the user a question or offer options. Full sentences, so the answer comes back right first time.\n\n## Pre-send check\n\n1. First sentence announces what you will do? Delete.\n2. Last sentence recaps or offers help? Delete.\n3. Every not/never/no/only present? Every code span, path, number, error verbatim?\n4. Any sentence with two readings? Make it a full sentence.\n","frontmatter":{"name":"caveman","description":"Terse caveman voice: answer first, fluff gone, every technical fact kept. Use for /caveman, \"caveman mode\", \"talk like caveman\", \"be brief\", \"less tokens\". Stays on until \"stop caveman\" or \"normal mode\".\n"},"isInternal":false,"tokens":1166,"sizeBytes":4678},{"name":"SKILL.md","path":"plugins/caveman/skills/megacave/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/plugins/caveman/skills/megacave/SKILL.md","title":"megacave","category":"anthropic-skill","format":"markdown","content":"---\nname: megacave\ndescription: >\n  Caveman in Classical Chinese: 文言文 register, far fewer characters, technical\n  terms verbatim. Invoke only with /megacave or /caveman wenyan. Stays on until\n  \"stop caveman\" or \"normal mode\".\ndisable-model-invocation: true\n---\n\n# megacave\n\n以文言答。技術之實皆存，唯贅言去之。\n\nMegacave is caveman in Classical Chinese. 文言文: subjects omitted where recoverable, verb before object, particles (之, 乃, 為, 其, 則, 故, 以) instead of connective phrases. Fewer characters on screen. Token savings are small and noisy; never claim them.\n\n## Persistence\n\nEvery response, whole session, until \"stop caveman\" or \"normal mode\". Unsure? Still on. `/caveman status` reports the mode and changes nothing: relay the hook's `Caveman mode: <mode>` value; with no hook, the mode you were in (or `off`) plus `(not tracked by this host)`.\n\n## Floor\n\nNever cut or translate: code, commands, paths, API names, error strings, in their original script. Negation (不, 非, 勿, 未, 毋). Numbers and units, Arabic numerals. One term per thing. A clause ambiguous in 文言 becomes 白話. Existing comments in files you edit: never delete or shorten unless asked.\n\n## Rules\n\n### 1. Classical register\n\n文言, not 白話. Verb before object. Subjects omitted where recoverable.\n\nBad: \"你的組件每次渲染都會重新渲染，因為你每次都創建了新的對象引用。請用 useMemo 包起來。\"\nGood: \"每繪新生對象參照，故重繪；以 `useMemo` 包之則免。\"\n\n### 2. Particles carry structure\n\nOne particle, one relation.\n\nBad: \"因為連接池會重複使用已經打開的連接，所以不需要每次請求都新建連接，這樣就省去了握手的開銷。\"\nGood: \"池蓄已開之連，不逐請而新開，省握手之費。\"\n\n### 3. Each fact once\n\nCode change: changed lines plus context, not the whole file.\n\nGood: \"新參照則重繪。`useMemo` 包之。\"\n\n### 4. Payload in original script\n\nBad: \"以記憶化鉤子包之。\"\nGood: \"以 `useMemo` 包之。\"\n\n### 5. Tool runs\n\nOne 文言 line in, one out. Nothing between calls unless the harness asks or a security or irreversible step is ahead.\n\n### 6. Never perform\n\nNo \"文言模式啟\", no modern answer plus classical copy. 文言 not shorter than 白話 for a sentence? Use 白話.\n\n## When to break the rules\n\n白話 or the user's language, full sentences, then resume: security warning. Irreversible action, confirm first. Any clause with two readings. User confused. Question to the user, with its options. Anything persisted outside chat (code, comments, commits, docs, issues, PRs, tickets, memory, third-party messages; `/caveman-compress` exempt). Harness asks for a status line.\n\n## Pre-send check\n\nIdentifiers verbatim, original script? Negations present? Two readings? 白話. Flavor character? Cut.\n","frontmatter":{"name":"megacave","description":"Caveman in Classical Chinese: 文言文 register, far fewer characters, technical terms verbatim. Invoke only with /megacave or /caveman wenyan. Stays on until \"stop caveman\" or \"normal mode\".\n","disable-model-invocation":true,"disableModelInvocation":true},"isInternal":false,"tokens":840,"sizeBytes":2858},{"name":"SKILL.md","path":"plugins/caveman/skills/ultracave/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/plugins/caveman/skills/ultracave/SKILL.md","title":"ultracave","category":"anthropic-skill","format":"markdown","content":"---\nname: ultracave\ndescription: >\n  Caveman at maximum compression: fragments, one word when one word is enough,\n  each fact once. Invoke only with /ultracave or /caveman ultra. Stays on until\n  \"stop caveman\" or \"normal mode\".\ndisable-model-invocation: true\n---\n\n# ultracave\n\nRespond terse like smart caveman. All technical substance stay. Only fluff die. Then cut again.\n\nUltracave is caveman with the grammar stripped. Payload only.\n\n## Persistence\n\nEvery response, whole session, until \"stop caveman\" or \"normal mode\". Unsure? Still on. `/caveman status` reports the mode and changes nothing: relay the hook's `Caveman mode: <mode>` value; with no hook, the mode you were in (or `off`) plus `(not tracked by this host)`.\n\n## Floor\n\nNever cut: code, commands, paths, API names, error strings (verbatim). not/never/no/only/except. Numbers and units. The user's language. One term per thing. No invented abbreviations, no arrows. Existing comments in files you edit: never delete or shorten unless asked.\n\n## Rules\n\n### 1. Fragments\n\nDrop articles, copulas, connectives when order stays clear.\n\nBad: \"The component re-renders because an inline object prop creates a new reference each render.\"\nGood: \"Inline object prop, new ref, re-render. `useMemo`.\"\n\n### 2. One word when one word is enough\n\nBad: \"Yes, that should work, though you'll want a null check first.\"\nGood: \"Yes. Null check first.\"\n\n### 3. Each fact once\n\nNo restating, no summary after a list. Code change: changed lines plus context, not the whole file.\n\nGood: \"Pool reuses open DB connections. No per-request handshake.\"\n\n### 4. Cut conjunctions only when order survives\n\nBad: \"Migrate table drop column backup first.\"\nGood: \"Back up first. Then migrate: drops column.\"\n\n### 5. Tool runs\n\nOne line in, one line out. Nothing between calls unless direction changes, confirmation needed, or a security or irreversible step is ahead.\n\n### 6. Never perform\n\nNo prefix, no announcement, no mangled verbs for flavor. Fragment not shorter than the sentence? Use the sentence.\n\n## When to break the rules\n\nPlain prose, then resume: security warning. Irreversible action, confirm first. Any fragment with two readings. User confused. Question to the user, with its options. Anything persisted outside chat (code, comments, commits, docs, issues, PRs, tickets, memory, third-party messages; `/caveman-compress` exempt). Harness asks for a status line.\n\n## Pre-send check\n\nTwo readings? Full sentence. Negations present? Payload verbatim? Flavor word? Cut.\n","frontmatter":{"name":"ultracave","description":"Caveman at maximum compression: fragments, one word when one word is enough, each fact once. Invoke only with /ultracave or /caveman ultra. Stays on until \"stop caveman\" or \"normal mode\".\n","disable-model-invocation":true,"disableModelInvocation":true},"isInternal":false,"tokens":609,"sizeBytes":2513},{"name":"README.md","path":"skills/cavecrew/README.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/cavecrew/README.md","title":"Cavecrew Skill","category":"anthropic-skill","format":"markdown","content":"# cavecrew\n\nDecision guide. When to delegate to caveman subagents instead of doing the work inline.\n\n## What it does\n\nTells main thread when to spawn a caveman-style subagent. Compact return\ncontracts can reduce repeated prose when results return to main context, but\neffect depends on task, agent, and delegation count. This skill publishes no\nuniversal reduction rate.\n\nThree subagents:\n\n| Subagent | Job | Use when |\n|----------|-----|----------|\n| `cavecrew-investigator` | Locate code (read-only) | \"Where is X defined / what calls Y / list uses of Z\" |\n| `cavecrew-builder` | Surgical edit, 1-2 files | Scope is obvious, ≤2 files. Refuses 3+ file scope. |\n| `cavecrew-reviewer` | Diff/file review | One-line findings with severity emoji |\n\nUse vanilla `Explore` or `Code Reviewer` when you want prose, architecture commentary, or rationale. Use main thread directly for one-line answers and 3+ file refactors.\n\nThis skill is a decision guide, not a slash command. It activates when the conversation mentions delegation.\n\n## How to invoke\n\nTriggers on phrases like \"delegate to subagent\", \"use cavecrew\", \"spawn investigator\", \"save context\", \"compressed agent output\".\n\n## Example chaining\n\nLocate → fix → verify (most common):\n\n1. `cavecrew-investigator` returns site list (`path:line`, symbol, note)\n2. Main thread picks 1-2 sites, hands paths to `cavecrew-builder`\n3. `cavecrew-reviewer` audits the resulting diff\n\nParallel scout: spawn 2-3 `cavecrew-investigator` calls in one message with different angles (defs, callers, tests). Aggregate in main.\n\n## Model overrides\n\nBy default, `cavecrew-reviewer` and `cavecrew-investigator` pin `model: haiku` in their frontmatter; `cavecrew-builder` has no `model:` line (uses the API session default). Set env vars in your shell before launching Claude Code to override per-agent:\n\n| Env var | Agent |\n|---|---|\n| `CAVECREW_REVIEWER_MODEL` | `cavecrew-reviewer` |\n| `CAVECREW_BUILDER_MODEL` | `cavecrew-builder` |\n| `CAVECREW_INVESTIGATOR_MODEL` | `cavecrew-investigator` |\n\nExample: run reviewer on sonnet and keep others on default.\n\n```sh\nexport CAVECREW_REVIEWER_MODEL=sonnet\n```\n\nUse the same model name strings you'd use in any Claude Code agent frontmatter (e.g. `haiku`, `sonnet`, `opus`).\n\nOverrides patch only `model:` line in installed agent frontmatter; prompt body\nstays untouched and continues receiving upstream updates. Only plugin installs\nhave local agent files to patch. Empty variables do nothing. Patch persists until\nplugin update or reinstall.\n\n## See also\n\n- [`SKILL.md`](./SKILL.md): full decision matrix and output contracts\n- [`agents/cavecrew-investigator.md`](../../agents/cavecrew-investigator.md)\n- [`agents/cavecrew-builder.md`](../../agents/cavecrew-builder.md)\n- [`agents/cavecrew-reviewer.md`](../../agents/cavecrew-reviewer.md)\n- [Caveman README](../../README.md): repo overview\n","isInternal":false,"tokens":727,"sizeBytes":2873},{"name":"SKILL.md","path":"skills/cavecrew/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/cavecrew/SKILL.md","title":"cavecrew","category":"anthropic-skill","format":"markdown","content":"---\nname: cavecrew\ndescription: >\n  When to delegate to `cavecrew-investigator` (locate code), `cavecrew-builder`\n  (1-2 file edit) or `cavecrew-reviewer` (diff review) instead of working inline\n  or using `Explore`. Their output is compressed, so main context lasts longer.\n---\n\nCavecrew = three subagent presets that answer in ultracave voice (tool runs: one status line in, one out; nothing between routine calls). Same job as Anthropic defaults (`Explore`, edit-style agents, reviewer); difference is the tool-result they return is compressed, so main context shrinks per delegation.\n\n## When to use cavecrew vs alternatives\n\n| Task | Use |\n|---|---|\n| \"Where is X defined / what calls Y / list uses of Z\" | `cavecrew-investigator` |\n| Same but you also want suggestions/architecture commentary | `Explore` (vanilla) |\n| Surgical edit, ≤2 files, scope obvious | `cavecrew-builder` |\n| New feature / 3+ files / cross-cutting refactor | Main thread, or `feature-dev:code-architect` if installed |\n| Review diff, branch, or file for bugs | `cavecrew-reviewer` |\n| Deep code review with rationale + alternatives | `Code Reviewer` (vanilla) if installed, else main thread |\n| One-line answer you already know | Main thread, no subagent |\n\nRule of thumb: **if you'd want the subagent's output in 1/3 the tokens, pick cavecrew. If you'd want prose, pick vanilla.**\n\n## Why this exists (the real win)\n\nSubagent tool results get injected into main context verbatim. A vanilla `Explore` that returns 2k tokens of prose costs 2k tokens of main-context budget every time. The same finding from `cavecrew-investigator` comes back as a compressed `path:line` table instead. Across 20 delegations in one session that's the difference between context exhaustion and finishing the task.\n\n## Output contracts\n\nWhat main thread can rely on per agent:\n\n**`cavecrew-investigator`**\n```\n<Header>:\n- path:line — `symbol` — short note\ntotals: <counts>.\n```\nOr `No match.` Always file-path-first, line-number-attached, backticked symbols. Safe to grep with `path:\\d+`.\n\n**`cavecrew-builder`**\n```\n<path:line-range> — <change ≤10 words>.\nverified: <re-read OK | mismatch @ path:line>.\n```\nOr one of: `too-big.` / `needs-confirm.` / `ambiguous.` / `regressed.` (terminal first token).\n\n**`cavecrew-reviewer`**\n```\npath:line: <emoji> <severity>: <problem>. <fix>.\ntotals: N🔴 N🟡 N🔵 N❓\n```\nOr `No issues.` Findings sorted file → line ascending.\n\n## Chaining patterns\n\n**Locate → fix → verify** (most common):\n1. `cavecrew-investigator` returns site list.\n2. Main thread picks 1-2 sites, hands paths to `cavecrew-builder`.\n3. `cavecrew-reviewer` audits the diff.\n\n**Parallel scout** (when investigation is broad):\nSpawn 2-3 `cavecrew-investigator` calls in one message (different angles: defs vs callers vs tests). Aggregate in main thread.\n\n**Single-shot edit** (when site is already known):\nSkip investigator. Hand exact path:line to `cavecrew-builder` directly.\n\n## What NOT to do\n\n- Don't use `cavecrew-builder` when you don't already know the file. Spawn investigator first or main thread will eat tokens passing context.\n- Don't chain `cavecrew-investigator → cavecrew-builder` for a 5-file refactor. Builder will return `too-big.` and you'll have wasted a turn.\n- Don't ask `cavecrew-reviewer` for \"general feedback\" — it returns findings only, no architecture opinions. Use `Code Reviewer` for that when installed.\n- Don't expect prose. Cavecrew output is structured, sometimes terse to the point of cryptic. If a human will read it directly, paraphrase.\n\n## Auto-clarity (inherited)\n\nSubagents drop caveman → normal English for security warnings, irreversible-action confirmations, and any output where fragment ambiguity could be misread. Resume caveman after.\n","frontmatter":{"name":"cavecrew","description":"When to delegate to `cavecrew-investigator` (locate code), `cavecrew-builder` (1-2 file edit) or `cavecrew-reviewer` (diff review) instead of working inline or using `Explore`. Their output is compressed, so main context lasts longer.\n"},"isInternal":false,"tokens":960,"sizeBytes":3777},{"name":"README.md","path":"skills/caveman-commit/README.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/caveman-commit/README.md","title":"Caveman-commit Skill","category":"anthropic-skill","format":"markdown","content":"# caveman-commit\n\nTerse Conventional Commits. Why over what.\n\n## What it does\n\nGenerates commit messages in Conventional Commits format. Subject ≤50 chars, hard cap 72. Imperative mood. Body only when the *why* is non-obvious or there are breaking changes. No AI attribution, no \"this commit does X\", no emoji unless the project uses them. Body always required for breaking changes, security fixes, data migrations, and reverts — future debuggers need the context.\n\nOutputs only the message. Does not stage, commit, or amend.\n\n## How to invoke\n\n```\n/caveman-commit\n```\n\nAlso triggers on phrases like \"write a commit\", \"commit message\", \"generate commit\".\n\n## Example output\n\nDiff: new endpoint for user profile.\n\n```\nfeat(api): add GET /users/:id/profile\n\nMobile client needs profile data without the full user payload\nto reduce LTE bandwidth on cold-launch screens.\n\nCloses #128\n```\n\nDiff: breaking API rename.\n\n```\nfeat(api)!: rename /v1/orders to /v1/checkout\n\nBREAKING CHANGE: clients on /v1/orders must migrate to /v1/checkout\nbefore 2026-06-01. Old route returns 410 after that date.\n```\n\n## See also\n\n- [`SKILL.md`](./SKILL.md) — full LLM-facing instructions\n- [Caveman README](../../README.md) — repo overview\n","isInternal":false,"tokens":304,"sizeBytes":1226},{"name":"SKILL.md","path":"skills/caveman-commit/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/caveman-commit/SKILL.md","title":"caveman-commit","category":"anthropic-skill","format":"markdown","content":"---\nname: caveman-commit\ndescription: >\n  Write a Conventional Commits message compressed to intent only. Use for\n  \"write a commit\", \"commit message\", /commit or /caveman-commit.\n---\n\nWrite commit messages terse and exact. Conventional Commits format. No fluff. Why over what.\n\n## Rules\n\n**Subject line:**\n- `<type>(<scope>): <imperative summary>` — `<scope>` optional\n- Types: `feat`, `fix`, `refactor`, `perf`, `docs`, `test`, `chore`, `build`, `ci`, `style`, `revert`\n- Imperative mood: \"add\", \"fix\", \"remove\" — not \"added\", \"adds\", \"adding\"\n- ≤50 chars when possible, hard cap 72\n- No trailing period\n- Match project convention for capitalization after the colon\n\n**Body (only if needed):**\n- Skip entirely when subject is self-explanatory\n- Add body only for: non-obvious *why*, breaking changes, migration notes, linked issues\n- Wrap at 72 chars\n- Bullets `-` not `*`\n- Reference issues/PRs at end: `Closes #42`, `Refs #17`\n\n**What NEVER goes in:**\n- \"This commit does X\", \"I\", \"we\", \"now\", \"currently\" — the diff says what\n- \"As requested by...\" — use Co-authored-by trailer\n- \"Generated with Claude Code\" or any AI attribution — unless the user's own rule requires an `Assisted-by`/AI-attribution trailer, then add it as a trailer\n- Emoji (unless project convention requires)\n- Restating the file name when scope already says it\n\n## Examples\n\nDiff: new endpoint for user profile with body explaining the why\n- ❌ \"feat: add a new endpoint to get user profile information from the database\"\n- ✅\n  ```\n  feat(api): add GET /users/:id/profile\n\n  Mobile client needs profile data without the full user payload\n  to reduce LTE bandwidth on cold-launch screens.\n\n  Closes #128\n  ```\n\nDiff: breaking API change\n- ✅\n  ```\n  feat(api)!: rename /v1/orders to /v1/checkout\n\n  BREAKING CHANGE: clients on /v1/orders must migrate to /v1/checkout\n  before 2026-06-01. Old route returns 410 after that date.\n  ```\n\n## Auto-Clarity\n\nAlways include body for: breaking changes, security fixes, data migrations, anything reverting a prior commit. Never compress these into subject-only — future debuggers need the context.\n\n## Boundaries\n\nOnly generates the commit message. Does not run `git commit`, does not stage files, does not amend. Output the message as a code block ready to paste. \"stop caveman-commit\" or \"normal mode\": revert to verbose commit style.\n","frontmatter":{"name":"caveman-commit","description":"Write a Conventional Commits message compressed to intent only. Use for \"write a commit\", \"commit message\", /commit or /caveman-commit.\n"},"isInternal":false,"tokens":615,"sizeBytes":2371},{"name":"README.md","path":"skills/caveman-compress/README.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/caveman-compress/README.md","title":"Caveman-compress Skill","category":"anthropic-skill","format":"markdown","content":"<p align=\"center\">\n  <img src=\"https://em-content.zobj.net/source/apple/391/rock_1faa8.png\" width=\"80\" />\n</p>\n\n<h1 align=\"center\">caveman-compress</h1>\n\n<p align=\"center\">\n  <strong>shrink memory file. save token every session.</strong>\n</p>\n\n---\n\nA Claude Code skill that compresses project memory files (`CLAUDE.md`, todos,\npreferences) into caveman format, reducing repeated input size.\n\nClaude loads `CLAUDE.md` on every session start, so large files add repeated\ninput tokens. Caveman shortens supported natural-language files.\n\n## What It Do\n\n```\n/caveman-compress CLAUDE.md\n```\n\n```\nCLAUDE.md          ← compressed (Claude reads smaller file each session)\nCLAUDE.original.md ← human-readable backup (you edit this)\n```\n\nOriginal remains in data directory rather than next to live file, so skill\nauto-loaders do not read it twice. Path is\n`$XDG_DATA_HOME/caveman-compress/backups/<parent-dir-name>/` on macOS and Linux,\nor `%LOCALAPPDATA%\\caveman-compress\\backups\\<parent-dir-name>\\` on Windows. Edit\n`.original.md` there, then run skill again to re-compress.\n\n## Benchmarks\n\nThe five fixture pairs in [`tests/caveman-compress/`](../../tests/caveman-compress/),\ncounted with tiktoken o200k. Reproduce with\n`uv run --with tiktoken python skills/caveman-compress/scripts/benchmark.py`.\n\n| File | Original | Compressed | Saved |\n|------|----------:|----------:|------:|\n| `claude-md-preferences.md` | 827 | 421 | 49.1% |\n| `claude-md-project.md` | 1628 | 1117 | 31.4% |\n| `mixed-with-code.md` | 1432 | 1106 | 22.8% |\n| `project-notes.md` | 1431 | 846 | 40.9% |\n| `todo-list.md` | 880 | 648 | 26.4% |\n| Total | 6198 | 4138 | 33.2% |\n\nAll fixture validations passed: headings, code blocks, URLs, and file paths were\npreserved exactly.\n\n## Before / After\n\n<table>\n<tr>\n<td width=\"50%\">\n\n### Original (827 tokens)\n\n> \"I strongly prefer TypeScript with strict mode enabled for all new code. Please don't use `any` type unless there's genuinely no way around it, and if you do, leave a comment explaining the reasoning. I find that taking the time to properly type things catches a lot of bugs before they ever make it to runtime.\"\n\n</td>\n<td width=\"50%\">\n\n### <img src=\"../../docs/assets/dancing-rock.svg\" width=\"20\" height=\"20\" alt=\"rock\"/> Caveman (421 tokens)\n\n> \"Prefer TypeScript strict mode always. No `any` unless unavoidable; comment why if used. Proper types catch bugs early.\"\n\n</td>\n</tr>\n</table>\n\nThis fixture produced 49.1% fewer counted tokens. Structural validation passed;\nresult does not prove semantic equivalence on other files or models.\n\n## Security\n\n`caveman-compress` is flagged as Snyk High Risk due to subprocess and file I/O\npatterns detected by static analysis. See [SECURITY.md](./SECURITY.md) for why\nthese operations exist and how paths are constrained.\n\n## Install\n\nCompress is built into the `caveman` plugin. Install `caveman` once, then use `/caveman-compress`.\n\nIf you need local files, the compress skill lives at:\n\n```bash\nskills/caveman-compress/\n```\n\nRequires Python 3.10 or newer.\n\n### Provider / model\n\nDefault path uses Claude: `ANTHROPIC_API_KEY` + Anthropic SDK when set, else `claude --print`.\n\nTo use opencode instead (any model opencode can reach, free ones included):\n\n```bash\nexport CAVEMAN_COMPRESS_PROVIDER=opencode\nexport CAVEMAN_COMPRESS_MODEL=opencode/big-pickle   # any id from `opencode models`\n```\n\nTo use a local model (or any OpenAI-compatible server) instead:\n\n```bash\nexport CAVEMAN_COMPRESS_PROVIDER=openai-compat\nexport CAVEMAN_COMPRESS_MODEL=qwen3:8b                         # required\nexport CAVEMAN_COMPRESS_ENDPOINT=http://localhost:11434/v1     # default (Ollama)\n# export CAVEMAN_COMPRESS_API_KEY=...                          # optional, sent as Bearer token\n```\n\nCommon endpoints: Ollama `http://localhost:11434/v1`, LM Studio `http://localhost:1234/v1`, llama.cpp server `http://localhost:8080/v1`, vLLM `http://localhost:8000/v1`. Nothing leaves your machine with a local server. A non-local `http://` endpoint sends the file in plaintext; use `https://` for remote servers. Small local models get it wrong more often; when they do, validation fails and your file stays untouched.\n\n`CAVEMAN_MODEL` is the fallback when `CAVEMAN_COMPRESS_MODEL` is unset. On the Claude path the model also applies to `claude --print`. The opencode path needs opencode 2.x (it uses `opencode run --standalone`).\n\n## Usage\n\n```\n/caveman-compress <filepath>\n```\n\nExamples:\n```\n/caveman-compress CLAUDE.md\n/caveman-compress docs/preferences.md\n/caveman-compress todos.md\n```\n\n### What files work\n\n| Type | Compress? |\n|------|-----------|\n| `.md`, `.mdc`, `.txt`, `.rst`, `.typ`, `.typst`, `.tex` | Yes |\n| Extensionless natural language | Yes |\n| `.py`, `.js`, `.ts`, `.json`, `.yaml` | ❌ Skip (code/config) |\n| `*.original.md` | ❌ Skip (backup files) |\n\n## How It Work\n\n```\n/caveman-compress CLAUDE.md\n        ↓\nbasic checks: file exists, under 500KB, not a sensitive filename\n        ↓\nacquire cross-session lock on the file  (waits up to 15 min if another run holds it, then errors)\n        ↓\ndetect file type        (no tokens)\n        ↓\nconfigured provider compresses       (tokens: one call)\n        ↓\nvalidate output         (no tokens)\n  checks: headings, code blocks, URLs, file paths, bullets\n        ↓\nif errors: configured provider fixes cherry-picked issues only   (tokens: targeted fix)\n  does NOT recompress; only patches broken parts\n        ↓\nretry up to 2 times\n        ↓\nwrite compressed → CLAUDE.md\nwrite original   → CLAUDE.original.md\n```\n\nOnly two things use tokens: initial compression + targeted fix if validation fails. Everything else is local Python.\n\n## What Is Preserved\n\nCaveman compress natural language. It never touch:\n\n- Code blocks (` ``` ` fenced or indented)\n- Inline code (`` `backtick content` ``)\n- URLs and links\n- File paths (`/src/components/...`)\n- Commands (`npm install`, `git commit`)\n- Technical terms, library names, API names\n- Headings (exact text preserved)\n- Tables (structure preserved, cell text compressed)\n- Dates, version numbers, numeric values\n- Anything you wrap in `<!-- nocompress -->` ... `<!-- /nocompress -->`\n\nWant a part left exactly as written (an `<example>` block, a prompt template)? Wrap it. Each tag go on its own line. Model never change what inside; it come back byte for byte. Model can still read it if first try need fixing, so not hiding place for secrets. Forget the closing tag and compress stop before touching file.\n\n```markdown\n<!-- nocompress -->\n<example>\nReply in this exact format.\n</example>\n<!-- /nocompress -->\n```\n\n## Why This Matter\n\n`CLAUDE.md` loads on every session start. A 1,000-token project memory file adds\n1,000 input tokens each time project opens, or 100,000 across 100 sessions.\n\nCaveman reduced counted tokens by 33.2% in total on the five listed fixtures\n(22.8% to 49.1% per file). Validators confirmed headings, code blocks, URLs,\nand file paths. They did not establish general semantic or task-quality\nequivalence.\n\n```\n┌────────────────────────────────────────────┐\n│  TOKEN SAVINGS, ALL FIVE   ███       33.2% │\n│  FIXTURES IN TABLE                       5 │\n│  STRUCTURAL VALIDATION       passed on all │\n│  SETUP TIME                █            1x │\n└────────────────────────────────────────────┘\n```\n\n## Part of Caveman\n\nThis skill is part of the [caveman](https://github.com/JuliusBrussee/caveman) toolkit.\n\n- `caveman`: ask Claude to answer in shorter prose\n- `caveman-compress`: shorten supported project-memory files with backups and validation\n","isInternal":false,"tokens":1994,"sizeBytes":7753},{"name":"SECURITY.md","path":"skills/caveman-compress/SECURITY.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/caveman-compress/SECURITY.md","title":"Caveman-compress Skill","category":"anthropic-skill","format":"markdown","content":"# Security\n\n## Snyk High Risk Rating\n\n`caveman-compress` receives a Snyk High Risk rating due to static analysis heuristics. This document explains what the skill does and does not do.\n\n### What triggers the rating\n\n1. **subprocess usage**: The skill calls the `claude` CLI via `subprocess.run()` as a fallback when `ANTHROPIC_API_KEY` is not set. It can also call `opencode run` when `CAVEMAN_COMPRESS_PROVIDER=opencode`. Subprocess calls use fixed argument lists — no shell interpolation occurs. Claude receives user file content via stdin; opencode receives the generated prompt through a temporary `--file` attachment that is deleted after the call.\n\n   The file being compressed is untrusted input, and opencode 2.x lets its agents use every tool that is not explicitly denied: shell, file edits, web search and fetch, MCP tools from your opencode config, subagents. Compression needs none of them. Compress therefore runs `opencode run --standalone --agent caveman-compress` with `OPENCODE_CONFIG_CONTENT` denying every permission (`\"*\": \"deny\"`) both globally and for that agent, and never passes `--auto`. The dedicated agent matters because opencode applies a per-agent rule after the global one, so an `agent.build.permission` allow in your own config would otherwise re-enable a tool. `--standalone` matters because the shared opencode background service does not read the caller's environment, so the deny rule only binds a private server. Checked on opencode 2.0.22: the model is offered no tools and the prompt attachment is still read. Unlike the Claude path (`--strict-mcp-config`), compress does not switch off the MCP servers in your opencode config: their tools are denied, but opencode may still start them.\n\n2. **File read/write**: The skill reads the file the user explicitly points it at, compresses it, and writes the result back to the same path. A `.original.md` backup is saved to an out-of-tree data dir (`$XDG_DATA_HOME/caveman-compress/backups/<parent-dir-name>/`, or `%LOCALAPPDATA%\\caveman-compress\\backups\\<parent-dir-name>\\` on Windows). The opencode provider writes a temporary prompt attachment in the OS temp directory and deletes it after the subprocess exits. Beyond the target file, that backup location, and the temporary attachment, no files are read or written.\n\n### What the skill does NOT do\n\n- Does not execute user file content as code\n- Does not make network requests except through the configured LLM provider CLI, SDK, or (for `openai-compat`) the one configured HTTP endpoint\n- Does not read unrelated files outside the path the user provides\n- Does not use shell=True or string interpolation in subprocess calls\n- Does not collect or transmit any data beyond the file being compressed\n\n### Auth behavior\n\nDefault path uses Claude. If `ANTHROPIC_API_KEY` is set, the skill uses the Anthropic Python SDK directly (no subprocess). If not set, it falls back to the `claude` CLI, which uses the user's existing Claude desktop authentication.\n\nSet `CAVEMAN_COMPRESS_PROVIDER=opencode` to use `opencode run` instead. Set `CAVEMAN_COMPRESS_MODEL=provider/model` for compress-specific model selection. `CAVEMAN_MODEL` is the fallback when `CAVEMAN_COMPRESS_MODEL` is unset.\n\nSet `CAVEMAN_COMPRESS_PROVIDER=openai-compat` to POST the prompt with Python's standard-library `urllib` (no subprocess, no extra dependency) to `$CAVEMAN_COMPRESS_ENDPOINT/chat/completions`, default `http://localhost:11434/v1` (Ollama). Other local servers: LM Studio `http://localhost:1234/v1`, llama.cpp `http://localhost:8080/v1`, vLLM `http://localhost:8000/v1`. `CAVEMAN_COMPRESS_API_KEY`, if set, is sent as a Bearer token. A non-local `http://` endpoint sends the file and that key in plaintext; use `https://` for anything off the machine.\n\n### File size limit\n\nFiles larger than 500KB are rejected before any API call is made.\n\n### Snyk W007 (\"insecure credential handling\")\n\nW007 fires on the rule that code blocks, commands, and exact error strings stay verbatim. See [Scanner warnings](../../SECURITY.md#scanner-warnings) in the repository SECURITY.md.\n\n### Reporting a vulnerability\n\nIf you believe you've found a genuine security issue, please open a GitHub issue with the label `security`.\n","isInternal":false,"tokens":989,"sizeBytes":4226},{"name":"SKILL.md","path":"skills/caveman-compress/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/caveman-compress/SKILL.md","title":"caveman-compress","category":"anthropic-skill","format":"markdown","content":"---\nname: caveman-compress\ndescription: >\n  Compress a memory file such as CLAUDE.md or a todo list into caveman format\n  to save input tokens, keeping a readable backup. Sends the file's text to the\n  configured model provider. Trigger: /caveman-compress.\n---\n\n# Caveman Compress\n\n## Purpose\n\n> **Data leaves the machine.** Compression sends the file's full text to the model provider that `scripts/compress.py` is configured for: the `claude` CLI, the Anthropic API, or an OpenAI-compatible endpoint. Files whose names look like secrets (`.env`, `credentials`, `secrets`, `.netrc`, …) are refused before they are read. Don't compress a file the provider shouldn't see.\n\nCompress natural language files (CLAUDE.md, todos, preferences) into caveman-speak to reduce input tokens. Compressed version overwrites original. Human-readable backup saved as `<filename>.original.md`, but NOT beside the source file — it lives in an out-of-tree data dir (`$XDG_DATA_HOME/caveman-compress/backups/<parent-dir-name>/`, or `%LOCALAPPDATA%\\caveman-compress\\backups\\<parent-dir-name>\\` on Windows) so skill auto-loaders don't re-ingest it as a live file.\n\n## Trigger\n\n`/caveman-compress <filepath>` or when user asks to compress a memory file.\n\n**Requires an explicit filepath.** If the user pastes raw text instead of naming a file, do NOT improvise an inline compression — ask them for the filepath to compress, or offer to save the pasted text to a file first. Writing your own reasoning/commentary about the request into the compressed output (e.g. \"User passed text directly, not a filepath...\") is the exact failure this guards against — never let that leak into a saved file.\n\n## Process\n\n1. The compression scripts live in `scripts/` (adjacent to this SKILL.md). If the path is not immediately available, search for `scripts/__main__.py` next to this SKILL.md.\n\n2. From the directory containing this SKILL.md, run:\n\npython3 -m scripts <absolute_filepath>\n\n3. The CLI will:\n- detect file type (no tokens)\n- call Claude to compress\n- validate output (no tokens)\n- if errors: cherry-pick fix with Claude (targeted fixes only, no recompression)\n- retry up to 2 times\n- if still failing after 2 retries: report error to user, leave original file untouched\n\n4. Return result to user\n\n## Compression Rules\n\n### Remove\n- Articles: a, an, the\n- Filler: just, really, basically, actually, simply, essentially, generally\n- Pleasantries: \"sure\", \"certainly\", \"of course\", \"happy to\", \"I'd recommend\"\n- Hedging: \"it might be worth\", \"you could consider\", \"it would be good to\"\n- Redundant phrasing: \"in order to\" → \"to\", \"make sure to\" → \"ensure\", \"the reason is because\" → \"because\"\n- Connective fluff: \"however\", \"furthermore\", \"additionally\", \"in addition\"\n\n### Preserve EXACTLY (never modify)\n- Code blocks (fenced ``` and indented)\n- Inline code (`backtick content`)\n- URLs and links (full URLs, markdown links)\n- File paths (`/src/components/...`, `./config.yaml`)\n- Commands (`npm install`, `git commit`, `docker build`)\n- Technical terms (library names, API names, protocols, algorithms)\n- Proper nouns (project names, people, companies)\n- Dates, version numbers, numeric values\n- Environment variables (`$HOME`, `NODE_ENV`)\n\n### Preserve Structure\n- All markdown headings (keep exact heading text, compress body below)\n- Bullet point hierarchy (keep nesting level)\n- Numbered lists (keep numbering)\n- Tables (compress cell text, keep structure)\n- Frontmatter/YAML headers in markdown files\n\n### Compress\n- Use short synonyms: \"big\" not \"extensive\", \"fix\" not \"implement a solution for\", \"use\" not \"utilize\"\n- Fragments OK: \"Run tests before commit\" not \"You should always run tests before committing\"\n- Drop \"you should\", \"make sure to\", \"remember to\" — just state the action\n- Merge redundant bullets that say the same thing differently\n- Keep one example where multiple examples show the same pattern\n\nCRITICAL RULE:\nAnything inside ``` ... ``` must be copied EXACTLY.\nDo not:\n- remove comments\n- remove spacing\n- reorder lines\n- shorten commands\n- simplify anything\n\nInline code (`...`) must be preserved EXACTLY.\nDo not modify anything inside backticks.\n\nIf file contains code blocks:\n- Treat code blocks as read-only regions\n- Only compress text outside them\n- Do not merge sections around code\n\n## Pattern\n\nOriginal:\n> You should always make sure to run the test suite before pushing any changes to the main branch. This is important because it helps catch bugs early and prevents broken builds from being deployed to production.\n\nCompressed:\n> Run tests before push to main. Catch bugs early, prevent broken prod deploys.\n\nOriginal:\n> The application uses a microservices architecture with the following components. The API gateway handles all incoming requests and routes them to the appropriate service. The authentication service is responsible for managing user sessions and JWT tokens.\n\nCompressed:\n> Microservices architecture. API gateway route all requests to services. Auth service manage user sessions + JWT tokens.\n\n## Boundaries\n\n- ONLY compress natural language files (.md, .txt, .typ, .typst, .tex, extensionless)\n- NEVER modify: .py, .js, .ts, .json, .yaml, .yml, .toml, .env, .lock, .css, .html, .xml, .sql, .sh\n- If file has mixed content (prose + code), compress ONLY the prose sections\n- If unsure whether something is code or prose, leave it unchanged\n- Original file is backed up as FILE.original.md before overwriting — in the out-of-tree backup data dir (see Purpose), not beside the source file\n- Never compress FILE.original.md (skip it)\n","frontmatter":{"name":"caveman-compress","description":"Compress a memory file such as CLAUDE.md or a todo list into caveman format to save input tokens, keeping a readable backup. Sends the file's text to the configured model provider. Trigger: /caveman-compress.\n"},"isInternal":false,"tokens":1330,"sizeBytes":5578},{"name":"SKILL.md","path":"skills/caveman-discover/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/caveman-discover/SKILL.md","title":"caveman-discover","category":"anthropic-skill","format":"markdown","content":"---\nname: caveman-discover\ndescription: >\n  Find and label every LLM workflow in the repository so Caveman Cloud groups\n  spend by workflow instead of one bucket. Use for \"discover workflows\" or\n  breaking LLM spend down by workflow.\n---\n\nYou are labeling this repository's LLM workflows for Caveman Cloud. A\n*workflow* is a job the code performs — \"answer a support ticket\", \"build the\nnightly digest\", \"run the eval suite\" — not a technology. Every gateway\nrequest can carry a workflow label; unlabeled traffic all lands in one\n`unlabeled-workflow` bucket. Your job: find the workflows, name them well,\nwire the labels, and verify nothing broke.\n\nThis changes code, so it goes through the user's normal review: **propose the\ntable first, apply after the user agrees.** Re-running on an already-labeled\nrepo must change nothing (idempotent).\n\nThis skill is operator-invoked. An `unlabeled-traffic` Cave Plan observation is\nreview-only and does not create an advisory file, proposal, or Draft PR. Do not\ninfer that telemetry selected a callsite or authorized an edit. Independently\ninventory the repository, present the labeling table, and wait for the user's\napproval before changing code.\n\n## Step 1 — Inventory the workflows\n\nWalk the repo from its entry points, not from its imports:\n\n- HTTP/RPC handlers that call an LLM (directly or through layers)\n- Scheduled jobs: cron definitions, queue consumers, workers, GitHub Actions\n  that invoke LLM code\n- CLI commands and scripts (`scripts/`, `bin/`, package.json scripts)\n- Eval / test harnesses that burn real tokens\n- Distinct agents or chains inside a framework (each LangGraph graph, each\n  crew, each agent definition is usually its own workflow)\n\nOne workflow = one job a human would name. Ten callsites inside the same\nrequest handler are one workflow; one shared `llm.ts` helper used by three\njobs is three workflows (label at the callers, never the shared helper).\n\n## Step 2 — Name them\n\nSlug grammar (the gateway enforces this): lowercase `[a-z0-9_-]`, 1–96 chars.\nName the job, not the tech:\n\n- Good: `support-reply`, `nightly-digest`, `pr-review`, `eval-suite`,\n  `onboarding-email`\n- Bad: `openai-calls` (tech), `main` (says nothing), `SupportReply` (invalid),\n  `johns-test-3` (won't age)\n\nNames are forever-ish — renaming later splits the spend history. When a job's\npurpose isn't clear from the code, derive the slug from the file name and mark\nit `review` in the table rather than inventing a purpose.\n\n## Step 3 — Propose, then apply\n\nPresent this table and ask to proceed:\n\n```\n| workflow | job | where | how it gets labeled |\n|---|---|---|---|\n| support-reply | answers inbound tickets | src/bot/reply.ts:41 | defaultHeaders on the reply client |\n| nightly-digest | 02:00 summary job | jobs/digest.ts:12 | header on the digest client |\n| eval-suite (review) | scripts/eval.ts:8 — purpose inferred from filename | scripts/eval.ts:8 | env override at invocation |\n```\n\nThen wire each label with the lightest mechanism available at that callsite:\n\n- **@caveman-ai/sdk / caveman_cloud SDK**: per-trace `workflow` option, or\n  `defaultWorkflow` on the client a single-job service constructs.\n- **Raw provider SDKs** (OpenAI/Anthropic/LangChain/LiteLLM/Vercel): add\n  `\"x-cave-workflow\": \"<slug>\"` to the same `defaultHeaders` /\n  `default_headers` / `extra_headers` block that already carries\n  `x-cave-api-key`. Shared client used by several jobs → pass the header per\n  call (every SDK above accepts per-request header overrides), or give each\n  job its own thin client.\n- **Wrapped coding agents** (`caveman wrap`): `--workflow <slug>` flag or\n  `CAVE_WORKFLOW=<slug>` env at the invocation site (cron line, CI step).\n- **Raw HTTP**: add the `x-cave-workflow` header to the request.\n\nLabel the callers, keep the diff minimal, match the repo's style. If a\ncallsite is not routed through the Caveman gateway at all, don't label it —\nlist it under \"not wired\" in the report (labels only travel on gateway\ntraffic; wiring is the caveman-setup skill's job).\n\n## Step 4 — Verify\n\nRun whatever the repo already uses to exercise one labeled path (a test, a\ndev script, one curl). Then confirm: the request still succeeds (the gateway\nrejects an invalid label with 400 `cave_invalid_request_header` — fix the slug\nif so). Labeled spend appears on the dashboard at `/activity?tab=workflows` as\neach workflow next runs; jobs on a schedule show up when the schedule fires,\nand that's worth saying in the report rather than pretending they're live.\n\n## Step 5 — Report\n\n```\n## Workflows labeled\n\n| workflow | job | where |\n|---|---|---|\n| support-reply | answers inbound tickets | src/bot/reply.ts:41 |\n| nightly-digest | 02:00 summary job | jobs/digest.ts:12 |\n\nVerified: <the labeled path you actually exercised, and what you observed>\nLands at: <DASHBOARD>/activity?tab=workflows — each row appears as that workflow\nnext runs. Anything still unlabeled shows as `unlabeled-workflow`.\nNot wired (no gateway routing, so no label): <list or \"none\">\nMarked review: <slugs whose purpose was inferred from filenames, or \"none\">\n```\n\nIf you found no LLM entry points at all: say exactly that, and point at the\nsetup skill (`<docs origin>/docs/agent-setup.md`) instead of manufacturing a\ntable.\n","frontmatter":{"name":"caveman-discover","description":"Find and label every LLM workflow in the repository so Caveman Cloud groups spend by workflow instead of one bucket. Use for \"discover workflows\" or breaking LLM spend down by workflow.\n"},"isInternal":false,"tokens":1315,"sizeBytes":5276},{"name":"SKILL.md","path":"skills/caveman-evidence-review/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/caveman-evidence-review/SKILL.md","title":"caveman-evidence-review","category":"anthropic-skill","format":"markdown","content":"---\nname: caveman-evidence-review\ndescription: >\n  Read-only review of Caveman Cloud evidence: cost, Cave Score, workflows,\n  traces, latency, errors, routing, savings. Use when asked what Caveman found\n  or where LLM spend goes.\n---\n\n# Review Caveman evidence\n\nAct as a read-only operator. Build conclusions from current Caveman data, not\nfrom repository guesses. Never start, approve, cancel, or roll back an\nexperiment from this skill.\n\n## Hard rules\n\n1. Keep these buckets separate:\n   - measured provider-complete list-price cost;\n   - `inferred` daily headroom;\n   - `verified` ledger savings;\n   - evidence cost.\n   Never add or relabel them.\n2. Do not fetch prompt, completion, tool, or artifact payloads unless the user\n   explicitly asks for payload review. Metadata, spans, timing, models, token\n   counts, status, and optimizer attribution are enough for the default review.\n3. Scope every read to the project selected by Caveman context. Never supply an\n   organization id.\n4. Empty results are evidence of no current signal, not zero cost or zero risk.\n5. Cite trace ids and exact time windows used. Do not claim a cause from an\n   aggregate alone.\n\n## Step 1 — Load context\n\nPrefer MCP:\n\n```text\ncaveman_context {}\n```\n\nCLI fallback:\n\n```bash\ncaveman cloud whoami\ncaveman cloud projects list\n```\n\nStop if login or project selection is missing. Ask the user to run\n`caveman login` or select a project; never guess.\n\n## Step 2 — Establish baseline\n\nUse `caveman_report` for:\n\n- `overview`\n- `costs`\n- `score`\n- `workflows`\n- `verified_savings`\n\nThen use `caveman_plan` for ranked daily headroom. If question is narrow, skip\nunrelated reports. Read shortest set that can answer it.\n\nCLI fallback:\n\n```bash\ncaveman cloud costs\ncaveman cloud score\ncaveman cloud plan --json\n```\n\nState report window and basis before interpreting direction.\n\n## Step 3 — Test the leading explanation with traces\n\nUse `caveman_trace_search`. Choose a bounded window and closed filters:\nworkflow, agent, model, provider, error code, runtime mode, cache status,\noptimization id, status class, token/cost/latency bounds, compression, or\nmonitor verdict.\n\nUseful groupings:\n\n- `workflow` — find jobs driving cost or failures;\n- `model` — compare model mix;\n- `session` — isolate retry or loop behavior;\n- ungrouped — identify exact traces.\n\nCompare a suspect cohort with a control cohort or earlier bounded window.\nDo not infer causality from one expensive trace.\n\nCLI fallback:\n\n```bash\ncaveman cloud traces search \\\n  --workflow <slug> \\\n  --from <RFC3339> \\\n  --to <RFC3339> \\\n  --sort total_cost_usd \\\n  --dir desc \\\n  --limit 25\n```\n\n## Step 4 — Inspect representative traces\n\nCall `caveman_trace_get` for a small number of high-signal trace ids. Inspect\nrequest and span metadata, latency, status, token counts, cache state, applied\noptimizers, and model route. Keep payload retrieval off.\n\nCLI fallback:\n\n```bash\ncaveman cloud traces show <trace-id> --spans\n```\n\n## Step 5 — Report\n\nUse this shape:\n\n```text\n## Caveman evidence review\n\nScope: <project> · <from> to <to>\nMeasured cost: <value and basis>\nVerified savings: <ledger value, kept separate>\nInferred headroom: <per-day band, kept separate>\n\nFindings:\n1. <finding> — <aggregate evidence> — traces <ids>\n2. <finding> — <aggregate evidence> — traces <ids>\n\nUnproven:\n- <plausible explanation lacking a control, trace, or eval>\n\nNext read-only check:\n- <one bounded query>\n\nPossible action:\n- <proposal only; use caveman-manage for read-only lifecycle review and safety gate>\n```\n\nIf data is missing, name missing signal and stop at strongest supported\nstatement. Never turn a catalog subtotal into an invoice or an experiment result\ninto verified savings.\n","frontmatter":{"name":"caveman-evidence-review","description":"Read-only review of Caveman Cloud evidence: cost, Cave Score, workflows, traces, latency, errors, routing, savings. Use when asked what Caveman found or where LLM spend goes.\n"},"isInternal":false,"tokens":911,"sizeBytes":3734},{"name":"package.json","path":"skills/caveman-explore/package.json","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/caveman-explore/package.json","title":"@caveman/skill-caveman-explore","category":"anthropic-skill","format":"json","content":"{\n  \"name\": \"@caveman/skill-caveman-explore\",\n  \"version\": \"1.0.0\",\n  \"license\": \"Apache-2.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"description\": \"Read-only FastContext exploration skill with parallel repository search and citation-only output.\",\n  \"files\": [\"SKILL.md\"],\n  \"scripts\": {\n    \"test\": \"node --test --test-force-exit tests/*.mjs\"\n  }\n}\n","frontmatter":{"name":"@caveman/skill-caveman-explore","version":"1.0.0","license":"Apache-2.0","private":true,"type":"module","description":"Read-only FastContext exploration skill with parallel repository search and citation-only output.","files":["SKILL.md"],"scripts":{"test":"node --test --test-force-exit tests/*.mjs"}},"isInternal":false,"tokens":107,"sizeBytes":354},{"name":"SKILL.md","path":"skills/caveman-explore/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/caveman-explore/SKILL.md","title":"caveman-explore","category":"anthropic-skill","format":"markdown","content":"---\nname: caveman-explore\ndescription: Read-only repository explorer for cold-start orientation, broad cross-file localization, or when a direct search failed. Skip it when the exact file or symbol is already named. Returns path:line citations only; its reads stay out of main context.\ntools: Read, Glob, Grep\nmodel: haiku\n---\n\nYou are FastContext, a fast, cheap, read-only repository explorer. Another agent\n(the solver) delegates a localization question to you. Your only job is to find\nWHERE the relevant code lives and report it as a compact list of file paths with\nline ranges. You never edit files, run commands, or propose a solution.\n\nHow to work:\n\n1. Issue several tool calls IN PARALLEL in your first turn — cast a broad net.\n   Cover complementary hypotheses at once: likely path patterns (Glob), symbol and\n   string matches (Grep), and reading the most promising files (Read). Do not probe\n   one file at a time when you can fan out.\n2. Follow the evidence over one or two more turns only if needed. Stop as soon as\n   you can name the relevant locations. You are optimizing for the solver's token\n   budget, so finish fast.\n3. Only cite line ranges you actually read. Never invent or estimate a range, and\n   never cite a range past the end of a file. A precise small range beats a vague\n   large one.\n\nYour reply MUST be ONLY an evidence block: one citation per line, nothing else.\nNo preamble, no explanation, no summary, no markdown headings. Use exactly this\nshape, one per line:\n\n  path/to/file.ext:START-END  reason it is relevant\n\nExample reply:\n\n  src/router/pick.go:42-71  route selection — where a model is chosen\n  src/router/pick_test.go:18-40  the table test covering pick()\n\nIf you genuinely cannot find anything relevant, reply with the single line:\n\n  no relevant locations found\n\nThat honest answer is better than a guess. The solver reads your citations and\nnothing else from your work, so keep the list short, specific, and correct.\n","frontmatter":{"name":"caveman-explore","description":"Read-only repository explorer for cold-start orientation, broad cross-file localization, or when a direct search failed. Skip it when the exact file or symbol is already named. Returns path:line citations only; its reads stay out of main context.","tools":"Read, Glob, Grep","model":"haiku"},"isInternal":false,"tokens":448,"sizeBytes":1970},{"name":"README.md","path":"skills/caveman-help/README.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/caveman-help/README.md","title":"Caveman-help Skill","category":"anthropic-skill","format":"markdown","content":"# caveman-help\n\nQuick-reference card. One shot, no mode change.\n\n## What it does\n\nPrints a cheat sheet of all caveman modes, sibling skills, deactivation triggers, and how to set the default mode via env var or config file. One-shot display — does not flip the active mode, write flag files, or persist anything. Use when you forget the slash commands.\n\n## How to invoke\n\n```\n/caveman-help\n```\n\nAlso triggers on \"caveman help\", \"what caveman commands\", \"how do I use caveman\".\n\n## Example output\n\n```\nModes:\n  /caveman              the voice (default)\n  /ultracave            grammar stripped\n  /megacave             classical Chinese\n  /caveman status       report the mode, change nothing\n\nSkills:\n  /caveman-commit       terse Conventional Commits\n  /caveman-review       one-line PR comments\n  /caveman-compress     smaller Markdown memory files\n  /caveman-stats        recorded token usage (savings unknown)\n\nDeactivate:\n  \"stop caveman\" or \"normal mode\"\n```\n\n## See also\n\n- [`SKILL.md`](./SKILL.md) — full reference card\n- [Caveman README](../../README.md) — repo overview\n","isInternal":false,"tokens":275,"sizeBytes":1085},{"name":"SKILL.md","path":"skills/caveman-help/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/caveman-help/SKILL.md","title":"caveman-help","category":"anthropic-skill","format":"markdown","content":"---\nname: caveman-help\ndescription: >\n  Quick-reference card for the three caveman skills and their commands.\n  Trigger: /caveman-help or \"caveman help\".\n---\n\n# Caveman Help\n\nDisplay this reference card when invoked. One-shot — do NOT change mode, write flag files, or persist anything. Output in caveman style.\n\n## Modes\n\n| Mode | Trigger | What change |\n|------|---------|-------------|\n| **caveman** | `/caveman` | The voice. Answer first, no fluff, every fact kept. Default. |\n| **ultracave** | `/ultracave` (alias `/caveman ultra`) | Grammar stripped. Fragments, one word when enough, each fact once. |\n| **megacave** | `/megacave` (alias `/caveman wenyan`) | Classical Chinese 文言文. Far fewer characters, technical terms verbatim. |\n\nMode stick until changed or session end.\n`/caveman status` reports current mode without changing it. Claude Code and the standalone OpenCode plugin read stored state; other hosts answer from the conversation and mark it `(not tracked by this host)`.\n\n## Skills\n\n| Skill | Trigger | What it do |\n|-------|---------|-----------|\n| **caveman-commit** | `/caveman-commit` | Terse commit messages. Conventional Commits. ≤50 char subject. |\n| **caveman-review** | `/caveman-review` | One-line PR comments: `L42: bug: user null. Add guard.` |\n| **caveman-compress** | `/caveman-compress <file>` | Compress .md files to caveman prose. 33.2% fewer tokens on the repo's five test files. |\n| **caveman-help** | `/caveman-help` | This card. |\n\n## Deactivate\n\nSay \"stop caveman\" or \"normal mode\". Resume anytime with `/caveman`.\n\n## Language\n\nKeep user's language by default — reply in the language user writes, never switch regardless of example text or multilingual context elsewhere. Compress the style, not the language. Technical terms, code, commands, commit types, and exact error strings stay verbatim unless user ask for translation.\n\n## Configure Default Mode\n\nDefault mode = `caveman`. Change it:\n\n**Environment variable** (highest priority):\n```bash\nexport CAVEMAN_DEFAULT_MODE=ultracave\n```\n\n**Config file** (`~/.config/caveman/config.json`):\n```json\n{ \"defaultMode\": \"ultracave\" }\n```\n\nFor opt-in activation in Claude Code, set `defaultMode` to `manual`. Sessions start off; `/caveman` or `talk like caveman` activates caveman. `off` also disables bare-command activation. `manual` is a startup policy, not a mode. OpenCode still starts in caveman mode because its installer also supplies static activation rules.\n\nSet `\"off\"` to suppress automatic and bare-command activation. Use `\"manual\"` when bare `/caveman` should activate.\n\nResolution: env var > repo config > user config > `caveman`. Legacy values `lite`, `full`, `ultra`, `wenyan*` still resolve.\n\n## More\n\nFull docs: https://github.com/JuliusBrussee/caveman\n","frontmatter":{"name":"caveman-help","description":"Quick-reference card for the three caveman skills and their commands. Trigger: /caveman-help or \"caveman help\".\n"},"isInternal":false,"tokens":712,"sizeBytes":2771},{"name":"CLAUDE.md","path":"skills/caveman-learn/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/caveman-learn/CLAUDE.md","title":"Caveman-learn Skill","category":"anthropic-skill","format":"markdown","content":"# skills/caveman-learn — the Caveman Learn editing skill (Apache-2.0, public)\n\nThe consent-gated half of `caveman learn`. The analyzer (the Go proxy) **measures**\nwhere an agent's tokens go and writes a ranked plan; this skill is what an agent\nloads to **act** on that plan — proposing each fix and applying it only with the\nuser's per-edit yes. It is the loop-closer the learn spec §10\ndescribes, plus the new `cavemem_offload` move.\n\n## Layout\n- `SKILL.md` — the canonical skill body (frontmatter `name: caveman-learn` + a\n  trigger-phrase `description`; body = the read-plan → per-class consent loop). This\n  file is the source of truth.\n- `tests/skill-file.test.mjs` — asserts the canonical file is well-formed and honest\n  (frontmatter present; the net-token-negative gate, the never-make-the-agent-dumber\n  guard, consent-per-edit, and reversibility are all stated; no imperative for\n  behavioral findings; no placeholders).\n\n## Install path\n`caveman tools skills install caveman-learn` (in `../../cli/src/index.ts`) writes this file\ninto a repo's `.claude/skills/caveman-learn/SKILL.md` (Claude Code) or\n`~/.codex/skills/caveman-learn/SKILL.md` (Codex). The CLI **embeds a byte-identical copy**\n(`CAVEMAN_LEARN_SKILL_MD`) because the published CLI ships no sibling assets;\n`../../cli/tests/skills.runtime.mjs` asserts the embedded copy equals this canonical\nfile (the drift guard). **Change this file and that constant together.**\n\n## Boundary (binding)\nThe skill — using the agent's own file tools — is the ONLY thing that edits a user's\nconfig. `caveman learn apply` stays read-only (it materializes candidates), and\n`caveman mem *` are mechanical store ops. The offload move enforces a net-token-negative\ngate and the never-make-the-agent-dumber guard before any trim.\n\nSee ../../mem/CLAUDE.md (cavemem) · ../caveman-explore/SKILL.md (the packaging precedent)\n","isInternal":false,"tokens":495,"sizeBytes":1886},{"name":"package.json","path":"skills/caveman-learn/package.json","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/caveman-learn/package.json","title":"@caveman/skill-caveman-learn","category":"anthropic-skill","format":"json","content":"{\n  \"name\": \"@caveman/skill-caveman-learn\",\n  \"version\": \"1.0.0\",\n  \"license\": \"Apache-2.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"description\": \"Consent-gated editing skill that closes the loop on a Caveman learn report: review ranked token sinks and apply cost-lowering fixes (trim config, offload recurring context to cavemem) with per-edit approval, a net-token-negative gate, and a never-make-the-agent-dumber guard.\",\n  \"files\": [\"SKILL.md\"],\n  \"scripts\": {\n    \"test\": \"node --test --test-force-exit tests/*.mjs\"\n  }\n}\n","frontmatter":{"name":"@caveman/skill-caveman-learn","version":"1.0.0","license":"Apache-2.0","private":true,"type":"module","description":"Consent-gated editing skill that closes the loop on a Caveman learn report: review ranked token sinks and apply cost-lowering fixes (trim config, offload recurring context to cavemem) with per-edit approval, a net-token-negative gate, and a never-make-the-agent-dumber guard.","files":["SKILL.md"],"scripts":{"test":"node --test --test-force-exit tests/*.mjs"}},"isInternal":false,"tokens":153,"sizeBytes":530},{"name":"README.md","path":"skills/caveman-learn/README.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/caveman-learn/README.md","title":"Caveman-learn Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fcaveman-learn%2FREADME.md -->","isInternal":false,"tokens":453,"sizeBytes":1829},{"name":"SKILL.md","path":"skills/caveman-learn/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/caveman-learn/SKILL.md","title":"caveman-learn","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fcaveman-learn%2FSKILL.md -->","frontmatter":{"name":"caveman-learn","description":"Act on a Caveman learn report - review the ranked token sinks, apply cost-lowering fixes with per-edit consent, and report what those fixes returned. Use when asked to lower an agent's token cost, what caveman has saved, to trim a heavy CLAUDE.md, or to offload re-pasted context into cavemem."},"isInternal":false,"tokens":2736,"sizeBytes":11515},{"name":"SKILL.md","path":"skills/caveman-manage/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/caveman-manage/SKILL.md","title":"caveman-manage","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fcaveman-manage%2FSKILL.md -->","frontmatter":{"name":"caveman-manage","description":"Inspect Caveman Cloud's experiment lifecycle and block unsafe execution. Use when asked to start, approve, cancel, promote or roll back a Caveman experiment.\n"},"isInternal":false,"tokens":854,"sizeBytes":3916},{"name":"SKILL.md","path":"skills/caveman-optimize/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/caveman-optimize/SKILL.md","title":"caveman-optimize","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fcaveman-optimize%2FSKILL.md -->","frontmatter":{"name":"caveman-optimize","description":"Turn a Caveman optimization observation into an operator-chosen candidate with a paired baseline evaluation. Use when asked to inspect or evaluate a Caveman optimization report. Needs explicit approval.\n"},"isInternal":false,"tokens":990,"sizeBytes":4723},{"name":"README.md","path":"skills/caveman-review/README.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/caveman-review/README.md","title":"Caveman-review Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fcaveman-review%2FREADME.md -->","isInternal":false,"tokens":320,"sizeBytes":1210},{"name":"SKILL.md","path":"skills/caveman-review/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/caveman-review/SKILL.md","title":"caveman-review","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fcaveman-review%2FSKILL.md -->","frontmatter":{"name":"caveman-review","description":"Compressed code review - one line per finding with location, problem and fix. Use for /caveman-review, \"review this PR\", or \"review the diff\".\n"},"isInternal":false,"tokens":640,"sizeBytes":2567},{"name":"SKILL.md","path":"skills/caveman-setup/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/caveman-setup/SKILL.md","title":"caveman-setup","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fcaveman-setup%2FSKILL.md -->","frontmatter":{"name":"caveman-setup","description":"Wire a repository through the Caveman Cloud gateway so every LLM request is measured, with no behavior change. Use for \"set up caveman\" or adding LLM spend observability.\n"},"isInternal":false,"tokens":2825,"sizeBytes":10524},{"name":"README.md","path":"skills/caveman-stats/README.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/caveman-stats/README.md","title":"Caveman-stats Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fcaveman-stats%2FREADME.md -->","isInternal":false,"tokens":580,"sizeBytes":2754},{"name":"SKILL.md","path":"skills/caveman-stats/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/caveman-stats/SKILL.md","title":"caveman-stats","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fcaveman-stats%2FSKILL.md -->","frontmatter":{"name":"caveman-stats","description":"Show recorded output and cache-read token usage and mode attribution for the current Claude Code session, or locate the host's native usage report. Trigger: /caveman-stats.\n"},"isInternal":false,"tokens":400,"sizeBytes":1964},{"name":"README.md","path":"skills/caveman/README.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/caveman/README.md","title":"Caveman Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fcaveman%2FREADME.md -->","isInternal":false,"tokens":787,"sizeBytes":3030},{"name":"SKILL.md","path":"skills/caveman/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/caveman/SKILL.md","title":"caveman","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fcaveman%2FSKILL.md -->","frontmatter":{"name":"caveman","description":"Terse caveman voice: answer first, fluff gone, every technical fact kept. Use for /caveman, \"caveman mode\", \"talk like caveman\", \"be brief\", \"less tokens\". Stays on until \"stop caveman\" or \"normal mode\".\n"},"isInternal":false,"tokens":1166,"sizeBytes":4678},{"name":"engine-mcp-tools.json","path":"skills/engine-mcp-tools.json","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/engine-mcp-tools.json","title":"Skills Skill","category":"anthropic-skill","format":"json","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fengine-mcp-tools.json -->","frontmatter":{"schema_version":"1","tools":["caveman_compress","caveman_retrieve","caveman_stats","caveman_toon_decode","caveman_toon_encode"]},"isInternal":false,"tokens":57,"sizeBytes":168},{"name":"pack.json","path":"skills/generated/aider/pack.json","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/generated/aider/pack.json","title":"Aider Skill","category":"anthropic-skill","format":"json","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fgenerated%2Faider%2Fpack.json -->","frontmatter":{"schema":"caveman.native-pack.v1","id":"caveman-local","version":"2.2.0","protocol":1,"core":{"source":"native/native-core.md","mandatory":true,"prompt_token_budget":560,"estimated_tokens":556,"estimate_basis":"ceil_utf8_bytes_divided_by_3","instructions":"Build simplest complete system. Trace behavior and invariants before editing. System, user, and repository instructions outrank this default.\n\nConsider in order; accept first with correctness and architectural fit without distortion:\n1. Required behavior already exists: reuse it or change nothing.\n2. Responsible layer, type, helper, or pattern owns it: extend there.\n3. Standard library, native platform, browser, database, runtime, or installed dependency owns it: use it.\n4. Small new implementation fits current architecture: build where invariant belongs.\n5. Existing structure obstructs clear ownership: make coherent refactor task needs.\n\nOptimize total system complexity, clarity, and ownership—not lines or files changed. Coherent wider change beats cramped patch, duplicated guard, or misplaced logic. Reuse fitting abstractions; do not contort code to avoid abstraction. Prefer consolidation. Fix root cause.\n\nAvoid speculative features and extension points, single-implementation interfaces, configuration for fixed values, premature services, and imagined scaffolding. Dependency or public surface is valid for correct design or lower lifecycle cost; explain material tradeoff.\n\nSimplicity never removes trust-boundary validation, authorization, security, data-loss prevention, error handling, accessibility, migration or rollback safety, concurrency protection, compatibility, required tests, or explicitly requested behavior. Ask only for material public, security, data, billing, or hard-to-reverse choices.\n\nRun smallest sufficient proof. Report material changes, proof, unresolved risks, and only material omissions. Stop when task is satisfied."},"targets":{"claude":{"core":"SessionStart","task":"UserPromptSubmit"},"codex":{"core":"developer_instructions+SessionStart","task":"UserPromptSubmit"},"hermes":{"core":"pre_llm_call","task":"pre_llm_call"},"gemini":{"core":"BeforeAgent","task":"BeforeAgent"},"opencode":{"core":"experimental.chat.system.transform","task":"chat.message"},"aider":{"core":"read_only_conventions","task":"native_repository_map_authoritative"}},"skills":[{"id":"investigate-first","summary":"Gather evidence and establish a credible mechanism before editing.","activation":"classified","task_types":["investigation"],"evidence_status":"structural-test-only","prompt_byte_budget":900,"conflicts":[],"precedence":100,"guardrails":["no_edit_before_credible_hypothesis","diagnosis_does_not_authorize_fix"],"entry_condition":"cause is ambiguous","stop_condition":"cause or exact blocker is evidence-backed","instructions":"# Investigate first\n\nGather evidence before changing product code.\n\n- Separate observed symptom from inferred cause.\n- Trace inputs, state transitions, ownership boundaries, and failure output.\n- Rank hypotheses by evidence and cheap falsification value.\n- Do not edit until one credible mechanism explains evidence.\n- Stop exploration when evidence is sufficient to name cause or exact blocker.\n\nReport cause and proof. Make no fix unless task authorizes implementation."},{"id":"lean-build","summary":"Deliver narrow complete feature outcomes with architectural fit on top of mandatory simplicity Core.","activation":"classified","task_types":["feature"],"evidence_status":"structural-test-only","prompt_byte_budget":900,"conflicts":[],"precedence":100,"guardrails":["core_architecture_first_simplicity","preserve_correctness_controls","justify_material_tradeoffs"],"entry_condition":"task adds product behavior","stop_condition":"focused acceptance proof passes","instructions":"# Lean build\n\nNative Core's architecture-first simplicity remains mandatory. Turn feature into complete narrow outcome fitting system.\n\n- Derive observable acceptance and explicit non-goals from request and repository.\n- Trace entry point through layers owning invariants.\n- Deliver coherent end-to-end path across responsible layers; never force work into one file, direct expression, or local patch.\n- Reuse fitting seam. Refactor when patching duplicates behavior, weakens ownership, or hides root cause.\n- Omit modes, providers, config, extensibility, and polish unless acceptance needs them.\n- Add surface, dependency, service, config, or migration only for lifecycle design or acceptance; state material tradeoff.\n- Keep work runnable; preserve Core safety.\n\nExercise path. Run focused proof. Stop when acceptance passes. Report only material omissions and trigger."},{"id":"migration","summary":"Execute a reversible compatibility-safe transition without hidden destruction.","activation":"classified","task_types":["migration"],"evidence_status":"structural-test-only","prompt_byte_budget":950,"conflicts":[],"precedence":100,"guardrails":["preserve_rollback","preserve_data","verify_compatibility"],"entry_condition":"task changes persisted or public shape","stop_condition":"requested migration stage passes without implicit contraction","instructions":"# Migration\n\nMap current readers, writers, data shape, compatibility window, and ownership before editing.\n\n- Define forward path and rollback path.\n- Preserve existing data; make destructive steps explicit and separately authorized.\n- Keep mixed-version operation safe where rollout can overlap.\n- Sequence expand, migrate, verify, then contract when applicable.\n- Make retries idempotent and partial failure observable.\n- Verify old and new paths at required transition stages.\n\nStop after requested stage passes; do not perform later destructive contraction implicitly."},{"id":"safe-refactor","summary":"Restructure code while preserving observable behavior.","activation":"classified","task_types":["refactor"],"evidence_status":"structural-test-only","prompt_byte_budget":900,"conflicts":[],"precedence":100,"guardrails":["preserve_behavior","keep_intermediate_states_testable"],"entry_condition":"task requests structural change without behavior change","stop_condition":"before-and-after proof matches","instructions":"# Safe refactor\n\nDefine behavior-preservation boundary and establish verification before structural edits.\n\n- Keep feature changes outside refactor.\n- Move one ownership boundary at a time.\n- Preserve public interfaces, failure behavior, ordering, and compatibility unless explicitly scoped.\n- Keep intermediate states buildable and testable.\n- Avoid dependency or configuration growth without correctness need.\n\nRun same proof after change. Stop when behavior matches and requested structure is achieved."},{"id":"surgical-patch","summary":"Fix the narrowest responsible layer with regression proof.","activation":"classified","task_types":["bugfix"],"evidence_status":"structural-test-only","prompt_byte_budget":850,"conflicts":[],"precedence":100,"guardrails":["avoid_unrelated_cleanup","preserve_surrounding_behavior"],"entry_condition":"task fixes incorrect behavior","stop_condition":"failure is fixed and regression proof passes","instructions":"# Surgical patch\n\nReproduce failure first when economical; otherwise capture strongest available evidence.\n\n- Trace symptom to responsible mechanism.\n- Change narrowest layer that owns incorrect behavior.\n- Preserve unrelated behavior and user changes.\n- Avoid cleanup, renaming, and abstraction outside fix.\n- Add only regression proof relevant to task.\n\nRun focused proof plus nearest affected gate. Stop when failure is fixed and regression proof passes."},{"id":"verify-and-stop","summary":"Run the smallest sufficient proof set and stop without scope growth.","activation":"classified","task_types":["verification"],"evidence_status":"structural-test-only","prompt_byte_budget":850,"conflicts":[],"precedence":100,"guardrails":["no_unrequested_product_edit","report_unavailable_proof_exactly"],"entry_condition":"task asks for proof or completion validation","stop_condition":"acceptance proof is complete","instructions":"# Verify and stop\n\nTranslate acceptance conditions into smallest sufficient proof set.\n\n- Reuse still-current results with matching repository state.\n- Run focused checks before wider gates.\n- Distinguish pass, fail, unavailable, and blocked exactly.\n- Do not edit product code unless verification request includes fixes.\n- Do not add polish, cleanup, or unrelated tests after criteria pass.\n\nStop immediately when acceptance proof is complete. Report commands, results, and unresolved risk only."}],"target":"aider","activation":{"core":"read_only_conventions","task":"native_repository_map_authoritative"}},"isInternal":false,"tokens":2124,"sizeBytes":9925},{"name":"pack.json","path":"skills/generated/claude/pack.json","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/generated/claude/pack.json","title":"Claude Skill","category":"anthropic-skill","format":"json","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fgenerated%2Fclaude%2Fpack.json -->","frontmatter":{"schema":"caveman.native-pack.v1","id":"caveman-local","version":"2.2.0","protocol":1,"core":{"source":"native/native-core.md","mandatory":true,"prompt_token_budget":560,"estimated_tokens":556,"estimate_basis":"ceil_utf8_bytes_divided_by_3","instructions":"Build simplest complete system. Trace behavior and invariants before editing. System, user, and repository instructions outrank this default.\n\nConsider in order; accept first with correctness and architectural fit without distortion:\n1. Required behavior already exists: reuse it or change nothing.\n2. Responsible layer, type, helper, or pattern owns it: extend there.\n3. Standard library, native platform, browser, database, runtime, or installed dependency owns it: use it.\n4. Small new implementation fits current architecture: build where invariant belongs.\n5. Existing structure obstructs clear ownership: make coherent refactor task needs.\n\nOptimize total system complexity, clarity, and ownership—not lines or files changed. Coherent wider change beats cramped patch, duplicated guard, or misplaced logic. Reuse fitting abstractions; do not contort code to avoid abstraction. Prefer consolidation. Fix root cause.\n\nAvoid speculative features and extension points, single-implementation interfaces, configuration for fixed values, premature services, and imagined scaffolding. Dependency or public surface is valid for correct design or lower lifecycle cost; explain material tradeoff.\n\nSimplicity never removes trust-boundary validation, authorization, security, data-loss prevention, error handling, accessibility, migration or rollback safety, concurrency protection, compatibility, required tests, or explicitly requested behavior. Ask only for material public, security, data, billing, or hard-to-reverse choices.\n\nRun smallest sufficient proof. Report material changes, proof, unresolved risks, and only material omissions. Stop when task is satisfied."},"targets":{"claude":{"core":"SessionStart","task":"UserPromptSubmit"},"codex":{"core":"developer_instructions+SessionStart","task":"UserPromptSubmit"},"hermes":{"core":"pre_llm_call","task":"pre_llm_call"},"gemini":{"core":"BeforeAgent","task":"BeforeAgent"},"opencode":{"core":"experimental.chat.system.transform","task":"chat.message"},"aider":{"core":"read_only_conventions","task":"native_repository_map_authoritative"}},"skills":[{"id":"investigate-first","summary":"Gather evidence and establish a credible mechanism before editing.","activation":"classified","task_types":["investigation"],"evidence_status":"structural-test-only","prompt_byte_budget":900,"conflicts":[],"precedence":100,"guardrails":["no_edit_before_credible_hypothesis","diagnosis_does_not_authorize_fix"],"entry_condition":"cause is ambiguous","stop_condition":"cause or exact blocker is evidence-backed","instructions":"# Investigate first\n\nGather evidence before changing product code.\n\n- Separate observed symptom from inferred cause.\n- Trace inputs, state transitions, ownership boundaries, and failure output.\n- Rank hypotheses by evidence and cheap falsification value.\n- Do not edit until one credible mechanism explains evidence.\n- Stop exploration when evidence is sufficient to name cause or exact blocker.\n\nReport cause and proof. Make no fix unless task authorizes implementation."},{"id":"lean-build","summary":"Deliver narrow complete feature outcomes with architectural fit on top of mandatory simplicity Core.","activation":"classified","task_types":["feature"],"evidence_status":"structural-test-only","prompt_byte_budget":900,"conflicts":[],"precedence":100,"guardrails":["core_architecture_first_simplicity","preserve_correctness_controls","justify_material_tradeoffs"],"entry_condition":"task adds product behavior","stop_condition":"focused acceptance proof passes","instructions":"# Lean build\n\nNative Core's architecture-first simplicity remains mandatory. Turn feature into complete narrow outcome fitting system.\n\n- Derive observable acceptance and explicit non-goals from request and repository.\n- Trace entry point through layers owning invariants.\n- Deliver coherent end-to-end path across responsible layers; never force work into one file, direct expression, or local patch.\n- Reuse fitting seam. Refactor when patching duplicates behavior, weakens ownership, or hides root cause.\n- Omit modes, providers, config, extensibility, and polish unless acceptance needs them.\n- Add surface, dependency, service, config, or migration only for lifecycle design or acceptance; state material tradeoff.\n- Keep work runnable; preserve Core safety.\n\nExercise path. Run focused proof. Stop when acceptance passes. Report only material omissions and trigger."},{"id":"migration","summary":"Execute a reversible compatibility-safe transition without hidden destruction.","activation":"classified","task_types":["migration"],"evidence_status":"structural-test-only","prompt_byte_budget":950,"conflicts":[],"precedence":100,"guardrails":["preserve_rollback","preserve_data","verify_compatibility"],"entry_condition":"task changes persisted or public shape","stop_condition":"requested migration stage passes without implicit contraction","instructions":"# Migration\n\nMap current readers, writers, data shape, compatibility window, and ownership before editing.\n\n- Define forward path and rollback path.\n- Preserve existing data; make destructive steps explicit and separately authorized.\n- Keep mixed-version operation safe where rollout can overlap.\n- Sequence expand, migrate, verify, then contract when applicable.\n- Make retries idempotent and partial failure observable.\n- Verify old and new paths at required transition stages.\n\nStop after requested stage passes; do not perform later destructive contraction implicitly."},{"id":"safe-refactor","summary":"Restructure code while preserving observable behavior.","activation":"classified","task_types":["refactor"],"evidence_status":"structural-test-only","prompt_byte_budget":900,"conflicts":[],"precedence":100,"guardrails":["preserve_behavior","keep_intermediate_states_testable"],"entry_condition":"task requests structural change without behavior change","stop_condition":"before-and-after proof matches","instructions":"# Safe refactor\n\nDefine behavior-preservation boundary and establish verification before structural edits.\n\n- Keep feature changes outside refactor.\n- Move one ownership boundary at a time.\n- Preserve public interfaces, failure behavior, ordering, and compatibility unless explicitly scoped.\n- Keep intermediate states buildable and testable.\n- Avoid dependency or configuration growth without correctness need.\n\nRun same proof after change. Stop when behavior matches and requested structure is achieved."},{"id":"surgical-patch","summary":"Fix the narrowest responsible layer with regression proof.","activation":"classified","task_types":["bugfix"],"evidence_status":"structural-test-only","prompt_byte_budget":850,"conflicts":[],"precedence":100,"guardrails":["avoid_unrelated_cleanup","preserve_surrounding_behavior"],"entry_condition":"task fixes incorrect behavior","stop_condition":"failure is fixed and regression proof passes","instructions":"# Surgical patch\n\nReproduce failure first when economical; otherwise capture strongest available evidence.\n\n- Trace symptom to responsible mechanism.\n- Change narrowest layer that owns incorrect behavior.\n- Preserve unrelated behavior and user changes.\n- Avoid cleanup, renaming, and abstraction outside fix.\n- Add only regression proof relevant to task.\n\nRun focused proof plus nearest affected gate. Stop when failure is fixed and regression proof passes."},{"id":"verify-and-stop","summary":"Run the smallest sufficient proof set and stop without scope growth.","activation":"classified","task_types":["verification"],"evidence_status":"structural-test-only","prompt_byte_budget":850,"conflicts":[],"precedence":100,"guardrails":["no_unrequested_product_edit","report_unavailable_proof_exactly"],"entry_condition":"task asks for proof or completion validation","stop_condition":"acceptance proof is complete","instructions":"# Verify and stop\n\nTranslate acceptance conditions into smallest sufficient proof set.\n\n- Reuse still-current results with matching repository state.\n- Run focused checks before wider gates.\n- Distinguish pass, fail, unavailable, and blocked exactly.\n- Do not edit product code unless verification request includes fixes.\n- Do not add polish, cleanup, or unrelated tests after criteria pass.\n\nStop immediately when acceptance proof is complete. Report commands, results, and unresolved risk only."}],"target":"claude","activation":{"core":"SessionStart","task":"UserPromptSubmit"}},"isInternal":false,"tokens":2120,"sizeBytes":9898},{"name":"pack.json","path":"skills/generated/codex/pack.json","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/generated/codex/pack.json","title":"Codex Skill","category":"anthropic-skill","format":"json","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fgenerated%2Fcodex%2Fpack.json -->","frontmatter":{"schema":"caveman.native-pack.v1","id":"caveman-local","version":"2.2.0","protocol":1,"core":{"source":"native/native-core.md","mandatory":true,"prompt_token_budget":560,"estimated_tokens":556,"estimate_basis":"ceil_utf8_bytes_divided_by_3","instructions":"Build simplest complete system. Trace behavior and invariants before editing. System, user, and repository instructions outrank this default.\n\nConsider in order; accept first with correctness and architectural fit without distortion:\n1. Required behavior already exists: reuse it or change nothing.\n2. Responsible layer, type, helper, or pattern owns it: extend there.\n3. Standard library, native platform, browser, database, runtime, or installed dependency owns it: use it.\n4. Small new implementation fits current architecture: build where invariant belongs.\n5. Existing structure obstructs clear ownership: make coherent refactor task needs.\n\nOptimize total system complexity, clarity, and ownership—not lines or files changed. Coherent wider change beats cramped patch, duplicated guard, or misplaced logic. Reuse fitting abstractions; do not contort code to avoid abstraction. Prefer consolidation. Fix root cause.\n\nAvoid speculative features and extension points, single-implementation interfaces, configuration for fixed values, premature services, and imagined scaffolding. Dependency or public surface is valid for correct design or lower lifecycle cost; explain material tradeoff.\n\nSimplicity never removes trust-boundary validation, authorization, security, data-loss prevention, error handling, accessibility, migration or rollback safety, concurrency protection, compatibility, required tests, or explicitly requested behavior. Ask only for material public, security, data, billing, or hard-to-reverse choices.\n\nRun smallest sufficient proof. Report material changes, proof, unresolved risks, and only material omissions. Stop when task is satisfied."},"targets":{"claude":{"core":"SessionStart","task":"UserPromptSubmit"},"codex":{"core":"developer_instructions+SessionStart","task":"UserPromptSubmit"},"hermes":{"core":"pre_llm_call","task":"pre_llm_call"},"gemini":{"core":"BeforeAgent","task":"BeforeAgent"},"opencode":{"core":"experimental.chat.system.transform","task":"chat.message"},"aider":{"core":"read_only_conventions","task":"native_repository_map_authoritative"}},"skills":[{"id":"investigate-first","summary":"Gather evidence and establish a credible mechanism before editing.","activation":"classified","task_types":["investigation"],"evidence_status":"structural-test-only","prompt_byte_budget":900,"conflicts":[],"precedence":100,"guardrails":["no_edit_before_credible_hypothesis","diagnosis_does_not_authorize_fix"],"entry_condition":"cause is ambiguous","stop_condition":"cause or exact blocker is evidence-backed","instructions":"# Investigate first\n\nGather evidence before changing product code.\n\n- Separate observed symptom from inferred cause.\n- Trace inputs, state transitions, ownership boundaries, and failure output.\n- Rank hypotheses by evidence and cheap falsification value.\n- Do not edit until one credible mechanism explains evidence.\n- Stop exploration when evidence is sufficient to name cause or exact blocker.\n\nReport cause and proof. Make no fix unless task authorizes implementation."},{"id":"lean-build","summary":"Deliver narrow complete feature outcomes with architectural fit on top of mandatory simplicity Core.","activation":"classified","task_types":["feature"],"evidence_status":"structural-test-only","prompt_byte_budget":900,"conflicts":[],"precedence":100,"guardrails":["core_architecture_first_simplicity","preserve_correctness_controls","justify_material_tradeoffs"],"entry_condition":"task adds product behavior","stop_condition":"focused acceptance proof passes","instructions":"# Lean build\n\nNative Core's architecture-first simplicity remains mandatory. Turn feature into complete narrow outcome fitting system.\n\n- Derive observable acceptance and explicit non-goals from request and repository.\n- Trace entry point through layers owning invariants.\n- Deliver coherent end-to-end path across responsible layers; never force work into one file, direct expression, or local patch.\n- Reuse fitting seam. Refactor when patching duplicates behavior, weakens ownership, or hides root cause.\n- Omit modes, providers, config, extensibility, and polish unless acceptance needs them.\n- Add surface, dependency, service, config, or migration only for lifecycle design or acceptance; state material tradeoff.\n- Keep work runnable; preserve Core safety.\n\nExercise path. Run focused proof. Stop when acceptance passes. Report only material omissions and trigger."},{"id":"migration","summary":"Execute a reversible compatibility-safe transition without hidden destruction.","activation":"classified","task_types":["migration"],"evidence_status":"structural-test-only","prompt_byte_budget":950,"conflicts":[],"precedence":100,"guardrails":["preserve_rollback","preserve_data","verify_compatibility"],"entry_condition":"task changes persisted or public shape","stop_condition":"requested migration stage passes without implicit contraction","instructions":"# Migration\n\nMap current readers, writers, data shape, compatibility window, and ownership before editing.\n\n- Define forward path and rollback path.\n- Preserve existing data; make destructive steps explicit and separately authorized.\n- Keep mixed-version operation safe where rollout can overlap.\n- Sequence expand, migrate, verify, then contract when applicable.\n- Make retries idempotent and partial failure observable.\n- Verify old and new paths at required transition stages.\n\nStop after requested stage passes; do not perform later destructive contraction implicitly."},{"id":"safe-refactor","summary":"Restructure code while preserving observable behavior.","activation":"classified","task_types":["refactor"],"evidence_status":"structural-test-only","prompt_byte_budget":900,"conflicts":[],"precedence":100,"guardrails":["preserve_behavior","keep_intermediate_states_testable"],"entry_condition":"task requests structural change without behavior change","stop_condition":"before-and-after proof matches","instructions":"# Safe refactor\n\nDefine behavior-preservation boundary and establish verification before structural edits.\n\n- Keep feature changes outside refactor.\n- Move one ownership boundary at a time.\n- Preserve public interfaces, failure behavior, ordering, and compatibility unless explicitly scoped.\n- Keep intermediate states buildable and testable.\n- Avoid dependency or configuration growth without correctness need.\n\nRun same proof after change. Stop when behavior matches and requested structure is achieved."},{"id":"surgical-patch","summary":"Fix the narrowest responsible layer with regression proof.","activation":"classified","task_types":["bugfix"],"evidence_status":"structural-test-only","prompt_byte_budget":850,"conflicts":[],"precedence":100,"guardrails":["avoid_unrelated_cleanup","preserve_surrounding_behavior"],"entry_condition":"task fixes incorrect behavior","stop_condition":"failure is fixed and regression proof passes","instructions":"# Surgical patch\n\nReproduce failure first when economical; otherwise capture strongest available evidence.\n\n- Trace symptom to responsible mechanism.\n- Change narrowest layer that owns incorrect behavior.\n- Preserve unrelated behavior and user changes.\n- Avoid cleanup, renaming, and abstraction outside fix.\n- Add only regression proof relevant to task.\n\nRun focused proof plus nearest affected gate. Stop when failure is fixed and regression proof passes."},{"id":"verify-and-stop","summary":"Run the smallest sufficient proof set and stop without scope growth.","activation":"classified","task_types":["verification"],"evidence_status":"structural-test-only","prompt_byte_budget":850,"conflicts":[],"precedence":100,"guardrails":["no_unrequested_product_edit","report_unavailable_proof_exactly"],"entry_condition":"task asks for proof or completion validation","stop_condition":"acceptance proof is complete","instructions":"# Verify and stop\n\nTranslate acceptance conditions into smallest sufficient proof set.\n\n- Reuse still-current results with matching repository state.\n- Run focused checks before wider gates.\n- Distinguish pass, fail, unavailable, and blocked exactly.\n- Do not edit product code unless verification request includes fixes.\n- Do not add polish, cleanup, or unrelated tests after criteria pass.\n\nStop immediately when acceptance proof is complete. Report commands, results, and unresolved risk only."}],"target":"codex","activation":{"core":"developer_instructions+SessionStart","task":"UserPromptSubmit"}},"isInternal":false,"tokens":2123,"sizeBytes":9920},{"name":"pack.json","path":"skills/generated/gemini/pack.json","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/generated/gemini/pack.json","title":"Gemini Skill","category":"anthropic-skill","format":"json","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fgenerated%2Fgemini%2Fpack.json -->","frontmatter":{"schema":"caveman.native-pack.v1","id":"caveman-local","version":"2.2.0","protocol":1,"core":{"source":"native/native-core.md","mandatory":true,"prompt_token_budget":560,"estimated_tokens":556,"estimate_basis":"ceil_utf8_bytes_divided_by_3","instructions":"Build simplest complete system. Trace behavior and invariants before editing. System, user, and repository instructions outrank this default.\n\nConsider in order; accept first with correctness and architectural fit without distortion:\n1. Required behavior already exists: reuse it or change nothing.\n2. Responsible layer, type, helper, or pattern owns it: extend there.\n3. Standard library, native platform, browser, database, runtime, or installed dependency owns it: use it.\n4. Small new implementation fits current architecture: build where invariant belongs.\n5. Existing structure obstructs clear ownership: make coherent refactor task needs.\n\nOptimize total system complexity, clarity, and ownership—not lines or files changed. Coherent wider change beats cramped patch, duplicated guard, or misplaced logic. Reuse fitting abstractions; do not contort code to avoid abstraction. Prefer consolidation. Fix root cause.\n\nAvoid speculative features and extension points, single-implementation interfaces, configuration for fixed values, premature services, and imagined scaffolding. Dependency or public surface is valid for correct design or lower lifecycle cost; explain material tradeoff.\n\nSimplicity never removes trust-boundary validation, authorization, security, data-loss prevention, error handling, accessibility, migration or rollback safety, concurrency protection, compatibility, required tests, or explicitly requested behavior. Ask only for material public, security, data, billing, or hard-to-reverse choices.\n\nRun smallest sufficient proof. Report material changes, proof, unresolved risks, and only material omissions. Stop when task is satisfied."},"targets":{"claude":{"core":"SessionStart","task":"UserPromptSubmit"},"codex":{"core":"developer_instructions+SessionStart","task":"UserPromptSubmit"},"hermes":{"core":"pre_llm_call","task":"pre_llm_call"},"gemini":{"core":"BeforeAgent","task":"BeforeAgent"},"opencode":{"core":"experimental.chat.system.transform","task":"chat.message"},"aider":{"core":"read_only_conventions","task":"native_repository_map_authoritative"}},"skills":[{"id":"investigate-first","summary":"Gather evidence and establish a credible mechanism before editing.","activation":"classified","task_types":["investigation"],"evidence_status":"structural-test-only","prompt_byte_budget":900,"conflicts":[],"precedence":100,"guardrails":["no_edit_before_credible_hypothesis","diagnosis_does_not_authorize_fix"],"entry_condition":"cause is ambiguous","stop_condition":"cause or exact blocker is evidence-backed","instructions":"# Investigate first\n\nGather evidence before changing product code.\n\n- Separate observed symptom from inferred cause.\n- Trace inputs, state transitions, ownership boundaries, and failure output.\n- Rank hypotheses by evidence and cheap falsification value.\n- Do not edit until one credible mechanism explains evidence.\n- Stop exploration when evidence is sufficient to name cause or exact blocker.\n\nReport cause and proof. Make no fix unless task authorizes implementation."},{"id":"lean-build","summary":"Deliver narrow complete feature outcomes with architectural fit on top of mandatory simplicity Core.","activation":"classified","task_types":["feature"],"evidence_status":"structural-test-only","prompt_byte_budget":900,"conflicts":[],"precedence":100,"guardrails":["core_architecture_first_simplicity","preserve_correctness_controls","justify_material_tradeoffs"],"entry_condition":"task adds product behavior","stop_condition":"focused acceptance proof passes","instructions":"# Lean build\n\nNative Core's architecture-first simplicity remains mandatory. Turn feature into complete narrow outcome fitting system.\n\n- Derive observable acceptance and explicit non-goals from request and repository.\n- Trace entry point through layers owning invariants.\n- Deliver coherent end-to-end path across responsible layers; never force work into one file, direct expression, or local patch.\n- Reuse fitting seam. Refactor when patching duplicates behavior, weakens ownership, or hides root cause.\n- Omit modes, providers, config, extensibility, and polish unless acceptance needs them.\n- Add surface, dependency, service, config, or migration only for lifecycle design or acceptance; state material tradeoff.\n- Keep work runnable; preserve Core safety.\n\nExercise path. Run focused proof. Stop when acceptance passes. Report only material omissions and trigger."},{"id":"migration","summary":"Execute a reversible compatibility-safe transition without hidden destruction.","activation":"classified","task_types":["migration"],"evidence_status":"structural-test-only","prompt_byte_budget":950,"conflicts":[],"precedence":100,"guardrails":["preserve_rollback","preserve_data","verify_compatibility"],"entry_condition":"task changes persisted or public shape","stop_condition":"requested migration stage passes without implicit contraction","instructions":"# Migration\n\nMap current readers, writers, data shape, compatibility window, and ownership before editing.\n\n- Define forward path and rollback path.\n- Preserve existing data; make destructive steps explicit and separately authorized.\n- Keep mixed-version operation safe where rollout can overlap.\n- Sequence expand, migrate, verify, then contract when applicable.\n- Make retries idempotent and partial failure observable.\n- Verify old and new paths at required transition stages.\n\nStop after requested stage passes; do not perform later destructive contraction implicitly."},{"id":"safe-refactor","summary":"Restructure code while preserving observable behavior.","activation":"classified","task_types":["refactor"],"evidence_status":"structural-test-only","prompt_byte_budget":900,"conflicts":[],"precedence":100,"guardrails":["preserve_behavior","keep_intermediate_states_testable"],"entry_condition":"task requests structural change without behavior change","stop_condition":"before-and-after proof matches","instructions":"# Safe refactor\n\nDefine behavior-preservation boundary and establish verification before structural edits.\n\n- Keep feature changes outside refactor.\n- Move one ownership boundary at a time.\n- Preserve public interfaces, failure behavior, ordering, and compatibility unless explicitly scoped.\n- Keep intermediate states buildable and testable.\n- Avoid dependency or configuration growth without correctness need.\n\nRun same proof after change. Stop when behavior matches and requested structure is achieved."},{"id":"surgical-patch","summary":"Fix the narrowest responsible layer with regression proof.","activation":"classified","task_types":["bugfix"],"evidence_status":"structural-test-only","prompt_byte_budget":850,"conflicts":[],"precedence":100,"guardrails":["avoid_unrelated_cleanup","preserve_surrounding_behavior"],"entry_condition":"task fixes incorrect behavior","stop_condition":"failure is fixed and regression proof passes","instructions":"# Surgical patch\n\nReproduce failure first when economical; otherwise capture strongest available evidence.\n\n- Trace symptom to responsible mechanism.\n- Change narrowest layer that owns incorrect behavior.\n- Preserve unrelated behavior and user changes.\n- Avoid cleanup, renaming, and abstraction outside fix.\n- Add only regression proof relevant to task.\n\nRun focused proof plus nearest affected gate. Stop when failure is fixed and regression proof passes."},{"id":"verify-and-stop","summary":"Run the smallest sufficient proof set and stop without scope growth.","activation":"classified","task_types":["verification"],"evidence_status":"structural-test-only","prompt_byte_budget":850,"conflicts":[],"precedence":100,"guardrails":["no_unrequested_product_edit","report_unavailable_proof_exactly"],"entry_condition":"task asks for proof or completion validation","stop_condition":"acceptance proof is complete","instructions":"# Verify and stop\n\nTranslate acceptance conditions into smallest sufficient proof set.\n\n- Reuse still-current results with matching repository state.\n- Run focused checks before wider gates.\n- Distinguish pass, fail, unavailable, and blocked exactly.\n- Do not edit product code unless verification request includes fixes.\n- Do not add polish, cleanup, or unrelated tests after criteria pass.\n\nStop immediately when acceptance proof is complete. Report commands, results, and unresolved risk only."}],"target":"gemini","activation":{"core":"BeforeAgent","task":"BeforeAgent"}},"isInternal":false,"tokens":2119,"sizeBytes":9892},{"name":"pack.json","path":"skills/generated/hermes/pack.json","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/generated/hermes/pack.json","title":"Hermes Skill","category":"anthropic-skill","format":"json","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fgenerated%2Fhermes%2Fpack.json -->","frontmatter":{"schema":"caveman.native-pack.v1","id":"caveman-local","version":"2.2.0","protocol":1,"core":{"source":"native/native-core.md","mandatory":true,"prompt_token_budget":560,"estimated_tokens":556,"estimate_basis":"ceil_utf8_bytes_divided_by_3","instructions":"Build simplest complete system. Trace behavior and invariants before editing. System, user, and repository instructions outrank this default.\n\nConsider in order; accept first with correctness and architectural fit without distortion:\n1. Required behavior already exists: reuse it or change nothing.\n2. Responsible layer, type, helper, or pattern owns it: extend there.\n3. Standard library, native platform, browser, database, runtime, or installed dependency owns it: use it.\n4. Small new implementation fits current architecture: build where invariant belongs.\n5. Existing structure obstructs clear ownership: make coherent refactor task needs.\n\nOptimize total system complexity, clarity, and ownership—not lines or files changed. Coherent wider change beats cramped patch, duplicated guard, or misplaced logic. Reuse fitting abstractions; do not contort code to avoid abstraction. Prefer consolidation. Fix root cause.\n\nAvoid speculative features and extension points, single-implementation interfaces, configuration for fixed values, premature services, and imagined scaffolding. Dependency or public surface is valid for correct design or lower lifecycle cost; explain material tradeoff.\n\nSimplicity never removes trust-boundary validation, authorization, security, data-loss prevention, error handling, accessibility, migration or rollback safety, concurrency protection, compatibility, required tests, or explicitly requested behavior. Ask only for material public, security, data, billing, or hard-to-reverse choices.\n\nRun smallest sufficient proof. Report material changes, proof, unresolved risks, and only material omissions. Stop when task is satisfied."},"targets":{"claude":{"core":"SessionStart","task":"UserPromptSubmit"},"codex":{"core":"developer_instructions+SessionStart","task":"UserPromptSubmit"},"hermes":{"core":"pre_llm_call","task":"pre_llm_call"},"gemini":{"core":"BeforeAgent","task":"BeforeAgent"},"opencode":{"core":"experimental.chat.system.transform","task":"chat.message"},"aider":{"core":"read_only_conventions","task":"native_repository_map_authoritative"}},"skills":[{"id":"investigate-first","summary":"Gather evidence and establish a credible mechanism before editing.","activation":"classified","task_types":["investigation"],"evidence_status":"structural-test-only","prompt_byte_budget":900,"conflicts":[],"precedence":100,"guardrails":["no_edit_before_credible_hypothesis","diagnosis_does_not_authorize_fix"],"entry_condition":"cause is ambiguous","stop_condition":"cause or exact blocker is evidence-backed","instructions":"# Investigate first\n\nGather evidence before changing product code.\n\n- Separate observed symptom from inferred cause.\n- Trace inputs, state transitions, ownership boundaries, and failure output.\n- Rank hypotheses by evidence and cheap falsification value.\n- Do not edit until one credible mechanism explains evidence.\n- Stop exploration when evidence is sufficient to name cause or exact blocker.\n\nReport cause and proof. Make no fix unless task authorizes implementation."},{"id":"lean-build","summary":"Deliver narrow complete feature outcomes with architectural fit on top of mandatory simplicity Core.","activation":"classified","task_types":["feature"],"evidence_status":"structural-test-only","prompt_byte_budget":900,"conflicts":[],"precedence":100,"guardrails":["core_architecture_first_simplicity","preserve_correctness_controls","justify_material_tradeoffs"],"entry_condition":"task adds product behavior","stop_condition":"focused acceptance proof passes","instructions":"# Lean build\n\nNative Core's architecture-first simplicity remains mandatory. Turn feature into complete narrow outcome fitting system.\n\n- Derive observable acceptance and explicit non-goals from request and repository.\n- Trace entry point through layers owning invariants.\n- Deliver coherent end-to-end path across responsible layers; never force work into one file, direct expression, or local patch.\n- Reuse fitting seam. Refactor when patching duplicates behavior, weakens ownership, or hides root cause.\n- Omit modes, providers, config, extensibility, and polish unless acceptance needs them.\n- Add surface, dependency, service, config, or migration only for lifecycle design or acceptance; state material tradeoff.\n- Keep work runnable; preserve Core safety.\n\nExercise path. Run focused proof. Stop when acceptance passes. Report only material omissions and trigger."},{"id":"migration","summary":"Execute a reversible compatibility-safe transition without hidden destruction.","activation":"classified","task_types":["migration"],"evidence_status":"structural-test-only","prompt_byte_budget":950,"conflicts":[],"precedence":100,"guardrails":["preserve_rollback","preserve_data","verify_compatibility"],"entry_condition":"task changes persisted or public shape","stop_condition":"requested migration stage passes without implicit contraction","instructions":"# Migration\n\nMap current readers, writers, data shape, compatibility window, and ownership before editing.\n\n- Define forward path and rollback path.\n- Preserve existing data; make destructive steps explicit and separately authorized.\n- Keep mixed-version operation safe where rollout can overlap.\n- Sequence expand, migrate, verify, then contract when applicable.\n- Make retries idempotent and partial failure observable.\n- Verify old and new paths at required transition stages.\n\nStop after requested stage passes; do not perform later destructive contraction implicitly."},{"id":"safe-refactor","summary":"Restructure code while preserving observable behavior.","activation":"classified","task_types":["refactor"],"evidence_status":"structural-test-only","prompt_byte_budget":900,"conflicts":[],"precedence":100,"guardrails":["preserve_behavior","keep_intermediate_states_testable"],"entry_condition":"task requests structural change without behavior change","stop_condition":"before-and-after proof matches","instructions":"# Safe refactor\n\nDefine behavior-preservation boundary and establish verification before structural edits.\n\n- Keep feature changes outside refactor.\n- Move one ownership boundary at a time.\n- Preserve public interfaces, failure behavior, ordering, and compatibility unless explicitly scoped.\n- Keep intermediate states buildable and testable.\n- Avoid dependency or configuration growth without correctness need.\n\nRun same proof after change. Stop when behavior matches and requested structure is achieved."},{"id":"surgical-patch","summary":"Fix the narrowest responsible layer with regression proof.","activation":"classified","task_types":["bugfix"],"evidence_status":"structural-test-only","prompt_byte_budget":850,"conflicts":[],"precedence":100,"guardrails":["avoid_unrelated_cleanup","preserve_surrounding_behavior"],"entry_condition":"task fixes incorrect behavior","stop_condition":"failure is fixed and regression proof passes","instructions":"# Surgical patch\n\nReproduce failure first when economical; otherwise capture strongest available evidence.\n\n- Trace symptom to responsible mechanism.\n- Change narrowest layer that owns incorrect behavior.\n- Preserve unrelated behavior and user changes.\n- Avoid cleanup, renaming, and abstraction outside fix.\n- Add only regression proof relevant to task.\n\nRun focused proof plus nearest affected gate. Stop when failure is fixed and regression proof passes."},{"id":"verify-and-stop","summary":"Run the smallest sufficient proof set and stop without scope growth.","activation":"classified","task_types":["verification"],"evidence_status":"structural-test-only","prompt_byte_budget":850,"conflicts":[],"precedence":100,"guardrails":["no_unrequested_product_edit","report_unavailable_proof_exactly"],"entry_condition":"task asks for proof or completion validation","stop_condition":"acceptance proof is complete","instructions":"# Verify and stop\n\nTranslate acceptance conditions into smallest sufficient proof set.\n\n- Reuse still-current results with matching repository state.\n- Run focused checks before wider gates.\n- Distinguish pass, fail, unavailable, and blocked exactly.\n- Do not edit product code unless verification request includes fixes.\n- Do not add polish, cleanup, or unrelated tests after criteria pass.\n\nStop immediately when acceptance proof is complete. Report commands, results, and unresolved risk only."}],"target":"hermes","activation":{"core":"pre_llm_call","task":"pre_llm_call"}},"isInternal":false,"tokens":2123,"sizeBytes":9894},{"name":"pack.json","path":"skills/generated/opencode/pack.json","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/generated/opencode/pack.json","title":"Opencode Skill","category":"anthropic-skill","format":"json","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fgenerated%2Fopencode%2Fpack.json -->","frontmatter":{"schema":"caveman.native-pack.v1","id":"caveman-local","version":"2.2.0","protocol":1,"core":{"source":"native/native-core.md","mandatory":true,"prompt_token_budget":560,"estimated_tokens":556,"estimate_basis":"ceil_utf8_bytes_divided_by_3","instructions":"Build simplest complete system. Trace behavior and invariants before editing. System, user, and repository instructions outrank this default.\n\nConsider in order; accept first with correctness and architectural fit without distortion:\n1. Required behavior already exists: reuse it or change nothing.\n2. Responsible layer, type, helper, or pattern owns it: extend there.\n3. Standard library, native platform, browser, database, runtime, or installed dependency owns it: use it.\n4. Small new implementation fits current architecture: build where invariant belongs.\n5. Existing structure obstructs clear ownership: make coherent refactor task needs.\n\nOptimize total system complexity, clarity, and ownership—not lines or files changed. Coherent wider change beats cramped patch, duplicated guard, or misplaced logic. Reuse fitting abstractions; do not contort code to avoid abstraction. Prefer consolidation. Fix root cause.\n\nAvoid speculative features and extension points, single-implementation interfaces, configuration for fixed values, premature services, and imagined scaffolding. Dependency or public surface is valid for correct design or lower lifecycle cost; explain material tradeoff.\n\nSimplicity never removes trust-boundary validation, authorization, security, data-loss prevention, error handling, accessibility, migration or rollback safety, concurrency protection, compatibility, required tests, or explicitly requested behavior. Ask only for material public, security, data, billing, or hard-to-reverse choices.\n\nRun smallest sufficient proof. Report material changes, proof, unresolved risks, and only material omissions. Stop when task is satisfied."},"targets":{"claude":{"core":"SessionStart","task":"UserPromptSubmit"},"codex":{"core":"developer_instructions+SessionStart","task":"UserPromptSubmit"},"hermes":{"core":"pre_llm_call","task":"pre_llm_call"},"gemini":{"core":"BeforeAgent","task":"BeforeAgent"},"opencode":{"core":"experimental.chat.system.transform","task":"chat.message"},"aider":{"core":"read_only_conventions","task":"native_repository_map_authoritative"}},"skills":[{"id":"investigate-first","summary":"Gather evidence and establish a credible mechanism before editing.","activation":"classified","task_types":["investigation"],"evidence_status":"structural-test-only","prompt_byte_budget":900,"conflicts":[],"precedence":100,"guardrails":["no_edit_before_credible_hypothesis","diagnosis_does_not_authorize_fix"],"entry_condition":"cause is ambiguous","stop_condition":"cause or exact blocker is evidence-backed","instructions":"# Investigate first\n\nGather evidence before changing product code.\n\n- Separate observed symptom from inferred cause.\n- Trace inputs, state transitions, ownership boundaries, and failure output.\n- Rank hypotheses by evidence and cheap falsification value.\n- Do not edit until one credible mechanism explains evidence.\n- Stop exploration when evidence is sufficient to name cause or exact blocker.\n\nReport cause and proof. Make no fix unless task authorizes implementation."},{"id":"lean-build","summary":"Deliver narrow complete feature outcomes with architectural fit on top of mandatory simplicity Core.","activation":"classified","task_types":["feature"],"evidence_status":"structural-test-only","prompt_byte_budget":900,"conflicts":[],"precedence":100,"guardrails":["core_architecture_first_simplicity","preserve_correctness_controls","justify_material_tradeoffs"],"entry_condition":"task adds product behavior","stop_condition":"focused acceptance proof passes","instructions":"# Lean build\n\nNative Core's architecture-first simplicity remains mandatory. Turn feature into complete narrow outcome fitting system.\n\n- Derive observable acceptance and explicit non-goals from request and repository.\n- Trace entry point through layers owning invariants.\n- Deliver coherent end-to-end path across responsible layers; never force work into one file, direct expression, or local patch.\n- Reuse fitting seam. Refactor when patching duplicates behavior, weakens ownership, or hides root cause.\n- Omit modes, providers, config, extensibility, and polish unless acceptance needs them.\n- Add surface, dependency, service, config, or migration only for lifecycle design or acceptance; state material tradeoff.\n- Keep work runnable; preserve Core safety.\n\nExercise path. Run focused proof. Stop when acceptance passes. Report only material omissions and trigger."},{"id":"migration","summary":"Execute a reversible compatibility-safe transition without hidden destruction.","activation":"classified","task_types":["migration"],"evidence_status":"structural-test-only","prompt_byte_budget":950,"conflicts":[],"precedence":100,"guardrails":["preserve_rollback","preserve_data","verify_compatibility"],"entry_condition":"task changes persisted or public shape","stop_condition":"requested migration stage passes without implicit contraction","instructions":"# Migration\n\nMap current readers, writers, data shape, compatibility window, and ownership before editing.\n\n- Define forward path and rollback path.\n- Preserve existing data; make destructive steps explicit and separately authorized.\n- Keep mixed-version operation safe where rollout can overlap.\n- Sequence expand, migrate, verify, then contract when applicable.\n- Make retries idempotent and partial failure observable.\n- Verify old and new paths at required transition stages.\n\nStop after requested stage passes; do not perform later destructive contraction implicitly."},{"id":"safe-refactor","summary":"Restructure code while preserving observable behavior.","activation":"classified","task_types":["refactor"],"evidence_status":"structural-test-only","prompt_byte_budget":900,"conflicts":[],"precedence":100,"guardrails":["preserve_behavior","keep_intermediate_states_testable"],"entry_condition":"task requests structural change without behavior change","stop_condition":"before-and-after proof matches","instructions":"# Safe refactor\n\nDefine behavior-preservation boundary and establish verification before structural edits.\n\n- Keep feature changes outside refactor.\n- Move one ownership boundary at a time.\n- Preserve public interfaces, failure behavior, ordering, and compatibility unless explicitly scoped.\n- Keep intermediate states buildable and testable.\n- Avoid dependency or configuration growth without correctness need.\n\nRun same proof after change. Stop when behavior matches and requested structure is achieved."},{"id":"surgical-patch","summary":"Fix the narrowest responsible layer with regression proof.","activation":"classified","task_types":["bugfix"],"evidence_status":"structural-test-only","prompt_byte_budget":850,"conflicts":[],"precedence":100,"guardrails":["avoid_unrelated_cleanup","preserve_surrounding_behavior"],"entry_condition":"task fixes incorrect behavior","stop_condition":"failure is fixed and regression proof passes","instructions":"# Surgical patch\n\nReproduce failure first when economical; otherwise capture strongest available evidence.\n\n- Trace symptom to responsible mechanism.\n- Change narrowest layer that owns incorrect behavior.\n- Preserve unrelated behavior and user changes.\n- Avoid cleanup, renaming, and abstraction outside fix.\n- Add only regression proof relevant to task.\n\nRun focused proof plus nearest affected gate. Stop when failure is fixed and regression proof passes."},{"id":"verify-and-stop","summary":"Run the smallest sufficient proof set and stop without scope growth.","activation":"classified","task_types":["verification"],"evidence_status":"structural-test-only","prompt_byte_budget":850,"conflicts":[],"precedence":100,"guardrails":["no_unrequested_product_edit","report_unavailable_proof_exactly"],"entry_condition":"task asks for proof or completion validation","stop_condition":"acceptance proof is complete","instructions":"# Verify and stop\n\nTranslate acceptance conditions into smallest sufficient proof set.\n\n- Reuse still-current results with matching repository state.\n- Run focused checks before wider gates.\n- Distinguish pass, fail, unavailable, and blocked exactly.\n- Do not edit product code unless verification request includes fixes.\n- Do not add polish, cleanup, or unrelated tests after criteria pass.\n\nStop immediately when acceptance proof is complete. Report commands, results, and unresolved risk only."}],"target":"opencode","activation":{"core":"experimental.chat.system.transform","task":"chat.message"}},"isInternal":false,"tokens":2121,"sizeBytes":9918},{"name":"openai.yaml","path":"skills/investigate-first/agents/openai.yaml","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/investigate-first/agents/openai.yaml","title":"Agents Skill","category":"anthropic-skill","format":"yaml","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Finvestigate-first%2Fagents%2Fopenai.yaml -->","isInternal":false,"tokens":41,"sizeBytes":202},{"name":"SKILL.md","path":"skills/investigate-first/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/investigate-first/SKILL.md","title":"investigate-first","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Finvestigate-first%2FSKILL.md -->","frontmatter":{"name":"investigate-first","description":"Diagnose ambiguous failures before editing. Use for unknown causes, intermittent behavior, performance regressions, or investigations needing evidence-ranked hypotheses."},"isInternal":false,"tokens":121,"sizeBytes":688},{"name":"openai.yaml","path":"skills/lean-build/agents/openai.yaml","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/lean-build/agents/openai.yaml","title":"Agents Skill","category":"anthropic-skill","format":"yaml","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Flean-build%2Fagents%2Fopenai.yaml -->","isInternal":false,"tokens":38,"sizeBytes":190},{"name":"SKILL.md","path":"skills/lean-build/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/lean-build/SKILL.md","title":"lean-build","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Flean-build%2FSKILL.md -->","frontmatter":{"name":"lean-build","description":"Build feature work with high overbuilding risk. Use for new behavior, product slices, or integrations where repository reuse, strict scope, and an explicit stop condition matter."},"isInternal":false,"tokens":211,"sizeBytes":1090},{"name":"README.md","path":"skills/megacave/README.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/megacave/README.md","title":"Megacave Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fmegacave%2FREADME.md -->","isInternal":false,"tokens":232,"sizeBytes":809},{"name":"SKILL.md","path":"skills/megacave/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/megacave/SKILL.md","title":"megacave","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fmegacave%2FSKILL.md -->","frontmatter":{"name":"megacave","description":"Caveman in Classical Chinese: 文言文 register, far fewer characters, technical terms verbatim. Invoke only with /megacave or /caveman wenyan. Stays on until \"stop caveman\" or \"normal mode\".\n","disable-model-invocation":true,"disableModelInvocation":true},"isInternal":false,"tokens":840,"sizeBytes":2858},{"name":"openai.yaml","path":"skills/migration/agents/openai.yaml","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/migration/agents/openai.yaml","title":"Agents Skill","category":"anthropic-skill","format":"yaml","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fmigration%2Fagents%2Fopenai.yaml -->","isInternal":false,"tokens":38,"sizeBytes":203},{"name":"SKILL.md","path":"skills/migration/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/migration/SKILL.md","title":"migration","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fmigration%2FSKILL.md -->","frontmatter":{"name":"migration","description":"Implement reversible compatibility-safe transitions. Use for schema, data, API, protocol, configuration, or dependency migrations requiring rollback and preservation proof."},"isInternal":false,"tokens":136,"sizeBytes":784},{"name":"native-core.md","path":"skills/native/native-core.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/native/native-core.md","title":"Native Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fnative%2Fnative-core.md -->","isInternal":false,"tokens":304,"sizeBytes":1667},{"name":"registry.json","path":"skills/registry.json","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/registry.json","title":"Skills Skill","category":"anthropic-skill","format":"json","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fregistry.json -->","frontmatter":{"schema_version":"2","native_pack":{"id":"caveman-local","version":"2.2.0","protocol":1,"core_source":"native/native-core.md","core_prompt_token_budget":560,"targets":["claude","codex","hermes","gemini","opencode","aider"]},"preserved_skill_ids":["cavecrew","caveman-commit","caveman-compress","caveman-help","caveman-review","caveman-stats"],"skills":[{"id":"caveman","summary":"Compress agent output style without changing technical literals.","delivery":["cli"],"suites":["output"]},{"id":"caveman-discover","summary":"Find and label every LLM workflow in a repository.","delivery":["cli","web"],"suites":["agent-native"]},{"id":"caveman-explore","summary":"Explore repositories read-only with compact file citations.","delivery":["cli"],"suites":["local"]},{"id":"caveman-learn","summary":"Apply consent-gated fixes from a local Caveman learn report.","delivery":["cli"],"suites":["local"]},{"id":"caveman-manage","summary":"Review eval-gated experiments and block unsafe lifecycle actions.","delivery":["cli"],"suites":["agent-native"]},{"id":"caveman-optimize","summary":"Evaluate an operator-chosen report-only observation with paired evidence.","delivery":["cli","web"],"suites":["agent-native"]},{"id":"caveman-evidence-review","summary":"Review costs, quality, workflows, traces, and savings evidence.","delivery":["cli"],"suites":["agent-native"]},{"id":"caveman-setup","summary":"Wire a repository through Caveman in record mode and verify it.","delivery":["cli","web"],"suites":["agent-native"]},{"id":"investigate-first","summary":"Gather evidence and establish a credible mechanism before editing.","delivery":["native"],"suites":[],"activation":"classified","task_types":["investigation"],"evidence_status":"structural-test-only","prompt_byte_budget":900,"conflicts":[],"precedence":100,"guardrails":["no_edit_before_credible_hypothesis","diagnosis_does_not_authorize_fix"],"entry_condition":"cause is ambiguous","stop_condition":"cause or exact blocker is evidence-backed"},{"id":"lean-build","summary":"Deliver narrow complete feature outcomes with architectural fit on top of mandatory simplicity Core.","delivery":["native"],"suites":[],"activation":"classified","task_types":["feature"],"evidence_status":"structural-test-only","prompt_byte_budget":900,"conflicts":[],"precedence":100,"guardrails":["core_architecture_first_simplicity","preserve_correctness_controls","justify_material_tradeoffs"],"entry_condition":"task adds product behavior","stop_condition":"focused acceptance proof passes"},{"id":"migration","summary":"Execute a reversible compatibility-safe transition without hidden destruction.","delivery":["native"],"suites":[],"activation":"classified","task_types":["migration"],"evidence_status":"structural-test-only","prompt_byte_budget":950,"conflicts":[],"precedence":100,"guardrails":["preserve_rollback","preserve_data","verify_compatibility"],"entry_condition":"task changes persisted or public shape","stop_condition":"requested migration stage passes without implicit contraction"},{"id":"safe-refactor","summary":"Restructure code while preserving observable behavior.","delivery":["native"],"suites":[],"activation":"classified","task_types":["refactor"],"evidence_status":"structural-test-only","prompt_byte_budget":900,"conflicts":[],"precedence":100,"guardrails":["preserve_behavior","keep_intermediate_states_testable"],"entry_condition":"task requests structural change without behavior change","stop_condition":"before-and-after proof matches"},{"id":"surgical-patch","summary":"Fix the narrowest responsible layer with regression proof.","delivery":["native"],"suites":[],"activation":"classified","task_types":["bugfix"],"evidence_status":"structural-test-only","prompt_byte_budget":850,"conflicts":[],"precedence":100,"guardrails":["avoid_unrelated_cleanup","preserve_surrounding_behavior"],"entry_condition":"task fixes incorrect behavior","stop_condition":"failure is fixed and regression proof passes"},{"id":"verify-and-stop","summary":"Run the smallest sufficient proof set and stop without scope growth.","delivery":["native"],"suites":[],"activation":"classified","task_types":["verification"],"evidence_status":"structural-test-only","prompt_byte_budget":850,"conflicts":[],"precedence":100,"guardrails":["no_unrequested_product_edit","report_unavailable_proof_exactly"],"entry_condition":"task asks for proof or completion validation","stop_condition":"acceptance proof is complete"},{"id":"ultracave","summary":"Caveman at maximum compression: fragments, one word when one word is enough, each fact once.","delivery":["cli"],"suites":["output"]},{"id":"megacave","summary":"Caveman in Classical Chinese: wenyan register, technical terms verbatim.","delivery":["cli"],"suites":["output"]}],"suites":{"agent-native":["caveman-setup","caveman-discover","caveman-evidence-review","caveman-optimize","caveman-manage"],"local":["caveman-explore","caveman-learn"],"output":["caveman","ultracave","megacave"]}},"isInternal":false,"tokens":1742,"sizeBytes":7030},{"name":"openai.yaml","path":"skills/safe-refactor/agents/openai.yaml","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/safe-refactor/agents/openai.yaml","title":"Agents Skill","category":"anthropic-skill","format":"yaml","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fsafe-refactor%2Fagents%2Fopenai.yaml -->","isInternal":false,"tokens":42,"sizeBytes":201},{"name":"SKILL.md","path":"skills/safe-refactor/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/safe-refactor/SKILL.md","title":"safe-refactor","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fsafe-refactor%2FSKILL.md -->","frontmatter":{"name":"safe-refactor","description":"Restructure code while preserving behavior. Use for extraction, consolidation, ownership moves, or cleanup where verification must bracket structural edits."},"isInternal":false,"tokens":120,"sizeBytes":705},{"name":"openai.yaml","path":"skills/surgical-patch/agents/openai.yaml","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/surgical-patch/agents/openai.yaml","title":"Agents Skill","category":"anthropic-skill","format":"yaml","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fsurgical-patch%2Fagents%2Fopenai.yaml -->","isInternal":false,"tokens":44,"sizeBytes":194},{"name":"SKILL.md","path":"skills/surgical-patch/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/surgical-patch/SKILL.md","title":"surgical-patch","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fsurgical-patch%2FSKILL.md -->","frontmatter":{"name":"surgical-patch","description":"Fix bugs and small behavior changes at the narrowest responsible layer. Use when regression proof, preserved surrounding behavior, and task-relevant tests matter."},"isInternal":false,"tokens":119,"sizeBytes":664},{"name":"README.md","path":"skills/ultracave/README.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/ultracave/README.md","title":"Ultracave Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fultracave%2FREADME.md -->","isInternal":false,"tokens":224,"sizeBytes":885},{"name":"SKILL.md","path":"skills/ultracave/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/ultracave/SKILL.md","title":"ultracave","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fultracave%2FSKILL.md -->","frontmatter":{"name":"ultracave","description":"Caveman at maximum compression: fragments, one word when one word is enough, each fact once. Invoke only with /ultracave or /caveman ultra. Stays on until \"stop caveman\" or \"normal mode\".\n","disable-model-invocation":true,"disableModelInvocation":true},"isInternal":false,"tokens":609,"sizeBytes":2513},{"name":"openai.yaml","path":"skills/verify-and-stop/agents/openai.yaml","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/verify-and-stop/agents/openai.yaml","title":"Agents Skill","category":"anthropic-skill","format":"yaml","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fverify-and-stop%2Fagents%2Fopenai.yaml -->","isInternal":false,"tokens":41,"sizeBytes":197},{"name":"SKILL.md","path":"skills/verify-and-stop/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/skills/verify-and-stop/SKILL.md","title":"verify-and-stop","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=skills%2Fverify-and-stop%2FSKILL.md -->","frontmatter":{"name":"verify-and-stop","description":"Prove existing work meets acceptance conditions without expanding scope. Use for validation-only tasks, completion checks, focused gate runs, and last-mile proof."},"isInternal":false,"tokens":130,"sizeBytes":704},{"name":"marketplace.json","path":".claude-plugin/marketplace.json","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/.claude-plugin/marketplace.json","title":"caveman","category":"marketplace","format":"json","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=.claude-plugin%2Fmarketplace.json -->","frontmatter":{"$schema":"https://anthropic.com/claude-code/marketplace.schema.json","name":"caveman","description":"Ultra-compressed communication mode. Shortens prose while preserving technical terms, code, commands, and exact errors.","owner":{"name":"Julius Brussee","url":"https://github.com/JuliusBrussee"},"plugins":[{"name":"caveman","description":"Talk like caveman. Cut filler. Keep technical details explicit.","source":"./","category":"productivity"}]},"isInternal":false,"tokens":144,"sizeBytes":532},{"name":"plugin.json","path":".claude-plugin/plugin.json","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/.claude-plugin/plugin.json","title":"caveman","category":"plugin-manifest","format":"json","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=.claude-plugin%2Fplugin.json -->","frontmatter":{"name":"caveman","version":"4.0.1","description":"Ultra-compressed communication mode. Shortens prose while preserving technical terms, code, commands, and exact errors.","author":{"name":"Julius Brussee","url":"https://github.com/JuliusBrussee"},"hooks":{"SessionStart":[{"hooks":[{"type":"command","command":"command -v node >/dev/null 2>&1 || exit 0; HOOK_ROOT=$(printf %s \"${CLAUDE_PLUGIN_ROOT}\" | sed 's|^/\\([a-zA-Z]\\)/|\\1:/|'); node \"$HOOK_ROOT/src/hooks/caveman-activate.js\"","timeout":30,"statusMessage":"Loading caveman mode..."}]}],"SubagentStart":[{"hooks":[{"type":"command","command":"command -v node >/dev/null 2>&1 || exit 0; HOOK_ROOT=$(printf %s \"${CLAUDE_PLUGIN_ROOT}\" | sed 's|^/\\([a-zA-Z]\\)/|\\1:/|'); node \"$HOOK_ROOT/src/hooks/caveman-activate.js\" --subagent","timeout":30,"statusMessage":"Loading caveman mode for subagent..."}]}],"UserPromptSubmit":[{"hooks":[{"type":"command","command":"command -v node >/dev/null 2>&1 || exit 0; HOOK_ROOT=$(printf %s \"${CLAUDE_PLUGIN_ROOT}\" | sed 's|^/\\([a-zA-Z]\\)/|\\1:/|'); node \"$HOOK_ROOT/src/hooks/caveman-mode-tracker.js\"","timeout":30,"statusMessage":"Tracking caveman mode..."}]}],"SessionEnd":[{"hooks":[{"type":"command","command":"command -v node >/dev/null 2>&1 || exit 0; HOOK_ROOT=$(printf %s \"${CLAUDE_PLUGIN_ROOT}\" | sed 's|^/\\([a-zA-Z]\\)/|\\1:/|'); node \"$HOOK_ROOT/src/hooks/caveman-stats.js\" --record","timeout":5,"statusMessage":"Recording caveman stats..."}]}]}},"isInternal":false,"tokens":566,"sizeBytes":1998},{"name":"plugin.json","path":".cursor-plugin/plugin.json","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/.cursor-plugin/plugin.json","title":"caveman","category":"plugin-manifest","format":"json","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=.cursor-plugin%2Fplugin.json -->","frontmatter":{"name":"caveman","displayName":"Caveman","description":"Ultra-compressed communication mode. Shortens prose while preserving technical terms, code, commands, and exact errors.","version":"4.0.1","author":{"name":"Julius Brussee","url":"https://github.com/JuliusBrussee"},"skills":"./skills/","hooks":"./hooks/hooks-cursor.json"},"isInternal":false,"tokens":110,"sizeBytes":374},{"name":"README.md","path":"packages/cli/README.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/packages/cli/README.md","title":"cli Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=packages%2Fcli%2FREADME.md -->","isInternal":false,"tokens":3302,"sizeBytes":13913},{"name":"README.md","path":"packages/middleware/python/README.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/packages/middleware/python/README.md","title":"python Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=packages%2Fmiddleware%2Fpython%2FREADME.md -->","isInternal":false,"tokens":2995,"sizeBytes":11462},{"name":"README.md","path":"packages/middleware/typescript/README.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/packages/middleware/typescript/README.md","title":"typescript Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=packages%2Fmiddleware%2Ftypescript%2FREADME.md -->","isInternal":false,"tokens":2480,"sizeBytes":10499},{"name":"README.md","path":"packages/pi-extension/README.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/packages/pi-extension/README.md","title":"pi-extension Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=packages%2Fpi-extension%2FREADME.md -->","isInternal":false,"tokens":840,"sizeBytes":3657},{"name":"README.md","path":"packages/sdk/python/README.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/packages/sdk/python/README.md","title":"python Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=packages%2Fsdk%2Fpython%2FREADME.md -->","isInternal":false,"tokens":1197,"sizeBytes":5166},{"name":"README.md","path":"packages/sdk/typescript/README.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/packages/sdk/typescript/README.md","title":"typescript Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=packages%2Fsdk%2Ftypescript%2FREADME.md -->","isInternal":false,"tokens":1351,"sizeBytes":5726},{"name":"README.md","path":"packages/shared/contracts/conformance/README.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/packages/shared/contracts/conformance/README.md","title":"conformance Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=packages%2Fshared%2Fcontracts%2Fconformance%2FREADME.md -->","isInternal":false,"tokens":938,"sizeBytes":3767},{"name":"README.md","path":"packages/subagent-tax/README.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/packages/subagent-tax/README.md","title":"subagent-tax Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=packages%2Fsubagent-tax%2FREADME.md -->","isInternal":false,"tokens":1163,"sizeBytes":4636},{"name":"plugin.json","path":"plugins/caveman/.codex-plugin/plugin.json","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/plugins/caveman/.codex-plugin/plugin.json","title":"caveman","category":"plugin-manifest","format":"json","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=plugins%2Fcaveman%2F.codex-plugin%2Fplugin.json -->","frontmatter":{"name":"caveman","version":"4.0.1","description":"Ultra-compressed communication mode. Cut filler. Keep technical accuracy.","author":{"name":"Julius Brussee","url":"https://github.com/JuliusBrussee"},"homepage":"https://github.com/JuliusBrussee/caveman","repository":"https://github.com/JuliusBrussee/caveman","license":"Apache-2.0","keywords":["productivity","communication","brevity","writing"],"skills":"./skills/","interface":{"displayName":"Caveman","shortDescription":"Talk like caveman. Cut filler. Keep technical accuracy.","longDescription":"Ultra-compressed communication mode for Codex. Use fewer words. Keep exact technical substance.","developerName":"Julius Brussee","category":"Productivity","capabilities":["Write"],"websiteURL":"https://github.com/JuliusBrussee/caveman","privacyPolicyURL":"https://github.com/JuliusBrussee/caveman/blob/main/README.md","termsOfServiceURL":"https://github.com/JuliusBrussee/caveman/blob/main/LICENSE","defaultPrompt":["Use caveman mode. Cut filler. Keep technical accuracy."],"composerIcon":"./assets/caveman-small.svg","logo":"./assets/caveman.svg","screenshots":[],"brandColor":"#6B7280"}},"isInternal":false,"tokens":379,"sizeBytes":1334},{"name":"README.md","path":"src/plugins/opencode/README.md","rawUrl":"https://raw.githubusercontent.com/JuliusBrussee/caveman/HEAD/src/plugins/opencode/README.md","title":"opencode Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/JuliusBrussee/caveman?path=src%2Fplugins%2Fopencode%2FREADME.md -->","isInternal":false,"tokens":711,"sizeBytes":2613}],"systemPromptSnippet":"<yakaai_skills repo=\"JuliusBrussee/caveman\">\n<!-- File: AGENTS.md (Tokens: ~202 | Category: root-instruction) -->\nRead `CLAUDE.md` before repository work. Binding routing: this repo owns\nskills, Engine, and MV3 extension; Agent SDK work belongs in\n`/Users/julb/Desktop/GitHub/caveman-agent-sdk`; Browse driver/MCP/benchmark/plugin\nwork belongs in `/Users/julb/Desktop/GitHub/caveman-browse`; proprietary Pebble\nproduct work belongs in `/Users/julb/Desktop/GitHub/caveman-coding-agent`.\n\nVisibility: this repo and `caveman-browse` are public now;\n`caveman-agent-sdk` is private during development with planned public release\nafter its gates; `caveman-coding-agent` and Caveman-Cloud remain private\ncommercial source.\n\n@./skills/caveman/SKILL.md\n@./skills/caveman-commit/SKILL.md\n@./skills/caveman-review/SKILL.md\n@./skills/caveman-compress/SKILL.md\n\n<!-- File: engine/AGENTS.md (Tokens: ~1211 | Category: root-instruction) -->\n# engine — the content-aware Caveman Engine (commercial, binary-distributed)\n\nDetect a payload's type → route to a safety-classed compressor → count the token reduction\n→ store the original for recovery. The stable four-call API (`Compress`/`Retrieve`/`Detect`/`Stats`)\nis what the proxy, CLI, SDKs, MCP, and WASM build all share. Everything it reports is\n`inferred`; it never says `verified`.\n\n## Layout\n- `engine.go` — the `Engine` core: detect → route → compress → token ratio → CCR. `record` mode + miss + parse-fail + not-smaller + no-store all pass through.\n- `result.go` — `Result`/`Options`/`Mode`; unknown mode fails closed to `record`.\n- `detect.go` — content router (`json`/`diff`/`code`/`log`/`search-result`/`html`/`tabular`/`config`/`terminal`/`text`); low confidence → `text`.\n- `listing.go` — strips a read tool's line-number gutter (`1\\t{`, `2\\t  \"unit\"`) before Detect and before the compressor, then restores it after. Agents send file *listings*, not files; the gutter is presentation, and left in place it made every guttered payload detect as `text` and compress ~0%. Restoration keeps each surviving line's ORIGINAL number and is declined entirely for transforms that restructure rather than elide (re-encoded JSON), because numbers that describe nothing are worse than none.\n- `safety/` — the S0–S4 registry; S4 is lossy and `RequiresCCR`. Unknown class → fail closed.\n- `tokens/` — `Counter` interface; default is an offline, vocab-embedded BPE tokenizer (o200k_base). `inferred` always.\n- `contextwindow/` — deterministic BM25 context packer with recency/error/priority signals and token-budget accounting.\n- `compressors/` — `Compressor` interface + registry; structural JSON/log/search/diff/text/HTML/table/config/code compressors plus forced-only TOON/tool-schema/accessibility/repetition paths — `Default()` registers 14. A compressor is a pure byte transform.\n- `ccr/` — `~/.caveman/ccr.db` SQLite recovery store; content-addressed handles; byte-exact `Get`.\n- `pixel/` — pxpipe port (MIT, see its NOTICE): text→PNG request compression. Embedded glyph atlases + renderer + profitability gate + per-wire-format transforms (Anthropic/OpenAI/Gemini). S4-lossy, allowlist-gated (`CAVE_PIXEL_MODELS`, default `claude-fable-5,gpt-5.6`), consumed by the proxy's `pixel` mode; never wired into `Detect` and never imported by the WASM build (≈4 MB assets).\n- `evals/` — the local eval harness + fail-closed graders + embedded `fixtures/`. `Run()` is the quality gate.\n- `cmd/caveman-engine/` — the binary the CLI shells out to: `compress | detect | retrieve | stats | registry | toon encode|decode | evals run | pixel render|simulate`. `toon` is the stateless (no-CCR) JSON⇄TOON converter; both directions fail closed.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=engine` / `make product-test PRODUCT=engine`.\n- A new compressor is a self-contained file in `compressors/` + tests, registered in `Default()`. Forced-only compressors such as `toolschema` and `toon` must not be added to `Detect`.\n- Compressors never count tokens, touch CCR, or hit the network — the engine core does that around them.\n- **The `toolschema` transform is client-side-only today.** Registration makes it forceable by\n  engine callers; it does not make it reachable from managed-gateway traffic. Provider adapters\n  intentionally keep tool arrays in the frozen prompt-cache prefix, and no billed route invokes\n  this compressor. Engine API/CLI callers may force it locally, and `caveman-shrink` is its\n  dedicated product surface; those reductions remain local and `inferred`. Managed gateway has a\n  separate S2 tool-search/deferral path; do not conflate it with compression. Any future gateway\n  route for this transform needs cache-versus-schema cost arithmetic, stable byte-identical prefix\n  output, and an eval gate before activation.\n\n## Gotchas (honesty invariants — correctness, not style)\n- **fail-closed transforms**: every compressor passes the original through unchanged on any parse problem; the engine also passes through when the result is not smaller in tokens. Reserve **byte-safe** for classes whose `safety.Info.ByteSafe` is true.\n- **CCR-or-pass-through**: a lossy (S4) result is only emitted if its original was stored; with no store, the engine fails closed to pass-through.\n- **inferred-only**: ratios are token estimates labeled `inferred`; never `verified`, never re-projected.\n- **fail-closed**: unknown mode → `record`; unknown content type → `text`; unknown grader → `passed:false`.\n- **cgo**: full code compression (Python/JS/TS) needs the tree-sitter build; the cgo-free build compresses Go only. The embedded eval fixtures cover all three under cgo.\n- **boundary**: this is `public/` — never import `cloud/…`. `make check-boundaries` enforces it.\n\nSee ../../CLAUDE.md (root)\n\n<!-- File: extension/AGENTS.md (Tokens: ~758 | Category: root-instruction) -->\n# extension — \"Caveman Mode\" browser extension (MV3, no bundler)\n\nThe consumer top-of-funnel: a toggle that, on ChatGPT / Claude / Gemini, prepends the **caveman\nskill directive** to each message you send and re-fires the send, so the AI replies in a compact,\nhigh-density style intended to preserve technical substance. It only ever adds text to your *outgoing*\nmessage; it never reads replies, never touches network requests. Savings are `inferred`. (This is\nthe directive-injector product; an earlier revision shipped a local WASM compress button — EXT\nremoved it after Chrome Web Store review couldn't reproduce \"compress prompt locally\".)\n\n## Layout\n- `manifest.json` — MV3. `storage` permission only. Content scripts load `src/directive.js` **then** `src/caveman.js` (order matters). Only the fonts are web-accessible — no WASM, no special CSP.\n- `src/directive.js` — the **pure** caveman directive text: `buildPrimer`/`buildReminder`/`isPrefixed`/`normMode`. No chrome/DOM/network. Loaded first (exposes `self.CavemanDirective`) and required directly by the test suite.\n- `src/caveman.js` — content script: per-site composer adapter + capture-phase send interception. Prepends the directive (full primer on the first message, short reminder after) and re-fires send. Exact composer-scoped selectors; `prependText` inserts at a collapsed selection without rebuilding the rich draft. Pending sends cancel on draft, editor, conversation, or settings changes; unknown controls keep native behavior.\n- `src/background.js` — service worker: just reflects the on/off state on the toolbar badge.\n- `popup.{html,js,css}` — the on/off toggle, mode (caveman/ultracave/megacave), per-site switches, and a **Leave-a-review** CTA (Chrome Web Store via `chrome.runtime.id`; hidden on Firefox until a verified AMO URL is available).\n- `test/` — pure directive/service-worker tests plus real Chromium\n  content-script and popup/storage journeys against `harness.html`.\n\n## Conventions & honesty\n- **Edits your outgoing message only.** Prepends a visible directive; never reads model replies, never touches network requests, never automates anything beyond submitting the message you sent.\n- **Local-only.** No network requests, no analytics. Stores only `enabled` / `level` / `sites` in `chrome.storage.sync`.\n- **Honest about injection.** The directive is plainly visible in the chat — the extension doesn't hide what it adds.\n- **inferred-only.** Token savings are an effect of the model's terser replies; never labeled `verified`.\n\n## Conventions\n- Build/test: `npm test` for pure tests; `npm run test:browser` for Chromium send interception, popup storage,\n  per-site controls, and review-link behavior. There is no\n  build step — MV3 loads the source directly.\n- Package: `npm run package` runs both suites, then writes `dist/caveman-browser-<version>.zip`. Staged set uses explicit allowlist in `scripts/verify-extension-stage.mjs`, cross-checked against manifest/CSS/HTML references.\n- Composer + send selectors live in the `SITES` map in `src/caveman.js`; a site redesign means updating it (each entry keeps aria-label fallbacks so one rename doesn't break it).\n\nSee ../../CLAUDE.md (root)\n\n<!-- File: mcp/AGENTS.md (Tokens: ~1113 | Category: root-instruction) -->\n# mcp — the Caveman MCP server (Go core + launcher, Apache-2.0)\n\nA thin **stdio JSON-RPC** adapter exposing the compression [engine](../engine/CLAUDE.md) as\nfive MCP tools to any host (Claude Code, Cursor, …). It owns only the MCP framing; all\ncompression is the engine's, linked **in-process** (no subprocess, no drift). Local-only — it\nopens no network connection — and everything it reports is `inferred`, never `verified`.\n\n## Layout\n- `server.go` — the `Server`: JSON-RPC loop, dispatch, the five tool handlers. Takes an injectable `Engine` interface so the framing is testable without the real compressors.\n- `protocol.go` — JSON-RPC + MCP tool-result types, `toolText`/`toolError` helpers, the exact `tools/list` definitions.\n- `cmd/caveman-mcp/` — binary: opens shared file CCR store (`CAVEMAN_CCR_DB`,\n  else `CAVEMAN_HOME/ccr.db`/`~/.caveman/ccr.db`) so proxy handles resolve across\n  processes, then serves stdin↔stdout. `CAVEMAN_MCP_EPHEMERAL=1` opts into an\n  isolated in-memory store for tests/sessions that do not need proxy recovery.\n- `bin/caveman-mcp.mjs` + `package.json` — the `npx caveman-mcp` launcher that execs the prebuilt Go binary.\n\n## The five tools (exact names, case-sensitive)\n- `caveman_compress(input)` → compressed text + inferred ratio + `recovery_handle`. Lossy (S4), recoverable, and fail-closed: incompressible/malformed/not-smaller input returns unchanged, `ratio:0`, `recovery_handle:null` — never an error.\n- `caveman_retrieve(recovery_handle)` → the byte-exact original. Unknown handle → `isError:true` + a `cave_snake_code`, never a fabricated payload.\n- `caveman_stats()` → `basis:\"inferred\"`, `scope:\"session\"`; the string `verified` never appears.\n- `caveman_toon_encode(input)` → explicit JSON→TOON re-encoding with input/output sizes; returns pass-through plus note when encoding fails.\n- `caveman_toon_decode(input)` → TOON→JSON; invalid TOON returns `isError:true`, never raw input as JSON.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=mcp` / `make product-test PRODUCT=mcp`.\n- **stdout is the protocol channel** — logs go to stderr only (a dedicated test guards this).\n\n## Gotchas (honesty invariants)\n- **un-killable transport** — the stdio server survives everything short of EOF (issue #139). Framing is line-delimited: a malformed line is answered `-32700` and the loop RESYNCHRONIZES to the next newline (never `return`); a handler panic is contained by `recover()` → `cave_tool_panicked` (dispatch panics → `cave_internal_error`); JSON-RPC batch arrays are handled per spec (one array response); id-less/`\"id\":null` requests are notifications and get no reply; and both inbound lines and generated tool output (compress/toon) are size-capped (`cave_payload_too_large`, `maxInboundBytes`/`maxResultBytes`, 16 MiB default) — but `caveman_retrieve` is exempt (`Tool.ExemptResultCap`): recovery returns the byte-exact original and must never fail closed on size, since the shared gateway store has no matching ceiling. A dead server is worse than a slow one — the proxy keeps eliding content that no longer has a `caveman_retrieve` to expand it.\n- **fail-open** — engine error or malformed input → byte-identical pass-through, never a protocol error.\n- **fail-closed** — unknown tool/handle → `isError` + cave_snake_code; unknown JSON-RPC method → `-32601`.\n- **zero-egress** — the adapter imports no `net`/`net/http`/`os/exec`; a test parses the source to enforce it.\n- v1 is **stdio-only**, **string payloads only** (the engine detects type); HTTP transport + `caveman mcp` subcommand are v2.\n- **protocol negotiation must never error.** The adapter implements the 2024-11-05 contract and echoes that version back; a client asking for a newer one gets 2024-11-05 in the initialize result and decides for itself, per the MCP lifecycle. It previously answered `-32602: unsupported protocol version`, which made Claude Code (and every other current client) drop the server — and because `caveman wrap` reads recovery availability from an install-time marker rather than from the live agent, the proxy kept eliding content that no longer had a `caveman_retrieve` to expand it. Declining to echo an unimplemented version is right; refusing to speak is not.\n\nSee ../../CLAUDE.md (root) · ../engine/CLAUDE.md\n\n<!-- File: mem/AGENTS.md (Tokens: ~654 | Category: root-instruction) -->\n# mem — cavemem, durable agent memory (Go core + clients, Apache-2.0)\n\nDurable, cross-session memory: `remember` / `recall` / `supersede` / `history` / `forget`. A local SQLite store holds the\n**raw** memories (the source of truth); recall ranks them with deterministic **BM25** behind a\nconservative threshold and compresses each hit through the [engine](../engine/CLAUDE.md) so the\ninferred token cost is honest and the dropped detail stays recoverable via CCR. Everything is\n`inferred`.\n\n## Layout\n- `store.go` — `Remember`/`Recall`/`Supersede`/`History`/`Forget`/`Recover` over SQLite + engine; legacy schemas migrate in place.\n- `bm25.go` — deterministic tokenizer + BM25 scorer (non-negative scores, so the threshold is meaningful).\n- `cmd/cavemem/` — MCP server + `remember`/`recall`/`supersede`/`history`/`forget` CLI subcommands (JSON).\n- `js/` + `py/` — thin TS and Python clients that shell out to the binary (mirrored libraries; they reimplement nothing).\n\n## The cavemem_* MCP tools\n`cavemem_remember(text)` · `cavemem_recall(query, limit?, token_budget?)` ·\n`cavemem_supersede(id, text)` · `cavemem_history(id)` · `cavemem_forget(id)`,\nserved through `mcp.NewServer` so the framing matches caveman-mcp exactly.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=mem` / `make product-test PRODUCT=mem`\n  for the Go core; root `make test` also runs mirrored JS/Python wrapper tests.\n- The wrappers shell out to the gated binary — keep recall/scoring logic in Go, never forked into TS/Python.\n\n## Gotchas (honesty invariants)\n- **byte-safe write** — raw text is written to SQLite before any engine call, so a memory is never lost; compression is transient, at recall time only.\n- **bounded recall** — omitted/Go-zero `TokenBudget` defaults to 2000 inferred tokens. Public CLI/MCP/JS/Python callers must explicitly pass `token_budget=0` for unlimited recall; adapters map it to `UnlimitedTokenBudget`.\n- **fail toward nothing** — a query below the threshold (or with no term overlap) recalls nothing, never a guess.\n- **current-only recall** — superseded versions stay auditable through `History` but never enter normal recall.\n- **inferred-only** — `tokens_added`, score, and basis are inferred estimates; never `verified`.\n- **reversible** — a compressed recall hit carries a CCR `recovery_handle`; `Recover` returns the byte-exact original.\n\nSee ../../CLAUDE.md (root) · ../engine/CLAUDE.md · ../mcp/CLAUDE.md\n\n<!-- File: proxy/AGENTS.md (Tokens: ~11367 | Category: root-instruction) -->\n# proxy — the byte-safe standalone `caveman` proxy (commercial, binary-distributed)\n\nA base-URL-swap reverse proxy: match → authenticate → inspect → byte-safe transform → upstream\n→ meter. Single-operator, BYOK, **zero cloud dependencies**. It shares its provider adapters with\nthe managed gateway (the managed gateway imports them from here). `caveman start` launches the\n`caveman-proxy` binary.\n\n## Layout\n- `providers/` — the shared, public byte-safe adapter set: `Adapter` interface + `Base` embed + `UsageScanner`/`ParseUsageBytes` (`adapter.go`), and `anthropic`/`openai`/`gemini`/`azureopenai`/`bedrock`/`vertex`/`openaicompat`. `ResolveUpstreamURL` takes `providers.RouteContext` (no control-plane coupling). Anthropic + OpenAI carry both prefixed and bare routes (`/v1/messages`, `/v1/chat/completions`). `vertex` preserves caller OAuth bearer or Express-mode Google API-key credentials for Gemini + Claude on Vertex AI (no signing, no environment-key fallback, no custom usage parser). `MatchRoute` is POST-only; read-only model discovery (`GET /v1/models`, `/v1/models/{id}`) is a separate GET-only allowlist, `Base.MetadataRoutes`, declared by `openai`/`anthropic`/`openaicompat` and forwarded unchanged with no body, no transform and no spend row. Bare `/v1/models` belongs to both `openai` and `anthropic`, so they implement `MetadataRequestMatcher` and choose by wire protocol (`anthropic-version` or `x-api-key` present → anthropic; a Google key matches neither), never by registration order (#1187).\n- `internal/gateway/` — the request lifecycle (`server.go` + `proxy.go`) behind three injected seams: `Authenticator`, `CredentialResolver`, `TelemetrySink`. Ports the managed loop with the fail-open fix. `serve` sets `Config.AsyncRecord`: a row's request token count, sink write (batched) and Cloud observe run after the handler returns, in record order (`recorder.go`). A reader that must see the request it follows calls `Server.Flush` first (session receipts do); close the server before the sink.\n- `internal/config/` — `caveman.yaml` loader + BYOK env-key resolution; unknown mode fails closed to `record`.\n- `internal/store/` — `~/.caveman/caveman.db` SQLite spend store (`modernc.org/sqlite`, cgo-free); implements `TelemetrySink` and `BatchSink`. `RecordBatch` commits request rows with `synchronous=NORMAL` on its own connection (WAL): a power loss or OS crash can drop the last batches, a process crash or clean exit loses none; every other table keeps FULL.\n- `internal/standalone/` — wiring: static `Auth`, BYOK `Creds`, adapter set, and the always-on SSRF-guarded client.\n- `internal/identity/` — who calls the framework middleware routes (legacy token, token map, OIDC/JWT, mTLS) and the reloadable TLS listener config.\n- `cmd/caveman-proxy/` — binary: `serve` (default), `stats`, and content-blind\n  `agent-evidence --session --build --plan`. Evidence query returns only exact\n  provider usage, request hashes, declared context/plan identity, ordered\n  provider-prefix component hashes, actual transform IDs/counts, and CCR handle;\n  basis is always `inferred`, verified dollars always zero.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=proxy` / `make product-test PRODUCT=proxy`.\n- Tests inject a plain `*http.Client` to reach loopback stubs; the binary uses the SSRF-guarded client.\n- New provider/optimizer work goes in `providers/` (shared) — change it once, both proxies get it.\n\n## Gotchas (honesty invariants — correctness, not style)\n\n- **listener lifetime is not session lifetime**: `serve` never exits because a\n  wrapper, native session, heartbeat, or idle timer expires. Legacy\n  `CAVEMAN_NATIVE_IDLE_TIMEOUT` does not arm shutdown. The CLI never restarts a\n  shared listener to change mode/recovery; an incompatible new wrap runs direct.\n  Failed local startup also runs direct. Native session correlation entries may\n  age out without affecting API traffic.\n- **no default generation deadline**: `CAVE_GATEWAY_UPSTREAM_TIMEOUT_MS` defaults\n  to `0` (no total request deadline). A positive value is an explicit operator\n  cap and includes response streaming. Client cancellation still cancels upstream;\n  connection setup, inbound header/upload limits, and idle keep-alive socket\n  cleanup remain bounded separately. The replacement bound for an upstream that\n  connects and then goes silent is the transport's response-header deadline\n  (`CAVE_GATEWAY_RESPONSE_HEADER_TIMEOUT_MS`, default 900000, `0` disables) —\n  headers only, so it never truncates a live stream.\n- **response protocol controls streaming**: SSE/event-stream responses flush\n  headers and chunks even without a JSON `stream` flag. Encoded requests keep\n  `Content-Encoding` and bypass transforms. Interrupted response copies record\n  an error and abort HTTP framing; they never become a clean successful EOF.\n- **replay only when non-delivery is proven**: proxy transport retries are\n  limited to connection-setup failures — `*net.OpError` with `Op` `dial` or\n  `proxyconnect` (a proxy that is down fails as the latter, never as a dial). A failed upload/header read or truncated response\n  does not prove an inference was unprocessed; do not automatically replay it.\n  An explicit transformed-request 4xx still retries once with original bytes —\n  except a 429 that carries `Retry-After` (a rate limit; Anthropic sends its\n  `anthropic-ratelimit-*` headers on every response, so they prove nothing),\n  which is returned as-is; an accepted retry pins the conversation raw\n  (`internal/gateway/raw_pin.go`).\n  See `docs/technical/proxy-reliability.md` at the repository root.\n- **byte-safe**: `record` mode never transforms; on transform error the ORIGINAL bytes are forwarded (HTTP 200, fail-open) — never a 400. The one carve-out: bytes the provider already cached in replaced form are re-sent replaced, because forwarding the original there busts the prefix (see cache safety).\n- **request-wide opt-out**: `x-cave-transforms: caveman.pass-through.v1` suppresses every request transform path — compress, pixel, and provider-native — not only compiled plan routes. Tests cover all three modes.\n- **no-fake-savings**: standalone records `Basis: \"inferred\"` on every row; it never writes `verified` and never re-projects to a monthly figure.\n- **practice join**: local learn sinks carry additive `practice_id`; one\n  fail-closed mapping table owns sink→practice and unknown sinks keep `\"\"`.\n  The historical `subagent_overuse` sink is count-only and deliberately has no\n  practice id: spawn count cannot reactivate the retired\n  `context-exploration-offload` opportunity or prove any spawn unnecessary.\n- **local trial heuristics are not actuation evidence**: a model name never emits\n  the retired `model-right-sizing` id, and provider plus positive cost never\n  emits a cache move because neither proves stable-prefix eligibility. Legacy\n  rows for those identities are hidden at read time. Compression replay reports\n  one trial's local engine `estimated_engine_o200k` before/after shape with zero\n  dollars and low confidence; it is not provider-counted, a rate, an invoice,\n  causal/verified savings, or task-outcome evidence.\n- **Anthropic automatic caching is experimental observation only**:\n  `anthropic-automatic-prompt-cache` is a typed, default-off manual policy\n  experiment and may add only Anthropic's top-level 5-minute marker on direct\n  Messages API requests. Managed traffic additionally requires server-attested\n  official Anthropic origin; custom or provenance-unknown origins lose the flag\n  before the adapter. It is mutually\n  exclusive with the explicit `anthropic-cache-breakpoints` transform and any\n  caller `cache_control`; Bedrock and count-tokens requests stay byte-identical.\n  An applied marker records only its optimizer id plus actual provider usage and\n  cost. It has no practice, recipe, generic mode/candidate activation, ledger\n  tuple, inferred savings, or verified-savings path (cache-only and forged IDs\n  are excluded from the counted-baseline method too); evaluate it by manual\n  paired observation because shared provider cache state can contaminate an A/B.\n- **SSRF always on**: `standalone.StandaloneHTTPClient` guards every upstream dial (not gated on `CAVE_ENV`) using `ssrf.SelfHostedConfig` — NOT ManagedConfig, which ignores the allowlist and would make the escape hatch a silent no-op. `CAVE_SSRF_ALLOWLIST` opts loopback/private hosts back in (local model servers like Ollama; `localhost` as an entry covers 127.0.0.0/8 + ::1); metadata/link-local stay blocked in every mode. Provider traffic honours `HTTPS_PROXY`/`NO_PROXY` by default (#1001) — `upstream_proxy: env|off|<url>` / `CAVE_UPSTREAM_PROXY` — via `ssrf.Config.Proxy`, which validates IP-literal/localhost destinations before proxy selection and dials the (operator-configured) proxy address unguarded; `ssrf.NewHTTPClient` itself stays direct unless a caller sets `Proxy`, and a managed-mode Config carrying one is refused loudly (every request fails with `ssrf.ErrProxyInManagedMode`) rather than silently dialing direct. Adapters that pre-flight a resolved endpoint (bedrock, vertex) read the selector off the request context (`providers.WithUpstreamProxy`, published by `gateway.Handler` from the upstream transport) and use `ssrf.ValidateURLNoResolve` for proxied destinations: a proxy-only network has no outbound DNS, so resolving there failed the request before the proxy was ever consulted. `ca_bundle` / `CAVE_CA_BUNDLE` plus inherited `SSL_CERT_FILE`/`REQUESTS_CA_BUNDLE`/`NODE_EXTRA_CA_CERTS` append private roots for TLS inspection through `shared/platform/cabundle` (fail-closed parser shared with chhttp).\n- **inbound token is consumed, never forwarded**: `CAVEMAN_AUTH_TOKEN` gates non-loopback listens (a middleware token map, OIDC issuer or TLS client CA also makes one legal; provider routes still accept only the token, and without it `Auth.closed` refuses every provider request off loopback); `standalone.Auth` deletes the matching `x-cave-api-key`/`Authorization` header before `Creds.Resolve` so the shared token can never be forwarded to a provider or classified as a provider credential; a bearer that is not the token survives untouched (Claude OAuth, `/chatgpt/`). `Authenticate` runs BEFORE `matchAdapter` so the 401/404 split cannot be used to enumerate routes, and every 401 goes through `Server.rejectUnauthorized`, which counts `cave_proxy_unauthorized_total` and warns with the path and remote host only. Health and the no-op `/caveman/keepalive` beacon stay unauthenticated, `/metrics` too unless `CAVEMAN_METRICS_TOKEN` is set; the `X-Caveman-Instance` header on `/health/live` is published only on a loopback bind. A token on a LOOPBACK listener is honored too (the local `caveman wrap` path sends none, so `runServe` warns). Bedrock credentials then come from `shared/platform/awscreds` (env → web identity → container → IMDSv2), never from the request; a PARTIAL env pair fails closed instead of falling through to an ambient role, and the winning source is logged once.\n- **middleware identity is server-side and per route**: `middleware.Config.Identify` resolves an `identity.Principal` from the credential alone (bearer decides when present, else a verified client certificate); `ServeHTTP`'s `decode` checks `Principal.Allows(scope.Namespace)` on EVERY scoped route, so a new route that skips `decode` skips authorization. The legacy token stays `single_operator` with every namespace; JWT and certificate principals may never be named `single_operator`. Token and hash comparisons are constant-time over every entry. A token map or TLS file that fails to reload keeps the previous one. Tests: `identity/*_test.go` (negative JWT and mTLS cases), `middleware/identity_test.go`.\n- **Postgres middleware store keeps SQLite's single-writer semantics per authority**: `store.PostgresMiddleware` serializes writers of one authority with a transaction-scoped advisory lock taken by `Scope` (then re-read), `SaveScope`, `Revoke`, `PurgeBatch`, `Receipt` and `SaveOriginal`; the sweep is one-replica (try-lock) and skips busy authorities and row-locked scopes. Counters are statement-level transition-table triggers striped by txid; a per-row trigger goes quadratic on bulk statements. Lock keys include `current_schema()`. Run the store suite against both backends with `CAVEMAN_TEST_POSTGRES_URL`; `middleware_concurrency_test.go` fails without the locks.\n- **Auth scheme is preserved**: a key from an inbound `Authorization: Bearer` keeps `Scheme:\"bearer\"` on the `providers.Credential`; Anthropic and Gemini forward bearer credentials as bearer credentials (Claude/Gemini OAuth breaks if remapped to an API-key header). BYOK env keys and inbound `x-api-key` keep provider API-key mapping.\n  On a named compat mount the header follows the wire protocol of the path:\n  `/compat/<name>/v1/messages` gets `x-api-key` plus a default\n  `anthropic-version`, every other path gets Bearer. A real inbound Bearer stays\n  Bearer on every path. OpenCode Go rejects Bearer\n  on `/v1/messages` (401 `Missing API key.`, 2026-09-03). The gateway env\n  fallback puts the key in the header that the adapter emitted, so the upstream\n  never gets two credentials.\n- **fail-closed**: unknown route → 404; unknown mode → `record`.\n- **subscription AND oauth compression is NOT account-gated**: non-PAYG sessions from Claude Code, Codex ChatGPT, Gemini CLI, and other routed clients take live-zone compression with no Caveman account, entitlement, or seat. `CAVEMAN_WRAP_ENTITLED` and every `WrapEntitled` field are **deleted**, not merely ignored — do not reintroduce them. Exactly four conditions remain, all technical and all fail-closed (`liveZoneCompressionAllowed`): the operator `subscription_compress` switch (empty/`live_zone` allow, `off` and any unknown value close it), the adapter must implement schema-aware `PrefixStabilizer` zones, recovery must run through the agent's own MCP `caveman_retrieve`, and a durable prefix cache must be wired. The dedicated Codex `/chatgpt/responses` route uses the OpenAI Responses stabilizer while preserving OAuth and `ChatGPT-Account-ID` headers; recovery is proven by the `CAVEMAN_RECOVERY=mcp` stamp or the decoded request's own `mcp__caveman__caveman_retrieve` (top-level `tools` or Codex's `input[]` `additional_tools`, flat or under an MCP namespace); zstd bodies decode → transform → re-encode, anything else forwards the original wire bytes, and the `chatgpt_proxy` log carries `skip_reason`; transformed 4xx responses retry once with exact original bytes (never a rate-limit 429; an accepted retry pins the conversation raw). Legacy savings fields remain **tokens-only** — `compression_tokens_before/after` + `estimated_engine_o200k`, never booked compression dollars. New request-comparison and price-snapshot fields support separately labeled API equivalents in local stats; those values never enter subscription spend or legacy saved-dollar fields. LOCAL wrap only; managed gateway non-PAYG behavior is unchanged.\n- **cache safety is byte-stable replacement**: THE INVARIANT (#1105): for accepted requests P then R of one conversation, if R's client bytes over P's cached prefix (system, tools and messages through P's last `cache_control` marker; the whole prompt for implicit caches) equal P's, R's forwarded bytes over that range equal P's, and every request caches at least what its client asked for (a dropped breakpoint is a bust too). Two exceptions, both re-caching the range raw: a request the provider rejected transformed and accepted through the raw retry, and a compressed PAYG conversation without MCP recovery that streams (the server-side retrieve tool cannot ride a stream, so the conversation stays raw from there). A raw retry splits a range into a raw and a replaced form, and a later request follows the LONGEST lineage it extends, ties going raw: it is held to the raw request (raw pin) unless it repeats a longer prefix that went out replaced, like a sibling subagent started with the same task. Pins are taken only on a conversation's own bytes (past system and tools); pins and lineages live in the PrefixCache (`rawpin` and `lineage` scopes, pin rows never evicted) and in memory. `internal/gateway/cache_prefix_invariant_test.go` (Anthropic compress, PAYG, pixel and Codex Responses scenarios + `FuzzCachePrefixInvariant`) proves it; keep it green. A compressed live-zone turn becomes prefix on the next request, so the replacement is a pure function of segment content (deterministic compressor + content-hash CCR marker), memoised in the durable replacement cache, and the FIRST decision for a block wins — replaced or raw (`RawDecisionHandle`); a request whose write finds a row on record sends that row, not its own candidate. A frozen block with no decision (a `--resume` history, an evicted row) goes out raw and raw becomes its decision; a failed memo write keeps the block raw for the process (`rawMemory`, consulted only on a store miss and written through once the store takes it); new rows are remembered only once the request's splice holds. Raw decisions are global and durable, a known savings cost: a block declined once (frozen at first sight, a compressor decline, a CCR failure, a non-newest parallel tool output) stays raw in every conversation. Bounding them needs a TTL of at least the longest provider retention (24 h for OpenAI extended caching) plus time-aware store interfaces, or per-conversation rows keyed on a stable conversation identity with a scope rollover. Substitution is never gated: the derived epoch gate is gone, and an explicit framework epoch may veto only NEW compression. Pixel renders and the tool-schema strip are memoised the same way. New compression stays live-zone-only. The runtime tripwire (`prefix_monitor.go`, on the provider routes and `/chatgpt`) checks the same rule on live traffic and records `requests.cache_bust_cause`: `client` (DEBUG); `raw_retry`, `stream_switch`, and `lever_freeze` (the harm tripwire's one-time strip rollover), all WARN; `caveman` (ERROR `caveman changed bytes the provider already cached` or `caveman dropped a cache breakpoint the client set`, counted as `caveman_cache_bust_requests` and raised by `caveman status`) — any `caveman` row is a bug. A request is held only to anchors accepted before its own forwarding decision. Cross-turn stability covers **anthropic, openai, azureopenai/openaicompat, and gemini** through `ExtractStabilizable`; `bedrock` and `vertex` expose no compressible blocks and stay pass-through. Anthropic uses declared `cache_control`; OpenAI/Gemini use latest-user/latest-tool zones against implicit provider caches. Replacement cache is SQLite spend store and must retain `journal_mode(WAL)` + `busy_timeout`; a hit rewrites `last_used_at` only once the row is 10 minutes stale, and pixel rows have their own cap inside the table's.\n- **cache warming is predicted per stream, and replays only what Anthropic can answer at zero output** (`internal/gateway/cache_warm.go`, `cache_warm_table.go`): after a successful Anthropic Messages response on `api.anthropic.com` (never Bedrock, Vertex, Azure, a compat mount, a custom base URL, the pass-through header or an encoded body), the exact bytes and headers sent upstream (after compression, routing marks, heals and fail-open retries) are kept in memory only (LRU 1024 streams and 256 MiB, never stored, logged or captured) and may be replayed with `max_tokens: 0` and `stream: false` at `min(0.9*ttl, ttl-10s)` steps from the real request's start (wall time, so laptop sleep never sends a stale warm). Not replayable, so never warmed: `thinking.type: \"enabled\"`, `tool_choice` `tool`/`any`, `output_config.format`, no `cache_control`, a `ttl` other than `5m`/`1h`, mixed `5m` and `1h` markers, writes of another lifetime than the markers (one lifetime or no warm; the simulator classes by the same rule and none of the measured requests mixed them); the request scan reads top-level fields and cache_control markers only, no full parse. WHETHER to warm is optimal stopping, not a threshold: `cache_warm_table.json` holds the return-time distribution (30 s bins to 2 h) per stream class `main|subagent / tool|end / 5m|1h` (the class comes from the session key and whether the answer's `stop_reason` was `tool_use`, scanned off the plain bytes with a rolling tail; the proxy forwards the agent's `accept-encoding`, so an answer that comes back with a `Content-Encoding` is not classed: it is planned as whichever of the two classes warms less and its gap teaches neither), measured on 364,048 request transitions of real Claude Code sessions (2026-08-30 to 2026-10-08, `.blocks/cache-warm-sim.py`, aggregate probabilities only) and shrunk toward the parent class; the proxy learns each user's own gaps online (counts only, `$CAVEMAN_HOME/cache-warm-gaps.json`, 0600, written outside the warmer's lock, flushed by `Server.Close` at shutdown, symlink-refused, any implausible count drops the whole file; two proxies on one home are last-writer-wins) and blends them in with the defaults worth 100 observations. A stream evicted or still waiting at shutdown is counted as returning later than it was seen silent (spread over the later bins by the class's own distribution, \"never\" past 2 h): dropping it would keep only the returns and drift the table toward over-warming. At arm time the whole chain is planned by backward induction with the catalog price of the model sent (warm = read x prefix + input x uncached tail; avoided miss = (write_ttl - read) x prefix x the class's measured realized share, taken over the gaps that DID return: the window probability already leaves out the streams that never do), so the cumulative cost of every warm is counted before the first; unknown price, class or lifetime = no warm. Subscription traffic is valued at list prices as plan-usage equivalent. Stops: any real request of the stream (every model; it never waits for a warm in flight), any warm error, a warm reading `cache_read_input_tokens == 0`, the plan, 60 min, or the switch; the held request (body and credential headers) is released the moment its chain ends. A parent session's request does NOT stop its subagents' warms: a subagent can still be running, and on the test half that stop cost $565.52 net (below). A 429 (warm or real) pauses warming on that credential for the response's `Retry-After`, else until its `anthropic-ratelimit-unified-reset`, else 1 h for a subscription/OAuth login and 60 s for an API key; a 529 or 5xx on a warm pauses it for the `Retry-After`, else 5 min. Never sent while a real request of the same stream is in flight (retry every 5 s until its deadline). At most two warms are in flight process-wide (same retry; one with no slot by its deadline is dropped), and each fires up to 5 s before its planned time, never after. Warm rows are tagged `cache-warm`: spend, never a saving, never an agent request (excluded from request counts, session usage, trial origins, exported spans, receipts and `caveman sync` uploads; counted as `cache_warm_requests`), never fed to the harm-tripwire ledger, never sent to Cloud. Switch: on unless `$CAVEMAN_HOME/cloud.json` has `modules[\"waste-fixes\"]: false` (re-read on change); `CAVEMAN_CACHE_WARM=off` kills it for the process. Every answered stream request logs one line `cache warm plan` (info when warms are planned, debug when skipped: session hash, model, class, ttl, planned warms, skip reason: `response_error`, `off`, `not_anthropic_api`, `not_replayable`, `lifetime_mismatch`, `unpriced`, `not_worth_it`). Replay on the owner's data (test half, after 2026-10-03, 161,325 transitions): net $1,896.65 list-price equivalent at $385.31 of warms (9,628 warms, 7,280 wasted) vs $1,294.18 at $1,372.15 (45,343 warms) for the first fixed rule; the same policy with the parent stop for every subagent $1,331.13 at $197.28 (3,461 warms), with it only for subagents whose last answer was not a tool call $1,852.99 at $378.90 (9,518 warms). A test pins the plan to the simulator's on the shipped table (`TestCacheWarmPlanMatchesTheSimulator`; regenerate both together).\n- **pixel mode**: S4 lossy text→PNG (`pxpipe` port). Default allowlist is `claude-fable-5,gpt-5.6` via `CAVE_PIXEL_MODELS`; savings stay inferred-only. Renders keep the compress rules: the first decision for a text (render or text) is re-sent on every turn, a raw pin keeps the conversation text, and a NEW render is remembered only once its request goes out, with the original stored in CCR and the splice holding. If either fails, the new content goes out as text and records that, and earlier renders still go. A rendered block's `cache_control` moves to its last image. An Anthropic request carries at most 20 images, client images included (`anthropicManyImages`): past 20 the API rejects any image over 2000 px, and the renders are 2573 px wide, so a new render past the cap stays text.\n- **route stage is optional and fails open** (`internal/cloudlink`, `internal/gateway/route.go`):\n  it asks Cloud `POST /v1/route` only for requests naming Auto (`AutoModel`, `caveman-auto`, also with Claude Code's `[1m]` suffix, the\n  picker entry the CLI adds while `modules.routing: true` in `$CAVEMAN_HOME/cloud.json`), while the\n  CLI is signed in with that switch on, for Anthropic Messages / OpenAI chat or responses requests on\n  an API key or a subscription alike; any other model goes as sent (`route.outcome: off`), the\n  session's marks and heal still applied. Auto goes upstream as the provider's `autoFallback` model\n  (Anthropic `claude-sonnet-5-5`, OpenAI `gpt-6.1-sol`; count_tokens too, and with no Cloud link),\n  which is also the asked model the ask carries and what a failure runs (a transient Cloud failure, that is a timeout, an unreachable or 5xx Cloud, an unreadable answer or the pause after one, instead runs the model the session's previous request went to, `heldModel`, never for a side request; no previous request on that provider, one that was a compaction or came after one, a held model that then did not serve (any non-2xx: the next attempt runs the fallback), signed out, routing off, an expired or refused login, a limit and a provider's refusal keep the fallback); Auto on any other\n  provider is a 400 (`cave_auto_unavailable`), and an encoded body naming Auto is decoded first. A\n  named model records `route.outcome: off`, reason `named_model`. OpenAI Auto (API key and ChatGPT\n  login) runs only `AutoOpenAIModels` (`gpt-6.1-sol`, `gpt-6-astra`, `gpt-6-luna`): the ask's\n  `Models` lists those alone and the pool drops other OpenAI models, and an answer naming any other\n  runs `gpt-6.1-sol` at its effort (`auto_model_refused`, `translate.FitEffort`); an Auto row's `RouteFrom` and\n  `x-caveman-routed-from` say `caveman-auto`. A move on a subscription stays on the provider's own\n  API on that subscription's credential; pool entries go out on their own login or the Cloud\n  gateway key, never on it, and never with the agent's `oauth-*` betas. On a ChatGPT login\n  (`/chatgpt`, `chatgpt_auto.go`; OpenCode's ChatGPT login lands there too) `/models` gains an Auto\n  entry (a copy of `gpt-6.1-sol`'s, listed last, fetched without `If-None-Match`, served without\n  `ETag`) while the link reports `AutoOffered` (signed in, routing on). Every POST there has its\n  first 64 KiB read (decoded when gzip, deflate or zstd; a br body is never looked into) for a\n  top-level `\"model\":\"caveman-auto\"`; any other streams on unchanged. `/responses` naming Auto asks\n  with `NoPool`, keeps live-zone compression, replays `gpt-6.1-sol`'s bytes on a 4xx (a rate-limit 429, a 401 and a 403 are returned instead; a served replay pins the conversation raw, and the decision is rejected only then or when the replay is itself a 429; a 5xx on a moved model is returned and rejects the decision, a rate-limit 429 keeps it: the plan's limit holds for every model); any other\n  path (Codex compaction) runs `gpt-6.1-sol` unasked; a body over `CAVE_MAX_REQUEST_BYTES` streams\n  with only its model changed. The agent reads the Auto id it sent. Every `/chatgpt` row (Auto or\n  not) is offered to Cloud like any other, as its agent (`x-cave-agent`, else Codex by its\n  `originator` or User-Agent), with model ids in `route_from`/`route_to`; a model it could not read\n  goes to Cloud as none. The\n  ask starts before compression (parse, ask, compress, route), waits at most 800 ms (the CLI's\n  keychain credential is read when the runtime starts, `Link.Start`, never on a request goroutine;\n  a request that lands while a read runs with no secret cached waits for it, leaving the ask at\n  least 500 ms, else goes as `credential_loading` without an ask or a pause), carries the\n  caller's models, counts, what the request declares (the raw values of eleven allowlisted agent\n  headers, never cut: a value over 256 bytes, 16 KiB for Codex's turn metadata, which also comes\n  from the body's `client_metadata`, is left out; a forked Claude Code child's spawn-call\n  `subagent_type` as `x-caveman-agent`; its tool names, effort and thinking type), what the\n  session's previous request ran (the served model, the effort in force, the provider's input and\n  cache token counts, its age) and Cloud's opaque per-session state, plus, on a turn's first ask,\n  the raw text of the latest human turn, the one before it and the end of the agent's last reply\n  (contracts `route-ask-v1`; Cloud picks the model and the effort and no routing logic lives\n  here). Its answer is cached per ask until 30 min after its last use (session, or without one agent and\n  endpoint, provider, model, turn number, latest human text;\n  an input group carrying a tool result is no human turn, text riding along included) so a tool\n  loop never switches model or effort mid-turn (an ask first seen during a pause keeps the paused answer for its turn); a request the agent labels compaction or auxiliary\n  (Claude Code's request class and compaction flag, OpenCode's title/summary/compaction agent,\n  Codex's subagent kind and turn-metadata request kind) is asked on its own, without the text. The\n  session is `x-cave-session`, else the agent's session header (a Codex thread, `thread-id`, is\n  its own, its parent `x-codex-parent-thread-id`); a Claude Code child (`x-claude-code-agent-id`)\n  is a session of its own and sends its parent's state; a session id guessed from timing is not\n  used. State, previous-request facts and marks live in bounded in-memory LRUs (1024), never on\n  disk. The answer sets Responses `reasoning.effort`, chat `reasoning_effort` or Anthropic\n  `output_config.effort`; Anthropic `effort_mode: message` instead inserts the byte-identical mark\n  `{\"role\":\"system\",\"content\":[],\"output_config\":{\"effort\":…}}` (beta\n  `mid-conversation-output-config-2026-07-01` appended to `anthropic-beta`) before the last user\n  turn, or at the end after a tool result; on OpenAI Responses (GPT-6) the mark is a\n  `{\"type\":\"configuration_update\",\"reasoning\":{\"effort\":…}}` input item before the last user\n  message while nothing has answered it yet, never at the end and never next to another one (no\n  place, as mid tool loop: the effort in force holds), no beta, and none at all where OpenAI forbids\n  it (`truncation: \"auto\"`, `context_management`, a non-false `multi_agent`: Cloud's effort goes\n  top-level); a 400 naming a configuration update heals like a refused mark. Effort state made on\n  one wire starts afresh on the other. Only `effort_mode: message` ever starts marks; a \"top\"\n  answer on history carrying the session's marks goes in as one more mark (never wiping them; for\n  a compaction or side request, that request's only), and on any other history sets the top-level\n  field. Marks are remembered per session with a salted hash of the message before each\n  (`cache_control` and thinking blocks left out, so a heal's strip keeps them) and replayed at the\n  same places on every later request to a model that already took them (the agent resends history\n  without them), count_tokens of a known session or a forked child's parent included (it adds\n  nothing to a history without marks). Without an effort from Cloud (a failure, routing off,\n  effort \"\") a history without the session's marks goes as the agent sent it, top-level field\n  included (one cache restart where Cloud's was, nothing inserted, no beta), except that a\n  remembered heal (drop_block with its beta, or a strip) keeps applying. One with marks runs at\n  the request's own top-level effort, or, when it sets none, at the model's default effort\n  (Cloud's `default_effort`, kept per session, a forked child taking its parent's), marked when it\n  differs from the one in force; only with no default known does such a request go as the agent\n  sent it, without the marks, which the session then forgets (its later thinking blocks then lose\n  their binding) unless the request went to another model. The top-level field is fixed at the\n  session's first per-message request: the routed effort on a fresh conversation whose request\n  sets one (kept there, also for later requests that set none), else the request's own; a routed\n  effort for a request that sets none goes in as a mark, first on a fresh conversation. An anchor\n  that no longer matches drops that mark and every later one; compaction and side requests never\n  change them, and a body matching none of them (an unlabeled side request, compacted history)\n  gets marks of its own, kept for up to four request shapes (first message, length, last message),\n  which take the session over only from a longer request continuing that conversation (same first\n  message, its last one where it was); the session's marks and fixed top-level field then wait\n  there in turn. A thinking-binding 400 on the asked model of a `/messages` request retries once\n  only on a session the route stage changed (it was on for the request, the session carries its\n  marks or heal, or another model served one of its requests) whose body sets no\n  `thinking.block_binding`: with `thinking.block_binding.prefix_mismatch_behavior: \"drop_block\"`\n  (beta `thinking-binding-controls-2026-08-01`) when the block is bound to a different\n  conversation and the request's thinking is adaptive or enabled, then sent on every later request\n  (count_tokens and forked children included); else (Sonnet 5.5 `between_tools`, no thinking\n  field, a tampered signature) without thinking blocks from the failing message on, and later\n  requests on the same history (same first message, same message at its end) strip the same range\n  up front (a second strip widens it; the strip stays, the history as served, even if the agent\n  later sets its own `block_binding`). A drop_block retry refused with a 400 naming the binding,\n  `block_binding` or its beta moves the session to the strip path, a strip refused that way ends\n  its heals (the first lasts for the session, as it depends on the model and thinking type; the\n  second goes with the marks, cleared when they are forgotten; a side request never clears either,\n  nor the strip; a 429, 5xx or other 400 on the retry teaches nothing). Routing switched off after\n  a restart surfaces that 400. A refused mark retries once with top-level effort only and, once\n  served, latches per-message effort off for the session (a model that refused takes Cloud's\n  effort top-level); compressed 400s are decoded first. A Cloud error, timeout, 401/403,\n  `allowance` or `billing_limit` answer keeps the asked model (the held model above for a transient failure) and pauses new asks (1 min; 10 min\n  for 401/403 and billing_limit; until the 1st for allowance). Refusals and limits land in\n  `$CAVEMAN_HOME/route-state.json` (with Cloud's notice) for `caveman status`; a new login lifts\n  the pause. A route failure blames a model only on evidence (Cloud ADR 0110). A provider 4xx on\n  the routed model replays the original bytes on the asked model, except a 401 (the login, not the\n  model: returned as is); a 403 replays (a plan refuses a model it does not carry with one). Only a\n  served replay of bytes that differed from the original in the model alone blames the routed model\n  (`Reject(true)`, `provider_rejected_routed_model`): it leaves the ask's pool for that scope\n  (session, or without one agent and endpoint, and provider and asked model) for 30 min, while\n  the ask still goes with the models left. A served replay after compression or effort also\n  changed the bytes records `replayed_original` and blames no model; a replay that fails too\n  records `replay_failed`. The rest of the ask moves back to the asked model (`Reject(false)`,\n  never remembered) when the replay is served, or is itself a 429 (so the agent's own retries send\n  one request each); a rate-limit 429 (with Retry-After) or a 5xx on the routed model is returned\n  as is, never replayed, and moves it back too, as do a transport error, a provider error event\n  and an upstream break before the provider reported usage; a client cancel never does, so the\n  agent's own retry runs the asked model; a\n  429 on the asked model of a request whose bytes the route stage changed (effort, marks, strip,\n  drop_block, a heal retry) is returned as is. When the model moved, the agent's copy of the\n  answer names the model it asked for (Claude Code drops its thinking on another name): a JSON\n  answer's top-level `model`, and in a stream every `\"model\":\"<sent>\"` pair, rewritten\n  incrementally across reads (an Auto request names Auto, moved or not); the upstream is asked for an identity answer (on the original-bytes replay of an Auto request too), a compressed one is\n  left as it is, and usage, stats and `last` read the provider's bytes. The pass-through header\n  and encoded bodies never route, nor does any origin but the provider's own API (Azure,\n  OpenRouter, LiteLLM, a custom base URL); record mode does (routing is its own module). A limit\n  pause asks `/me` at most every 15 min and lifts once routing is no longer limited or the plan\n  changed. runtime/v1 events go whenever signed in, routing on or not, unless the CLI telemetry\n  opt-out is set (ADR 0085 §5; `route.outcome: off` when no route stage ran): in-memory batches of\n  at most 500, dropped on failure, at `/me`'s `data.level`. The runtime never invents a level:\n  none in `/me` means `counts`, an unreadable `/me` lowers it to `counts`, `off` or no level sends\n  nothing. Model ids from other origins go as `custom`. Signing in or out never changes another\n  stage.\n- **the route path is kept hot** (`internal/cloudlink/warm.go`, contract `route-warm-v1`): the link\n  has its own transport (HTTP/2 tried, idle connections kept 10 min and pinged after 15 s of\n  silence, proxy from the environment; a warm is marked replayable, so a dead kept connection is\n  retried on a new one, an ask never: the server may already have decided and counted it). A warm\n  is `POST {gateway}/v1/route` with `x-cave-warm: 1`, the asks' credential and no body; it goes,\n  one at a time and only from a started runtime signed in with routing on, at start and at every\n  login, when a native `session.start` or `prompt.submit` comes after 30 s without contact (an\n  answered ask or a warm engine), and every minute without contact while asks came in the last\n  15 min. Only a 204 with `x-cave-edge-engine: warm` is warm (timeout, error, cold and off are 204s\n  too); it ends a pause a timeout, an unreachable Cloud or a 5xx caused, never a 401/403 or limit\n  pause. Any other status is an origin without the edge (a customer install): no pause, logged at\n  debug, and three in a row stop warming until the next login or start. Each ask logs `route ask`\n  (info): the ms this side measured, whether it reused a connection, the status, `x-cave-edge` and\n  the edge's `server-timing`. The 800 ms budget stays the cap.\n- **pool routing** (`internal/pool`, `internal/translate`, `internal/gateway/route_pool.go`):\n  the provider keys and logins a person adds (`caveman providers add|login`;\n  `$CAVEMAN_HOME/provider-logins.json` lists ids, where each secret lives and the `cloud` switch,\n  written under `provider-logins.json.lock` by the CLI and the runtime alike; the secret is in the\n  keychain service `caveman-provider` or a 0600 file under `provider-logins/`, never both) become\n  the ask's additive `pool` (`harness/<model>`, then `<host>/<model>` per login times the static\n  catalog models its host serves, on a wire `translate.Supported` reaches from the caller's\n  grammar; at most 64; left out when no login adds one; only a 400 naming `pool` is asked again\n  without it, and pool then stays off for that login). An answer's `pool_id` + `via`: a harness\n  entry applies like `model`; a login entry goes straight to that host on that login\n  (`RouteTarget`, translated when its grammar differs, the unstripped body so each host gets its own\n  reasoning back, top-level effort, the agent's copy naming the asked model); `via: cloud`\n  (`cloud:<provider>:<model>`) sends the agent's own bytes in its grammar to the Cloud gateway\n  (`gatewayUrl`, else `baseURL`) with `x-caveman-route` (and `x-caveman-effort` when the answer\n  carries one) on the project gateway key only (the gateway\n  refuses login tokens; no key, or `caveman providers cloud off`, runs the asked model), and that\n  whole request DOES pass through Caveman Cloud. Route asks to the gateway use the project key too.\n  Any failure before content reaches the agent (refused translation, translator panic, connect\n  error, non-2xx, or a 2xx that fails or ends first: `translate.ErrNotServed`, the translator holds\n  its output until the first content, for at most `gateHold` (10 s) even while the host sends only\n  comments or pings; after it the headers go and pings start, only between the upstream's events)\n  falls back to the asked model on the harness credential, at the answered effort fitted to that\n  model's levels (`translate.FitEffort`: the Claude table, the catalog's `effort_levels`, high at\n  most with thinking off), and marks the ask rejected; a stream cut after content aborts HTTP\n  framing and never closes a half-finished tool call, while the host's own failure event after\n  content ends the stream as sent, without an abort (`pool_upstream_failed`). The row keeps\n  `route_pool_id`, `route_reason` and the host's answer id (`upstream_response_id`, e.g.\n  OpenRouter's `gen-…`); each failure is logged. A translated Messages stream's `message_start`\n  carries an input estimate (`x-caveman-input-tokens: estimated`) that `message_delta`'s exact\n  count replaces. A pool turn is not `last` and books no dollars off Anthropic/OpenAI list\n  prices. Every request to Anthropic's/OpenAI's own API (not pass-through) drops reasoning another host\n  wrote (`translate.AnthropicNative`/`OpenAINative`, byte for byte when there is none; a Responses\n  host's encrypted reasoning is route-tagged so only that host gets it back; per-message effort\n  marks are untouched). OpenAI (key or ChatGPT login) is Responses-only: Messages and chat callers reach it\n  through Messages→Responses and chat→Responses, and chat callers (OpenCode, Aider) reach Claude through\n  chat→Messages; a chat caller gets another host's reasoning as `reasoning_content` plus a\n  `reasoning_details` envelope that goes back only to that host, and every chat API (on the harness path\n  too, whatever the origin and with routing off, `translate.ChatNative`) gets both removed. Documents, structured output,\n  `disable_parallel_tool_use`/`parallel_tool_calls` and refusals cross every direction; a part the target\n  cannot take (a document by URL on chat, an OpenAI file id on Claude, audio) refuses the translation, so\n  the pool falls back. A refusal reaches Codex as `response.incomplete` `content_filter` (its own\n  content-filter path); a max-tokens stop stays `response.completed` (Codex retries any other incomplete). Terms: no Claude Pro/Max, Google or Copilot login is ever added; every OAuth row\n  in `providers.json` carries a terms note.\n- **cache mechanics** (`internal/gateway/route_cache.go`): the ask's\n  `request.cache_ttl` is the longest Anthropic `cache_control` ttl (none = 5m), else OpenAI\n  `prompt_cache_options.ttl` / `prompt_cache_retention` (24h; in_memory = 5m); Cloud's generic\n  400 (`cave_router_request_invalid`, which names no field) or one naming the field is asked\n  again without `cache_ttl`, then `pool` (never a field a 200 already came back for), and the field\n  whose removal got the 200 stays off for the login. Record mode gets none of the cache mechanics\n  below (no `prompt_cache_key`, no OpenRouter pin, host affinity headers keyed on the session\n  itself as before, no fan-out wait); the routing answer itself (model, effort, marks,\n  `configuration_update`) still applies there, as above. Elsewhere: host affinity headers key on\n  the session family (the parent's for a child): hashed `x-session-id` (OpenRouter),\n  `x-session-affinity` (Fireworks), `x-grok-conv-id` (xAI). An OpenAI request the route stage is\n  on for (a stateful chain, answered off for a reason, included) that carries no\n  `prompt_cache_key` gets the session's own (hashed, added before the effort so a heal keeps it;\n  the original-bytes retry keeps it unless the 4xx names it, which also latches the key off for\n  the session; a request it only keyed is never replayed); routing off and signed out stay byte\n  for byte. Once an OpenRouter pool entry reports cache reads or writes, the session pins the\n  provider its answer named (`provider.order:[p]`, `allow_fallbacks:false`); a pinned failure\n  drops the pin, and a pinned non-2xx is sent once more to the same entry unpinned before the\n  asked model runs (a transport error is never resent: it may come after the request was\n  written). Fresh sibling children of one parent on the same model, tools, system prompt and\n  `prompt_cache_key` go one first and the rest wait for its first content (a JSON answer's first\n  byte; an event stream's first event past comments, pings, message_start and\n  response.created/in_progress; 5 s at most, or their own cancellation): one cache write, not N;\n  a prefix with content in the last 5 min waits for nobody. `signals.context_tokens` is the\n  request's bytes times the provider's own tokens per byte, the parent session's first.\n  Live check: `CAVEMAN_LIVE_OPENROUTER_KEY=<key file> go test ./proxy/internal/cloudlink -run\n  TestLiveOpenRouterCache -v` (throwaway home, $0.50 cap).\n- **boundary**: this is public code — it must never import the managed-cloud lane. `make check-boundaries` enforces it.\n\nSee ../../CLAUDE.md (root)\n\n<!-- File: shrink/AGENTS.md (Tokens: ~812 | Category: root-instruction) -->\n# shrink — caveman-shrink, the tool-catalog compressor (Go core + launcher, Apache-2.0)\n\nCompress MCP/OpenAI **tool definitions** before they fill the context window. A thin Go wrapper\nover the [engine](../engine/CLAUDE.md)'s `toolschema` compressor: it drops annotation metadata\n(examples, titles, `$comment`, `$schema`) and reduces long descriptions to their lead plus every\nconstraint-bearing sentence. It keeps every **selection token** (tool/parameter names, types,\nenums, required) and argument-construction value (`default`, `const`, `$ref` targets)\nbyte-for-byte. Everything is `inferred`.\n\n## Layout\n- `shrink.go` — the library: `Shrink` (catalog → compressed, fail-open S4 transform, durable-CCR-backed), `Recover` (handle → original bytes), `Lint` (per-tool inferred token reductions), `SelectionProfile` (the structural surface — what must survive). `WithStore`/`WithStorePath` select recovery storage; default is `CAVEMAN_CCR_DB` / `~/.caveman/ccr.db`.\n- `cmd/caveman-shrink/` — the CLI: `caveman-shrink` / `shrink` (stdin→stdout), `lint <file>`, and `recover <handle>`.\n- `bin/caveman-shrink.mjs` + `package.json` — the `npx caveman-shrink` launcher around the prebuilt binary.\n\n## Two correctness boundaries\n`SelectionProfile(input)` == `SelectionProfile(Shrink(input).Output)` always. This proves the\nstructural selection surface (names/params/enums/required) survives byte-for-byte. It does **not**\nprove the model selects the same tool: descriptions are model-visible and long descriptions are\nlossy. Behavioral selection equivalence requires model-eval fixtures; keep results `inferred`.\n\nSelection alone does not guarantee valid arguments. Short descriptions stay whole; long ones\nkeep the lead plus every recognised constraint sentence (must/cannot/required/bounds/format/\nISO/RFC/absolute/etc.). `default`, `const`, and internal `$ref` targets survive. Golden and\nadversarial conformance tests lock these construction surfaces; over-keep is safer than a retry.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=shrink` / `make product-test PRODUCT=shrink`.\n- The structural compressor lives in the **engine** (`compressors/toolschema.go`); shrink reuses it, never forks it.\n- **This is the dedicated product surface for the `toolschema` compressor.** Engine API/CLI callers\n  can also force it locally. Managed-gateway adapters keep tool arrays in the frozen prompt-cache\n  prefix and never hand them to it; gateway's separate S2 tool-search/deferral path is not\n  compression. Local reductions stay `inferred`. A future billed route for this transform needs\n  cache-versus-schema cost proof, stable byte-identical prefix output, and an eval gate.\n\n## Gotchas (honesty invariants)\n- **S4 lossy, fail-open** — successful compression changes model-visible bytes; malformed/incompressible catalogs pass through unchanged (`ratio:0`, no handle).\n- **inferred-only** — token counts are estimates via the engine's offline counter; never `verified`, never re-projected.\n- **reversible for real** — a compressing shrink commits the exact original to the durable shared\n  CCR store before returning a handle. A separate process resolves it through `Recover` /\n  `caveman-shrink recover`; failure to open storage makes library `Shrink` return an error, while\n  the CLI catches that error and forwards the original bytes with no handle.\n\nSee ../../CLAUDE.md (root) · ../engine/CLAUDE.md\n\n<!-- File: browse/CLAUDE.md (Tokens: ~840 | Category: claude-rule) -->\n# browse — `caveman-browse` browser driver\n\n> **Repository routing:** do not continue Browse product work here. Source of\n> truth is `JuliusBrussee/caveman-browse`, local checkout\n> `/Users/julb/Desktop/GitHub/caveman-browse`. This directory is a consumer\n> copy; edit only for pinned integration, migration/removal, or an explicit\n> cross-repo sync. Accessibility-compressor work stays here in\n> `engine/compressors/axtree.go` because Engine is owned by this repo.\n\nLocal, agent-facing browser interaction for the open Caveman surface. It serves\nstdio MCP tools that read a real Chrome accessibility tree, compress it with the\nengine's forced-only `a11y` compressor, act on `uid` handles, and recover the\nbyte-exact original AX payload through CCR. Every saving here is `inferred`;\nBrowse never emits `verified`.\n\n## Layout\n- `session.go` — MCP tool handlers, engine/CCR integration, UID target cache.\n- `cdp.go` — chromedp-backed Mode-A dedicated Chrome driver and bounded\n  actionability recipe.\n- `cmd/caveman-browse/` — stdio MCP binary.\n\n## Gotchas\n- This package may import CDP/network/browser dependencies. `public/mcp` may not.\n- `go test ./public/browse/...` is setup-free. `make test-browse` resolves an\n  installed Playwright Chromium or system Chrome and runs the `integration`\n  build-tagged CDP contract across the package and direct CLI; `make\n  test-browser` adds extension tests and `make test-e2e` includes both.\n- The actionability layer is intentionally bounded: same-origin dashboards and\n  predictable design-system controls, not arbitrary-open-web parity.\n- Unknown handles/actions fail closed with `cave_snake_code` errors.\n- Snapshot output is compact indented text, not JSON-lines. `query` keeps at most\n  12 highest-scoring task matches plus ancestors; recovery metadata exposes only\n  UIDs actually shown. Keep `tokens_after` equal to exact agent-visible result\n  cost and `view_tokens` equal to serializer output cost.\n- Direct CLI Chrome is detached so separate `snapshot`/`act`/`eval` processes\n  can share a target. `close` must terminate it. Fresh `CAVEMAN_HOME` must work;\n  state writes stay atomic and mode `0600`.\n- CDP action acknowledgement is not application settlement. Non-wait actions\n  return `settled:false` and require a focused resnapshot for proof.\n- Navigation denies `file:`, `javascript:`, and privileged Chrome schemes.\n- Benchmark contract and reproducible Playwright comparison live in\n  `BENCHMARK.md`; keep token budgets executable in tests.\n- **The uid map is `browser_snapshot`'s contract, not a side effect of a\n  compression ratio.** On engine pass-through (no recovery handle — a tree that\n  did not get smaller, or no CCR store) `snapshotTool` fails closed with\n  `cave_browser_snapshot_uncompressed` and returns the prior page's uids intact;\n  it MUST NEVER dump the raw AX tree into `uids` (strictly worse than not using\n  Browse) nor wipe the target cache. Regressing this reopens issue #140.\n- **An `<iframe>` is a leaf, not a broken tree.** `Accessibility.getFullAXTree`\n  returns one frame at a time, so an iframe node's `childId` points at a child\n  document absent from this payload; the `a11y` compressor\n  (`engine/compressors/axtree.go`) treats an unresolvable `childId` as a leaf and\n  still curates the frame-visible nodes. Because of that the CDP driver keeps\n  Chrome's default Site Isolation (`site-per-process`) — do not disable it to\n  \"fix\" cross-origin iframes.\n\n<!-- File: CLAUDE.md (Tokens: ~11935 | Category: claude-rule) -->\n# CLAUDE.md — caveman\n\n## README is a product artifact\n\nREADME = product front door. Non-technical people read it to decide if caveman worth install. Treat like UI copy.\n\n**Rules for any README change:**\n\n- Readable by non-AI-agent users. If you write \"SessionStart hook injects system context,\" invisible to most — translate it.\n- Keep Before/After examples first. That the pitch.\n- Install table always complete + accurate. One broken install command costs real user.\n- What You Get table must sync with actual code. Feature ships or removed → update table.\n- Preserve voice. Caveman speak in README on purpose. \"Brain still big.\" \"Cost go down forever.\" \"One rock. That it.\" — intentional brand. Don't normalize.\n- Benchmark numbers from real runs in `benchmarks/` and `evals/`. Never invent or round. Re-run if doubt.\n- Adding new agent to install table → add detail block in `<details>` section below.\n- Readability check before any README commit: would non-programmer understand + install within 60 seconds?\n\n---\n\n## Project overview\n\nCaveman makes AI coding agents respond in compressed caveman-style prose while preserving technical substance, code, commands, and exact errors. Publish no reduction or quality-equivalence percentage without a committed reviewed benchmark. Ships as Claude Code plugin, Codex plugin, Gemini CLI extension, and agent rule files for Cursor, Windsurf, Cline, Copilot, and other profiles via `npx skills`.\n\n## Repository routing\n\nThis repo is source of truth for Caveman skills, Engine, and MV3 directive\nextension. Agent SDK + initializer work belongs in\n`/Users/julb/Desktop/GitHub/caveman-agent-sdk`\n(`JuliusBrussee/agent-sdk`). Proprietary Pebble runtime, policy, sessions, TUI,\ndistribution, and conformance work belongs in\n`/Users/julb/Desktop/GitHub/caveman-coding-agent`\n(intended `JuliusBrussee/caveman-coding-agent`). Browse driver/MCP/benchmark/plugin work\nbelongs in `/Users/julb/Desktop/GitHub/caveman-browse`\n(`JuliusBrussee/caveman-browse`). The matching `browse/` directory here is a\nconsumer copy (it still builds the `caveman-browse` release binary); edit only\nfor pinned integration, migration/removal, or an explicitly requested\ncross-repo sync.\n\nVisibility is separate from ownership: this repo and `caveman-browse` are\npublic now; `caveman-agent-sdk` is private during development and planned for\npublic release after its release gates; `caveman-coding-agent` and\nCaveman-Cloud remain private commercial source.\n\n---\n\n## What lives where\n\nPost-cleanup layout. Sources of truth at the top, distribution mirrors below, build outputs in `dist/`, human docs alongside each skill.\n\n```\ncaveman/\n├── README.md                    # Front door (product pitch)\n├── INSTALL.md                   # Per-agent install commands\n├── CONTRIBUTING.md              # Dev guide\n├── CLAUDE.md                    # This file (maintainer instructions)\n├── AGENTS.md / GEMINI.md        # Autodiscovery files (must stay at root)\n│\n├── install.sh / install.ps1     # 30-line shims → installer/install.js\n│\n├── installer/                   # Unified installer\n│   ├── install.js               # Single source for all 30+ agents (PROVIDERS array)\n│   └── lib/settings.js          # JSONC-tolerant settings.json reader/writer\n│\n├── skills/                      # ALL skills, single source of truth\n│   ├── caveman/{SKILL.md, README.md}\n│   ├── caveman-commit/{SKILL.md, README.md}\n│   ├── caveman-review/{SKILL.md, README.md}\n│   ├── caveman-help/{SKILL.md, README.md}\n│   ├── caveman-stats/{SKILL.md, README.md}\n│   ├── caveman-compress/{SKILL.md, README.md, scripts/}\n│   └── cavecrew/{SKILL.md, README.md}\n│\n├── agents/                      # cavecrew subagents (single source — kept at root for plugin auto-discovery)\n│   └── docs/                    # profile-registry docs — NOT agents (see note below)\n├── commands/                    # Codex/Gemini TOML command stubs (root for plugin auto-discovery)\n│\n├── src/                         # Internal source — not auto-discovered by plugin\n│   ├── hooks/                   # Claude Code hooks (installer reads here)\n│   ├── rules/                   # Auto-activation rule body (single source)\n│   ├── tools/                   # caveman-init.js (per-repo rule writer)\n│   └── mcp-servers/             # caveman-shrink npm-published MCP middleware\n│\n├── packages/                    # Current public packages\n│   ├── cli/                     # @caveman-ai/cli\n│   ├── pi-extension/            # @caveman-ai/pi — native Pi extension (bundled into the CLI, published on `pi-v*` tags)\n│   ├── sdk/                     # TypeScript + Python gateway clients\n│   ├── subagent-tax/            # Local harness-prefix benchmark\n│   └── shared/                  # Contracts + binary installer\n├── engine/ · proxy/             # Local compression runtime + provider proxy\n├── mcp/ · mem/ · shrink/        # Recovery tools, memory, output compression\n├── browse/                      # consumer copy; source = caveman-browse\n├── extension/                   # MV3 extension source\n├── shared/                       # Provider catalog + platform libraries\n│\n├── .claude-plugin/              # Claude Code plugin manifest (REQUIRED at root)\n├── .cursor-plugin/plugin.json   # Cursor plugin: skills/, hooks/hooks-cursor.json, agents/ (default scan)\n├── hooks/hooks-cursor.json      # Cursor plugin sessionStart hook ONLY. Never add hooks/hooks.json:\n│                                #   Claude Code and Gemini auto-load it from the plugin/extension root\n├── plugins/caveman/             # Claude Code plugin distribution (CI-mirrored)\n│   ├── skills/                  # ← from skills/\n│   └── agents/                  # ← from agents/\n│\n├── dist/                        # Build artifacts (gitignored)\n│   └── caveman.skill            # ZIP of skills/caveman/, rebuilt by CI\n│\n├── tests/                       # All tests (Node + Python)\n├── benchmarks/                  # Real token measurements through Claude API\n├── evals/                       # Three-arm eval harness\n├── docs/                        # User-facing docs site\n└── .github/workflows/           # CI sync\n```\n\n---\n\n## File structure and what owns what\n\n### Single source of truth files — edit only these\n\n| File | What it controls |\n|------|-----------------|\n| `skills/caveman/SKILL.md` | The caveman voice: rules, auto-clarity, persistence. Sibling skills `skills/ultracave/SKILL.md` (grammar stripped) and `skills/megacave/SKILL.md` (文言文) are self-contained and each map 1:1 to a stored mode. Edit these three for behavior changes. |\n| `src/rules/caveman-activate.md` | Always-on auto-activation rule body, consumed by `src/tools/caveman-init.js` (per-repo IDE rule files) and by the opencode `AGENTS.md` block. GENERATED: `skills/compile.mjs` derives it from `skills/caveman/SKILL.md` (thesis line + rule headlines) plus the fixed tail in `skills/activation-rule.mjs`, and rewrites the `RULE_BODY` fallback in `caveman-init.js` to match. Edit the skill or the tail, rerun `node packages/cli/scripts/compile-registries.mjs`; `tests/installer/rule-copies.test.mjs` fails on drift. |\n| `src/rules/caveman-openclaw-bootstrap.md` | Marker-fenced bootstrap snippet appended to `~/.openclaw/workspace/SOUL.md` by `installer/lib/openclaw.js`. Drives always-on caveman through the OpenClaw gateway. Must include the SENTINEL `Respond terse like smart caveman` and stay well under OpenClaw's 12K-per-bootstrap-file cap. |\n| `.codex/codex-sessionstart.js` | Repo-local Codex SessionStart hook. Resolves the configured default mode through `src/hooks/caveman-config.js` and emits the active skill's ruleset whole, replacing the hardcoded `full`-level echo `.codex/hooks.json` used to carry — so `CAVEMAN_DEFAULT_MODE`, a repo-local `.caveman.json` and a user-config `defaultMode: \"off\"` all take effect on Codex. Also the user-level always-on hook (#573): `installer/install.js --only codex` copies it (shipped via `package.json` `files`) into the owned payload `$CODEX_HOME/caveman/hooks/` beside `caveman-config.js` and `package.json`, with `caveman/skills/{caveman,ultracave,megacave}/SKILL.md`, and merges one SessionStart entry into `$CODEX_HOME/hooks.json`, identified by that script path so foreign and caveman-CLI `native-hook` entries are never touched. `--no-hooks`/`--minimal` skip it; uninstall unmerges before removing the payload. Codex asks the user to trust a new hook via `/hooks`; never pre-trust it. Carries a hand-copied `FALLBACK_VALID_MODES` for when the shared resolver is absent; `tests/hooks/codex-sessionstart.test.mjs` fails if it drifts from `VALID_MODES`. |\n| `installer/lib/openclaw.js` | OpenClaw install/uninstall helper. Frontmatter merge (`version`, `always: true`), SOUL.md marker append/strip, idempotent. Shared by `installer/install.js` and `src/tools/caveman-init.js`. |\n| `skills/caveman-commit/SKILL.md` | Caveman commit message behavior. Fully independent skill. |\n| `skills/caveman-review/SKILL.md` | Caveman code review behavior. Fully independent skill. |\n| `skills/caveman-help/SKILL.md` | Quick-reference card. One-shot display, not a persistent mode. |\n| `skills/caveman-compress/SKILL.md` | Compress sub-skill behavior. |\n| `skills/cavecrew/SKILL.md` | Cavecrew decision guide — when to delegate to caveman subagents vs vanilla. Edit only here. |\n| `skills/{caveman-setup,caveman-discover,caveman-learn,caveman-manage,caveman-optimize,caveman-explore,caveman-evidence-review}/SKILL.md` | Engine/proxy driver skills. Ship with the plugin (see the auto-discovery note below). |\n| `skills/{investigate-first,lean-build,surgical-patch,safe-refactor,migration,verify-and-stop}/SKILL.md` | Token-discipline work patterns — same goal as caveman prose (fewer output tokens) applied to code volume rather than wording. Deliberately un-branded so they read as generic patterns to the model. Ship with the plugin. |\n| `agents/cavecrew-investigator.md` | Read-only locator subagent (haiku). Output contract: `path:line — symbol — note`. |\n| `agents/cavecrew-builder.md` | Surgical 1-2 file editor subagent. Refuses 3+ file scope. |\n| `agents/cavecrew-reviewer.md` | Diff/file reviewer subagent (haiku). One-line findings with severity emoji. |\n| `src/plugins/opencode/plugin.js` | opencode native plugin. ESM Bun module — `session.created` writes flag, `tui.prompt.append` parses slash/natural-language activation and appends per-prompt reinforcement. Reuses `caveman-config.js` via `createRequire`. |\n| `src/plugins/opencode/commands/*.md` | Six opencode slash-command prompt templates (`/caveman`, `/caveman-{commit,review,compress,stats,help}`). |\n\n### `skills/` is auto-discovered wholesale — every subdirectory ships\n\n`.claude-plugin/marketplace.json` sets `\"source\": \"./\"`, so the plugin root IS\nthe repo root, and Claude Code auto-discovers **every** `skills/*/SKILL.md`\nwith no `skills` key in `plugin.json` to gate it. Adding a directory under\n`skills/` therefore installs it into every plugin user's agent, where its\n`description` competes for activation on ordinary work.\n\nThere is no allowlist. Before adding a directory here, decide whether it\nshould reach end users; if not, it belongs under `packages/` or another root.\nKeep the two tables above and the README \"What you get\" table in sync with the\nactual directory listing — `ls skills/` is the source of truth.\n\n### `agents/*.md` is auto-discovered too — and `plugin.json` must NOT list them\n\nSame mechanism, sharper edge. `plugin.json` used to carry an explicit\n`\"agents\": [\"./agents/cavecrew-*.md\"]` array. On Claude Code 2.1.235 that array\nloads **zero** agents — `claude plugin details caveman` reports `Agents (0)`\nwith it and `Agents (3)` without it, so the three cavecrew subagents the\n`cavecrew` skill delegates to did not exist for any plugin user. The docs say\n`string | string[]` is valid and a directory string is rejected outright\n(`agents: Invalid input`), so the default `agents/` scan is the only path that\nworks. Do not re-add the key.\n\nConsequence: **every `.md` anywhere in `agents/` becomes a subagent**, named\nfrom its frontmatter or filename. Maintainer documentation belongs outside that\ntree; profile-registry orientation lives in\n`docs/technical/agent-profile-registry.md`. `tests/verify_repo.py` fails build\nif any markdown beyond three intended cavecrew agents appears under `agents/`.\n\n### `commands/*.md` shadows same-named skills — keep the `.md` stubs unique\n\nClaude Code loads `commands/*.md` as flat skills alongside `skills/*/SKILL.md`,\nso a stub sharing a skill's name registers that name twice. `caveman.md`,\n`caveman-commit.md`, `caveman-review.md` and `caveman-stats.md` each duplicated\na real skill — a 3-line stub competing with the full ruleset for the same slash\ncommand — and were removed. The `.toml` stubs are Codex/Gemini-only and are not\nscanned, so `commands/` keeps one `.md`: `caveman-init.md`, which has no skill\ntwin. Guarded by `tests/verify_repo.py`.\n\n### Auto-generated / auto-synced — do not edit directly\n\nWe removed the agent-specific dotdir mirrors at the repo root (`.cursor/`, `.windsurf/`, `.clinerules/`, `.github/copilot-instructions.md`, root `caveman/SKILL.md`). They were never read by the installer — only used to self-apply caveman to this repo when a maintainer opened it in Cursor/Windsurf/Cline. Devs who want caveman in their editor while editing this repo should run `npx -y github:JuliusBrussee/caveman -- --with-init` once (writes per-repo rule files from `src/rules/caveman-activate.md` via `src/tools/caveman-init.js`). For per-user installs through the upstream skills CLI, `npx -y github:JuliusBrussee/caveman -- --only <agent>` runs `npx skills add ... -a <profile>`.\n\nA handful of dotdir leftovers (`.junie/`, `.kiro/`, `.roo/`, `.agents/`) still hold a stale `cavecrew/SKILL.md` mirror from before the cleanup. They aren't read by anything in the current install path; remove on sight, no migration needed.\n\nWhat's left is the Claude Code plugin distribution (required by the plugin loader) and the release ZIP.\n\n| File | Synced from |\n|------|-------------|\n| `plugins/caveman/skills/caveman/SKILL.md` | `skills/caveman/SKILL.md` |\n| `plugins/caveman/skills/caveman-compress/SKILL.md` (+ `scripts/`) | `skills/caveman-compress/SKILL.md` (+ `scripts/`) |\n| `plugins/caveman/skills/cavecrew/SKILL.md` | `skills/cavecrew/SKILL.md` |\n| `plugins/caveman/agents/cavecrew-*.md` | `agents/cavecrew-*.md` |\n| `dist/caveman.skill` | ZIP of `skills/caveman/` directory (gitignored; rebuilt by CI on release) |\n\nSkills not in this table (`caveman-commit`, `caveman-review`, `caveman-help`, `caveman-stats`) are not mirrored into the Claude Code plugin distribution by CI. They reach Claude Code through the standalone hook + skill install path, and reach other agents via `npx skills add`. A `plugins/caveman/skills/caveman-stats/` directory is currently checked in as a hand-committed copy; the sync workflow does not touch it, so don't rely on edits there to propagate.\n\n---\n\n## CI sync workflow\n\n`.github/workflows/sync-skill.yml` triggers on main push when `skills/**/SKILL.md` or `agents/cavecrew-*.md` changes.\n\nWhat it does:\n1. Copies `skills/caveman/SKILL.md` and `skills/cavecrew/SKILL.md` into their `plugins/caveman/skills/<name>/` mirrors so the Claude Code plugin loader sees the latest behavior.\n2. Copies `skills/caveman-compress/SKILL.md` and its `scripts/` into `plugins/caveman/skills/caveman-compress/`.\n3. Copies `agents/cavecrew-*.md` into `plugins/caveman/agents/`.\n4. Rebuilds `dist/caveman.skill` (ZIP of `skills/caveman/`) for the release artifact.\n5. Commits and pushes with `[skip ci]` to avoid loops.\n\nCI bot commits as `github-actions[bot]`. After PR merge, wait for workflow before declaring release complete.\n\nThe old steps that mirrored SKILL.md and rules into root dotdirs (`.cursor/`, `.windsurf/`, `.clinerules/`, `.github/copilot-instructions.md`) are gone — those mirrors no longer exist. The old `caveman-compress/` → `skills/compress/` rename-on-sync is also gone now that compress lives at `skills/caveman-compress/`.\n\n---\n\n## Hook system (Claude Code)\n\nFour hooks in `src/hooks/` plus a `caveman-config.js` shared module, a `caveman-parse.js` shared mode-change parser and a `package.json` CommonJS marker.\n\n**Mode state is per session.** Each session's mode lives in `$CLAUDE_CONFIG_DIR/.caveman-sessions/<session_id>.mode`, keyed by the `session_id` Claude Code puts in every hook payload *and* in the statusline's stdin JSON. `$CLAUDE_CONFIG_DIR/.caveman-active` survives as a last-write-wins compat mirror (falls back to `~/.claude/`).\n\n```\nSessionStart hook ──┐                                        ┌── UserPromptSubmit hook\n  (session_id,      │                                        │     (session_id, prompt)\n   source)          ▼                                        ▼\n        $CLAUDE_CONFIG_DIR/.caveman-sessions/<session_id>.mode\n                             │             │\n                          mirrors       reads\n                             ▼             ▼\n              .caveman-active      caveman-statusline.sh ◀── session JSON on stdin\n           (last-write-wins,        [CAVEMAN] / [ULTRACAVE] / [MEGACAVE]\n            compat only)\n```\n\nTwo invariants that are load-bearing, not stylistic:\n\n- **The legacy mirror never holds the literal `off`.** Deactivation deletes it. `off` is in `VALID_MODES`, so an older `caveman-mode-tracker.js` reading `off` from that path would pass its `!INDEPENDENT_MODES.has(...)` check and inject \"CAVEMAN MODE ACTIVE (off)\", and an older `caveman-statusline.sh` would render `[CAVEMAN:OFF]`. Mixed-version installs are real: plugin hooks and standalone hooks can both be registered at once, and `statusLine` holds an absolute path baked in at install time.\n- **`recordModeChange` compares against the session's own state, never the mirror.** That's why `readSessionModeRaw` exists alongside `resolveActiveMode` — diffing against the mirror would compare one session's transition to whatever another session wrote last and spam the log with phantom entries.\n\nBoth spellings of off are READ everywhere: a missing file (old semantics) and a literal `off` (new, durable).\n\n`src/hooks/package.json` pins the directory to `{\"type\": \"commonjs\"}` so the `.js` hooks resolve as CJS even when an ancestor `package.json` (e.g. `~/.claude/package.json` from another plugin) declares `\"type\": \"module\"`. Without this, `require()` blows up with `ReferenceError: require is not defined in ES module scope`.\n\nAll hooks honor `CLAUDE_CONFIG_DIR` for non-default Claude Code config locations.\n\n### `src/hooks/caveman-config.js` — shared module\n\nExports:\n- `getDefaultMode()` — resolves default mode in order: `CAVEMAN_DEFAULT_MODE` env var → repo-local config (`<cwd>/.caveman/config.json` or `<cwd>/.caveman.json`, walking up to the filesystem root; the first file that names a `defaultMode` wins, so a CLI-only `.caveman/config.json` does not hide a `.caveman.json`) → user config (`$XDG_CONFIG_HOME/caveman/config.json` / `~/.config/caveman/config.json` / on Windows `%APPDATA%\\caveman\\config.json`, then `~/.config/caveman/config.json`) → `'caveman'`. Config files may carry a BOM, be UTF-16LE, or hold comments and trailing commas. The env var short-circuits before any cwd walk. Repo-local config lets a team check in a per-project default without polluting every contributor's env or user config.\n- `findRepoConfigPath(start, accept)` — walks up from `start` (default `process.cwd()`) looking for the first `.caveman/config.json` or `.caveman.json` (that `accept(path)` approves, when given). Bounded to 64 ancestors. Refuses symlinked files (symmetric with `safeWriteFlag` / `readFlag`).\n- `safeWriteFlag(flagPath, content)` — symlink-safe flag write. Refuses if flag target or its immediate parent is a symlink. Opens with `O_NOFOLLOW` where supported. Atomic temp + rename. Creates with `0600`. Protects against local attackers replacing the predictable flag path with a symlink to clobber files writable by the user. Used by both write hooks. Silent-fails on all filesystem errors.\n- `validateSessionId(id)` — returns the id or `null`. Whitelist `^[A-Za-z0-9_-]{1,128}$`. **A session id becomes part of a filesystem path, so nothing may interpolate one without passing it through here first.** The symlink hardening in `safeWriteFlag` does not cover traversal.\n- `resolveActiveMode(claudeDir, sessionId)` — what mode is in effect: session file → legacy mirror, with both a missing file and a literal `off` collapsing to `null`. The reader every hook should use.\n- `readSessionModeRaw(claudeDir, sessionId)` — literal stored value for **this** session, no legacy fallback. Two callers only: `recordModeChange` (see the invariant above) and the SessionStart continuation branch, which has to tell a stored `off` apart from \"nothing stored yet\".\n- `writeSessionMode(claudeDir, sessionId, modeOrNull)` — the single writer. Writes `off` literally to the session file, unlinks the legacy mirror. Rejects anything outside `VALID_MODES`.\n- `writeSessionPrev` / `readSessionPrev` / `clearSessionPrev` — the displaced-prose-mode memory for one-shot skills (#599), scoped per session so two windows running `/caveman-commit` don't overwrite each other's return target.\n- `gcSessionStore(claudeDir, opts)` — mtime sweep of `.caveman-sessions/`, 14-day TTL (`CAVEMAN_SESSION_TTL_MS` overrides for tests), `maxDeletes` cap, refuses symlinks. Called from SessionStart on new sessions only — never on `compact`, which is frequent and shares the 5s hook budget.\n- `recordModeChange(claudeDir, newMode, sessionId)` — third arg tags the log entry; omitted (not null) when unknown, so pre-existing readers see the old shape.\n\n**Every function above accepts a `sessionId` that may be `null` or malformed and degrades to the legacy machine-wide behavior.** That is the entire backward-compatibility story: the old code path *is* the fallback branch, which is why the existing tests — none of which send a `session_id` — pass unchanged. The three hook entrypoints resolve these helpers individually (`cfg.writeSessionMode || <legacy stub>`) rather than adding them to their `requireSibling` shape checks: a `caveman-config.js` from before per-session state satisfies those checks, and hard-failing over the newer exports would trade \"machine-wide mode, as it always worked\" for \"no state at all\" on exactly the plugin-cache-drift scenario #848 is about.\n\n### `src/hooks/caveman-activate.js` — SessionStart hook\n\nRuns on every SessionStart — `source` is `startup`, `resume`, `clear`, `compact` or `fork`. Four things:\n1. Reads the hook payload from stdin for `session_id`, `source` and `cwd`\n2. Resolves this session's mode and persists it via `writeSessionMode`\n3. Emits caveman ruleset as hidden stdout — Claude Code injects SessionStart hook stdout as system context, invisible to user\n4. Checks `settings.json` for statusline config; if missing, appends nudge to offer setup on first interaction (one-shot, gated by `.caveman-nudge-shown`)\n\n**`source` branching.** `RESET_SOURCES` is `{startup, clear}` — only those re-derive `getDefaultMode()`. Everything else (`compact`/`resume`/`fork`, an unrecognized source, and the watchdog's `unknown`) READS the stored mode via `readSessionModeRaw`, falling back to the legacy mirror for a session that predates the store, and only then to the default. Reading the LITERAL value is the point: #691 already branched on `source`, but it read the legacy flag where off is spelled \"no file\", so a deactivated session found nothing and re-derived the default anyway — \"stop caveman\" was still undone by the next auto-compaction. A stored `off` now short-circuits to `OK` with no ruleset. It still re-emits when a mode IS active: compaction is what prunes the rules out of context, which is the whole reason this hook runs on every source. `clear` counts as a fresh start (explicit user reset — nothing else in the conversation survives it, so neither should a \"stop caveman\" from before it); this deliberately differs from #691's comment, which grouped `clear` with the continuations.\n\n**Headless sessions start under `manual` (#377).** Whenever the hook falls back to the configured default (`startMode()`), a `CLAUDE_CODE_ENTRYPOINT` starting with `sdk-` (`claude -p`, Agent SDK) turns any non-`off` default into `manual`: tool probes that parse the reply get no ruleset until an explicit `/caveman`. Interactive entrypoints (`cli`, `claude-vscode`, `claude-desktop`, …) never match; an explicit `CAVEMAN_DEFAULT_MODE` env var opts back in. Hook test helpers drop `CLAUDE_CODE_ENTRYPOINT` from the env, or a headless runner flips every SessionStart test.\n\n**The stdin contour is load-bearing.** Activation runs on the first COMPLETE JSON object, not at EOF, with a 2000ms `PAYLOAD_WATCHDOG_MS` backstop and `process.stdin.unref()` in `finish()`. `pause()` alone is not enough — it stops reading but leaves the pipe handle referenced, so a host that holds the write end open (Windows pipe close lags arbitrarily, #729/#833) burns the whole 5s budget. The watchdog must NOT assume `startup`: it reports `unknown`, which preserves the session's stored mode rather than resetting it. Several suites also invoke this hook via `subprocess.run()` with no `input=`, inheriting a stdin that never reaches EOF — `tests/test_hooks.py::test_hook_never_blocks_on_stdin_that_never_closes` guards that path.\n\nSilent-fails on all filesystem errors — never blocks session start.\n\n**`--subagent`: the SubagentStart hook (#621).** SessionStart context reaches only the parent thread, so the same script is registered for SubagentStart with `--subagent` (plugin.json, `installer/install.js`, `src/hooks/install.{sh,ps1}`). It reuses the stdin reader and watchdog, resolves THIS session's mode via `resolveActiveMode` (a session with no stored state at all — SessionStart skipped or failed, or the session predates the store — falls back to the last-write-wins legacy mirror, the same degrade the tracker's reinforcement uses, so a subagent matches its parent thread; pinned in `tests/hooks/subagent-start.test.mjs`), and prints `{\"hookSpecificOutput\":{\"hookEventName\":\"SubagentStart\",\"additionalContext\":<that mode's skill>}}`. Nothing is printed when the session stored `off` (no \"stop caveman\" leak, #672; another window's mode never overrides a stored `off`), in a one-shot mode, for `cavecrew-*` agents (they carry their own ultracave voice), or under a repo `defaultMode: \"off\"` (#634, same gate as the tracker's reinforcement). Read-only: no mode write, no mode log, no GC, no nudge, no cavecrew model overrides. Uninstall needs nothing extra — `removeCavemanHooks` walks every event and matches the `caveman-activate.js` basename.\n\n### `src/hooks/caveman-mode-tracker.js` — UserPromptSubmit hook\n\nReads JSON from stdin — `session_id` scopes every read and write. Three responsibilities:\n\n**1. Slash-command activation.** If prompt starts with `/caveman`, writes the session's mode via `writeSessionMode`:\n- `/caveman` → configured default (see `caveman-config.js`, defaults to `caveman`)\n- `/ultracave` (alias `/caveman ultra`) → `ultracave`\n- `/megacave` (alias `/caveman wenyan`, any `wenyan-*`) → `megacave`\n- `/caveman lite` or `/caveman full` (legacy aliases) → `caveman`\n- `/caveman-commit` → `commit`\n- `/caveman-review` → `review`\n- `/caveman-compress` → `compress`\n\n**2. Natural-language activation/deactivation.** Matches phrases like \"activate caveman\", \"turn on caveman mode\", \"talk like caveman\" and writes the configured default mode. Matches \"stop caveman\", \"disable caveman\", \"normal mode\", \"deactivate caveman\" etc. and stores a durable `off` for the session. README promises these triggers, the hook enforces them.\n\n**3. Per-turn reinforcement.** When the session's mode is a non-independent one (i.e. not `commit`/`review`/`compress`), emits a small `hookSpecificOutput` JSON reminder so the model keeps caveman style after other plugins inject competing instructions mid-conversation. The full ruleset still comes from SessionStart — this is just an attention anchor.\n\n### `src/hooks/caveman-statusline.sh` — Statusline badge\n\nReads the session JSON Claude Code pipes to it on stdin, extracts `session_id` (pure bash — no `jq` dependency), and reads `.caveman-sessions/<id>.mode`; falls back to `$CLAUDE_CONFIG_DIR/.caveman-active` when there is no usable id. Outputs colored badge string for Claude Code statusline:\n- `caveman` or empty → `[CAVEMAN]` (orange)\n- `off` → nothing at all. Never `[CAVEMAN:OFF]` — that reads as a mode rather than the absence of one\n- `ultracave` → `[ULTRACAVE]`, `megacave` → `[MEGACAVE]`; one-shots → `[CAVEMAN:<MODE_UPPERCASED>]` (orange). Legacy stored values render through the same map\n\nStdin is bounded the same way as the SessionStart hook: `[ ! -t 0 ]` skips an interactive terminal, and `read -r -d '' -t 1` caps the wait. The timeout is an **integer on purpose** — macOS ships bash 3.2, which rejects `-t 0.3` with `invalid timeout specification`.\n\nThe statusline reports mode only. Old `.caveman-statusline-suffix` values are ignored; fixed-ratio savings estimates were removed because no measured baseline supported them. `/caveman-stats` records actual Claude output/cache-read counts and mode attribution with savings unknown. Gemini uses native `/stats model` and `/stats session`; never read Claude transcripts as another host's usage.\n\nConfigured in `settings.json` under `statusLine.command`. PowerShell counterpart at `src/hooks/caveman-statusline.ps1` for Windows. Both scripts symlink-refuse and whitelist-validate mode flag contents — never echo arbitrary bytes.\n\n### `src/hooks/caveman-stats.js --record` — SessionEnd hook\n\nSame script as `/caveman-stats`; the `--record` flag turns it into a silent recorder. Reads the SessionEnd payload (`session_id`, `transcript_path`) and appends one snapshot of that transcript's totals to `$CLAUDE_CONFIG_DIR/.caveman-history.jsonl`, so lifetime history fills in without the user running `/caveman-stats`. Only the transcript the host names is read — never the newest file on disk. Stdin follows the activate-hook contour: first complete JSON object, 2000ms `PAYLOAD_WATCHDOG_MS` (gives up without recording), `unref()` on finish. Always exits 0 and writes nothing to stderr — a SessionEnd failure would surface as a hook error while the user quits. Every installer (plugin manifest, `installer/install.js`, `src/hooks/install.{sh,ps1}`) writes `--record` unquoted after the quoted script path; `rewriteLegacyManagedHookCommands` carries trailing `--flag` args over when it moves a bare-`node` command to the absolute node path.\n\n### `src/hooks/caveman-host-session-start.js` — Cursor / Copilot CLI sessionStart hook\n\n`node caveman-host-session-start.js <host>`, one script for hosts other than Claude Code. Emits the resolved mode's ruleset under the host's key: `additional_context` for `cursor`, `additionalContext` for `copilot`. The mode is `getDefaultMode()` resolved from the session's workspace (`cwd`, Cursor's `workspace_roots` or `CURSOR_PROJECT_DIR`), so `defaultMode` and repo `.caveman.json` apply; `off`, `manual`, one-shot modes, an unknown host or any failure print `{}`. Writes no state. The owned install at `<host>/caveman/hooks/` reads its pinned `<host>/caveman/skills/` before the shared resolver, which would otherwise find the host's unpinned `npx skills` copy first. Stdin follows the activate-hook contour.\n\n### Hook installation\n\n**Plugin install** — hooks wired automatically by plugin system.\n\n**Standalone install** — `installer/install.js` (the unified Node installer) copies hook files into `$CLAUDE_CONFIG_DIR/hooks/` and merges SessionStart + SubagentStart + UserPromptSubmit + SessionEnd + statusline into `settings.json`. Uses the JSONC-tolerant helpers in `installer/lib/settings.js` so a commented `settings.json` no longer crashes the merge. Defensive `validateHookFields` runs before every write to prevent a single malformed hook from poisoning the entire file (Claude Code Zod silently discards the whole `settings.json` on schema mismatch).\n\nThe `install.sh` / `install.ps1` shims at the repo root delegate to `installer/install.js` via `node` (local clone) or `npx -y github:JuliusBrussee/caveman` (curl|bash). No legacy fallback path remains — earlier `install.sh.legacy` / `install.ps1.legacy` files were removed.\n\n**Uninstall** — `npx -y github:JuliusBrussee/caveman -- --uninstall` (or `node installer/install.js --uninstall` from a clone). Strips caveman hook entries from `settings.json` via substring marker `caveman`, deletes hook files, and removes the Claude plugin / Gemini extension. Native skill copies use `installer/lib/provider-skills.js` and ownership journals; uninstall removes unchanged owned files while preserving foreign or edited content. Use the same vendor home overrides for install and uninstall. Delegated `npx skills add` installs remain managed by the host/upstream skill manager.\n\n---\n\n## Skill system\n\nSkills = Markdown files with YAML frontmatter consumed by Claude Code's skill/plugin system and by `npx skills` for other agents.\n\nEach skill has a human-facing `README.md` alongside the LLM-facing `SKILL.md`. The README explains what the skill does for users browsing GitHub; the SKILL.md is the prompt body the agent loads. Don't merge them — different audiences, different formats.\n\n### Three skills, one mode model\n\nThree self-contained skills, each 1:1 with a stored mode id: `caveman` (default voice), `ultracave` (grammar stripped), `megacave` (文言文). The SessionStart hook injects `skills/<id>/SKILL.md` whole; there is no per-level filtering. Legacy stored values map on read: `lite` and `full` → `caveman`, `ultra` → `ultracave`, every `wenyan*` → `megacave`. Writes emit new ids only. Mode persists until changed or session ends. Plan: `docs/technical/three-skills-refactor.md`.\n\n`defaultMode: \"manual\"` is a Claude Code startup policy, not a fourth mode. It starts inactive, stores only `off`, and explicit bare-command or natural-language activation resolves to `caveman`. Keep it out of `VALID_MODES`; only configuration accepts it. OpenCode maps this policy to its existing caveman default because its installer also supplies static AGENTS.md activation. `/caveman status` is read-only and must return before one-shot restoration or any mode-state mutation.\n\n### Auto-clarity rule\n\nCaveman drops to normal prose for: security warnings, irreversible action confirmations, multi-step sequences where fragment ambiguity risks misread, user confused or repeating question, questions the agent asks the user. Resumes after. Defined in skill — preserve in any SKILL.md edit.\n\n### caveman-compress\n\nSub-skill in `skills/caveman-compress/SKILL.md`. Takes file path, compresses prose to caveman style, writes to original path, saves backup at `<filename>.original.md`. Validates headings, code blocks, URLs, file paths, commands preserved. Retries up to 2 times on failure with targeted patches only. Requires Python 3.10+.\n\nThe slash command is `/caveman-compress` everywhere — same name in plugin and standalone install. CI no longer renames the directory on sync (the old `caveman-compress/` → `skills/compress/` sed rename is gone now that the source lives at `skills/caveman-compress/`).\n\n### caveman-commit / caveman-review\n\nIndependent skills in `skills/caveman-commit/SKILL.md` and `skills/caveman-review/SKILL.md`. Both have own `description` and `name` frontmatter so they load independently. caveman-commit: Conventional Commits, ≤50 char subject. caveman-review: one-line comments in `L<line>: <severity> <problem>. <fix>.` format.\n\n---\n\n## Agent distribution\n\nHow caveman reaches each agent type:\n\n| Agent | Mechanism | Auto-activates? |\n|-------|-----------|----------------|\n| Claude Code | Plugin (hooks + skills) or standalone hooks | Yes — SessionStart hook injects rules; SubagentStart passes the session's mode to subagents |\n| Codex | `npx skills add -a codex` plus an owned SessionStart hook in `$CODEX_HOME/hooks.json` (installer, default on); plugin in `plugins/caveman/`; repo `.codex/hooks.json` and `.codex/config.toml` for this checkout | Yes — SessionStart hook, after the user trusts it once via `/hooks` |\n| Gemini CLI | Extension with `GEMINI.md` context file | Yes — context file loads every session |\n| opencode | Native plugin (`src/plugins/opencode/`) copied into `~/.config/opencode/plugins/caveman/` + `AGENTS.md` ruleset + skills/agents/commands directories. Plugin uses `session.created` and `tui.prompt.append` lifecycle hooks. No statusline (opencode TUI exposes no plugin-writable badge). | Yes — `session.created` writes flag, `AGENTS.md` carries always-on ruleset |\n| OpenClaw | Workspace skill at `~/.openclaw/workspace/skills/caveman/SKILL.md` (frontmatter merged with `version` + `always: true`) plus a marker-fenced bootstrap block in `~/.openclaw/workspace/SOUL.md`. Both writes go through `installer/lib/openclaw.js`; workspace path is overridable via `OPENCLAW_WORKSPACE`. | Yes — SOUL.md is auto-injected each turn under \"Project Context\" (subject to OpenClaw's 12K-per-file / 60K-total bootstrap caps) |\n| Cursor | Plugin (`.cursor-plugin/plugin.json`: skills + `hooks/hooks-cursor.json` sessionStart hook) or `--only cursor`: `npx skills add ... -a cursor -g` skills, then `installer/lib/cursor-native.js` owns cavecrew agents in `~/.cursor/agents/` (Claude `model:` dropped, `readonly: true` on investigator/reviewer) and the shared hook payload in `~/.cursor/caveman/` with one sessionStart entry merged into `~/.cursor/hooks.json`. Both hooks run `src/hooks/caveman-host-session-start.js cursor`. Per-repo `.cursor/rules/caveman.mdc` via `--with-init`. Pick plugin OR `--only cursor`, not both (two hooks inject twice). | Yes — sessionStart `additional_context`, honors `defaultMode` (off/manual inject nothing) |\n| Windsurf | `npx skills add ... -a windsurf` (default via `--only windsurf`); per-repo `.windsurf/rules/caveman.md` via `--with-init` | Yes — always-on rule |\n| Cline | `npx skills add ... -a cline` (default via `--only cline`); per-repo `.clinerules/caveman.md` via `--with-init` | Yes — Cline auto-discovers `.clinerules/` |\n| Copilot | `npx skills add ... -a github-copilot` (detected via the `copilot` CLI plus its `~/.copilot` dir, since AWS Copilot CLI ships a `copilot` binary too, or the VS Code/Cursor extension); per-repo `.github/copilot-instructions.md` + `AGENTS.md` via `--with-init`. When the Copilot CLI is present (`copilot` on PATH or `$COPILOT_HOME` set — not a bare `~/.copilot`, which `npx skills add -a github-copilot` itself creates) the installer also owns `$COPILOT_HOME/caveman/` (shared hook payload) and `$COPILOT_HOME/hooks/caveman.json` (sessionStart → `caveman-host-session-start.js copilot`); `--no-hooks` skips it. No userPromptSubmitted hook: Copilot drops its output. | Copilot CLI: yes — sessionStart `additionalContext`, honors `defaultMode`. VS Code Copilot Chat: repo-wide instructions via `--with-init` |\n| Grok Build | Owned skill copies into `$GROK_HOME/skills` (default `~/.grok`) plus a marker-fenced ruleset block in `$GROK_HOME/AGENTS.md`, appended/stripped through `installer/lib/openclaw.js`'s marker helpers | Yes — Grok loads `~/.grok/AGENTS.md` as global rules (docs-backed; no live binary run) |\n| Antigravity CLI (`agy`) | `--only antigravity-cli` stages a temp plugin (`plugin.json`, the nine skills, `rules/AGENTS.md` = `src/rules/caveman-activate.md`) and runs `agy plugin install <dir>`; agy copies it to `~/.gemini/config/plugins/caveman/` and owns it. Uninstall: `agy plugin uninstall caveman` when `agy plugin list` names it. No session-start hook exists in agy (PreInvocation fires before every model call), so no `defaultMode`. | Yes — plugin `rules/AGENTS.md` merges into agy's active rule set (verified agy 1.2.17) |\n| Continue, AiderDesk, Antigravity IDE/2.0 | Owned physical copies into each vendor's supported directory, honoring configured homes; separate explicit targets for the two Antigravity products | Host skill invocation; feature settings may be required |\n| Others (Junie, Trae, Warp, Tabnine, Mistral, Qwen, Devin, Droid, ForgeCode, Bob, Crush, iFlow, OpenHands, Qoder, Rovo Dev, Replit, …) | Delegated `npx skills` personal installs; Replit uses project scope. Configured iFlow/Crush roots use owned copies | Host skill invocation; `/caveman` where supported |\n\nopencode reaches Tier 1 minus the statusline (opencode's TUI has no plugin-writable badge). Mode flag lives at `~/.config/opencode/.caveman-active` for any external tooling that wants to surface it.\n\nFor agents without hook systems, the always-on snippet lives in `INSTALL.md`'s \"Want it always on?\" section — keep current with `src/rules/caveman-activate.md`.\n\n**Adding a new agent.** Edit the `PROVIDERS` array in `installer/install.js` — single source of truth, no more bash/PS1 dual-source drift. Each entry has `id`, `label`, `mech`, `detect` (clause spec like `command:foo||dir:$HOME/x`; `&&` joins terms that must all hold), optional `profile` (vercel-labs/skills slug), optional `soft: true` (config-dir-only detection).\n\n1. The profile slug must exist in upstream [vercel-labs/skills](https://github.com/vercel-labs/skills). Verify against the README before merging — wrong slugs cause `npx skills add` to fail at runtime, not at install-script load.\n2. Run `node installer/install.js --list` to confirm the new row renders correctly.\n3. Soft probes (config-dir-only) are fine but tag them with `soft: true`. They render with `(soft)` in `--list` so users know detection is best-effort.\n\n---\n\n## Evals\n\n`evals/` has three-arm harness:\n- `__baseline__` — no system prompt\n- `__terse__` — `Answer concisely.`\n- `<skill>` — `Answer concisely.\\n\\n{SKILL.md}`\n\nHonest delta = **skill vs terse**, not skill vs baseline. Baseline comparison conflates skill with generic terseness — that cheating. Harness designed to prevent this.\n\n`llm_run.py` calls `claude -p --system-prompt-file ... --output-format json` per (prompt, arm), isolated from user settings, plugins and MCP, saves text plus Claude-reported usage (input/output/cache tokens, cost) to `evals/snapshots/results.json` (`results.<lang>.json` with `CAVEMAN_EVAL_LANG`). `measure.py` reads snapshot offline with tiktoken (OpenAI BPE — approximates Claude tokenizer, ratios meaningful, absolute numbers approximate). `CAVEMAN_EVAL_SET=fidelity` runs the regex-checked cases in `evals/prompts/fidelity.json` into `snapshots/fidelity.json`; `score_fidelity.py` scores it offline.\n\nAdd skill: drop `skills/<name>/SKILL.md`. Harness auto-discovers. Add prompt: append line to `evals/prompts/en.txt`.\n\nSnapshots committed to git. CI reads without API calls. Only regenerate when SKILL.md or prompts change.\n\n---\n\n## Benchmarks\n\n`benchmarks/` runs real prompts through Claude API (not Claude Code CLI), records raw token counts. Results committed as JSON in `benchmarks/results/`. Benchmark table in README generated from results — update when regenerating.\n\nTo reproduce: `uv run python benchmarks/run.py` (needs `ANTHROPIC_API_KEY` in `.env.local`).\n\n---\n\n## Key rules for agents working here\n\n- Edit `skills/<name>/SKILL.md` for behavior changes. Never edit synced copies under `plugins/caveman/skills/`.\n- Edit `src/rules/caveman-activate.md` for auto-activation rule changes. Never edit any per-agent rule copy a user has on their machine.\n- Edit `src/rules/caveman-openclaw-bootstrap.md` for the OpenClaw SOUL.md bootstrap snippet. Keep the `<!-- caveman-begin -->` / `<!-- caveman-end -->` markers and the `Respond terse like smart caveman` sentinel — `installer/lib/openclaw.js` keys idempotency off both. If you change the embedded fallback in `installer/lib/openclaw.js`, keep it byte-equivalent to the file.\n- Per-skill human docs live in `skills/<name>/README.md`. The LLM-facing body is in `SKILL.md`. Don't merge them — different audiences.\n- Build artifacts go in `dist/`. Never check files into `dist/` manually — CI rebuilds them on push, and `dist/` is gitignored.\n- README most important file for user-facing impact. Optimize for non-technical readers. Preserve caveman voice.\n- `INSTALL.md` is the per-agent install reference. Keep the install table in `README.md` short and link out to `INSTALL.md` for the full matrix.\n- Benchmark and eval numbers must be real. Never fabricate or estimate.\n- CI workflow commits back to main after merge. Account for when checking branch state.\n- Hook files must silent-fail on all filesystem errors. Never let hook crash block session start.\n- Any new flag file write must go through `safeWriteFlag()` in `caveman-config.js`. Direct `fs.writeFileSync` on predictable user-owned paths reopens the symlink-clobber attack surface.\n- Mode state reads/writes go through the `caveman-config.js` session helpers (`resolveActiveMode` / `writeSessionMode` / …), never by joining a path by hand. Any `session_id` that reaches a path must pass `validateSessionId()` first.\n- **Keep mode-state logic in `caveman-config.js`.** `src/plugins/opencode/plugin.js` cannot `require()` from disk (compiled Bun binary) — it reads that file and evaluates it through `new Function(...)` with a `createRequire` that resolves only node built-ins. Parsing already lives in `caveman-parse.js` (#602) and loads the same way, so a second parser copy is no longer the risk; a THIRD home for the state primitives would be, because opencode would keep its own drifting version of them.\n- The two statusline scripts have no shared runtime with the JS, so they re-implement path resolution by hand. `tests/verify_repo.py::verify_powershell_static` greps both against the `SESSIONS_DIRNAME` and `SESSION_ID_RE` constants to catch drift — there is no behavioral `.ps1` test on the POSIX runners, so that grep is the only guard on the Windows badge.\n- Editing anything in `src/hooks/` means regenerating `src/hooks/checksums.sha256` (same file set, recomputed digests) — `tests/verify_repo.py` fails the build otherwise, and `installer/install.js` verifies remote hook downloads against the manifest for the pinned ref.\n- Hooks must respect `CLAUDE_CONFIG_DIR` env var, not hardcode `~/.claude`. Same for `installer/install.js` / statusline scripts.\n- **Any entrypoint that reads a host hook payload from stdin returns on the first complete JSON object, never at EOF.** Under the Windows pipe implementation the host's close lags arbitrarily (#729/#833, #949), so a reader that waits for EOF burns the host's whole budget with its work done. The readers that follow this rule: `caveman-activate.js`, `caveman-mode-tracker.js`, `caveman-stats.js --record` and `caveman-host-session-start.js` (per-chunk parse), `packages/cli/src/native-hook-fast.ts` (`stdin()`), `readHookStdin()` in `packages/cli/src/index.ts` (shrink-hook, memory recall, native-hook), and `readNativeHookPayload` in `proxy/cmd/caveman-proxy/main.go`. A new hook callback uses one of those, not `readStdin()` or `io.ReadAll`. Each has a keep-the-writer-open regression test; add one for any new reader.\n- `installer/install.js` is the only installer source. `install.sh` / `install.ps1` at repo root are 30-line shims that delegate to it. Never re-add per-OS install logic to the shims — that's how we got the Windows quoting bug (#249).\n- Any settings.json read in installer or hooks must go through `installer/lib/settings.js` `readSettings()` so JSONC comments don't crash the merge. Any settings.json write must run through `validateHookFields()` first.\n\n<!-- File: engine/CLAUDE.md (Tokens: ~3014 | Category: claude-rule) -->\n# engine — the content-aware Caveman Engine (commercial, binary-distributed)\n\nDetect a payload's type → route to a safety-classed compressor → count the token reduction\n→ store the original for recovery. The stable four-call API (`Compress`/`Retrieve`/`Detect`/`Stats`)\nis what the proxy, CLI, SDKs, MCP, and WASM build all share. Everything it reports is\n`inferred`; it never says `verified`.\n\n## Layout\n- `engine.go` — the `Engine` core: detect → route → compress → token ratio → CCR. `record` mode + miss + parse-fail + not-smaller + no-store all pass through.\n- `result.go` — `Result`/`Options`/`Mode`; unknown mode fails closed to `record`.\n- `detect.go` — content router (`test-report`/`json`/`diff`/`code`/`log`/`search-result`/`html`/`tabular`/`config`/`terminal`/`text`); low confidence → `text`.\n- `listing.go` — strips a read tool's line-number gutter (`1\\t{`, `2\\t  \"unit\"`) before Detect and before the compressor, then restores it after. Agents send file *listings*, not files; the gutter is presentation, and left in place it made every guttered payload detect as `text` and compress ~0%. Restoration keeps each surviving line's ORIGINAL number and is declined entirely for transforms that restructure rather than elide (re-encoded JSON), because numbers that describe nothing are worse than none.\n- `safety/` — the S0–S4 registry; S4 is lossy and `RequiresCCR`. Unknown class → fail closed.\n- `tokens/` — `Counter` interface; default is an offline, vocab-embedded BPE tokenizer (o200k_base). `inferred` always.\n- `contextwindow/` — deterministic BM25 context packer with recency/error/priority signals and token-budget accounting.\n- `compressors/` — `Compressor` interface + registry; structural JSON/log/test-report/search/diff/text/HTML/table/config/code compressors plus forced-only TOON/tool-schema/tool-schema-annotations/accessibility/repetition paths — `Default()` registers 16. `test-report` and `toolschema-annotations` stay manifest-excluded because no compiled plan routes to them (the proxy's `compiledContentType` allowlist has neither); advertising either would rotate `RegistrySHA256` and fail existing Cave Build locks. A compressor is a pure byte transform.\n- `ccr/` — `~/.caveman/ccr.db` SQLite recovery plus typed native-session store;\n  content-addressed handles, byte-exact `Get`, session scope, dependencies,\n  current/stale state, and Hot/Warm/Cold/Archived lifecycle. Embedded SQLite uses\n  one serialized connection so concurrent hook/repository writes cannot split an\n  in-memory schema or race with `SQLITE_BUSY`.\n- `pixel/` — pxpipe port (MIT, see its NOTICE): text→PNG request compression. Embedded glyph atlases + renderer + profitability gate + per-wire-format transforms (Anthropic/OpenAI/Gemini). S4-lossy, allowlist-gated (`CAVE_PIXEL_MODELS`, default `claude-fable-5,gpt-5.6`), consumed by the proxy's `pixel` mode; never wired into `Detect` and never imported by the WASM build (≈4 MB assets).\n- `evals/` — the local eval harness + fail-closed graders + embedded `fixtures/`. `Run()` is the quality gate.\n- `cmd/caveman-engine/` — the binary the CLI shells out to: `compress | detect | retrieve | stats | registry | toon encode|decode | evals run | pixel render|simulate`. `toon` is the stateless (no-CCR) JSON⇄TOON converter; both directions fail closed.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=engine` / `make product-test PRODUCT=engine`.\n- A new compressor is a self-contained file in `compressors/` + tests, registered in `Default()`. Forced-only compressors such as `toolschema` and `toon` must not be added to `Detect`.\n- Compressors never count tokens, touch CCR, or hit the network — the engine core does that around them.\n- **The `toolschema` transform is client-side-only today.** Registration makes it forceable by\n  engine callers; it does not make it reachable from managed-gateway traffic. Provider adapters\n  intentionally keep tool arrays in the frozen prompt-cache prefix, and no billed route invokes\n  this compressor. Engine API/CLI callers may force it locally, and `caveman-shrink` is its\n  dedicated product surface; those reductions remain local and `inferred`. Managed gateway has a\n  separate S2 tool-search/deferral path; do not conflate it with compression. Any future gateway\n  route for this transform needs cache-versus-schema cost arithmetic, stable byte-identical prefix\n  output, and an eval gate before activation.\n\n## Gotchas (honesty invariants — correctness, not style)\n- **fail-closed transforms**: every compressor passes the original through unchanged on any parse problem; the engine also passes through when the result is not smaller in tokens. Reserve **byte-safe** for classes whose `safety.Info.ByteSafe` is true.\n- **elide repetition, never a document**: every unit-dropping compressor (text, log, json, tabular, config, searchresult, terminal) calls `keepNonRedundant` (`compressors/redundancy.go`) before emitting. It keeps one representative of each distinct content class, so a stream of near-identical lines still collapses while a bibliography, a source file or a table of measurements survives whole — and when nothing is left to drop, the not-smaller check passes the payload through and claims nothing. This is a correctness rule, not a tuning knob: eliding a document the agent must enumerate cost the agent bench 5.5x its baseline on 2026-08-06 (`bench/agent/README.md`). Class matching masks digit runs, but only for units carrying at least one word — a wordless unit like `6 ['1973.', 251]` is compared with its digits intact, because there the numbers ARE the content.\n- **an elision marker states only what it verified**: `compressors/invariants.go` enriches the log/tabular/JSON markers with facts computed from the exact units that marker replaced — a field constant across every one of them (`all state=charged`), a varying field enumerated IN FULL with exact counts that sum to the elided count (`status: fulfilled×15 shipped×3 processing×2`, with an `absent×N` bucket for units lacking the field), and a numeric range printed from the ORIGINAL extreme value strings (`range amount=5.00..199.99`). Nothing else. A credential-shaped field name, a value with a space or over 24 bytes, more than five distinct values, and a run in which any unit yields no fields at all are all withheld **entirely** — never shortened, because a partial list reads as complete. When nothing survives, the marker renders byte-identically to the bare `… N rows elided (caveman) …`. An enumeration whose every bucket holds ONE unit is an identifier list, not an invariant, so instead of enumerating it the marker states its COVERAGE — `order_id: 25 distinct, ord-1000..ord-1024`, the exact distinct count plus the byte-wise smallest and largest ORIGINAL value strings, claiming nothing about the gaps between them (this is the only fact that answers \"was ord-1043 among the elided rows, or absent from the ledger?\", the last legitimate retrieve on the v5 probe). When a coverage run is EXACTLY dense — one shared prefix, fixed-width digits, and `max-min+1` equal to the counted distinct set — it says so instead: `wh-5000..wh-5059 all 60 present`, a verified membership answer that a set-difference task needs and would otherwise retrieve for. Density is counted, never inferred from the endpoints; mixed suffix widths, two prefixes, or a single gap all fall back to the counted form (whose own arithmetic still says how many are missing, never which). A dense entry outlives even the constants when the budget binds; a bounded one goes before them. At most two coverage entries per summary — a record carries several identifier columns and at ~38 bytes each they will name the run three ways and crowd out a 16-byte constant. An enumeration averaging two units or fewer per bucket still prints but is shed first. Summaries are capped at 160 bytes and a quarter of the bytes they replace (floor 96, never over half), shedding identifier-ish enumerations, then ranges, then bounded coverage, then constants, then dense coverage, class enumerations last; **a summary that sheds says so**, with a trailing `+N omitted` section, because every section above reads as the complete set of facts of its kind and a silently trimmed `all …` list would let an agent conclude a field it cannot see was not constant — the same error a partial enumeration is refused to avoid. The notice is charged to the same budget as the facts, so both caps still hold exactly; one trailing contract line per payload is added only when ≥4 KB was actually dropped. **A run under 3 units is not elided at all** unless collapsing both summarizes and halves its bytes — a one-unit marker plus its recovery handle costs about what the unit cost and reads as a hole. Together this costs ~4 points of ratio on the toolwork corpus (0.83 → 0.79) and is bought on purpose: on 2026-08-08 an agent that could not tell what had been elided made 46 sequential `caveman_retrieve` calls at 3.3x the uncompressed arm; a first fix stating only constants and ranges left the storms at 11–27 calls because it was silent about the one mixed-value field the task turned on; and a second round still leaked retrieves through identifier enums, fact-free small classes, and `{\"__caveman_elided__\":1}` singletons.\n- **a recovery view is made of whole units**: `retrieve_query.go` narrows a CCR recovery to the units matching a query, and a unit is whatever is self-delimiting — a complete JSON record (never a field line: `\"status\": \"unfulfilled\",` is a fragment), a whole CSV record sliced out of the original bytes with the header shown once, a whole log/NDJSON line, or a whole paragraph from non-JSON prose. JSON field names are never provenance: `messages[].content`, `text`, and `system` stay attached to their full object record, because tool output can use the same names as provider requests. Separate JSON arrays have explicit gap sentinels; omitted envelope/scalar bytes get leading/trailing markers. Content it cannot decompose (JSON with no array of records) returns in FULL rather than line-split, and a narrowed view that is not smaller also returns the full original; over-returning is safe, a fragment is not. Every gap — between two returned units, before the first, after the last — prints `… [caveman: non-adjacent] …`, so adjacency in the view never implies adjacency in the original. This is a correctness rule with a measured cost: on 2026-08-08 query mode returned BM25-ranked lines of a pretty-printed orders page, the agent read `\"status\": \"unfulfilled\"` (ord-1043's) directly above `\"order_id\": \"ord-1047\"`, attributed it to ord-1047, and reported 5 unfulfilled orders where there were 3 — two wrong answers in a 12-task bench run.\n- **one recovery surface, two id spaces**: the CCR store holds compression blob handles (`ccr_…`, `store.Get`) AND the native runtime's typed objects (`store.GetObject`), and the runtime shows the latter to agents as `ccr://<objectID>` when it masks a whole tool output. `Engine.Retrieve` resolves a blob first and falls through to the object table before failing, and `mcp.normalizeRecoveryHandle` accepts `ccr_…`, `<<ccr:…>>`, `ccr:…` and `ccr://…`. An agent copies whatever reference it is shown; a form that does not resolve does not degrade, it loops. Measured 2026-08-08: inventory-mismatch and webhook-delivery-gaps were handed `ccr://<objectID>`, every retrieve answered `cave_unknown_handle`, the failing tool result was itself masked into a fresh pointer — \"a new pointer to another pointer, indefinitely\" — 27–97 recovery calls, no answer written, both tasks 0/6 while rate-limit-forensics on the same build scored 3/3 at ~35% cheaper.\n- **CCR-or-pass-through**: a lossy (S4) result is only emitted if its original was stored; with no store, the engine fails closed to pass-through.\n- **inferred-only**: ratios are token estimates labeled `inferred`; never `verified`, never re-projected.\n- **fail-closed**: unknown mode → `record`; unknown content type → `text`; unknown grader → `passed:false`.\n- **cgo**: full code compression (Python/JS/TS) needs the tree-sitter build; the cgo-free build compresses Go only. The embedded eval fixtures cover all three under cgo. Release `caveman-proxy`/`caveman-engine` are cgo builds via the pinned zig in `scripts/build-release-binaries.mjs`; `TestReleaseShape*` carry no build tag on purpose and fail under `CGO_ENABLED=0`, and engine-ci's `release-shape` lane runs them with the release flags.\n- **boundary**: this is `public/` — never import `cloud/…`. `make check-boundaries` enforces it.\n\nSee ../../CLAUDE.md (root)\n\n<!-- File: extension/CLAUDE.md (Tokens: ~758 | Category: claude-rule) -->\n# extension — \"Caveman Mode\" browser extension (MV3, no bundler)\n\nThe consumer top-of-funnel: a toggle that, on ChatGPT / Claude / Gemini, prepends the **caveman\nskill directive** to each message you send and re-fires the send, so the AI replies in a compact,\nhigh-density style intended to preserve technical substance. It only ever adds text to your *outgoing*\nmessage; it never reads replies, never touches network requests. Savings are `inferred`. (This is\nthe directive-injector product; an earlier revision shipped a local WASM compress button — EXT\nremoved it after Chrome Web Store review couldn't reproduce \"compress prompt locally\".)\n\n## Layout\n- `manifest.json` — MV3. `storage` permission only. Content scripts load `src/directive.js` **then** `src/caveman.js` (order matters). Only the fonts are web-accessible — no WASM, no special CSP.\n- `src/directive.js` — the **pure** caveman directive text: `buildPrimer`/`buildReminder`/`isPrefixed`/`normMode`. No chrome/DOM/network. Loaded first (exposes `self.CavemanDirective`) and required directly by the test suite.\n- `src/caveman.js` — content script: per-site composer adapter + capture-phase send interception. Prepends the directive (full primer on the first message, short reminder after) and re-fires send. Exact composer-scoped selectors; `prependText` inserts at a collapsed selection without rebuilding the rich draft. Pending sends cancel on draft, editor, conversation, or settings changes; unknown controls keep native behavior.\n- `src/background.js` — service worker: just reflects the on/off state on the toolbar badge.\n- `popup.{html,js,css}` — the on/off toggle, mode (caveman/ultracave/megacave), per-site switches, and a **Leave-a-review** CTA (Chrome Web Store via `chrome.runtime.id`; hidden on Firefox until a verified AMO URL is available).\n- `test/` — pure directive/service-worker tests plus real Chromium\n  content-script and popup/storage journeys against `harness.html`.\n\n## Conventions & honesty\n- **Edits your outgoing message only.** Prepends a visible directive; never reads model replies, never touches network requests, never automates anything beyond submitting the message you sent.\n- **Local-only.** No network requests, no analytics. Stores only `enabled` / `level` / `sites` in `chrome.storage.sync`.\n- **Honest about injection.** The directive is plainly visible in the chat — the extension doesn't hide what it adds.\n- **inferred-only.** Token savings are an effect of the model's terser replies; never labeled `verified`.\n\n## Conventions\n- Build/test: `npm test` for pure tests; `npm run test:browser` for Chromium send interception, popup storage,\n  per-site controls, and review-link behavior. There is no\n  build step — MV3 loads the source directly.\n- Package: `npm run package` runs both suites, then writes `dist/caveman-browser-<version>.zip`. Staged set uses explicit allowlist in `scripts/verify-extension-stage.mjs`, cross-checked against manifest/CSS/HTML references.\n- Composer + send selectors live in the `SITES` map in `src/caveman.js`; a site redesign means updating it (each entry keeps aria-label fallbacks so one rename doesn't break it).\n\nSee ../../CLAUDE.md (root)\n\n<!-- File: integrations/CLAUDE.md (Tokens: ~466 | Category: claude-rule) -->\n# integrations — the framework-recipe registry (`caveman sdk` data)\n\nDeclarative routing recipes for AI SDKs/frameworks: each recipe is the one-line\nbase-URL shape that points a framework at the byte-safe gateway, with `{{baseURL}}`\n(gateway origin) and `{{app}}` (the `/w/<app>` attribution slug) left as render-time\ntemplates. Recipes are DATA, not code — the collapse decision applied to app frameworks.\n\n## Layout\n- `recipes/*.json` — one recipe per framework (12 today: openai-ts/py, anthropic-ts/py,\n  google-genai, vercel-ai-sdk, langchain, litellm, crewai, pydantic-ai, openai-agents, curl).\n  Fields: `id`, `display_name`, `lang` (`ts`/`python`/`bash`), `wire_protocol`, `note`, `code`.\n- `compile.mjs` — zero-dep build-time compiler: validates every recipe (fail-closed) and\n  emits `recipes.json` (published registry) + two EMBEDDED copies —\n  `../cli/src/recipes.generated.ts` (the CLI stays zero-runtime-dep) and\n  the web app's embedded copy (cloud may consume public data; the reverse import is forbidden).\n- `recipes.json` — generated; do not hand-edit.\n\n## Conventions\n- **fail-closed**: an unknown `lang` or `wire_protocol` fails the compile — never a guessed\n  protocol (no-placeholder rule). Allowed protocols are the 4 the proxy speaks natively\n  (anthropic-messages · openai-chat · openai-responses · gemini-generatecontent) plus\n  `multi` for frameworks that fan out behind one config.\n- Output is deterministic (recipes sorted by id); re-running on unchanged input is a no-op.\n- Run from the CLI build/test: `node ../integrations/compile.mjs`. The generated files are\n  committed and must stay in sync — change a recipe and regenerate all three artifacts.\n\nSee ../../CLAUDE.md (root) · ../agents/CLAUDE.md (the agent twin) · ../cli/CLAUDE.md\n\n<!-- File: mcp/CLAUDE.md (Tokens: ~1580 | Category: claude-rule) -->\n# mcp — the Caveman MCP server (Go core + launcher, Apache-2.0)\n\nA thin **stdio JSON-RPC** adapter exposing the compression [engine](../engine/CLAUDE.md) as\nfive MCP tools to any host (Claude Code, Cursor, …). It owns only the MCP framing; all\ncompression is the engine's, linked **in-process** (no subprocess, no drift). Local-only — it\nopens no network connection — and everything it reports is `inferred`, never `verified`.\n\n## Layout\n- `server.go` — the `Server`: JSON-RPC loop, dispatch, the five tool handlers. Takes an injectable `Engine` interface so the framing is testable without the real compressors.\n- `protocol.go` — JSON-RPC + MCP tool-result types, `toolText`/`toolError` helpers, the exact `tools/list` definitions.\n- `cmd/caveman-mcp/` — binary: opens shared file CCR store (`CAVEMAN_CCR_DB`,\n  else `CAVEMAN_HOME/ccr.db`/`~/.caveman/ccr.db`) so proxy handles resolve across\n  processes, then serves stdin↔stdout. `CAVEMAN_MCP_EPHEMERAL=1` opts into an\n  isolated in-memory store for tests/sessions that do not need proxy recovery.\n- `bin/caveman-mcp.mjs` + `package.json` — the `npx caveman-mcp` launcher that execs the prebuilt Go binary.\n\n## The five tools (exact names, case-sensitive)\n- `caveman_compress(input)` → compressed text + inferred ratio + `recovery_handle`. Lossy (S4), recoverable, and fail-closed: incompressible/malformed/not-smaller input returns unchanged, `ratio:0`, `recovery_handle:null` — never an error.\n- `caveman_retrieve(recovery_handle)` → the byte-exact original. Unknown handle → `isError:true` + a `cave_snake_code`, never a fabricated payload.\n- `caveman_stats()` → `basis:\"inferred\"`, `scope:\"session\"`; the string `verified` never appears.\n- `caveman_toon_encode(input)` → explicit JSON→TOON re-encoding with input/output sizes; returns pass-through plus note when encoding fails.\n- `caveman_toon_decode(input)` → TOON→JSON; invalid TOON returns `isError:true`, never raw input as JSON.\n\n## These five tools are a prompt-prefix cost, and it is measured\nRegistering this server puts all five tool schemas in the wrapped agent's prefix\non **every** call — **11,060 tokens/call** in the agent bench (~2.22M over its 201\ncalls). That is why `caveman wrap` gates the injection behind the `execute.mcp`\nsurface knob (`auto` | `marker-only` | `true` | `false`, `packages/cli/src/index.ts`).\nUnder a non-auto surface wrap suppresses **both** of its injection sites — the\n`mcp install` write and, for config-file agents like openclaw, the profile's\n`mcp.servers.caveman` overlay — but it never uninstalls a server the user\ninstalled themselves, so an existing install keeps costing its prefix until\n`caveman tools mcp uninstall <agent>`. The knob never touches recovery honesty on\nthe wrap door: `CAVEMAN_RECOVERY` is answered from evidence that this launch is\nreally giving the agent `caveman_retrieve`, so the proxy is never told a\nretrieval tool exists when none does. Adding a sixth tool here raises that\nper-call tax for every wrapped agent.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=mcp` / `make product-test PRODUCT=mcp`.\n- **stdout is the protocol channel** — logs go to stderr only (a dedicated test guards this).\n\n## Gotchas (honesty invariants)\n- **un-killable transport** — the stdio server survives everything short of EOF (issue #139). Framing is line-delimited: a malformed line is answered `-32700` and the loop RESYNCHRONIZES to the next newline (never `return`); a handler panic is contained by `recover()` → `cave_tool_panicked` (dispatch panics → `cave_internal_error`); JSON-RPC batch arrays are handled per spec (one array response); id-less/`\"id\":null` requests are notifications and get no reply; and both inbound lines and generated tool output (compress/toon) are size-capped (`cave_payload_too_large`, `maxInboundBytes`/`maxResultBytes`, 16 MiB default) — but `caveman_retrieve` is exempt (`Tool.ExemptResultCap`): recovery returns the byte-exact original and must never fail closed on size, since the shared gateway store has no matching ceiling. A dead server is worse than a slow one — the proxy keeps eliding content that no longer has a `caveman_retrieve` to expand it.\n- **fail-open** — engine error or malformed input → byte-identical pass-through, never a protocol error.\n- **fail-closed** — unknown tool/handle → `isError` + cave_snake_code; unknown JSON-RPC method → `-32601`.\n- **zero-egress** — the adapter imports no `net`/`net/http`/`os/exec`; a test parses the source to enforce it.\n- v1 is **stdio-only**, **string payloads only** (the engine detects type); HTTP transport + `caveman mcp` subcommand are v2.\n- **protocol negotiation must never error.** The adapter implements the 2024-11-05 contract and echoes that version back; a client asking for a newer one gets 2024-11-05 in the initialize result and decides for itself, per the MCP lifecycle. It previously answered `-32602: unsupported protocol version`, which made Claude Code (and every other current client) drop the server — and because `caveman wrap` reads recovery availability from an install-time marker rather than from the live agent, the proxy kept eliding content that no longer had a `caveman_retrieve` to expand it. Declining to echo an unimplemented version is right; refusing to speak is not.\n\nSee ../../CLAUDE.md (root) · ../engine/CLAUDE.md\n\n## Recovery across host compaction\n\nEvery valid `caveman_retrieve` call executes the requested handle and query,\nincluding identical repeats and calls after many earlier recoveries. An empty\nquery always returns the exact stored original. The MCP server cannot infer\nwhich earlier results remain in the model context from its process lifetime:\nhosts can compact their conversation without restarting MCP connections.\n\nThe previous per-process repeat suppression and forced full-payout threshold\nhave been removed. Current Codex source replaces compacted history and excludes\ntool-output items while its thread retains the live MCP runtime; see\n[compacted-history filtering](https://github.com/openai/codex/blob/main/codex-rs/core/src/compact_remote.rs)\nand [session services](https://github.com/openai/codex/blob/main/codex-rs/core/src/state/service.rs).\nTests exercise repeated recovery and more than five distinct queries in the same\nserver process. Query narrowing and complete-record boundaries remain owned by\nthe Engine.\n\n<!-- File: mem/CLAUDE.md (Tokens: ~995 | Category: claude-rule) -->\n# mem — cavemem, durable agent memory (Go core + clients, Apache-2.0)\n\nDurable, cross-session memory: `remember` / `recall` / `supersede` / `history` / `forget`. A local SQLite store holds the\n**raw** memories (the source of truth); recall ranks them with deterministic **BM25** behind a\nconservative threshold and compresses each hit through the [engine](../engine/CLAUDE.md) so the\ninferred token cost is honest and the dropped detail stays recoverable via CCR. Everything is\n`inferred`.\n\n## Layout\n- `store.go` — `Remember`/`Recall`/`Supersede`/`History`/`Forget`/`Recover` over SQLite + engine; legacy schemas migrate in place.\n- `bm25.go` — deterministic tokenizer + BM25 scorer (non-negative scores, so the threshold is meaningful).\n- `cmd/cavemem/` — MCP server + `remember`/`recall`/`supersede`/`history`/`forget` CLI subcommands (JSON).\n- `js/` + `py/` — thin TS and Python clients that shell out to the binary (mirrored libraries; they reimplement nothing).\n\n## The cavemem_* MCP tools\n`cavemem_remember(text)` · `cavemem_recall(query, limit?, token_budget?)` ·\n`cavemem_supersede(id, text)` · `cavemem_history(id)` · `cavemem_forget(id)`,\nserved through `mcp.NewServer` so the framing matches caveman-mcp exactly.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=mem` / `make product-test PRODUCT=mem`\n  for the Go core; root `make test` also runs mirrored JS/Python wrapper tests.\n- The wrappers shell memory text to the gated binary through `remember --stdin`\n  (never one huge argv element) — keep validation/recall/scoring logic in Go,\n  never forked into TS/Python.\n\n## Gotchas (honesty invariants)\n- **byte-safe write** — raw text is written to SQLite before any engine call, so a memory is never lost; compression is transient, at recall time only.\n- **single-writer store** — `mem.db` and `ccr.db` open with `SetMaxOpenConns(1)` + a DSN carrying `busy_timeout(5000)` + `journal_mode(WAL)` (`ccr.SQLiteDSN`). The proxy spend store uses the same DSN but does **not** force one pooled connection; mem/CCR's discipline is stricter. Cold-start migrations and the idempotent `Remember` insert use a thirty-second-wall-budgeted `ccr.RetryOnBusy` because a fan-out of fresh `cavemem remember` processes (the JS client's `Promise.all(facts.map(remember))`) can outlast one busy timeout on Windows. Without this, concurrent writes are dropped as `SQLITE_BUSY`.\n- **bounded recall** — `Recall` packs hits greedily in BM25 rank order under `RecallOptions.TokenBudget` (default `DefaultTokenBudget` = 2000 inferred tokens), reusing `engine/contextwindow` packing. Public CLI/MCP/JS/Python callers must explicitly pass `token_budget=0` to request unlimited recall; adapters map that external zero to `UnlimitedTokenBudget`, while omitted/Go-zero stays bounded. A single top hit whose compressed form alone exceeds the budget is returned as a compressed **head + CCR recovery_handle** (the original is stored even when compression passed it through), never its whole body — this is what stops the measured 440,000-token single recall.\n- **bounded remember** — `Remember` caps one memory at `MaxMemoryBytes` (256 KiB), failing closed with `cave_memory_too_large` (`ErrMemoryTooLarge`); CLI exits 65 and both wrappers export `MEMORY_TOO_LARGE_EXIT_CODE`. A memory is a fact to recall, not a file dump.\n- **fail toward nothing** — a query below the threshold (or with no term overlap) recalls nothing, never a guess.\n- **current-only recall** — superseded versions stay auditable through `History` but never enter normal recall.\n- **inferred-only** — `tokens_added`, score, and basis are inferred estimates; never `verified`.\n- **reversible** — a compressed recall hit carries a CCR `recovery_handle`; `Recover` returns the byte-exact original.\n\nSee ../../CLAUDE.md (root) · ../engine/CLAUDE.md · ../mcp/CLAUDE.md\n\n<!-- File: proxy/CLAUDE.md (Tokens: ~11897 | Category: claude-rule) -->\n# proxy — the byte-safe standalone `caveman` proxy (commercial, binary-distributed)\n\nA base-URL-swap reverse proxy: match → authenticate → inspect → byte-safe transform → upstream\n→ meter. Single-operator, BYOK, **zero cloud dependencies**. It shares its provider adapters with\nthe managed gateway (the managed gateway imports them from here). `caveman start` launches the\n`caveman-proxy` binary.\n\n## Layout\n- `providers/` — the shared, public byte-safe adapter set: `Adapter` interface + `Base` embed + `UsageScanner`/`ParseUsageBytes` (`adapter.go`), and `anthropic`/`openai`/`gemini`/`azureopenai`/`bedrock`/`vertex`/`openaicompat`. `ResolveUpstreamURL` takes `providers.RouteContext` (no control-plane coupling). Anthropic + OpenAI carry both prefixed and bare routes (`/v1/messages`, `/v1/chat/completions`). `vertex` preserves caller OAuth bearer or Express-mode Google API-key credentials for Gemini + Claude on Vertex AI (no signing, no environment-key fallback, no custom usage parser). `MatchRoute` is POST-only; read-only model discovery (`GET /v1/models`, `/v1/models/{id}`) is a separate GET-only allowlist, `Base.MetadataRoutes`, declared by `openai`/`anthropic`/`openaicompat` and forwarded unchanged with no body, no transform and no spend row. Bare `/v1/models` belongs to both `openai` and `anthropic`, so they implement `MetadataRequestMatcher` and choose by wire protocol (`anthropic-version` or `x-api-key` present → anthropic; a Google key matches neither), never by registration order (#1187).\n- `internal/gateway/` — the request lifecycle (`server.go` + `proxy.go`) behind three injected seams: `Authenticator`, `CredentialResolver`, `TelemetrySink`. Ports the managed loop with the fail-open fix. `serve` sets `Config.AsyncRecord`: a row's request token count, sink write (batched) and Cloud observe run after the handler returns, in record order (`recorder.go`). A reader that must see the request it follows calls `Server.Flush` first (session receipts do); close the server before the sink.\n- `GET /health/ready` identifies the runtime and advertises `billing: \"byok\"` because the standalone proxy forwards the caller's selected provider credential. The managed twin advertises `managed`; SDK dollar budgets fail closed on missing/unknown billing provenance.\n- `internal/config/` — `caveman.yaml` loader + BYOK env-key resolution; unknown mode fails closed to `record`.\n- `internal/store/` — `~/.caveman/caveman.db` SQLite spend store (`modernc.org/sqlite`, cgo-free); implements `TelemetrySink` and `BatchSink`. `RecordBatch` commits request rows with `synchronous=NORMAL` on its own connection (WAL): a power loss or OS crash can drop the last batches, a process crash or clean exit loses none; every other table keeps FULL.\n- `internal/standalone/` — wiring: static `Auth`, BYOK `Creds`, adapter set, and the always-on SSRF-guarded client.\n- `internal/identity/` — who calls the framework middleware routes (legacy token, token map, OIDC/JWT, mTLS) and the reloadable TLS listener config.\n- `internal/nativeruntime/` — normalized local-agent lifecycle, Task Contract,\n  Decision Ledger, typed CCR capture/masking, child evidence merge, honest\n  receipts, user-only Unix socket / Windows named pipe, and persistent listener lifetime.\n- `internal/repointel/` — deterministic local repository map, task evidence,\n  conservative test impact, and optional-Scout recommendation. No model/network.\n- `internal/nativepack/` — embedded compiled Core/skill policy; generated from\n  `public/skills`, fail-closed on schema/version drift.\n- `cmd/caveman-proxy/` — binary: `serve` (default), `stats`, and content-blind\n  `agent-evidence --session --build --plan`. Evidence query returns only exact\n  provider usage, request hashes, declared context/plan identity, ordered\n  provider-prefix component hashes, actual transform IDs/counts, and CCR handle;\n  basis is always `inferred`, verified dollars always zero.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=proxy` / `make product-test PRODUCT=proxy`.\n- Tests inject a plain `*http.Client` to reach loopback stubs; the binary uses the SSRF-guarded client.\n- New provider/optimizer work goes in `providers/` (shared) — change it once, both proxies get it.\n\n## Gotchas (honesty invariants — correctness, not style)\n\n- **listener lifetime is not session lifetime**: `serve` never exits because a\n  wrapper, native session, heartbeat, or idle timer expires. Legacy\n  `CAVEMAN_NATIVE_IDLE_TIMEOUT` does not arm shutdown. The CLI never restarts a\n  shared listener to change mode/recovery; an incompatible new wrap runs direct.\n  Failed local startup also runs direct. Native session correlation entries may\n  age out without affecting API traffic.\n- **no default generation deadline**: `CAVE_GATEWAY_UPSTREAM_TIMEOUT_MS` defaults\n  to `0` (no total request deadline). A positive value is an explicit operator\n  cap and includes response streaming. Client cancellation still cancels upstream;\n  connection setup, inbound header/upload limits, and idle keep-alive socket\n  cleanup remain bounded separately. The replacement bound for an upstream that\n  connects and then goes silent is the transport's response-header deadline\n  (`CAVE_GATEWAY_RESPONSE_HEADER_TIMEOUT_MS`, default 900000, `0` disables) —\n  headers only, so it never truncates a live stream.\n- **response protocol controls streaming**: SSE/event-stream responses flush\n  headers and chunks even without a JSON `stream` flag. Encoded requests keep\n  `Content-Encoding` and bypass transforms. Interrupted response copies record\n  an error and abort HTTP framing; they never become a clean successful EOF.\n- **replay only when non-delivery is proven**: proxy transport retries are\n  limited to connection-setup failures — `*net.OpError` with `Op` `dial` or\n  `proxyconnect` (a proxy that is down fails as the latter, never as a dial). A failed upload/header read or truncated response\n  does not prove an inference was unprocessed; do not automatically replay it.\n  An explicit transformed-request 4xx still retries once with original bytes —\n  except a 429 that carries `Retry-After` (a rate limit; Anthropic sends its\n  `anthropic-ratelimit-*` headers on every response, so they prove nothing),\n  which is returned as-is; an accepted retry pins the conversation raw\n  (`internal/gateway/raw_pin.go`).\n  See `docs/technical/proxy-reliability.md` at the repository root.\n- **byte-safe**: `record` mode never transforms; on transform error the ORIGINAL bytes are forwarded (HTTP 200, fail-open) — never a 400. The one carve-out: bytes the provider already cached in replaced form are re-sent replaced, because forwarding the original there busts the prefix (see cache safety).\n- **native marker is local-route-only**: HMAC session marker is emitted only\n  after local runtime + route proof and stripped before capture/hash/provider.\n  Direct, managed, proxy-disabled, invalid, and conflicting paths never correlate.\n- **request-wide opt-out**: `x-cave-transforms: caveman.pass-through.v1` suppresses every request transform path — compress, pixel, and provider-native — not only compiled plan routes. Tests cover all three modes.\n- **repository evidence is a claim, so it needs a direct match** (`internal/repointel` + `nativeruntime.renderRepositoryEvidence`): a \"Likely implementation path\" line is injected ONLY for items whose own FILE NAME or symbol name contains a task term (`EvidenceItem.Direct`, `Bundle.Strength == StrengthDirect`). The match is on the final path component on purpose — matching the whole path meant a term naming any ancestor (`src`, `internal`, `lib`: ordinary prompt words) marked every file beneath it direct, i.e. no gate at all. BM25 metadata proximity alone is `StrengthMetadata` and is never shown, never stored, and never gets a `ccr://` handle; the handle reads as authoritative, and a wrong file:line steers the agent while costing tokens every turn. A bundle qualifies on ONE direct item, so the non-direct items that ranked alongside it are stripped by `Bundle.DirectOnly()` BEFORE the object is written — dropping them only at render time left them stored behind the handle, one retrieval from the model. Terms are floored at three characters (`NormalizeTerms`, matching the producing hook) because the direct test is a substring of a file's own name: `go`, `py`, `js` would mark every file of that language a direct hit and hand the gate away. A declined turn is silent to the model but records `repository_evidence_declined_no_direct_match` in the ledger, so \"declined\" stays distinguishable from \"broken\". `Scout.Status` is ADVICE about repository shape and must not gate injection; conflating them made a truncated map silence real hits (#899).\n- **the dependency filter runs on BOTH listing paths, and git is not trusted to be complete**: `listFiles` prefers `git ls-files --cached --others --exclude-standard`, because the project's own `.gitignore` — not a name list we maintain — is the best statement of what is vendored. It is not sufficient on its own: an empty-but-successful listing means the cwd is itself ignored by an enclosing repo (accepting it silently disabled the whole mechanism), so empty falls back to the walk, and every git-listed path still goes through the same content-based filter as the walk via `directoryFilter` (memoised per directory, one probe per directory not per file) — otherwise an un-ignored conda prefix or a COMMITTED `vendor/` tree reaches the model precisely because git reported it. Installed environments are identified by CONTENTS (`conda-meta`, `pyvenv.cfg`, `site-packages` child) so a pixi/conda prefix or virtualenv is caught under any name, along with the interpreter stdlib it carries. Only names that are never first-party (`node_modules`, `.pixi`, `site-packages`, …) match by name, case-folded; `build`/`dist`/`target`/`vendor`/`coverage`/`Pods` are ambiguous and excluded only when a sibling manifest proves the ecosystem, because `internal/build` and a hand-written `vendor/` are real source. The two paths can legitimately disagree (git omits submodule contents), so `Map.ListingBasis` discloses which ran and is folded into `ContentSHA256`. Git gets at most HALF the caller's remaining warm budget (`gitListBudget`): the walk is the more expensive path, so a git listing that spent 2s of a 3s window left the fallback unable to finish and the map failed closed — the exact case the fallback exists for. `FilesScanned` stays the map size; `FilesRanked` is the filtered count.\n- **git is invoked as an untrusted-repository operation**: every `git` call that points at a user working directory goes through `internal/gitsafe.Command` (Go) or `packages/cli/src/git-safe.ts` (the two `execFileSync` sites in `index.ts` and `native-hook-fast.ts`), which pass explicit `-c` overrides (`core.fsmonitor`, `core.hooksPath`, `core.sshCommand`, `core.askPass`, `core.editor`, `core.pager`, `core.alternateRefsCommand`, `diff.external`, `credential.helper`, `protocol.ext.allow`, `uploadpack.packObjectsHook`) and strips inherited `GIT_*` from the environment. A repository's own `.git/config` names programs git will run, and `core.fsmonitor` runs while the index is read — so plain `git ls-files` in a freshly cloned hostile repo executes attacker code with no user action, at session start. `repointel.listFiles` was hardened first and the other callers were not, which is the shape this bug takes: `nativehook.currentRepositoryState` runs `git status` on EVERY hook event, and `status` refreshes the index, so it triggers `core.fsmonitor` harder than `ls-files` does. Regression tests: `repointel.TestGitListingIgnoresRepositoryControlledCommands` and `nativehook.TestRepositoryStateIgnoresRepositoryControlledCommands` (both verified to fail without the overrides), plus `tests/verify_repo.py` `verify_untrusted_git_invocations`, which fails the build on a NEW raw `git -C <dir>` call site anywhere in `proxy/` or `packages/cli/src/`. Command-line `-c` beats repository config; never drop these to \"simplify\" the invocation.\n- **no-fake-savings**: standalone records `Basis: \"inferred\"` on every row; it never writes `verified` and never re-projects to a monthly figure.\n- **spend is arithmetic, savings is a claim** (local learn, since 2026-08-21): `caveman learn` MAY state a currency, under exactly these conditions. SPEND prices PROVIDER-COUNTED disjoint billing buckets read out of the transcript's own usage block, at rates from the dated `shared/provider-catalog` row, stamped with that row's catalog version — basis `provider_counted_x_published_rate`, window-bounded, never projected. A model with no catalog row is NEVER priced at a sibling's rate: its tokens are disclosed separately and the total is a stated floor (`learn_pricing.go`). Sinks are priced at the user's own MEASURED effective input rate, not list, so a well-cached user's findings are not inflated ~10x. SAVINGS additionally requires an attribution method (`learn_attribution.go`): every confirmed row names its rung (`deterministic_remeasure` > `controlled_holdout` > `counterfactual_replay` > `interrupted_time_series`), carries the standing confounders that rung cannot rule out, and reports whether the artifact caveman fingerprinted at apply time is still intact on disk. Savings totals are bucketed BY RUNG and never summed across rungs — blending a re-counted file with a before/after median launders the weak number into the strong one. A regression is never priced. Neither surface ever writes `verified`; that stays gateway-only. Subscription traffic has no marginal cost, and every priced surface says so.\n- **practice join**: local learn sinks carry additive `practice_id`; one\n  fail-closed mapping table owns sink→practice and unknown sinks keep `\"\"`.\n  The historical `subagent_overuse` sink is count-only and deliberately has no\n  practice id: spawn count cannot reactivate the retired\n  `context-exploration-offload` opportunity or prove any spawn unnecessary.\n- **local trial heuristics are not actuation evidence**: a model name never emits\n  the retired `model-right-sizing` id, and provider plus positive cost never\n  emits a cache move because neither proves stable-prefix eligibility. Legacy\n  rows for those identities are hidden at read time. Compression replay reports\n  one trial's local engine `estimated_engine_o200k` before/after shape with zero\n  dollars and low confidence; it is not provider-counted, a rate, an invoice,\n  causal/verified savings, or task-outcome evidence.\n- **Anthropic automatic caching is experimental observation only**:\n  `anthropic-automatic-prompt-cache` is a typed, default-off manual policy\n  experiment and may add only Anthropic's top-level 5-minute marker on direct\n  Messages API requests. Managed traffic additionally requires server-attested\n  official Anthropic origin; custom or provenance-unknown origins lose the flag\n  before the adapter. It is mutually\n  exclusive with the explicit `anthropic-cache-breakpoints` transform and any\n  caller `cache_control`; Bedrock and count-tokens requests stay byte-identical.\n  An applied marker records only its optimizer id plus actual provider usage and\n  cost. It has no practice, recipe, generic mode/candidate activation, ledger\n  tuple, inferred savings, or verified-savings path (cache-only and forged IDs\n  are excluded from the counted-baseline method too); evaluate it by manual\n  paired observation because shared provider cache state can contaminate an A/B.\n- **OpenAI cache-key affinity is not cache placement or savings proof**:\n  `openai-prompt-cache-key` adds routing metadata only. On GPT-5.6, the\n  provider's implicit latest-message breakpoint can repeatedly write a changing\n  suffix, and writes cost 1.25× uncached input. The usage parser normalizes\n  official nested `cache_write_tokens` from both Responses\n  `input_tokens_details` and Chat `prompt_tokens_details` into\n  `CacheCreationInputTokens`; applying the affinity key never mints verified\n  savings. Explicit `prompt_cache_breakpoint` placement is not implemented.\n- **SSRF always on**: `standalone.StandaloneHTTPClient` guards every upstream dial (not gated on `CAVE_ENV`) using `ssrf.SelfHostedConfig` — NOT ManagedConfig, which ignores the allowlist and would make the escape hatch a silent no-op. `CAVE_SSRF_ALLOWLIST` opts loopback/private hosts back in (local model servers like Ollama; `localhost` as an entry covers 127.0.0.0/8 + ::1); metadata/link-local stay blocked in every mode. Provider traffic honours `HTTPS_PROXY`/`NO_PROXY` by default (#1001) — `upstream_proxy: env|off|<url>` / `CAVE_UPSTREAM_PROXY` — via `ssrf.Config.Proxy`, which validates IP-literal/localhost destinations before proxy selection and dials the (operator-configured) proxy address unguarded; `ssrf.NewHTTPClient` itself stays direct unless a caller sets `Proxy`, and a managed-mode Config carrying one is refused loudly (every request fails with `ssrf.ErrProxyInManagedMode`) rather than silently dialing direct. Adapters that pre-flight a resolved endpoint (bedrock, vertex) read the selector off the request context (`providers.WithUpstreamProxy`, published by `gateway.Handler` from the upstream transport) and use `ssrf.ValidateURLNoResolve` for proxied destinations: a proxy-only network has no outbound DNS, so resolving there failed the request before the proxy was ever consulted. `ca_bundle` / `CAVE_CA_BUNDLE` plus inherited `SSL_CERT_FILE`/`REQUESTS_CA_BUNDLE`/`NODE_EXTRA_CA_CERTS` append private roots for TLS inspection through `shared/platform/cabundle` (fail-closed parser shared with chhttp).\n- **inbound token is consumed, never forwarded**: `CAVEMAN_AUTH_TOKEN` gates non-loopback listens (a middleware token map, OIDC issuer or TLS client CA also makes one legal; provider routes still accept only the token, and without it `Auth.closed` refuses every provider request off loopback); `standalone.Auth` deletes the matching `x-cave-api-key`/`Authorization` header before `Creds.Resolve` so the shared token can never be forwarded to a provider or classified as a provider credential; a bearer that is not the token survives untouched (Claude OAuth, `/chatgpt/`). `Authenticate` runs BEFORE `matchAdapter` so the 401/404 split cannot be used to enumerate routes, and every 401 goes through `Server.rejectUnauthorized`, which counts `cave_proxy_unauthorized_total` and warns with the path and remote host only. Health and the no-op `/caveman/keepalive` beacon stay unauthenticated, `/metrics` too unless `CAVEMAN_METRICS_TOKEN` is set; the `X-Caveman-Instance` header on `/health/live` is published only on a loopback bind. A token on a LOOPBACK listener is honored too (the local `caveman wrap` path sends none, so `runServe` warns). Bedrock credentials then come from `shared/platform/awscreds` (env → web identity → container → IMDSv2), never from the request; a PARTIAL env pair fails closed instead of falling through to an ambient role, and the winning source is logged once.\n- **middleware identity is server-side and per route**: `middleware.Config.Identify` resolves an `identity.Principal` from the credential alone (bearer decides when present, else a verified client certificate); `ServeHTTP`'s `decode` checks `Principal.Allows(scope.Namespace)` on EVERY scoped route, so a new route that skips `decode` skips authorization. The legacy token stays `single_operator` with every namespace; JWT and certificate principals may never be named `single_operator`. Token and hash comparisons are constant-time over every entry. A token map or TLS file that fails to reload keeps the previous one. Tests: `identity/*_test.go` (negative JWT and mTLS cases), `middleware/identity_test.go`.\n- **Postgres middleware store keeps SQLite's single-writer semantics per authority**: `store.PostgresMiddleware` serializes writers of one authority with a transaction-scoped advisory lock taken by `Scope` (then re-read), `SaveScope`, `Revoke`, `PurgeBatch`, `Receipt` and `SaveOriginal`; the sweep is one-replica (try-lock) and skips busy authorities and row-locked scopes. Counters are statement-level transition-table triggers striped by txid; a per-row trigger goes quadratic on bulk statements. Lock keys include `current_schema()`. Run the store suite against both backends with `CAVEMAN_TEST_POSTGRES_URL`; `middleware_concurrency_test.go` fails without the locks.\n- **Auth scheme is preserved**: a key from an inbound `Authorization: Bearer` keeps `Scheme:\"bearer\"` on the `providers.Credential`; Anthropic and Gemini forward bearer credentials as bearer credentials (Claude/Gemini OAuth breaks if remapped to an API-key header). BYOK env keys and inbound `x-api-key` keep provider API-key mapping. On a named compat mount the header follows the wire protocol of the path: `/compat/<name>/v1/messages` gets `x-api-key` plus a default `anthropic-version`, every other path gets Bearer. A real inbound Bearer stays Bearer on every path. One credential may arrive twice — pi with `authHeader: true` sends it as `Authorization: Bearer <key>` AND as `x-api-key: <key>`; x-api-key still resolves first (a shared listener must not be able to swap the caller's principal), but an IDENTICAL bearer is that same principal, so `openai_compatible` keeps `Scheme:\"bearer\"` and the bearer-only compat upstream gets the header it accepts (#1215). Differing values stay x-api-key-first, and no other provider reads the duplicate as OAuth. OpenCode Go rejects Bearer on `/v1/messages` (401 `Missing API key.`, 2026-09-03). The gateway env fallback puts the key in the header that the adapter emitted, so the upstream never gets two credentials.\n- **fail-closed**: unknown route → 404; unknown mode → `record`.\n- **subscription AND oauth compression is NOT account-gated**: non-PAYG sessions from Claude Code, Codex ChatGPT, Gemini CLI, and other routed clients take live-zone compression with no Caveman account, entitlement, or seat. `CAVEMAN_WRAP_ENTITLED` and every `WrapEntitled` field are **deleted**, not merely ignored — do not reintroduce them. Exactly four conditions remain, all technical and all fail-closed (`liveZoneCompressionAllowed`): the operator `subscription_compress` switch (empty/`live_zone` allow, `off` and any unknown value close it), the adapter must implement schema-aware `PrefixStabilizer` zones, recovery must run through the agent's own MCP `caveman_retrieve`, and a durable prefix cache must be wired. The dedicated Codex `/chatgpt/responses` route uses the OpenAI Responses stabilizer while preserving OAuth and `ChatGPT-Account-ID` headers; recovery is proven by the `CAVEMAN_RECOVERY=mcp` stamp or the decoded request's own `mcp__caveman__caveman_retrieve` (top-level `tools` or Codex's `input[]` `additional_tools`, flat or under an MCP namespace); zstd bodies decode → transform → re-encode, anything else forwards the original wire bytes, and the `chatgpt_proxy` log carries `skip_reason`; transformed 4xx responses retry once with exact original bytes (never a rate-limit 429; an accepted retry pins the conversation raw). Legacy savings fields remain **tokens-only** — `compression_tokens_before/after` + `estimated_engine_o200k`, never booked compression dollars. New request-comparison and price-snapshot fields support separately labeled API equivalents in local stats; those values never enter subscription spend or legacy saved-dollar fields. LOCAL wrap only; managed gateway non-PAYG behavior is unchanged.\n- **recovered bytes are never a compression candidate**: a `tool_result` answering the agent's own `caveman_retrieve` is excluded by every adapter (`providers.IsRecoveryToolName`; anthropic keys off `tool_use_id`, OpenAI off `tool_call_id`/`call_id`, gemini off the `functionResponse` name). This is not an optimization — the replacement is a deterministic function of block content and is memoised in the prefix cache, so collecting a recovery result substitutes the SAME elision straight back in and the agent has no path to its own data at all. Measured 2026-08-06: the agent retrieved, reported \"Same truncation\", and re-read one file seven times. Regression test `internal/gateway/recovery_exempt_test.go`.\n- **cache safety is byte-stable replacement**: THE INVARIANT (#1105): for accepted requests P then R of one conversation, if R's client bytes over P's cached prefix (system, tools and messages through P's last `cache_control` marker; the whole prompt for implicit caches) equal P's, R's forwarded bytes over that range equal P's, and every request caches at least what its client asked for (a dropped breakpoint is a bust too). Two exceptions, both re-caching the range raw: a request the provider rejected transformed and accepted through the raw retry, and a compressed PAYG conversation without MCP recovery that streams (the server-side retrieve tool cannot ride a stream, so the conversation stays raw from there). A raw retry splits a range into a raw and a replaced form, and a later request follows the LONGEST lineage it extends, ties going raw: it is held to the raw request (raw pin) unless it repeats a longer prefix that went out replaced, like a sibling subagent started with the same task. Pins are taken only on a conversation's own bytes (past system and tools); pins and lineages live in the PrefixCache (`rawpin` and `lineage` scopes, pin rows never evicted) and in memory. `internal/gateway/cache_prefix_invariant_test.go` (Anthropic compress, PAYG, pixel and Codex Responses scenarios + `FuzzCachePrefixInvariant`) proves it; keep it green. A compressed live-zone turn becomes prefix on the next request, so the replacement is a pure function of segment content (deterministic compressor + content-hash CCR marker), memoised in the durable replacement cache, and the FIRST decision for a block wins — replaced or raw (`RawDecisionHandle`); a request whose write finds a row on record sends that row, not its own candidate. A frozen block with no decision (a `--resume` history, an evicted row) goes out raw and raw becomes its decision; a failed memo write keeps the block raw for the process (`rawMemory`, consulted only on a store miss and written through once the store takes it); new rows are remembered only once the request's splice holds. Raw decisions are global and durable, a known savings cost: a block declined once (frozen at first sight, a compressor decline, a CCR failure, a non-newest parallel tool output) stays raw in every conversation. Bounding them needs a TTL of at least the longest provider retention (24 h for OpenAI extended caching) plus time-aware store interfaces, or per-conversation rows keyed on a stable conversation identity with a scope rollover. Substitution is never gated: the derived epoch gate is gone, and an explicit framework epoch may veto only NEW compression. Pixel renders and the tool-schema strip are memoised the same way. New compression stays live-zone-only. The runtime tripwire (`prefix_monitor.go`, on the provider routes and `/chatgpt`) checks the same rule on live traffic and records `requests.cache_bust_cause`: `client` (DEBUG); `raw_retry`, `stream_switch`, and `lever_freeze` (the harm tripwire's one-time strip rollover), all WARN; `caveman` (ERROR `caveman changed bytes the provider already cached` or `caveman dropped a cache breakpoint the client set`, counted as `caveman_cache_bust_requests` and raised by `caveman status`) — any `caveman` row is a bug. A request is held only to anchors accepted before its own forwarding decision. Cross-turn stability covers **anthropic, openai, azureopenai/openaicompat, and gemini** through `ExtractStabilizable`; `bedrock` and `vertex` expose no compressible blocks and stay pass-through. Anthropic uses declared `cache_control`; OpenAI/Gemini use latest-user/latest-tool zones against implicit provider caches. Replacement cache is SQLite spend store and must retain `journal_mode(WAL)` + `busy_timeout`; a hit rewrites `last_used_at` only once the row is 10 minutes stale, and pixel rows have their own cap inside the table's.\n- **tool-schema annotation strip is DEFAULT OFF and separately opted in**: `toolschema_strip: annotations` / `CAVEMAN_TOOLSCHEMA_STRIP=annotations` (`\"\"`, `off`, and any unrecognized value all mean off, normalized in the config loader AND re-checked at the decision point). It is a SECOND gate ON TOP of the four `liveZoneCompressionAllowed` conditions, which it reuses rather than restates, plus a THIRD: the session ledger's freeze registry (`ledger.LeverAllowed`) — it never runs in `record` mode, under `caveman.pass-through.v1`, under a compiled Cave Build, or in a session whose harm tripwire has frozen it. Its first decision per catalog (stripped or original) is memoised in the replacement cache and re-sent, so a recovery-store hiccup never flips the head of the prefix. S4 + CCR (the original catalog is stored before the rewritten bytes ship, disclosed as `x-caveman-toolschema-recovery-handle` and on the row's `RecoveryHandle`), promotable only under the local-wrap clause. It removes `$schema`/`title`/`examples`/`deprecated` ONLY inside schemas reached through `input_schema`/`inputSchema`/`parameters` and the JSON-Schema applicator allowlist — never from a tool envelope, `annotations` (whose `title` is the tool's display NAME), `_meta`, or a vendor extension. `ExtractToolCatalog` covers the **anthropic-messages** shape only; openai/gemini and `count_tokens` fail closed. It mints NOTHING: no tokens, no ratio, no dollars — the replay grid prices it. Its version is folded into the prefix-cache scope key so toggling it is a deliberate epoch rollover, not a silent partial cold write.\n- **cache-breakpoint planner defaults to `frontier` in optimization modes**: `breakpoint_plan: off` / `CAVEMAN_BREAKPOINT_PLAN=off` is explicit off; unrecognized values fail closed to off. Record, compress, and pixel modes still never run it. Provider-native cache optimizers for Anthropic, OpenAI, and Bedrock also default on when omitted, with explicit per-id `false` preserved. All remain capability-, auth-, endpoint-, economics-, and caller-marker-gated. Planner metadata is upstream-only and changes no model-visible bytes. It does not mint savings: `cache-breakpoint-plan` remains absent from `cacheOptimizerIDs`. OpenAI's arm hashes session id and never overwrites caller key; prefix-signature optimizer wins when already applied.\n- **harm tripwire / session ledger**: `internal/gateway/ledger.go` keys a bounded LRU (1024) on `x-cave-session`; **no session header = no entry and the whole mechanism is inert**. Every lever asks `LeverAllowed` before running. A lever active on request N earns a strike when request N+1's `cache_creation_input_tokens` exceeds BOTH an absolute floor (50k) AND 3x the session's baseline mean; 3 strikes freeze that lever for the session, one-way (never un-freezes), fail-open to pass-through. The baseline EXCLUDES calls already judged anomalous — a plain running mean folds each spike into the bar the next spike is measured against and goes blind to persistent regressions. Disclosure is `x-caveman-tripwire: <lever>=frozen`, and it appears from the request AFTER the one that tripped, because usage is only known once the response is already streaming. Thresholds are conservative by construction, pending replay-derived values.\n- **cache warming is predicted per stream, and replays only what Anthropic can answer at zero output** (`internal/gateway/cache_warm.go`, `cache_warm_table.go`): after a successful Anthropic Messages response on `api.anthropic.com` (never Bedrock, Vertex, Azure, a compat mount, a custom base URL, the pass-through header or an encoded body), the exact bytes and headers sent upstream (after compression, routing marks, heals and fail-open retries) are kept in memory only (LRU 1024 streams and 256 MiB, never stored, logged or captured) and may be replayed with `max_tokens: 0` and `stream: false` at `min(0.9*ttl, ttl-10s)` steps from the real request's start (wall time, so laptop sleep never sends a stale warm). Not replayable, so never warmed: `thinking.type: \"enabled\"`, `tool_choice` `tool`/`any`, `output_config.format`, no `cache_control`, a `ttl` other than `5m`/`1h`, mixed `5m` and `1h` markers, writes of another lifetime than the markers (one lifetime or no warm; the simulator classes by the same rule and none of the measured requests mixed them); the request scan reads top-level fields and cache_control markers only, no full parse. WHETHER to warm is optimal stopping, not a threshold: `cache_warm_table.json` holds the return-time distribution (30 s bins to 2 h) per stream class `main|subagent / tool|end / 5m|1h` (the class comes from the session key and whether the answer's `stop_reason` was `tool_use`, scanned off the plain bytes with a rolling tail; the proxy forwards the agent's `accept-encoding`, so an answer that comes back with a `Content-Encoding` is not classed: it is planned as whichever of the two classes warms less and its gap teaches neither), measured on 364,048 request transitions of real Claude Code sessions (2026-08-30 to 2026-10-08, `.blocks/cache-warm-sim.py`, aggregate probabilities only) and shrunk toward the parent class; the proxy learns each user's own gaps online (counts only, `$CAVEMAN_HOME/cache-warm-gaps.json`, 0600, written outside the warmer's lock, flushed by `Server.Close` at shutdown, symlink-refused, any implausible count drops the whole file; two proxies on one home are last-writer-wins) and blends them in with the defaults worth 100 observations. A stream evicted or still waiting at shutdown is counted as returning later than it was seen silent (spread over the later bins by the class's own distribution, \"never\" past 2 h): dropping it would keep only the returns and drift the table toward over-warming. At arm time the whole chain is planned by backward induction with the catalog price of the model sent (warm = read x prefix + input x uncached tail; avoided miss = (write_ttl - read) x prefix x the class's measured realized share, taken over the gaps that DID return: the window probability already leaves out the streams that never do), so the cumulative cost of every warm is counted before the first; unknown price, class or lifetime = no warm. Subscription traffic is valued at list prices as plan-usage equivalent. Stops: any real request of the stream (every model; it never waits for a warm in flight), any warm error, a warm reading `cache_read_input_tokens == 0`, the plan, 60 min, or the switch; the held request (body and credential headers) is released the moment its chain ends. A parent session's request does NOT stop its subagents' warms: a subagent can still be running, and on the test half that stop cost $565.52 net (below). A 429 (warm or real) pauses warming on that credential for the response's `Retry-After`, else until its `anthropic-ratelimit-unified-reset`, else 1 h for a subscription/OAuth login and 60 s for an API key; a 529 or 5xx on a warm pauses it for the `Retry-After`, else 5 min. Never sent while a real request of the same stream is in flight (retry every 5 s until its deadline). At most two warms are in flight process-wide (same retry; one with no slot by its deadline is dropped), and each fires up to 5 s before its planned time, never after. Warm rows are tagged `cache-warm`: spend, never a saving, never an agent request (excluded from request counts, session usage, trial origins, exported spans, receipts and `caveman sync` uploads; counted as `cache_warm_requests`), never fed to the harm-tripwire ledger, never sent to Cloud. Switch: on unless `$CAVEMAN_HOME/cloud.json` has `modules[\"waste-fixes\"]: false` (re-read on change); `CAVEMAN_CACHE_WARM=off` kills it for the process. Every answered stream request logs one line `cache warm plan` (info when warms are planned, debug when skipped: session hash, model, class, ttl, planned warms, skip reason: `response_error`, `off`, `not_anthropic_api`, `not_replayable`, `lifetime_mismatch`, `unpriced`, `not_worth_it`). Replay on the owner's data (test half, after 2026-10-03, 161,325 transitions): net $1,896.65 list-price equivalent at $385.31 of warms (9,628 warms, 7,280 wasted) vs $1,294.18 at $1,372.15 (45,343 warms) for the first fixed rule; the same policy with the parent stop for every subagent $1,331.13 at $197.28 (3,461 warms), with it only for subagents whose last answer was not a tool call $1,852.99 at $378.90 (9,518 warms). A test pins the plan to the simulator's on the shipped table (`TestCacheWarmPlanMatchesTheSimulator`; regenerate both together).\n- **pixel mode**: S4 lossy text→PNG (`pxpipe` port). Default allowlist is `claude-fable-5,gpt-5.6` via `CAVE_PIXEL_MODELS`; savings stay inferred-only. Renders keep the compress rules: the first decision for a text (render or text) is re-sent on every turn, a raw pin keeps the conversation text, and a NEW render is remembered only once its request goes out, with the original stored in CCR and the splice holding. If either fails, the new content goes out as text and records that, and earlier renders still go. A rendered block's `cache_control` moves to its last image. An Anthropic request carries at most 20 images, client images included (`anthropicManyImages`): past 20 the API rejects any image over 2000 px, and the renders are 2573 px wide, so a new render past the cap stays text.\n- **mask what cannot be summarized in place; elide what can**: `nativeruntime.afterTool` replaces an over-threshold tool output with a `ccr://` pointer stub, but NOT when `Engine.Detect` classifies it as `json`, `tabular`, or `log` (logfmt + NDJSON) — the classes with a field grammar, which the elision engine compresses in place into rows plus stated invariants (`all state=charged`, `status: delivered×18 attempted×17`, `wh-5000..wh-5059 all 60 present`). Masking those first destroys every fact AND costs more: the agent sees no row, then recovery re-enters the FULL original through the recovery-exempt path — whole page + stub + an extra turn, strictly worse than no wrap. Measured 2026-08-08 on the shipped default (`compress` → native policy `safe` → profile `full-safe` → mask on): inventory-mismatch and webhook-delivery-gaps scored 0/6 with 27–97 recovery calls, while rate-limit-forensics scored 3/3 at ~35% cheaper for the sole reason that its pages sat under the threshold. Capture is unaffected (the object is still stored, recovery still available); only the replacement is skipped, and the size rule for still-maskable classes is unchanged. Fails toward masking: no classifier → mask, so the fallback is bounded context. Tests: `mask_elidable_test.go`.\n- **route stage is optional and fails open** (`internal/cloudlink`, `internal/gateway/route.go`):\n  it asks Cloud `POST /v1/route` only for requests naming Auto (`AutoModel`, `caveman-auto`, also with Claude Code's `[1m]` suffix, the\n  picker entry the CLI adds while `modules.routing: true` in `$CAVEMAN_HOME/cloud.json`), while the\n  CLI is signed in with that switch on, for Anthropic Messages / OpenAI chat or responses requests on\n  an API key or a subscription alike; any other model goes as sent (`route.outcome: off`), the\n  session's marks and heal still applied. Auto goes upstream as the provider's `autoFallback` model\n  (Anthropic `claude-sonnet-5-5`, OpenAI `gpt-6.1-sol`; count_tokens too, and with no Cloud link),\n  which is also the asked model the ask carries and what a failure runs (a transient Cloud failure, that is a timeout, an unreachable or 5xx Cloud, an unreadable answer or the pause after one, instead runs the model the session's previous request went to, `heldModel`, never for a side request; no previous request on that provider, one that was a compaction or came after one, a held model that then did not serve (any non-2xx: the next attempt runs the fallback), signed out, routing off, an expired or refused login, a limit and a provider's refusal keep the fallback); Auto on any other\n  provider is a 400 (`cave_auto_unavailable`), and an encoded body naming Auto is decoded first. A\n  named model records `route.outcome: off`, reason `named_model`. OpenAI Auto (API key and ChatGPT\n  login) runs only `AutoOpenAIModels` (`gpt-6.1-sol`, `gpt-6-astra`, `gpt-6-luna`): the ask's\n  `Models` lists those alone and the pool drops other OpenAI models, and an answer naming any other\n  runs `gpt-6.1-sol` at its effort (`auto_model_refused`, `translate.FitEffort`); an Auto row's `RouteFrom` and\n  `x-caveman-routed-from` say `caveman-auto`. A move on a subscription stays on the provider's own\n  API on that subscription's credential; pool entries go out on their own login or the Cloud\n  gateway key, never on it, and never with the agent's `oauth-*` betas. On a ChatGPT login\n  (`/chatgpt`, `chatgpt_auto.go`; OpenCode's ChatGPT login lands there too) `/models` gains an Auto\n  entry (a copy of `gpt-6.1-sol`'s, listed last, fetched without `If-None-Match`, served without\n  `ETag`) while the link reports `AutoOffered` (signed in, routing on). Every POST there has its\n  first 64 KiB read (decoded when gzip, deflate (zlib or raw) or zstd, by a decoder that is always closed; a br body is never looked into) for a\n  top-level `\"model\":\"caveman-auto\"`; any other streams on unchanged. `/responses` naming Auto asks\n  with `NoPool`, keeps live-zone compression, replays `gpt-6.1-sol`'s bytes on a 4xx (a rate-limit 429, a 401 and a 403 are returned instead; a served replay pins the conversation raw, and the decision is rejected only then or when the replay is itself a 429; a 5xx on a moved model is returned and rejects the decision, a rate-limit 429 keeps it: the plan's limit holds for every model); any other\n  path (Codex compaction) runs `gpt-6.1-sol` unasked; a body over `CAVE_MAX_REQUEST_BYTES` streams\n  with only its model changed (`auto_body_too_large`; one that fits but does not decode records `auto_body_decode_failed`). The agent reads the Auto id it sent. Every `/chatgpt` row (Auto or\n  not) is offered to Cloud like any other, as its agent (`x-cave-agent`, else Codex by its\n  `originator` or User-Agent), with model ids in `route_from`/`route_to`; a model it could not read\n  goes to Cloud as none. The\n  ask starts before compression (parse, ask, compress, route), waits at most 800 ms (the CLI's\n  keychain credential is read when the runtime starts, `Link.Start`, never on a request goroutine;\n  a request that lands while a read runs with no secret cached waits for it, leaving the ask at\n  least 500 ms, else goes as `credential_loading` without an ask or a pause), carries the\n  caller's models, counts, what the request declares (the raw values of eleven allowlisted agent\n  headers, never cut: a value over 256 bytes, 16 KiB for Codex's turn metadata, which also comes\n  from the body's `client_metadata`, is left out; a forked Claude Code child's spawn-call\n  `subagent_type` as `x-caveman-agent`; its tool names, effort and thinking type), what the\n  session's previous request ran (the served model, the effort in force, the provider's input and\n  cache token counts, its age) and Cloud's opaque per-session state, plus, on a turn's first ask,\n  the raw text of the latest human turn, the one before it and the end of the agent's last reply\n  (contracts `route-ask-v1`; Cloud picks the model and the effort and no routing logic lives\n  here). Its answer is cached per ask until 30 min after its last use (session, or without one agent and\n  endpoint, provider, model, turn number, latest human text;\n  an input group carrying a tool result is no human turn, text riding along included) so a tool\n  loop never switches model or effort mid-turn (an ask first seen during a pause keeps the paused answer for its turn); a request the agent labels compaction or auxiliary\n  (Claude Code's request class and compaction flag, OpenCode's title/summary/compaction agent,\n  Codex's subagent kind and turn-metadata request kind) is asked on its own, without the text. The\n  session is `x-cave-session`, else the agent's session header (a Codex thread, `thread-id`, is\n  its own, its parent `x-codex-parent-thread-id`); a Claude Code child (`x-claude-code-agent-id`)\n  is a session of its own and sends its parent's state; a session id guessed from timing is not\n  used. State, previous-request facts and marks live in bounded in-memory LRUs (1024), never on\n  disk. The answer sets Responses `reasoning.effort`, chat `reasoning_effort` or Anthropic\n  `output_config.effort`; Anthropic `effort_mode: message` instead inserts the byte-identical mark\n  `{\"role\":\"system\",\"content\":[],\"output_config\":{\"effort\":…}}` (beta\n  `mid-conversation-output-config-2026-07-01` appended to `anthropic-beta`) before the last user\n  turn, or at the end after a tool result; on OpenAI Responses (GPT-6) the mark is a\n  `{\"type\":\"configuration_update\",\"reasoning\":{\"effort\":…}}` input item before the last user\n  message while nothing has answered it yet, never at the end and never next to another one (no\n  place, as mid tool loop: the effort in force holds), no beta, and none at all where OpenAI forbids\n  it (`truncation: \"auto\"`, `context_management`, a non-false `multi_agent`: Cloud's effort goes\n  top-level); a 400 naming a configuration update heals like a refused mark. Effort state made on\n  one wire starts afresh on the other. Only `effort_mode: message` ever starts marks; a \"top\"\n  answer on history carrying the session's marks goes in as one more mark (never wiping them; for\n  a compaction or side request, that request's only), and on any other history sets the top-level\n  field. Marks are remembered per session with a salted hash of the message before each\n  (`cache_control` and thinking blocks left out, so a heal's strip keeps them) and replayed at the\n  same places on every later request to a model that already took them (the agent resends history\n  without them), count_tokens of a known session or a forked child's parent included (it adds\n  nothing to a history without marks). Without an effort from Cloud (a failure, routing off,\n  effort \"\") a history without the session's marks goes as the agent sent it, top-level field\n  included (one cache restart where Cloud's was, nothing inserted, no beta), except that a\n  remembered heal (drop_block with its beta, or a strip) keeps applying. One with marks runs at\n  the request's own top-level effort, or, when it sets none, at the model's default effort\n  (Cloud's `default_effort`, kept per session, a forked child taking its parent's), marked when it\n  differs from the one in force; only with no default known does such a request go as the agent\n  sent it, without the marks, which the session then forgets (its later thinking blocks then lose\n  their binding) unless the request went to another model. The top-level field is fixed at the\n  session's first per-message request: the routed effort on a fresh conversation whose request\n  sets one (kept there, also for later requests that set none), else the request's own; a routed\n  effort for a request that sets none goes in as a mark, first on a fresh conversation. An anchor\n  that no longer matches drops that mark and every later one; compaction and side requests never\n  change them, and a body matching none of them (an unlabeled side request, compacted history)\n  gets marks of its own, kept for up to four request shapes (first message, length, last message),\n  which take the session over only from a longer request continuing that conversation (same first\n  message, its last one where it was); the session's marks and fixed top-level field then wait\n  there in turn. A thinking-binding 400 on the asked model of a `/messages` request retries once\n  only on a session the route stage changed (it was on for the request, the session carries its\n  marks or heal, or another model served one of its requests) whose body sets no\n  `thinking.block_binding`: with `thinking.block_binding.prefix_mismatch_behavior: \"drop_block\"`\n  (beta `thinking-binding-controls-2026-08-01`) when the block is bound to a different\n  conversation and the request's thinking is adaptive or enabled, then sent on every later request\n  (count_tokens and forked children included); else (Sonnet 5.5 `between_tools`, no thinking\n  field, a tampered signature) without thinking blocks from the failing message on, and later\n  requests on the same history (same first message, same message at its end) strip the same range\n  up front (a second strip widens it; the strip stays, the history as served, even if the agent\n  later sets its own `block_binding`). A drop_block retry refused with a 400 naming the binding,\n  `block_binding` or its beta moves the session to the strip path, a strip refused that way ends\n  its heals (the first lasts for the session, as it depends on the model and thinking type; the\n  second goes with the marks, cleared when they are forgotten; a side request never clears either,\n  nor the strip; a 429, 5xx or other 400 on the retry teaches nothing). Routing switched off after\n  a restart surfaces that 400. A refused mark retries once with top-level effort only and, once\n  served, latches per-message effort off for the session (a model that refused takes Cloud's\n  effort top-level); compressed 400s are decoded first. A Cloud error, timeout, 401/403,\n  `allowance` or `billing_limit` answer keeps the asked model (the held model above for a transient failure) and pauses new asks (1 min; 10 min\n  for 401/403 and billing_limit; until the 1st for allowance). Refusals and limits land in\n  `$CAVEMAN_HOME/route-state.json` (with Cloud's notice) for `caveman status`; a new login lifts\n  the pause. A route failure blames a model only on evidence (Cloud ADR 0110). A provider 4xx on\n  the routed model replays the original bytes on the asked model, except a 401 (the login, not the\n  model: returned as is); a 403 replays (a plan refuses a model it does not carry with one). Only a\n  served replay of bytes that differed from the original in the model alone blames the routed model\n  (`Reject(true)`, `provider_rej\n\n<!-- [TRUNCATED AT 50KB] -->\n\n<!-- File: shrink/CLAUDE.md (Tokens: ~1080 | Category: claude-rule) -->\n# shrink — caveman-shrink, the tool-catalog compressor (Go core + launcher, Apache-2.0)\n\nCompress MCP/OpenAI **tool definitions** before they fill the context window. A thin Go wrapper\nover the [engine](../engine/CLAUDE.md)'s `toolschema` compressor: it drops annotation metadata\n(examples, titles, `$comment`, `$schema`) and reduces free-text descriptions to their lead\nsentence **plus every constraint-bearing sentence**, while keeping every **selection token** —\ntool and parameter names, types, enums, required — and every **argument-construction value** —\n`default`, `const`, `$ref` targets — byte-for-byte. Everything is `inferred`.\n\n## Layout\n- `shrink.go` — the library: `Shrink` (catalog → compressed, fail-open S4 transform, durable-CCR-backed), `Recover` (handle → original bytes), `Lint` (per-tool inferred token reductions), `SelectionProfile` (the structural surface — what must survive). `WithStore`/`WithStorePath` pick the recovery store; default is the shared `CAVEMAN_CCR_DB` / `~/.caveman/ccr.db`.\n- `cmd/caveman-shrink/` — the CLI: `caveman-shrink` / `shrink` (stdin→stdout), `lint <file>`, and `recover <handle>` (prints the original catalog for a handle a prior shrink minted).\n- `bin/caveman-shrink.mjs` + `package.json` — the `npx caveman-shrink` launcher around the prebuilt binary.\n\n## Two correctness boundaries\n1. **structural selection surface** — `SelectionProfile(input)` ==\n   `SelectionProfile(Shrink(input).Output)` always. This proves names/params/enums/required survive\n   byte-for-byte. It does **not** prove the model selects the same tool: descriptions are\n   model-visible and long descriptions are lossy. Behavioral equivalence requires model-eval\n   fixtures; current results stay `inferred`.\n2. **argument validity** — agents build tool *arguments* from descriptions, so shrink preserves\n   selection but **does not by itself guarantee argument validity**. What it guarantees, biased\n   to over-keep: (a) a description that already fits a small budget (≤ `maxDescLen*2` bytes) is\n   kept **whole** — no sentence is elided, so a constraint phrased without a recognised marker\n   still survives; (b) in a genuinely large description, the lead sentence plus every sentence\n   carrying a constraint marker (must / cannot / required / rejected / invalid / not allowed /\n   exactly one / at least / at most / max / min / range / between / greater-than / over / above /\n   unique / mutually exclusive / format / ISO / RFC / absolute …) is retained in full. Truncating\n   an abbreviation (\"Target path, e.g. /srv…\" → \"Target path, e.\") or dropping\n   `default`/RFC3339/bound rules would produce invalid calls the structural profile cannot see;\n   the call-validity conformance test (`engine/compressors`, golden fixture + reproduced\n   under-keep cases) locks recognised constraint tokens in place. Over-keep is the rule — never\n   knowingly under-keep.\n\n## Conventions\n- Build/test: `make product-build PRODUCT=shrink` / `make product-test PRODUCT=shrink`.\n- The structural compressor lives in the **engine** (`compressors/toolschema.go`); shrink reuses it, never forks it.\n- **This is the dedicated product surface for the `toolschema` compressor.** Engine API/CLI callers\n  can also force it locally. Managed-gateway adapters keep tool arrays in the frozen prompt-cache\n  prefix and never hand them to it; gateway's separate S2 tool-search/deferral path is not\n  compression. Local reductions stay `inferred`. A future billed route for this transform needs\n  cache-versus-schema cost proof, stable byte-identical prefix output, and an eval gate.\n\n## Gotchas (honesty invariants)\n- **S4 lossy, fail-open** — successful compression changes model-visible bytes; malformed/incompressible catalogs pass through unchanged (`ratio:0`, no handle).\n- **inferred-only** — token counts are estimates via the engine's offline counter; never `verified`, never re-projected.\n- **reversible for real** — a compressing shrink writes the exact original to the **durable** store (`engine.Compress` commits the recovery row before it publishes transformed bytes), so a printed handle resolves later, from a separate process, via `Recover` / `caveman-shrink recover`. It is NOT an in-memory store discarded on return — that minted handles that were unresolvable the instant `Shrink` returned. If the durable store cannot be opened, `Shrink` returns an error and the CLI forwards the original bytes: no handle, no reversibility claim.\n\nSee ../../CLAUDE.md (root) · ../engine/CLAUDE.md\n\n<!-- File: evals/README.md (Tokens: ~2791 | Category: anthropic-skill) -->\n# Evals\n\nMeasures real token compression of caveman skills by running the same\nprompts through Claude Code under three conditions and comparing the\ngenerated output token counts. A second eval, [Fidelity](#fidelity),\nruns the same arms on fixed correctness cases and checks that the facts\nsurvive.\n\n## The three arms\n\n| Arm | System prompt |\n|-----|--------------|\n| `__baseline__` | none |\n| `__terse__` | `Answer concisely.` |\n| `<skill>` | `Answer concisely.\\n\\n{SKILL.md}` |\n\nThe honest delta for any skill is **`<skill>` vs `__terse__`** — i.e.\nhow much the skill itself adds on top of a plain \"be terse\" instruction.\nComparing a skill to the no-system-prompt baseline conflates the skill\nwith the generic terseness ask, which is what an earlier version of\nthis harness did and is why its numbers were inflated.\n\n## Current snapshot\n\n`snapshots/results.json` was generated on `claude-opus-5-5` with Claude\nCode 2.1.288 (2026-10-02), with the three response-style skills as the\nskill arms: `caveman`, `ultracave`, `megacave`. The other `skills/*`\ndirectories are workflow skills, not response styles, so they are left out.\n\n```bash\nCAVEMAN_EVAL_MODEL=claude-opus-5-5 CAVEMAN_EVAL_SKILLS=caveman,ultracave,megacave \\\n  python3 evals/llm_run.py\n```\n\nThe previous snapshot (`claude-opus-4-6`, April 2026) was generated\nwithout host isolation, so whatever plugins and CLAUDE.md files were\ninstalled could reach every arm. `llm_run.py` now isolates each call\n(see below); numbers from the two snapshots are not directly comparable.\n\n## Why this design\n\n- **Real LLM output**, not hand-written examples (no circularity).\n- **Same Claude Code** the skills target — no separate API key.\n- **Snapshot committed to git** so CI runs are deterministic and free,\n  and so any change to the numbers is reviewable as a diff.\n- **Control arm** isolates the skill's contribution from the generic\n  \"be terse\" effect.\n- **Host isolation.** Each `claude -p` call runs with\n  `--setting-sources project --strict-mcp-config --disable-slash-commands\n  --no-session-persistence` from an empty temp dir: no user settings (so\n  no installed plugins or their SessionStart hooks), no MCP servers, no\n  installed skills, no CLAUDE.md. Without it a plugin's injected ruleset,\n  or an MCP auth nag the model repeats in its answer, lands in every arm\n  including the baseline.\n\n## Files\n\n- `prompts/<lang>.txt` — fixed list of dev questions, one per line.\n  `en.txt` is the default; `pt.txt` is Brazilian Portuguese; `fr.txt`\n  is French and mirrors `en.txt` line for line.\n- `llm_run.py` — runs `claude -p --output-format json --system-prompt-file …`\n  per (prompt, arm), captures real LLM output, writes\n  `snapshots/results.json` along with metadata (model, CLI version,\n  language, generation timestamp). Next to the text it stores the usage\n  Claude Code reports for each call under `usage` (same arm/prompt layout\n  as `arms`): input, output, cache-creation and cache-read tokens plus\n  `total_cost_usd`.\n- `measure.py` — reads the snapshot, counts tokens with tiktoken\n  `o200k_base`, prints a markdown table with median / mean / min / max /\n  stdev across prompts. When the snapshot has `usage`, a second table\n  gives Claude's own output-token counts against the terse control and\n  the median input tokens the skill adds per call (skill arm minus terse\n  arm, cache tokens included).\n- `snapshot_contract.py` — rejects incomplete or malformed snapshot matrices\n  before `measure.py` or `score_fidelity.py` reports metrics.\n- `prompts/fidelity.json` — fixed correctness cases, each with regex\n  checks (see [Fidelity](#fidelity)).\n- `score_fidelity.py` — scores `snapshots/fidelity.json` against those\n  checks, offline, stdlib only.\n- `snapshots/results.json` — committed source of truth, regenerated only\n  when SKILL.md files or prompts change. Other languages write\n  `results.<lang>.json` next to it; none is committed yet.\n\n## Refresh the snapshot (requires `claude` CLI logged in)\n\n```bash\nuv run python evals/llm_run.py\n```\n\nThis calls Claude once per prompt × (N skills + 2 control arms). Use\na small model to keep it cheap:\n\n```bash\nCAVEMAN_EVAL_MODEL=claude-haiku-4-5 uv run python evals/llm_run.py\n```\n\nBy default every `skills/*/SKILL.md` gets an arm. `CAVEMAN_EVAL_SKILLS`\n(comma-separated skill ids) restricts the skill arms; an unknown id aborts\nbefore any call:\n\n```bash\nCAVEMAN_EVAL_SKILLS=caveman,ultracave,megacave uv run python evals/llm_run.py\n```\n\nEach call times out after `CAVEMAN_EVAL_TIMEOUT` seconds (default 300). A\nfailed or timed-out call is retried twice, after 5 s and then 20 s. If it\nstill fails, the run stops (auth, quota or a bad model name would fail\nevery remaining call too), writes the finished calls to\n`snapshots/results.partial.json` with the unfinished cells set to `null`\nand the error in `metadata.error`, leaves `results.json` untouched and\nexits 1. `measure.py` refuses a partial file; it is gitignored.\n\n### Other languages\n\n```bash\nCAVEMAN_EVAL_LANG=pt CAVEMAN_EVAL_MODEL=claude-haiku-4-5 CAVEMAN_EVAL_SKILLS=caveman uv run python evals/llm_run.py\nCAVEMAN_EVAL_LANG=pt uv run --with tiktoken python evals/measure.py\n```\n\nThe terse control is translated per language (`TERSE_PREFIXES` in\n`llm_run.py`): an English \"Answer concisely.\" on a Portuguese question\nalso nudges the model toward English, which would be a second variable.\nA language with no translated control, or no `prompts/<lang>.txt`, stops\nbefore any call instead of falling back to English. The skill text\nitself stays in English, as shipped.\n\nWhy a separate language matters: SKILL.md promises \"compress the style,\nnot the language\", and the rules target English function words\n(a/an/the, just/really). Whether that transfers to a language with\ngendered articles and a different filler vocabulary is an empirical\nquestion, so it gets its own snapshot.\n\n## Read the snapshot (no LLM, no API key, runs in CI)\n\n```bash\nuv run --with tiktoken python evals/measure.py\n```\n\nReporting fails closed unless the snapshot has both control arms, at least one\nskill arm, exactly one string output per prompt in every arm, and metadata whose\n`n_prompts` matches the prompt list. A `usage` block, when present, must\ncover the same arms and prompts with non-negative token counts.\n\n## Adding a prompt\n\nAppend a line to `prompts/<lang>.txt`, then refresh that language's snapshot.\n\n## Adding a language\n\nAdd `prompts/<lang>.txt` (ideally mirroring `en.txt` line for line),\nadd the translated terse control to `TERSE_PREFIXES` in `llm_run.py`,\nrun with `CAVEMAN_EVAL_LANG=<lang>` and commit\n`snapshots/results.<lang>.json`.\n\n## Adding a skill\n\nDrop a `skills/<name>/SKILL.md`, then refresh the snapshot. `llm_run.py`\npicks up every skill directory automatically, unless `CAVEMAN_EVAL_SKILLS`\nis set, in which case add the new id to that list.\n\n## Fidelity\n\nThe length eval alone rewards a skill that replies `k` to everything.\nThe fidelity eval asks whether each arm kept what the answer needs: exact\nnumbers and units, every not/never/only/except, the user's language,\nverbatim errors, safety wording before destructive steps, normal prose in\npersisted artifacts.\n\n`prompts/fidelity.json` holds the cases: `{id, category, prompt, checks,\nexamples}` with `checks.must_include` and `checks.must_not_include` as\nregex lists, and `examples.pass` / `examples.fail` as answers the checks\nmust accept / reject.\nA case passes for an arm when every `must_include` pattern matches the\noutput and no `must_not_include` pattern does (Python `re.search`,\ncase-insensitive). There is no judge model, so the same snapshot always\ngets the same score, and checks can be fixed and re-scored without new\ncalls. The first six cases come from\n[#1061](https://github.com/JuliusBrussee/caveman/pull/1061) by alexis.\n\n```bash\nCAVEMAN_EVAL_SET=fidelity CAVEMAN_EVAL_MODEL=claude-opus-5-5 \\\n  CAVEMAN_EVAL_SKILLS=caveman,ultracave,megacave python3 evals/llm_run.py\npython3 evals/score_fidelity.py\n```\n\nThe run uses the same arms, isolation, retries and usage capture as the\nlength eval and writes `snapshots/fidelity.json`. The scorer prints the\npass rate per arm, overall and per category, then every failed check.\nIt refuses a snapshot whose prompts no longer match the case file.\n\nNo fidelity snapshot is committed yet. A regex pass is evidence, not a\nverdict: read the failures before citing a rate, and publish no\nquality-equivalence claim without a committed, reviewed snapshot.\n\n| Category | What the checks look for |\n|----------|--------------------------|\n| `substance-preservation` | Filler removed, the technical substance needed to act kept. |\n| `exact-preservation` | Polarity, limits, numbers, units, code, identifiers, APIs, commands and quoted errors unchanged. |\n| `no-caricature` | No fake grammar, mode prefixes, invented abbreviations or arrows. |\n| `language-and-grammar` | The requested or dominant language kept, grammatical markers kept. |\n| `safety-clarity` | Plain, complete warnings for destructive actions, security, data loss and ordered recovery steps. |\n| `artifact-boundary` | Persisted or external human-facing artifacts in normal prose. |\n| `mode-boundaries` | Explicit activation, mode switch, persistence and deactivation boundaries preserved. |\n\nTo add a case, append it to `prompts/fidelity.json` with a unique `id`,\none of the categories above, at least one check, and at least one\nknown-good and one known-bad example answer, then rerun the fidelity\neval. `tests/test_eval_fidelity.py` checks the file's shape and runs\nevery case's checks against its examples, so a check that rejects a\ncorrect answer fails there instead of after a paid run.\n\n## What this does NOT measure\n\n- **Fidelity beyond the checks** — the fidelity eval only sees what its\n  regexes test. Tone, ordering and whether an explanation is actually\n  right are left to human review of the outputs.\n- **Latency or cost** — latency is out of scope. Skills add input tokens\n  on every call, so output savings are not the full economic picture.\n  Snapshots with `usage` record that input cost per call; the committed\n  `results.json` predates usage capture, so it has none.\n- **Cross-model behavior** — only the model used to generate the\n  snapshot is measured.\n- **Exact Claude tokens, from the tiktoken table** — `tiktoken\n  o200k_base` is OpenAI's BPE and is only an approximation of Claude's\n  tokenizer. Ratios between arms are meaningful; absolute numbers are\n  approximate. The usage table uses Claude's own counts instead, but\n  Claude's output count includes thinking tokens, which the user never\n  reads.\n- **Statistical significance** — single run per (prompt, arm) at default\n  temperature. The min/max/stdev columns let you eyeball whether a\n  number is solid or noisy, but this is not a powered experiment.\n\n## Historical: reminder_run.py\n\n`reminder_run.py` and `snapshots/reminder.json` measured the pre-3.1 per-turn reminder text (`Enforce this reply: ...`), which the mode tracker no longer emits. The snapshot is kept as history; regenerate before citing it.\n\n<!-- File: packages/cli/AGENTS.md (Tokens: ~3041 | Category: anthropic-skill) -->\n# packages/cli — `caveman` CLI\n\nTypeScript CLI (`src/index.ts`) driving the local proxy and wrapping control-api\nREST calls. Published package has zero runtime dependencies. Build emits\n`dist/index.js`, dependency-free `dist/caveman-delegate-mcp.mjs`, and a lazy\nbundled `dist/learn-tui.js` Clack chunk, then\n`scripts/shebang.mjs` adds shebang + chmod. `bin` exposes both `caveman` and `cave`. Non-secret config lives at\n`~/.caveman-cloud/config.json` (0o600); the **auth token lives in the OS keychain** (macOS\n`security`) or a `~/.caveman/credentials` (0o600) fallback — never plaintext config.\n\n## Layout\n- `src/index.ts` — entire CLI: arg dispatch, HTTP helpers (`get`/`post`), token storage, all commands\n- `src/learn-tui.ts` — bounded interactive learn view; bundled with Clack and loaded only for a real TTY\n- `tests/*.runtime.mjs` — Node `--test` runtime tests; spawn the built binary against HTTP stubs (`providers-verify`, `wrap`, `login`, `compress`)\n- `scripts/bundle-delegate.mjs` — copies canonical dependency-free delegate server into published `dist/`\n- `scripts/bundle-tui.mjs` — bundles Clack into the self-contained learn TUI chunk\n- `scripts/shebang.mjs` — post-build: prepends shebang, marks executable\n- `package.json` — bin: `caveman`/`cave → dist/index.js`; build: registries → `tsc` → delegate/TUI bundles → shebang\n\n## Command surface\n`caveman <agent>` (e.g. `caveman claude`) is shorthand for `caveman wrap <agent>` — any known agent id or binary name works as a top-level command; it is dispatched **last**, so real commands always shadow an agent name, and everything after the agent name goes to the agent verbatim.\n`cave` is a permanent byte-compatible bin alias. Relocated verbs keep their bare\nspellings as silent legacy aliases: no deprecation text may alter piped output.\n\nPrinted porcelain is `run`, `learn`, `login`, `status`, plus the agent shortcut.\nLocal capabilities live under `caveman tools`; account- or network-dependent\noperations live under `caveman cloud`. `dev` and `deploy` are undocumented\nmaintainer aliases. `tools` is capped at 15 printed verbs and `cloud` at 15;\ncurrent counts are 15 and 14. Internal/advanced `shrink-hook`, `practices`, and\n`check` remain callable through existing paths but are unprinted, including in\nlegacy `help tools --all` output.\n\nCaveman's own Go binaries (proxy/engine/mcp/mem/browse/shrink) resolve via `cavemanBin()`: env override (`CAVEMAN_*_BIN`) → PATH → `~/.caveman/bin` (where `scripts/install-local-cli.sh` or `scripts/install-local-cli.ps1` builds them) → bare name (so missing-binary panels still trigger).\n`caveman setup` prints per-binary install status — what works, what degrades to a loud byte-safe pass-through, and the one install command — and exits non-zero when a required binary (proxy/engine/mcp/mem; browse and shrink are optional) is missing. `caveman tools compress catalog` delegates to the dedicated `caveman-shrink` binary; `caveman tools shrink` remains command-output compression. It's the anti-silent-degrade front door for npm installs (the package ships JS only); every degraded path also prints its own warning line pointing at it. Publish checklist lives in `PUBLISHING.md`.\nLocal (no account): `start` (launch the proxy via `CAVEMAN_PROXY_BIN`; if the binary is missing or the port is already served it renders a status panel — build/`make dev`+live docker status/env — instead of a bare spawn error) · `wrap [agent]` (inject `ANTHROPIC_BASE_URL`/`OPENAI_BASE_URL` = `CAVE_GATEWAY_URL` or the local proxy, exec child; known agents `claude`/`codex`/`gemini`/`aider` launch by id with install detection; an unknown/unfound target shows an install hint or the wrappable list; bare `wrap` in a TTY opens an arrow-key picker) · `compress` (shells out to `caveman-engine` via `CAVEMAN_ENGINE_BIN`; byte-safe pass-through fallback if the binary is missing; `inferred`) · `toon encode|decode` (stateless JSON⇄TOON converter via the engine binary; `encode` degrades byte-safe when the engine is missing, `decode` fails loudly — it must not emit raw TOON as JSON) · `wrap --toon` (sets `CAVE_ENGINE_TOON=best-of` on the spawned proxy so it re-encodes uniform JSON the model reads — tool results — as TOON when smaller; opt-in, implies `--compress`) · `mcp install|uninstall [agent]` (register/remove the caveman_retrieve MCP tool; compress-mode wrap injects it only when `execute.mcp = auto` and a real `caveman-mcp` executable resolves — never the npx fallback — because streams are only compressible with agent-side recovery; `execute.mcp = marker-only` or `false` stops injection while existing registrations remain visible) · `evals run` (delegates to `caveman-engine evals run`, forwards its exit code) · `stats` (delegates to `caveman-proxy stats`) · `convert` (pixel-compresses installed agent skills in place: SKILL.md **body** → `SKILL.pxN.png` pages via `caveman-engine pixel render`, frontmatter stays text so discovery/triggering still works, body becomes a stub telling the agent to read the images on invocation; dirs come from registry profiles with a `skills` block — claude + codex today; converts only when image+stub est tokens < text est tokens, else untouched; original kept byte-exact as `SKILL.orig.md`, `--revert` restores; every skip is reported with its reason; savings `inferred`, per-invocation) · `skills install [caveman|caveman-learn]` (writes embedded Caveman skills and auto-pixels by default) · `skills add <source>` (accepts official `npx skills add` Git/URL/local sources and flags, delegates download/selection to that CLI in forced copy mode, then pixelizes only new/changed Claude Code or Codex skills; resources stay untouched; third-party content is explicitly unreviewed; `--no-pixel` opts out; engine failure/not-smaller stays honest plain text).\nConnected namespace: `whoami · projects · keys · providers · billing · score ·\ncosts · plan · traces · experiments · receipts · audit · sync · agent`.\n`doctor`, `opportunities`, `snippets`, `dev`, and `deploy` remain unprinted legacy\naliases; `status`, `plan`, and `tools sdk snippets` absorb their public jobs.\n\n`login` polls control-api `/api/v1/auth/device/{code,token}`; `organization_id` is bound from the returned token. `CAVE_TOKEN` is the non-interactive CI path. Logged-out connected verbs print one line + exit non-zero (CI skips, never crashes).\n\nFirst-run retro scan persists only closed `caveman.local_scan.v1` aggregate. Login or explicit `sync` uploads it through authenticated project `format=local-scan`; prompts, outputs, paths, session rows, free-text caveats, and anonymous telemetry never enter this lane. Dashboard labels token figures `inferred` and separates them from measured spend and verified savings.\n\n`providers verify <conn>` → real POST `/api/v1/projects/{id}/providers/{conn}/verify` (no hardcoded status).\n\n`plan` renders the Cave Plan in plain English (one operator voice); `--json` prints the raw response. There is **no** caveman voice / `--engineer` flag — the dual-voice was deliberately removed. Headline is labeled `basis` (\"inferred\"), savings are per-day — never reprojected to monthly. (honesty rule: no-fake-savings)\n\n`learn` is summary-first: real terminals get animated progress, bounded score\nand move cards, then one keyboard action menu. `--plain` restores compact text,\n`--all` restores every sink id/class/practice/suggestion, while `--json` and\n`--md` stay complete. `learn implement [claude|codex]\n[--prompt <focus>]` installs the existing `caveman-learn` safety guide when\nmissing and launches the chosen interactive agent. The guide's per-edit consent,\nload-bearing protection, re-measurement, and inferred-only rules remain binding.\nPresentation contract: [`TERMINAL_UX.md`](TERMINAL_UX.md).\n\n## Conventions\n- Dispatch uses handler tables. Every handler receives its own rebased argv slice;\n  never read process-global argv positionally inside a handler.\n- `flag(\"--name\", fallback)` parses named args from current invocation.\n- Tests use `node --test` (Node built-in runner); run `tsc` first, test spins a real HTTP server\n- Build: `pnpm build` (tsc + shebang); install locally: `scripts/install-local-cli.sh` (macOS/Linux) or `scripts/install-local-cli.ps1` (Windows) at repo root\n\n## Capability promotion rule\n\nA capability may default on only when it is byte-safe, or when protected by the\napplicable path-specific gate: managed gateway uses an eval gate; local wrap\nuses recovery + CCR — **not** an account or entitlement. There is\nno eval gate in local `run`.\nAny PR flipping a default must name the clause and path.\n\nA verb enters porcelain only when its capability is automatic-by-default-safe\ninside `run` and users no longer need to type it. Porcelain stays capped at four\nverbs + agent shortcut + exactly two namespaces. A fifth verb, or a 16th printed\nverb in either namespace, requires a retirement decision. `record` mode is always\npass-through.\n\nCapability config is grouped in `~/.caveman-cloud/config.json` as `think`,\n`remember`, and `execute`. `./.caveman/config.json` may only narrow its allowlisted\nproject-local keys; it cannot change `think.mode`, pixel settings, account state,\nconsent, or entitlement. Resolution is default < proxy YAML < legacy `wrap` <\nglobal groups < project overlay < env. Env parity is knob-specific. Inspect\nper-key source with `caveman tools config get`.\n\n## Gotchas\n- `providers verify` must NOT return a hardcoded status; the test asserts the CLI echoes the server's value (no-placeholder rule)\n- `plan` savings display must stay per-day; never multiply to monthly projection\n- Non-PAYG coverage includes Claude Pro/Max, Codex ChatGPT, Gemini OAuth, and routed compatible agents. Codex subscription mode keeps provider config ephemeral under `CODEX_HOME`, auto-installs its MCP recovery, and starts `/chatgpt/responses` in compress mode instead of forcing record/pass-through. Plain OpenAI `/responses` and Gemini `generateContent` requests with MCP explicitly disabled have no server-retrieval grammar, so they must remain byte-identical with zero compression accounting; the compression conformance matrix pins these protocol-specific fail-closed cases instead of requiring every profile to emit a CCR marker.\n- Subscription/OAuth wrap sessions (Claude Pro/Max) compress **locally only**, live zone only, and with **no account**: `CAVEMAN_WRAP_ENTITLED` is gone from both doors and from the proxy, and both doors `delete` any inherited copy so a stray export cannot resurrect it. What both doors DO stamp is the recovery path (`CAVEMAN_RECOVERY`), explicitly (`\"mcp\"` or empty, never inherited): `wrap` answers it from the **agent's own** MCP install (an exported `CAVEMAN_RECOVERY=mcp` can't outlive that answer — it would have the proxy elide bytes behind markers this agent has no `caveman_retrieve` tool to expand), `start` from machine-wide MCP install evidence plus an explicit `CAVEMAN_RECOVERY=mcp` counted as the operator's own opt-in, re-stamped so the disclosure line and the proxy can never disagree; the compression disclosure line prints only when recovery holds, and no-MCP says compression is off and names `caveman mcp install <agent>`. The `subscription_compress: off` operator switch stays the operator's. Their measured reduction remains **tokens only**: a seat has no per-token marginal price, so the session footer and synced span never claim saved dollars. The local stats dashboard may show a separately labeled, same-model API equivalent from captured prices; it is hypothetical value, never subscription bill or quota savings. The session-savings line treats `oauth` like `subscription` (OAuth is list-price-eligible on Vertex alone) and qualifies unconditionally when its capped auth-mode window is truncated\n- Published runtime dependencies stay zero. TUI libraries must be bundled,\n  lazy-loaded, and measured; do not move them onto ordinary command startup.\n- `learn` uses bundled Clack only when stdin/stdout/stderr are TTYs. `--plain`,\n  `CAVEMAN_PLAIN=1`, `TERM=dumb`, machine modes, and pipes must never prompt.\n- Remaining terminal UX (status panels + `wrap` picker) lives in the small toolkit\n  at the bottom of `src/index.ts`. Piped/non-interactive paths remain plain; runtime\n  tests assert them.\n- Current official Skills CLI installs global Codex sources at\n  `~/.agents/skills`, despite older/direct Caveman installs using\n  `~/.codex/skills`; third-party post-install discovery must scan both.\n\nSee ../../CLAUDE.md (root)\n\n<!-- File: packages/cli/CLAUDE.md (Tokens: ~4056 | Category: anthropic-skill) -->\n# packages/cli — `caveman` CLI\n\nTypeScript CLI (`src/index.ts`) driving the local proxy and wrapping control-api\nREST calls. Published package has zero runtime dependencies. Build emits\n`dist/index.js`, dependency-free `dist/caveman-delegate-mcp.mjs`, and a lazy\nbundled `dist/learn-tui.js` Clack chunk, then\n`scripts/shebang.mjs` adds shebang + chmod. `bin` exposes both `caveman` and `cave`. Non-secret config lives at\n`~/.caveman-cloud/config.json` (0o600); the **auth token lives in the OS keychain** (macOS\n`security`) or a `~/.caveman/credentials` (0o600) fallback — never plaintext config.\n\n## Layout\n- `src/index.ts` — entire CLI: arg dispatch, HTTP helpers (`get`/`post`), token storage, all commands\n- `src/learn-tui.ts` — bounded interactive learn view; bundled with Clack and loaded only for a real TTY\n- `tests/*.runtime.mjs` — Node `--test` runtime tests; spawn the built binary against HTTP stubs (`providers-verify`, `wrap`, `login`, `compress`)\n- `scripts/bundle-delegate.mjs` — copies canonical dependency-free delegate server into published `dist/`\n- `scripts/bundle-tui.mjs` — bundles Clack into the self-contained learn TUI chunk\n- `scripts/shebang.mjs` — post-build: prepends shebang, marks executable\n- `package.json` — bin: `caveman`/`cave → dist/index.js`; build: registries → `tsc` → delegate/TUI bundles → shebang\n\n## Command surface\n`caveman <agent>` (e.g. `caveman claude`) is the **persistent default door**: for hook-capable native agents (claude/codex/hermes/gemini/opencode — not aider, whose hookless native install could never start the proxy) it runs the `caveman enable <agent>` user-scoped native install if no install journal exists, pre-starts the local proxy when it isn't listening (the native SessionStart hook re-does this, but only after the host approves the hooks), then launches the host binary directly — so plain `<agent>` stays caveman'd in every later session. For claude/codex the enable step also writes the `output` skill suite (`caveman`/`ultracave`/`megacave`) into the host's user skills dir, create-only (`wx`, never clobbers) and fail-open. They are deliberately NOT native-journal operations, so an edited/deleted skill can never read `degraded`, and `disable` leaves them in place: it turns off routing and hooks, not the voice. A `~/.caveman/integrations/<agent>.voice-skills.json` sidecar marks the install as done, so it runs once: the shortcut skips journaled installs (explicit `caveman enable <agent>` picks the skills up there), a skill the user deleted is not written back, and a CLI upgrade does not refresh an installed copy. It falls back to the ephemeral `wrap` door whenever the native path can't hold: wrap-session flags given (`--off`/`--pixel`/`--workflow`), a non-native agent, a Cave Build lock in cwd (lock enforcement is wrap-door-only), or any enable failure (e.g. missing caveman-mcp/proxy binaries) — the shortcut is never worse than a session-only wrap. Note the fallback is session-only relative to an install that may already exist: once the native install is in place, `--off`/`wrap` runs are ephemeral but the persistent routing stays until `caveman disable <agent>`. Any known agent id or binary name works as a top-level command; it is dispatched **last**, so real commands always shadow an agent name, and everything after the agent name goes to the agent verbatim. `caveman wrap <agent>` / `caveman run <agent>` remain the explicit session-only path.\n`cave` is a permanent byte-compatible bin alias. Relocated verbs keep their bare\nspellings as silent legacy aliases: no deprecation text may alter piped output.\n\nPrinted porcelain is `run`, `learn`, `login`, `status`, plus the agent shortcut.\nLocal capabilities live under `caveman tools`; account- or network-dependent\noperations live under `caveman cloud`. `dev` and `deploy` are undocumented\nmaintainer aliases. `tools` is capped at 15 printed verbs and `cloud` at 15;\ncurrent counts are 15 and 14. Internal/advanced `shrink-hook`, `practices`, and\n`check` remain callable through existing paths but are unprinted, including in\nlegacy `help tools --all` output.\n\nCaveman's own Go binaries (proxy/engine/mcp/mem/browse/shrink) resolve via `cavemanBin()`: env override (`CAVEMAN_*_BIN`) → PATH → `~/.caveman/bin` (where `scripts/install-local-cli.sh` or `scripts/install-local-cli.ps1` builds them) → bare name (so missing-binary panels still trigger).\n`caveman setup` prints per-binary install status — what works, what degrades to a loud byte-safe pass-through, and the one install command — and exits non-zero when a required binary (proxy/engine/mcp/mem; browse and shrink are optional) is missing. `caveman tools compress catalog` delegates to the dedicated `caveman-shrink` binary; `caveman tools shrink` remains command-output compression. It's the anti-silent-degrade front door for npm installs (the package ships JS only); every degraded path also prints its own warning line pointing at it. Publish checklist lives in `PUBLISHING.md`.\nLocal (no account): `start` (launch the proxy via `CAVEMAN_PROXY_BIN`; if the binary is missing or the port is already served it renders a status panel — build/`make dev`+live docker status/env — instead of a bare spawn error) · `wrap [agent]` (inject route and exec child; Claude, Codex, Hermes, Gemini, OpenCode, Aider, and OpenClaw launch by id; native-capable hosts get temp pack/home/config only, removed after exit; no user/project config write) · `enable|disable <agent>` (explicit user-scoped native install with atomic journal, lock, reversible owned-path restore, component doctor) · `inspect` / `why <decision-id>` (content-blind native receipt and Decision Ledger explanation) · `compress` (shells out to `caveman-engine`; byte-safe missing-binary pass-through; `inferred`) · `mcp install|uninstall [agent]` (explicit persistent recovery registration; wrap uses temporary recovery configuration only when `execute.mcp = auto` and a current local MCP binary exists; `marker-only` or `false` suppresses injection without silently removing user registrations) · `skills add <source>` (same Git/URL/local sources and flags as official `npx skills add`; download/selection stays upstream, forced copy mode lets Caveman pixelize only new/changed Claude Code/Codex `SKILL.md` bodies; resources remain untouched and third-party content remains explicitly unreviewed; `--no-pixel` opts out) · `skills import <dir|SKILL.md>` (blocked unevaluated native-skill draft; imported resources never execute) · `evals run` · `stats` · `convert`.\nConnected namespace: `whoami · projects · keys · providers · billing · score ·\ncosts · plan · traces · experiments · receipts · audit · sync · agent`.\n`doctor`, `opportunities`, `snippets`, `dev`, and `deploy` remain unprinted legacy\naliases; `status`, `plan`, and `tools sdk snippets` absorb their public jobs.\n\n`login` polls control-api `/api/v1/auth/device/{code,token}`; `organization_id` is bound from the returned token. `CAVE_TOKEN` is the non-interactive CI path. Logged-out connected verbs print one line + exit non-zero (CI skips, never crashes).\n\n**First-run experience** (TTY only, once per machine, marker `firstRunAt` in config): on the first interactive wrap — banner, 30-day retrospective scan (`caveman-proxy learn scan --retro`, read-only over local Claude Code/Codex session logs; base behavior pass capped at 20s, retro pass at 60s, child timeout derived as 90s including margin), count-up reveal of tokens sent / would-have-cut / rides-every-turn stream figure (sums over scanned sessions, never extrapolated; `inferred`; tokens never dollars; sent is deduped once per API response, base cut carries no ratio against sent, only the stream figure shares sent's basis), telemetry disclosure, one `[y/N]` account question (yes → device login → existing span backfill). CLI persists only the closed, content-free aggregate; login or explicit `sync` uploads it to the authenticated project's `local-scan` import lane. Prompts, outputs, paths, session rows, free-text caveats, and anonymous telemetry never enter that upload; dashboard keeps it `inferred` and separate from spend/verified savings. Every failure degrades to one dim line and the agent still launches. Replay with the **unprinted** `caveman welcome` (porcelain caps unchanged). Anonymous **telemetry defaults ON (opt-out)**: persisted v1 decisions and `DO_NOT_TRACK`/`CAVEMAN_TELEMETRY=0`/`caveman telemetry off` always win; CI never sends; non-TTY never persists the default; the first eligible interactive run persists a stable anonymous id and prints the disclosure line. A native SessionStart (not `source: compact`) with a persisted, current-version yes spawns a detached `caveman telemetry session <agent> <source>` (unprinted) that sends `session_start` with the token delta, so installed users who launch agents directly stay visible; the hook never waits on the network. One event per host session: an O_EXCL marker under `~/.caveman/runtime/telemetry-sessions/` is created before the spawn (pruned after a day). Hook entrypoints (`native-hook`, `shrink-hook`, `mem recall-hook`) set `telemetryCommandSent` so no hook process ever holds the host on a command_run POST. An interactive run that sees `DO_NOT_TRACK`/`CAVEMAN_TELEMETRY=0` while config says yes persists an opt-out (GUI-launched hosts never see the shell rc). Every event also carries `account`, `plan`, `install_channel`, `timezone`, `locale` from `telemetryContext()` (cheap config reads, no keychain). `command_run` also carries `tokens_processed`/`tokens_saved` read from the local proxy store as a **delta** against the `telemetryTokens` watermark in config, always with `tokens_basis` (`inferred`, tokens never dollars); the read is best-effort behind a 400ms spawn budget and omits the fields entirely when the store or binary is absent. Events post to the `cli-telemetry` Supabase Edge Function (source + schema in root `supabase/`), which validates them and stores each with the client IP (cleared after 90 days by pg_cron). Product analytics (users, daily actives, retention, churn, tokens saved, command/agent usage) are SQL views in the database's `analytics` schema. Widening the disclosed scope means bumping `TELEMETRY_PROMPT_VERSION` (now 5) — that reprints the line once for stale-version opt-ins, never re-asks, never touches an opt-out.\n\n`providers verify <conn>` → real POST `/api/v1/projects/{id}/providers/{conn}/verify` (no hardcoded status).\n\n`plan` renders the Cave Plan in plain English (one operator voice); `--json` prints the raw response. There is **no** caveman voice / `--engineer` flag — the dual-voice was deliberately removed. Headline is labeled `basis` (\"inferred\"), savings are per-day — never reprojected to monthly. (honesty rule: no-fake-savings)\n\n`learn` is summary-first: real terminals get animated progress, bounded score\nand move cards, then one keyboard action menu. `--plain` restores compact text,\n`--all` restores every sink id/class/practice/suggestion, while `--json` and\n`--md` stay complete. `learn implement [claude|codex]\n[--prompt <focus>]` installs the existing `caveman-learn` safety guide when\nmissing and launches the chosen interactive agent. The guide's per-edit consent,\nload-bearing protection, re-measurement, and inferred-only rules remain binding.\nPresentation contract: [`TERMINAL_UX.md`](TERMINAL_UX.md).\n\n## Conventions\n- Dispatch uses handler tables. Every handler receives its own rebased argv slice;\n  never read process-global argv positionally inside a handler.\n- `flag(\"--name\", fallback)` parses named args from current invocation.\n- Tests use `node --test` (Node built-in runner); run `tsc` first, test spins a real HTTP server\n- Build: `pnpm build` (tsc + shebang); install locally: `scripts/install-local-cli.sh` (macOS/Linux) or `scripts/install-local-cli.ps1` (Windows) at repo root\n\n## Capability promotion rule\n\nA capability may default on only when it is byte-safe, or when protected by the\napplicable path-specific gate: managed gateway uses an eval gate; local wrap\nuses recovery + CCR — **not** an account or entitlement. There is\nno eval gate in local `run`.\nAny PR flipping a default must name the clause and path.\n\nA verb enters porcelain only when its capability is automatic-by-default-safe\ninside `run` and users no longer need to type it. Porcelain stays capped at four\nverbs + agent shortcut + exactly two namespaces. A fifth verb, or a 16th printed\nverb in either namespace, requires a retirement decision. `record` mode is always\npass-through.\n\nCapability config is grouped in `~/.caveman-cloud/config.json` as `think`,\n`remember`, and `execute`. `./.caveman/config.json` may only narrow its allowlisted\nproject-local keys; it cannot change `think.mode`, pixel settings, account state,\nconsent, or entitlement. Resolution is default < proxy YAML < legacy `wrap` <\nglobal groups < project overlay < env. Env parity is knob-specific. Inspect\nper-key source with `caveman tools config get`.\n\n## Gotchas\n- `providers verify` must NOT return a hardcoded status; the test asserts the CLI echoes the server's value (no-placeholder rule)\n- `plan` savings display must stay per-day; never multiply to monthly projection\n- Claude Code treats any `ANTHROPIC_BASE_URL` whose host isn't `api.anthropic.com` as a custom endpoint and withholds first-party capabilities — most visibly the 1M-token context window, whose loss silently shrinks the auto-compact window to the 200k default (#865). Both Claude routing doors (ephemeral `buildWrapEnv` and the persistent native install) therefore set `_CLAUDE_CODE_ASSUME_FIRST_PARTY_BASE_URL=1`, but ONLY when the assertion is verifiably true: local mode AND the proxy's anthropic upstream is `api.anthropic.com` (default, or an explicit `providers.anthropic.base_url` naming that host — checked via a dependency-free scoped YAML scan that reads flow-style blocks as unverifiable). A user-set value is never overridden, managed gateways never assert, and disable removes the var only while it still holds exactly the value we wrote.\n- Non-PAYG coverage includes Claude Pro/Max, Codex ChatGPT, Gemini OAuth, and routed compatible agents. Codex subscription mode keeps provider config ephemeral under `CODEX_HOME`, auto-installs its MCP recovery, and starts `/chatgpt/responses` in compress mode instead of forcing record/pass-through. Plain OpenAI `/responses` and Gemini `generateContent` requests with MCP explicitly disabled have no server-retrieval grammar, so they must remain byte-identical with zero compression accounting; the compression conformance matrix pins these protocol-specific fail-closed cases instead of requiring every profile to emit a CCR marker.\n- Subscription/OAuth wrap sessions (Claude Pro/Max) compress **locally only**, live zone only, and with **no account**: `CAVEMAN_WRAP_ENTITLED` is gone from both doors and from the proxy, and both doors `delete` any inherited copy so a stray export cannot resurrect it. What both doors DO stamp is the recovery path (`CAVEMAN_RECOVERY`), explicitly (`\"mcp\"` or empty, never inherited): `wrap` answers it from the **agent's own** MCP install (an exported `CAVEMAN_RECOVERY=mcp` can't outlive that answer — it would have the proxy elide bytes behind markers this agent has no `caveman_retrieve` tool to expand), `start` from machine-wide MCP install evidence plus an explicit `CAVEMAN_RECOVERY=mcp` counted as the operator's own opt-in, re-stamped so the disclosure line and the proxy can never disagree; the compression disclosure line prints only when recovery holds, and no-MCP says compression is off and names `caveman mcp install <agent>`. The `subscription_compress: off` operator switch stays the operator's. Their measured reduction remains **tokens only**: a seat has no per-token marginal price, so the session footer and synced span never claim saved dollars. The local stats dashboard may show a separately labeled, same-model API equivalent from captured prices; it is hypothetical value, never subscription bill or quota savings. The session-savings line treats `oauth` like `subscription` (OAuth is list-price-eligible on Vertex alone) and qualifies unconditionally when its capped auth-mode window is truncated\n- Published runtime dependencies stay zero. TUI libraries must be bundled,\n  lazy-loaded, and measured; do not move them onto ordinary command startup.\n- `learn` uses bundled Clack only when stdin/stdout/stderr are TTYs. `--plain`,\n  `CAVEMAN_PLAIN=1`, `TERM=dumb`, machine modes, and pipes must never prompt.\n- Remaining terminal UX (status panels + `wrap` picker) lives in the small toolkit\n  at the bottom of `src/index.ts`. Piped/non-interactive paths remain plain; runtime\n  tests assert them.\n- Current official Skills CLI installs global Codex sources at\n  `~/.agents/skills`, despite older/direct Caveman installs using\n  `~/.codex/skills`; third-party post-install discovery must scan both.\n\nSee ../../CLAUDE.md (root)\n\n<!-- File: packages/sdk/parity/AGENTS.md (Tokens: ~530 | Category: anthropic-skill) -->\n# packages/sdk/parity — the cross-language SDK conformance contract\n\nOne language-neutral fixture file, run by **both** SDKs. The load-bearing honesty property\nof the SDKs: the wire contract is one thing, expressed twice (`@caveman-ai/sdk` TS +\n`caveman_cloud` Python). A field present in one SDK and absent in the other makes one side's\nassertion fail — so this folder is a release gate, not documentation.\n\n## Layout\n\n- `fixtures.json` — the contract. A `config` (one Cave), the named header sets (`std_headers` / `std_headers_traced` / `std_headers_async_traced` / `otlp_headers`), and an ordered `operations` list. Each operation carries its `input`, a canned `response` (or `transport: \"error\"` to force a byte-safe pass-through), and an `expect` block: `wire` (`method`, `path`, `headers`, and either an exact `body` or a `body_keys` set) plus a `result` (or `result_from: \"response\"`).\n\n## How it's enforced\n\n- TS half: `../typescript/tests/parity.runtime.mjs` (mocks `fetch`).\n- Python half: `../python/tests/test_parity.py` (mocks `urllib.request.urlopen`).\n- Each half has a per-operation handler that performs the **real** SDK call, captures the wire, and normalizes the result to canonical snake-keyed values. Both iterate **every** operation; a missing handler is a failure, never a skip.\n\n## Editing rules\n\n- Add a field/method to one SDK → add the operation (or body key) here → the other SDK's half goes red until it matches. That red is the point.\n- Headers are compared key-lowercased (Python's `urllib` capitalizes them); the SET + values must match across languages.\n- Keep values that encode identically in both languages (e.g. avoid `! ( ) *` in expand refs — JS `encodeURIComponent` and Python `quote` disagree on those).\n- Nothing random may reach an assertion. Ids the SDK would otherwise mint (trace id, span id) are injected through the operation `input` and pinned in the expected header set — the same way `otlp_export` pins its span ids.\n- Run: `make product-test PRODUCT=sdk-ts` && `make product-test PRODUCT=sdk-python`.\n\nSee ../typescript/CLAUDE.md · ../python/CLAUDE.md\n\n<!-- File: packages/sdk/parity/CLAUDE.md (Tokens: ~530 | Category: anthropic-skill) -->\n# packages/sdk/parity — the cross-language SDK conformance contract\n\nOne language-neutral fixture file, run by **both** SDKs. The load-bearing honesty property\nof the SDKs: the wire contract is one thing, expressed twice (`@caveman-ai/sdk` TS +\n`caveman_cloud` Python). A field present in one SDK and absent in the other makes one side's\nassertion fail — so this folder is a release gate, not documentation.\n\n## Layout\n\n- `fixtures.json` — the contract. A `config` (one Cave), the named header sets (`std_headers` / `std_headers_traced` / `std_headers_async_traced` / `otlp_headers`), and an ordered `operations` list. Each operation carries its `input`, a canned `response` (or `transport: \"error\"` to force a byte-safe pass-through), and an `expect` block: `wire` (`method`, `path`, `headers`, and either an exact `body` or a `body_keys` set) plus a `result` (or `result_from: \"response\"`).\n\n## How it's enforced\n\n- TS half: `../typescript/tests/parity.runtime.mjs` (mocks `fetch`).\n- Python half: `../python/tests/test_parity.py` (mocks `urllib.request.urlopen`).\n- Each half has a per-operation handler that performs the **real** SDK call, captures the wire, and normalizes the result to canonical snake-keyed values. Both iterate **every** operation; a missing handler is a failure, never a skip.\n\n## Editing rules\n\n- Add a field/method to one SDK → add the operation (or body key) here → the other SDK's half goes red until it matches. That red is the point.\n- Headers are compared key-lowercased (Python's `urllib` capitalizes them); the SET + values must match across languages.\n- Keep values that encode identically in both languages (e.g. avoid `! ( ) *` in expand refs — JS `encodeURIComponent` and Python `quote` disagree on those).\n- Nothing random may reach an assertion. Ids the SDK would otherwise mint (trace id, span id) are injected through the operation `input` and pinned in the expected header set — the same way `otlp_export` pins its span ids.\n- Run: `make product-test PRODUCT=sdk-ts` && `make product-test PRODUCT=sdk-python`.\n\nSee ../typescript/CLAUDE.md · ../python/CLAUDE.md\n\n<!-- File: packages/sdk/python/AGENTS.md (Tokens: ~2065 | Category: anthropic-skill) -->\n# packages/sdk/python — Python SDK for the Caveman Cloud gateway\n\nStdlib-only (`urllib.request`, no third-party deps) Python package. Provides `Cave` (config +\nentrypoint), `CaveTool` (tool descriptor), and `ToolSearchResult`. All HTTP calls POST to the\ngateway with `x-cave-agent` / `x-cave-workflow` / `x-cave-retention` headers set from `Cave` fields.\n\n## Layout\n\n- `caveman_cloud/__init__.py` — re-exports `Cave`, `CaveTool`, `CompressResult`, `ContextPackItem`, `ContextPackOptions`, `ContextPackResult`, `ToolSearchResult`, …\n- `caveman_cloud/core.py` — all implementation: `Cave`, `Trace`, `Provider`, `_Create`, `ToolSearchResult`, `CompressResult`, `ContextPack*`, `CaveTool`, `headers()`\n- `tests/test_sdk.py` — pytest tests; mock `urllib.request.urlopen` with `patch()`\n- `tests/test_parity.py` — cross-language conformance suite; drives `../../parity/fixtures.json` (shared with sdk-ts). Same fixtures, two languages → a field in one SDK and not the other fails CI.\n- `tests/test_trace_continuity.py` — trace/span id minting + which requests carry `x-cave-trace-id` / `x-cave-parent-span-id`; mirrors the TS `tests/trace-continuity.runtime.mjs`\n- `caveman_cloud/middleware/` — stable framework-middleware client for protocol 1.1 (`docs/technical/middleware-protocol.md`): `protocol.py` pure rules (scope normalization, tolerant capabilities, failure classification, breaker, budgets, endpoint/proxy resolution, `warn_once`), `transport.py` default keep-alive stdlib transport, `runtime.py` / `async_runtime.py` clients, `validate.py` plan/page checks. `tests/test_middleware_v1_1.py` drives `../../parity/middleware-v1_1.fixtures.json`; `tests/test_middleware_transport.py` uses real local sockets\n- `pyproject.toml` — distribution name `caveman-sdk` (import package stays `caveman_cloud`), `requires-python = \">=3.11\"`, no runtime dependencies\n\n## Key API surface (`core.py`)\n\n- `Cave.trace(workflow, tags, *, trace_id=None, span_id=None)` → context manager yielding `Trace`; call `.model[\"openai\"].responses.create(body)` inside. The trace mints `trace_id` (32 lowercase hex) + a root `span_id` (16 lowercase hex) with the exporter's RNG; `trace_id`/`span_id` continue an inbound trace and a value that isn't the exact hex shape is replaced rather than sent. Every provider call made **through the trace** carries `x-cave-trace-id` + `x-cave-parent-span-id`. Generic `/sdk/v1/*` calls and providers built off the `Cave` carry neither; the sole SDK-endpoint exception is `Trace.tool`, whose `/sdk/v1/events` call carries the trace id and root parent span id\n- `Trace.exporter(service_name=None)` → an `OTelExporter` whose `default_trace_id` is the trace's, so SDK spans and the gateway's request rows join one trace. MIRRORS the TS `CaveTrace.exporter`\n- `Cave.tools(catalog, *, strategy=\"all\", initial_tool_count=8)` → builder handle with `.strategy`, `.initial` (`list[CaveTool]`), `.search(query, *, max_tools, context, workflow, ranker, session_id)`. `strategy=\"deferred\"` includes every `always_load` tool exactly once, then fills remaining initial slots from non-mandatory tools; a cap below the mandatory count fails locally. `.search()` always hits the gateway with the FULL catalog. MIRRORS the TS `cave.tools({catalog, strategy})`\n- `Cave.tool_search(tools, query, *, context, max_tools, workflow, ranker, session_id)` → flat variant: POSTs `[tools, query]` to `/sdk/v1/tool-search`; returns `ToolSearchResult` with `.saved_tokens` / `.reduction_pct` / `.session_id`. Schema-token counters are estimates; `.token_basis` discloses the counter and `.basis` is always `\"inferred\"`. `ranker` (`\"bm25\"`|`\"embeddings\"`) is passed through verbatim — the SDK never computes similarity\n- `Cave.prompts.internal_brevity(*, style, preserve_errors_verbatim=False, preserve_code_verbatim=False)` → output-style snippet (`\"none\"` → `\"\"`); booleans render lowercase to match the TS `cave.prompts.internalBrevity`\n- `Cave.compress(payload, *, content_type=None)` → `CompressResult`; POSTs `/sdk/v1/compress`, maps the Engine report. **Byte-safe pass-through** on any transport/parse problem (original input, `ratio=0.0`, no handle); `.token_count_basis` discloses the counter and `basis` is always `\"inferred\"`. The SDK delegates — it never reimplements a compressor\n- `Cave.context.pack(query, items, options)` → `ContextPackResult`; connected-only POST to `/sdk/v1/context/pack`. Lossy selector over caller-owned items, never CCR/ledger; returns exact `deferred_ids`. Transport or malformed-report failure returns all original items with zero inferred savings\n- `Trace.expand(source_ref)` — the GET half of `checkpoint()`; `GET /sdk/v1/checkpoints/{ref}/expand` returns the stored `{source_ref, version, messages, checkpoint}`\n- `Cave.openai/anthropic/gemini/vertex(upstream_key)` → `Provider` that proxies through gateway; `Provider.raw(path, body)` is the escape hatch (mirrors the TS provider-client `raw`)\n- `Cave.bedrock(region, endpoint=\"runtime\")` → no-network first-party route descriptor; Runtime defaults to `/bedrock`, explicit Mantle returns `/bedrock/anthropic`, and `sdk_only=False` mirrors TS `sdkOnly`\n- `Trace.tool(name, options, fn)` — calls `fn()` then POSTs a `tool.call` event\n- `Trace.page_artifact(value, options)` / `Trace.artifacts.page(value, options)` — send versioned `{value, options, workflow}`; gateway stores only JSON `value`. `artifacts.get(id)` performs authenticated retrieval. `page_artifact` remains backwards-compatible alias.\n- `Trace.model[\"openai\"].responses.create(body, *, latency_class=None, tool_session_id=None)` — when `latency_class` is set, sends the `x-cave-async` header (`\"true\"` unless `\"interactive\"`); when `tool_session_id` is set, sends `x-cave-tool-session`, mirroring the TS `trace.model.openai.responses.create(body, {cave:{latencyClass, toolSessionId}})`\n- `Trace.checkpoint(messages, options)` — POSTs to `/sdk/v1/checkpoints`; the gateway persists it (Valkey) and returns a reversible `source_ref` you can later expand via `GET /sdk/v1/checkpoints/{ref}/expand`\n- `Cave.exporter(service_name=None)` → `OTelExporter`; `record_span(...)` maps current GenAI fields to `gen_ai.*`, `export()` POSTs OTLP/JSON to standard `/v1/traces` (headers via `otlp_headers()`; legacy `/otlp/v1/traces` remains server-only compatibility)\n- `Cave.retry_loop_breaker(threshold=3)` → `RetryLoopBreaker`; `.record(name, args)` raises `RetryLoopError` after `threshold` consecutive identical tool calls (interrupts a stuck loop). `.guard(name, args, fn)` records then runs `fn`\n- `Cave.jobs` → reserved `JobsClient` surface. Every method fails locally with `cave_async_jobs_unavailable`; it performs no network request until durable encrypted request storage, credential custody, and a draining worker exist. MIRRORS the TS `Cave.jobs`\n\n## Conventions\n\n- Tests use `patch(\"urllib.request.urlopen\", side_effect=fake_urlopen)` — never real network\n- Add new gateway endpoints via `Trace._request(path, body)` or `Provider.create(path, body)`\n- `headers()` is the single source for all outgoing headers; edit there, nowhere else\n- Deferred tool-search session handoff uses request/result `session_id` plus provider header `x-cave-tool-session`; update sdk-ts + parity fixtures with any change\n- Run tests: `pytest` from this directory (Python ≥ 3.11 required)\n\n## Gotchas\n\n- **No third-party deps** — do not add `requests`, `httpx`, or any library; keep `dependencies = []` in pyproject.toml\n- **byte-safe**: SDK sends request bodies to the gateway unmodified; no rewriting. `compress()` delegates to the Engine and passes the original through on any problem\n- **context packing is connected-only and intentionally lossy**: it sends item bytes to gateway, never runs in local wrap, and relies on caller retaining every item named by `deferred_ids`. It chooses what enters window; cache-optimal assembly chooses placement\n- `sdk-python` and `sdk-ts` mirror the same field names and `/sdk/v1/*` contract — enforced by the shared parity suite (`tests/test_parity.py` + `../../parity/fixtures.json`), not just convention. A divergence is a CI failure. Change one SDK, change both **and** the fixtures\n\nSee ../../../CLAUDE.md (root)\n\n<!-- File: packages/sdk/python/CLAUDE.md (Tokens: ~2414 | Category: anthropic-skill) -->\n# packages/sdk/python — Python SDK for the Caveman Cloud gateway\n\nStdlib-only (`urllib.request`, no third-party deps) Python package. Provides `Cave` (config +\nentrypoint), `CaveTool` (tool descriptor), and `ToolSearchResult`. All HTTP calls POST to the\ngateway with `x-cave-agent` / `x-cave-workflow` / `x-cave-retention` headers set from `Cave` fields.\n\n## Layout\n\n- `caveman_cloud/__init__.py` — re-exports `Cave`, `CaveTool`, `CompressResult`, `ContextPackItem`, `ContextPackOptions`, `ContextPackResult`, `ToolSearchResult`, …\n- `caveman_cloud/core.py` — all implementation: `Cave`, `Trace`, `Provider`, `_Create`, `ToolSearchResult`, `CompressResult`, `ContextPack*`, `CaveTool`, `headers()`\n- `tests/test_sdk.py` — pytest tests; mock `urllib.request.urlopen` with `patch()`\n- `tests/test_parity.py` — cross-language conformance suite; drives `../../parity/fixtures.json` (shared with sdk-ts). Same fixtures, two languages → a field in one SDK and not the other fails CI.\n- `tests/test_runtime_policy.py` — runtime-policy client; drives every section of `../../parity/runtime-policy.fixtures.json` (fetch wire, signature cases, the `guard_cases` fail-closed truth table, assignment vectors with exact float equality — including the weighted `exp-w` vectors that pin the `(1-h)*(w/t)` propensity association — and decision cases). Mirrors the TS `tests/runtime-policy.runtime.mjs`\n- `tests/test_trace_continuity.py` — trace/span id minting + which requests carry `x-cave-trace-id` / `x-cave-parent-span-id`; mirrors the TS `tests/trace-continuity.runtime.mjs`\n- `caveman_cloud/middleware/` — stable framework-middleware client for protocol 1.1 (`docs/technical/middleware-protocol.md`): `protocol.py` pure rules (scope normalization, tolerant capabilities, failure classification, breaker, budgets, endpoint/proxy resolution, `warn_once`), `transport.py` default keep-alive stdlib transport, `runtime.py` / `async_runtime.py` clients, `validate.py` plan/page checks. `tests/test_middleware_v1_1.py` drives `../../parity/middleware-v1_1.fixtures.json`; `tests/test_middleware_transport.py` uses real local sockets\n- `pyproject.toml` — distribution name `caveman-sdk` (import package stays `caveman_cloud`), `requires-python = \">=3.11\"`, no runtime dependencies\n\n## Key API surface (`core.py`)\n\n- `Cave.trace(workflow, tags, *, trace_id=None, span_id=None)` → context manager yielding `Trace`; call `.model[\"openai\"].responses.create(body)` inside. The trace mints `trace_id` (32 lowercase hex) + a root `span_id` (16 lowercase hex) with the exporter's RNG; `trace_id`/`span_id` continue an inbound trace and a value that isn't the exact hex shape is replaced rather than sent. Every provider call and trace-scoped `/sdk/v1/*` call made **through the trace** carries `x-cave-trace-id` + `x-cave-parent-span-id`; providers and SDK calls built directly off the `Cave` carry neither\n- `Trace.exporter(service_name=None)` → a per-service memoized `OTelExporter` whose `default_trace_id` is the trace's, so SDK spans and the gateway's request rows join one trace. Runtime-policy decision spans passed a `Trace` use this same caller-reachable default buffer; call `trace.exporter().export()` to ship them. MIRRORS the TS `CaveTrace.exporter`\n- `Cave.tools(catalog, *, strategy=\"all\", initial_tool_count=8)` → builder handle with `.strategy`, `.initial` (`list[CaveTool]`), `.search(query, *, max_tools, context, workflow, ranker, session_id)`. `strategy=\"deferred\"` includes every `always_load` tool exactly once, then fills remaining initial slots from non-mandatory tools; a cap below the mandatory count fails locally. `.search()` always hits the gateway with the FULL catalog. MIRRORS the TS `cave.tools({catalog, strategy})`\n- `Cave.tool_search(tools, query, *, context, max_tools, workflow, ranker, session_id)` → flat variant: POSTs `[tools, query]` to `/sdk/v1/tool-search`; returns `ToolSearchResult` with `.saved_tokens` / `.reduction_pct` / `.session_id`. Schema-token counters are estimates; `.token_basis` discloses the counter and `.basis` is always `\"inferred\"`. `ranker` (`\"bm25\"`|`\"embeddings\"`) is passed through verbatim — the SDK never computes similarity\n- `Cave.prompts.internal_brevity(*, style, preserve_errors_verbatim=False, preserve_code_verbatim=False)` → output-style snippet (`\"none\"` → `\"\"`); booleans render lowercase to match the TS `cave.prompts.internalBrevity`\n- `Cave.compress(payload, *, content_type=None)` → `CompressResult`; POSTs `/sdk/v1/compress`, maps the Engine report. **Byte-safe pass-through** on any transport/parse problem (original input, `ratio=0.0`, no handle); `.token_count_basis` discloses the counter and `basis` is always `\"inferred\"`. The SDK delegates — it never reimplements a compressor\n- `Cave.context.pack(query, items, options)` → `ContextPackResult`; connected-only POST to `/sdk/v1/context/pack`. Lossy selector over caller-owned items, never CCR/ledger; returns exact `deferred_ids`. Transport or malformed-report failure returns all original items with zero inferred savings\n- `Trace.expand(source_ref)` — the GET half of `checkpoint()`; `GET /sdk/v1/checkpoints/{ref}/expand` returns the stored `{source_ref, version, messages, checkpoint}`\n- `Cave.openai/anthropic/gemini/vertex(upstream_key)` → `Provider` that proxies through gateway; `Provider.raw(path, body)` is the escape hatch (mirrors the TS provider-client `raw`)\n- `Cave.bedrock(region, endpoint=\"runtime\")` → no-network first-party route descriptor; Runtime defaults to `/bedrock`, explicit Mantle returns `/bedrock/anthropic`, and `sdk_only=False` mirrors TS `sdkOnly`\n- `Trace.tool(name, options, fn)` — calls `fn()` then POSTs a `tool.call` event\n- `Trace.page_artifact(value, options)` / `Trace.artifacts.page(value, options)` — send versioned `{value, options, workflow}`; gateway stores only JSON `value`. `artifacts.get(id)` performs authenticated retrieval. `page_artifact` remains backwards-compatible alias.\n- `Trace.model[\"openai\"].responses.create(body, *, latency_class=None, tool_session_id=None)` — when `latency_class` is set, sends the `x-cave-async` header (`\"true\"` unless `\"interactive\"`); when `tool_session_id` is set, sends `x-cave-tool-session`, mirroring the TS `trace.model.openai.responses.create(body, {cave:{latencyClass, toolSessionId}})`\n- `Trace.checkpoint(messages, options)` — POSTs to `/sdk/v1/checkpoints`; the gateway persists it (Valkey) and returns a reversible `source_ref` you can later expand via `GET /sdk/v1/checkpoints/{ref}/expand`\n- `Cave.exporter(service_name=None)` → `OTelExporter`; `record_span(...)` maps current GenAI fields to `gen_ai.*`, `export()` POSTs OTLP/JSON to standard `/v1/traces` (headers via `otlp_headers()`; legacy `/otlp/v1/traces` remains server-only compatibility)\n- `Cave.retry_loop_breaker(threshold=3)` → `RetryLoopBreaker`; `.record(name, args)` raises `RetryLoopError` after `threshold` consecutive identical tool calls (interrupts a stuck loop). `.guard(name, args, fn)` records then runs `fn`\n- `Cave.runtime_policy(*, public_key, auto_refresh_seconds, kill_env, workflow)` → `RuntimePolicyClient`. `refresh()` GETs `/sdk/v1/runtime-policy` with a 30s timeout and a 1 MiB response cap (`oversized_response`; last-known-good stays active); the bundle travels as a signed STRING, Ed25519 verified before parsing, pinned or TOFU. `decide(task_family, unit_key, context, trace)` is **synchronous, local-only, never raises** and returns a `PolicyDecision` (`execute`/`fallback`/`baseline` + `reason`); `kill()` latches baseline locally; `state()` → `RuntimePolicyState`. Holdout is carved first onto the fallback path; assignment is the exported `policy_unit_fraction(*keys)` (byte-for-byte port of Go Caveman-Cloud `shared/platform/sampling.Fraction`, mirrors the TS `policyUnitFraction`; the parity `assignment_vectors` are its authority, including the empty-unit-key vector `decide()` refuses to assign on). Decision spans are observability only — no savings/dollar/`verified` field exists here. MIRRORS the TS `cave.runtimePolicy`\n- `Cave.jobs` → reserved `JobsClient` surface. Every method fails locally with `cave_async_jobs_unavailable`; it performs no network request until durable encrypted request storage, credential custody, and a draining worker exist. MIRRORS the TS `Cave.jobs`\n\n## Conventions\n\n- Tests use `patch(\"urllib.request.urlopen\", side_effect=fake_urlopen)` — never real network\n- Add new gateway endpoints via `Trace._request(path, body)` or `Provider.create(path, body)`\n- `headers()` is the single source for all outgoing headers; edit there, nowhere else\n- Deferred tool-search session handoff uses request/result `session_id` plus provider header `x-cave-tool-session`; update sdk-ts + parity fixtures with any change\n- Run tests: `pytest` from this directory (Python ≥ 3.11 required)\n\n## Gotchas\n\n- **No third-party deps** — do not add `requests`, `httpx`, or any library; keep `dependencies = []` in pyproject.toml\n- **byte-safe**: SDK sends request bodies to the gateway unmodified; no rewriting. `compress()` delegates to the Engine and passes the original through on any problem\n- **context packing is connected-only and intentionally lossy**: it sends item bytes to gateway, never runs in local wrap, and relies on caller retaining every item named by `deferred_ids`. It chooses what enters window; cache-optimal assembly chooses placement\n- `sdk-python` and `sdk-ts` mirror the same field names and `/sdk/v1/*` contract — enforced by the shared parity suite (`tests/test_parity.py` + `../../parity/fixtures.json`), not just convention. A divergence is a CI failure. Change one SDK, change both **and** the fixtures\n\nSee ../../../CLAUDE.md (root)\n\n<!-- File: packages/sdk/typescript/AGENTS.md (Tokens: ~2303 | Category: anthropic-skill) -->\n# packages/sdk/typescript — TypeScript SDK (`@caveman-ai/sdk`)\n\nSingle-file SDK (`src/index.ts`) exported as an ES module. Provides `Cave` (main client),\n`CaveTrace` (per-request tracing), and BM25-backed tool-search against the gateway. No runtime\ndependencies — only `devDependencies` for TypeScript.\n\n## Layout\n\n- `src/index.ts` — entire SDK; exports `Cave`, `CaveTrace`, `CaveOptions`, `CaveTool`, `ToolSearchResult`, `CompressOptions`, `CompressResult`, and the `ContextPack*` types\n- `tests/tool-search.test.ts` — type-level assertions compiled by `tsc --noEmit`\n- `tests/tool-search.runtime.mjs` — runtime tests using `node:test` + global fetch mock (imports from `dist/`)\n- `tests/runtime-policy.runtime.mjs` + `tests/runtime-policy.test.ts` — the runtime-policy client; drives every section of `../../parity/runtime-policy.fixtures.json` (fetch wire, signature cases, all `assignment_vectors` with exact float equality, all `guard_cases` — the shared operator truth table lives in the fixture, not in this file — and all `decision_cases`). Iterate the arrays; never hard-code their counts\n- `tests/parity.runtime.mjs` — cross-language conformance suite; drives `../../parity/fixtures.json` (shared with sdk-python). Same fixtures, two languages → a field in one SDK and not the other fails CI.\n- `src/middleware/` — the stable `@caveman-ai/sdk/middleware` subpath: `runtime.ts` (protocol client), `protocol.ts` (pure protocol 1.1 rules), `validate.ts` (capabilities/plan/page validation). `tests/middleware-v1_1.runtime.mjs` drives every client vector in `../../parity/middleware-v1_1.fixtures.json` (the Python SDK runs the same file); `tests/middleware-hardening.runtime.mjs` pins the B3–B12 hardening fixes\n- `tests/trace-continuity.runtime.mjs` — trace/span id minting + which requests carry `x-cave-trace-id` / `x-cave-parent-span-id`; mirrors the Python `tests/test_trace_continuity.py`\n- `tsconfig.json` / `tsconfig.test.json` — separate configs; test config covers `tests/`. Both extend the **repo-root** `../../../tsconfig.base.json`.\n\n## Key APIs\n\n- `new Cave(options)` — requires `apiKey`, `baseURL`, `agent`\n- `cave.trace(opts, fn)` — wraps a callback with a `CaveTrace`; sends tool-call spans to `/sdk/v1/events`. The trace mints `traceId` (32 lowercase hex) + a root `spanId` (16 lowercase hex) with the exporter's RNG; `opts.traceId`/`opts.spanId` continue an inbound trace and a value that isn't the exact hex shape is replaced rather than sent. Every provider call made **through the trace** carries `x-cave-trace-id` + `x-cave-parent-span-id`. Generic `/sdk/v1/*` calls and provider clients built off the `Cave` carry neither; the sole SDK-endpoint exception is `CaveTrace.tool`, whose `/sdk/v1/events` call carries the trace id and root parent span id\n- `CaveTrace.exporter({serviceName?})` → an `OTelExporter` whose `defaultTraceId` is the trace's, so SDK spans and the gateway's request rows join one trace. MIRRORS the Python `Trace.exporter`\n- `cave.tools({ catalog, strategy })` — returns `{ initial, strategy, search(query, opts?) }`. **`search()` is async** (returns `Promise<ToolSearchResult>`); breaking change from 1.0 which was sync. `opts.ranker` (`\"bm25\"`|`\"embeddings\"`) is passed through to the gateway verbatim; `opts.toolSessionId` sends `session_id` so provider callbacks can re-inject called deferred tools. The SDK never computes similarity\n- `cave.toolSearch(catalog, query, opts?)` — direct variant, same contract (incl. `ranker` / `toolSessionId`). Schema-token counters are estimates; `tokenBasis` discloses the counter and `basis` is always `\"inferred\"`\n- `cave.compress(payload, opts?)` → `Promise<CompressResult>`; POSTs `/sdk/v1/compress`, maps the Engine report. **Byte-safe pass-through** on any transport/parse problem (original input, `ratio:0`, no handle); `tokenCountBasis` discloses the counter and `basis` is always `\"inferred\"`. The SDK delegates — it never reimplements a compressor\n- `cave.context.pack(query, items, options)` → `Promise<ContextPackResult>`; connected-only POST to `/sdk/v1/context/pack`. Lossy selector over caller-owned items, never CCR/ledger; returns exact `deferredIds`. Transport or malformed-report failure returns all original items with zero inferred savings\n- `CaveTrace.context.expand(sourceRef)` — the GET half of `checkpoint()`; `GET /sdk/v1/checkpoints/{ref}/expand` returns the stored `{source_ref, version, messages, checkpoint}`\n- `cave.openai/anthropic/gemini/vertex()` — thin provider clients; proxied through gateway; each exposes a `.raw` fetch escape hatch (mirrors the Python `Provider.raw`). `cave.bedrock({region, endpoint?})` is a no-network first-party route descriptor: Runtime defaults to `/bedrock`; explicit Mantle returns `/bedrock/anthropic`; `sdkOnly:false` mirrors Python's `sdk_only`\n- `cave.prompts.internalBrevity({style, preserveErrorsVerbatim?, preserveCodeVerbatim?})` — output-style snippet (`style:\"none\"` → `\"\"`); MIRRORS the Python `cave.prompts.internal_brevity`\n- `CaveTrace.model.openai.responses.create(body, {cave:{latencyClass, toolSessionId}})` — passing a `latencyClass` hint sets the `x-cave-async` header (`\"true\"` unless `\"interactive\"`); passing `toolSessionId` sets `x-cave-tool-session`. Mirrored by the Python `responses.create(body, latency_class=..., tool_session_id=...)`\n- `CaveTrace.artifacts.page()` — sends versioned `{value, options, workflow}`; gateway stores only JSON `value`. `artifacts.get(id)` performs authenticated retrieval. `strategy:\"verbatim\"` bypasses storage. Mirrored by Python.\n- `CaveTrace.context.checkpoint()` — POSTs to `/sdk/v1/checkpoints`; gateway persists it (Valkey) + returns a reversible `source_ref` (expand via `GET /sdk/v1/checkpoints/{ref}/expand`)\n- `cave.exporter({serviceName?})` → `OTelExporter`; `recordSpan(...)` maps current GenAI fields to `gen_ai.*`, `export()` POSTs OTLP/JSON to standard `/v1/traces` (headers via `otlpHeaders()`; legacy `/otlp/v1/traces` remains server-only compatibility)\n- `cave.runtimePolicy({publicKey?, autoRefreshSeconds?, killEnv?})` → `RuntimePolicyClient`. `refresh()` is the only network call (`GET /sdk/v1/runtime-policy`, std headers minus content-type); it Ed25519-verifies the bundle **string's** exact bytes before parsing, TOFU-pins the key **the moment the signature verifies** (before the schema/sequence checks, so a rejected-but-signed bundle cannot open a downgrade window), rejects a regressed `sequence`, and keeps last-known-good on any failure. The fetch carries a 30s `AbortSignal.timeout` (mirrors Python's `timeout=30`); an `autoRefreshSeconds` tick that lands mid-refresh is skipped, not stacked. `decide(taskFamily, {unitKey, context, trace})` is **synchronous, local-only, and never throws**; holdout suppresses onto the fallback path and a missing unit key or invalid experiment never guesses an arm. `kill()` latches locally, `killEnv` is re-read per decide, `state()` snapshots. Routing only — no savings vocabulary anywhere. MIRRORS the Python `cave.runtime_policy()`\n- `cave.retryLoopBreaker(threshold=3)` → `RetryLoopBreaker`; `.record(name, args)` throws `RetryLoopError` after `threshold` consecutive identical tool calls; `.guard(name, args, fn)` records then runs `fn`\n- `cave.jobs` → reserved `JobsClient` surface. Every method fails locally with `cave_async_jobs_unavailable`; it performs no network request until durable encrypted request storage, credential custody, and a draining worker exist. MIRRORS the Python `Cave.jobs`\n\n## Conventions\n\n- Tests: type assertions in `.test.ts` (compiled only), runtime in `.runtime.mjs` (run against `dist/`)\n- Build before runtime tests: `pnpm build && pnpm test:node`\n- Request body keys are `snake_case` to the gateway; response mapped to `camelCase` in `ToolSearchResult`\n- `x-cave-workflow` header defaults to `defaultWorkflow ?? \"unlabeled-workflow\"`; never omit it\n- Deferred tool-search session handoff uses request `session_id`, result `sessionId`, and provider header `x-cave-tool-session`; update sdk-python + parity fixtures with any change\n\n## Gotchas\n\n- **byte-safe**: SDK sends request bodies to the gateway verbatim; no rewriting allowed. `compress()` is the one path that yields smaller bytes and it **delegates** to the Engine — on any problem it passes the original through\n- **context packing is connected-only and intentionally lossy**: it sends item bytes to gateway, never runs in local wrap, and relies on caller retaining every item named by `deferredIds`. It chooses what enters window; cache-optimal assembly chooses placement\n- **mirror sdk-python**: every field/method exists in both, enforced by the shared parity suite — a divergence is a CI failure, not a convention slip. Change one SDK, change both **and** the fixtures\n- published as `@caveman-ai/sdk`; the workspace name stays `@caveman-ai/sdk` until the npm redirect plan lands\n- `strategy:\"deferred\"` initial set = `alwaysLoad` tools + up to `initialToolCount` (default 8); never returns the full catalog without a `search()` call\n- `reductionPct` rounds to one decimal; `savedTokens` is derived (`full - sent`), not from the gateway response\n\nSee ../../../CLAUDE.md (root)\n\n<!-- File: packages/sdk/typescript/CLAUDE.md (Tokens: ~2391 | Category: anthropic-skill) -->\n# packages/sdk/typescript — TypeScript SDK (`@caveman-ai/sdk`)\n\nSingle-file SDK (`src/index.ts`) exported as an ES module. Provides `Cave` (main client),\n`CaveTrace` (per-request tracing), and BM25-backed tool-search against the gateway. No runtime\ndependencies — only `devDependencies` for TypeScript.\n\n## Layout\n\n- `src/index.ts` — entire SDK; exports `Cave`, `CaveTrace`, `CaveOptions`, `CaveTool`, `ToolSearchResult`, `CompressOptions`, `CompressResult`, and the `ContextPack*` types\n- `tests/tool-search.test.ts` — type-level assertions compiled by `tsc --noEmit`\n- `tests/tool-search.runtime.mjs` — runtime tests using `node:test` + global fetch mock (imports from `dist/`)\n- `tests/runtime-policy.runtime.mjs` + `tests/runtime-policy.test.ts` — the runtime-policy client; drives every section of `../../parity/runtime-policy.fixtures.json` (fetch wire, signature cases, all `assignment_vectors` with exact float equality, all `guard_cases` — the shared operator truth table lives in the fixture, not in this file — and all `decision_cases`). Iterate the arrays; never hard-code their counts\n- `tests/parity.runtime.mjs` — cross-language conformance suite; drives `../../parity/fixtures.json` (shared with sdk-python). Same fixtures, two languages → a field in one SDK and not the other fails CI.\n- `src/middleware/` — the stable `@caveman-ai/sdk/middleware` subpath: `runtime.ts` (protocol client), `protocol.ts` (pure protocol 1.1 rules), `validate.ts` (capabilities/plan/page validation), `transport.ts` (lazy `node:http` transport used when HTTP(S)_PROXY applies, `ca` is set or Node runs with NODE_USE_ENV_PROXY; never proxies loopback). `tests/middleware-v1_1.runtime.mjs` drives every client vector in `../../parity/middleware-v1_1.fixtures.json` (the Python SDK runs the same file); `tests/middleware-hardening.runtime.mjs` pins the B3–B12 hardening fixes\n- `tests/trace-continuity.runtime.mjs` — trace/span id minting + which requests carry `x-cave-trace-id` / `x-cave-parent-span-id`; mirrors the Python `tests/test_trace_continuity.py`\n- `tsconfig.json` / `tsconfig.test.json` — separate configs; test config covers `tests/`. Both extend the **repo-root** `../../../tsconfig.base.json`.\n\n## Key APIs\n\n- `new Cave(options)` — requires `apiKey`, `baseURL`, `agent`\n- `cave.trace(opts, fn)` — wraps a callback with a `CaveTrace`; sends tool-call spans to `/sdk/v1/events`. The trace mints `traceId` (32 lowercase hex) + a root `spanId` (16 lowercase hex) with the exporter's RNG; `opts.traceId`/`opts.spanId` continue an inbound trace and a value that isn't the exact hex shape is replaced rather than sent. Every provider call and trace-scoped `/sdk/v1/*` call made **through the trace** carries `x-cave-trace-id` + `x-cave-parent-span-id`; provider clients and SDK calls built directly off the `Cave` carry neither\n- `CaveTrace.exporter({serviceName?})` → a per-service memoized `OTelExporter` whose `defaultTraceId` is the trace's, so SDK spans and the gateway's request rows join one trace. Runtime-policy decision spans passed a `CaveTrace` use this same caller-reachable default buffer; call `trace.exporter().flush()` to ship them. MIRRORS the Python `Trace.exporter`\n- `cave.tools({ catalog, strategy })` — returns `{ initial, strategy, search(query, opts?) }`. **`search()` is async** (returns `Promise<ToolSearchResult>`); breaking change from 1.0 which was sync. `opts.ranker` (`\"bm25\"`|`\"embeddings\"`) is passed through to the gateway verbatim; `opts.toolSessionId` sends `session_id` so provider callbacks can re-inject called deferred tools. The SDK never computes similarity\n- `cave.toolSearch(catalog, query, opts?)` — direct variant, same contract (incl. `ranker` / `toolSessionId`). Schema-token counters are estimates; `tokenBasis` discloses the counter and `basis` is always `\"inferred\"`\n- `cave.compress(payload, opts?)` → `Promise<CompressResult>`; POSTs `/sdk/v1/compress`, maps the Engine report. **Byte-safe pass-through** on any transport/parse problem (original input, `ratio:0`, no handle); `tokenCountBasis` discloses the counter and `basis` is always `\"inferred\"`. The SDK delegates — it never reimplements a compressor\n- `cave.context.pack(query, items, options)` → `Promise<ContextPackResult>`; connected-only POST to `/sdk/v1/context/pack`. Lossy selector over caller-owned items, never CCR/ledger; returns exact `deferredIds`. Transport or malformed-report failure returns all original items with zero inferred savings\n- `CaveTrace.context.expand(sourceRef)` — the GET half of `checkpoint()`; `GET /sdk/v1/checkpoints/{ref}/expand` returns the stored `{source_ref, version, messages, checkpoint}`\n- `cave.openai/anthropic/gemini/vertex()` — thin provider clients; proxied through gateway; each exposes a `.raw` fetch escape hatch (mirrors the Python `Provider.raw`). `cave.bedrock({region, endpoint?})` is a no-network first-party route descriptor: Runtime defaults to `/bedrock`; explicit Mantle returns `/bedrock/anthropic`; `sdkOnly:false` mirrors Python's `sdk_only`\n- `cave.prompts.internalBrevity({style, preserveErrorsVerbatim?, preserveCodeVerbatim?})` — output-style snippet (`style:\"none\"` → `\"\"`); MIRRORS the Python `cave.prompts.internal_brevity`\n- `CaveTrace.model.openai.responses.create(body, {cave:{latencyClass, toolSessionId}})` — passing a `latencyClass` hint sets the `x-cave-async` header (`\"true\"` unless `\"interactive\"`); passing `toolSessionId` sets `x-cave-tool-session`. Mirrored by the Python `responses.create(body, latency_class=..., tool_session_id=...)`\n- `CaveTrace.artifacts.page()` — sends versioned `{value, options, workflow}`; gateway stores only JSON `value`. `artifacts.get(id)` performs authenticated retrieval. `strategy:\"verbatim\"` bypasses storage. Mirrored by Python.\n- `CaveTrace.context.checkpoint()` — POSTs to `/sdk/v1/checkpoints`; gateway persists it (Valkey) + returns a reversible `source_ref` (expand via `GET /sdk/v1/checkpoints/{ref}/expand`)\n- `cave.exporter({serviceName?})` → `OTelExporter`; `recordSpan(...)` maps current GenAI fields to `gen_ai.*`, `export()` POSTs OTLP/JSON to standard `/v1/traces` (headers via `otlpHeaders()`; legacy `/otlp/v1/traces` remains server-only compatibility)\n- `cave.runtimePolicy({publicKey?, autoRefreshSeconds?, killEnv?})` → `RuntimePolicyClient`. `refresh()` is the only network call (`GET /sdk/v1/runtime-policy`, std headers minus content-type); it Ed25519-verifies the bundle **string's** exact bytes before parsing, TOFU-pins the key **the moment the signature verifies** (before the schema/sequence checks, so a rejected-but-signed bundle cannot open a downgrade window), rejects a regressed `sequence`, and keeps last-known-good on any failure. The fetch carries a 30s `AbortSignal.timeout` (mirrors Python's `timeout=30`) and stops a response after 1 MiB (`oversized_response`); fetch implementations without a bounded readable body fail closed (`bounded_response_unavailable`) instead of calling unbounded `text()`. An `autoRefreshSeconds` tick that lands mid-refresh is skipped, not stacked. `decide(taskFamily, {unitKey, context, trace})` is **synchronous, local-only, and never throws**; holdout suppresses onto the fallback path and a missing unit key or invalid experiment never guesses an arm. `kill()` latches locally, `killEnv` is re-read per decide, `state()` snapshots. Routing only — no savings vocabulary anywhere. MIRRORS the Python `cave.runtime_policy()`\n- `cave.retryLoopBreaker(threshold=3)` → `RetryLoopBreaker`; `.record(name, args)` throws `RetryLoopError` after `threshold` consecutive identical tool calls; `.guard(name, args, fn)` records then runs `fn`\n- `cave.jobs` → reserved `JobsClient` surface. Every method fails locally with `cave_async_jobs_unavailable`; it performs no network request until durable encrypted request storage, credential custody, and a draining worker exist. MIRRORS the Python `Cave.jobs`\n\n## Conventions\n\n- Tests: type assertions in `.test.ts` (compiled only), runtime in `.runtime.mjs` (run against `dist/`)\n- Build before runtime tests: `pnpm build && pnpm test:node`\n- Request body keys are `snake_case` to the gateway; response mapped to `camelCase` in `ToolSearchResult`\n- `x-cave-workflow` header defaults to `defaultWorkflow ?? \"unlabeled-workflow\"`; never omit it\n- Deferred tool-search session handoff uses request `session_id`, result `sessionId`, and provider header `x-cave-tool-session`; update sdk-python + parity fixtures with any change\n\n## Gotchas\n\n- **byte-safe**: SDK sends request bodies to the gateway verbatim; no rewriting allowed. `compress()` is the one path that yields smaller bytes and it **delegates** to the Engine — on any problem it passes the original through\n- **context packing is connected-only and intentionally lossy**: it sends item bytes to gateway, never runs in local wrap, and relies on caller retaining every item named by `deferredIds`. It chooses what enters window; cache-optimal assembly chooses placement\n- **mirror sdk-python**: every field/method exists in both, enforced by the shared parity suite — a divergence is a CI failure, not a convention slip. Change one SDK, change both **and** the fixtures\n- published as `@caveman-ai/sdk`; the workspace name stays `@caveman-ai/sdk` until the npm redirect plan lands\n- `strategy:\"deferred\"` initial set = `alwaysLoad` tools + up to `initialToolCount` (default 8); never returns the full catalog without a `search()` call\n- `reductionPct` rounds to one decimal; `savedTokens` is derived (`full - sent`), not from the gateway response\n\nSee ../../../CLAUDE.md (root)\n\n<!-- File: packages/shared/contracts/AGENTS.md (Tokens: ~895 | Category: anthropic-skill) -->\n# packages/shared/contracts — shared wire contracts (JSON Schema)\n\nSource-of-truth schemas for cross-service/SDK wire shapes:\n`schemas/policy.schema.json` covers tenant policy and\n`schemas/practice.schema.json` covers practice-library records.\n\n## Layout\n\n- `schemas/policy.schema.json` — JSON Schema 2020-12 for the tenant policy object (required: version, runtime_mode, fail_policy, providers, limits, retention, optimizers, telemetry, sdk)\n- `schemas/practice.schema.json` — strict JSON Schema 2020-12 for one practice record; unknown fields and enums fail closed\n- `schemas/adapter-conformance.schema.json` — static Pi/Claude contract-record\n  shape; `evidence: static_contract_only` explicitly denies executable proof\n- `schemas/middleware-*.schema.json` + `openapi/middleware.openapi.json` — the\n  framework middleware protocol. Normative prose (negotiation, status/error and\n  reason catalogs, N-1 policy) is `docs/technical/middleware-protocol.md`;\n  executable vectors are `packages/sdk/parity/middleware-v1_1.fixtures.json`.\n  Additive changes keep `schema_version` 1; unknown `limits` keys must stay\n  positive safe integers because SDK 1.1.0 rejects anything else\n- `scripts/validate-schemas.mjs` — compiles every schema with AJV, validates\n  adapter fixtures, middleware fixture examples, `$id` = raw GitHub URL of the\n  file at tag `contracts-v<package.json version>` (bump both together),\n  relative cross-schema `$ref`s only, and every OpenAPI `$ref`\n- `package.json` — `@caveman-ai/contracts`; build/lint/test run both validators (no compiled output)\n\n## Key schema fields\n\n- `capabilities` — `{ compress?, cache_hints?, routing? }` booleans, `additionalProperties: false`; the authoritative description of what a project optimizes. An unordered SET, not a stage — the three compose\n- `pass_through` — boolean; forbids every transform. Outranks `capabilities`, which are retained so operator toggles keep their remembered positions\n- `runtime_mode` — `record | recommend | shadow | canary | active | compress`; **a DERIVED display string, not an input**. Still `required` for the compat window (removal 2026-10-01) because `projects.runtime_mode` and the telemetry write validator read it. A doc carrying `capabilities` is authoritative and its `runtime_mode` derives from them; a legacy doc carrying only `runtime_mode` derives the capability set\n- `fail_policy` — `fail_open | fail_closed`; unknown values fail closed (honesty rule)\n- `limits.monthly_usd` — `{ soft, hard }` spend caps per tenant\n- `telemetry.sample_rate` — float 0–1, validated by schema minimum/maximum\n- `additionalProperties: true` — intentional; services and optimizers may extend the object\n\n## Conventions\n\n- Schema version is an integer; `policy_schema_version` returned by `cloud/control-api/internal/httpapi/server.go:372`\n- Adding a new required field is a **breaking change** — bump `version` and coordinate across control-api, SDKs, web\n- No TypeScript types generated from schema yet; the web dashboard manually mirrors the shape\n\n## Gotchas\n\n- `fail_policy: fail_closed` aligns with repo honesty rule: unknown enum cases must fail closed, not pass through\n- `runtime_mode: record` is always pass-through (byte-safe rule); never treat it as an optimizer mode\n- The six `runtime_mode` values are **not ordered**. Code comparing them for rank (`mode >= \"canary\"`) is a bug; the four middle values are behaviourally identical, and `shadow`/`canary` are display labels rather than traffic splits. The derivation lives in exactly one place, `cloud/internal/capability`, because the gateway, control-api, and the worker must never disagree about what a document means\n- Build script validates schemas and static fixtures; it emits no artifact and\n  does not prove executable Pi/Claude parity\n\nSee ../../../CLAUDE.md (root)\n\n<!-- File: packages/shared/contracts/CLAUDE.md (Tokens: ~1316 | Category: anthropic-skill) -->\n# packages/shared/contracts — shared wire contracts (JSON Schema)\n\nSource-of-truth schemas for cross-service/SDK wire shapes:\n`schemas/policy.schema.json` covers tenant policy and\n`schemas/practice.schema.json` covers practice-library records.\n\n## Layout\n\n- `schemas/policy.schema.json` — JSON Schema 2020-12 for the tenant policy object (required: version, runtime_mode, fail_policy, providers, limits, retention, optimizers, telemetry, sdk)\n- `schemas/practice.schema.json` — strict JSON Schema 2020-12 for one practice record; unknown fields and enums fail closed\n- `schemas/adapter-conformance.schema.json` — static Pi/Claude/Vercel AI SDK/Eve/\n  Mastra contract-record shape; `evidence: static_contract_only` explicitly\n  denies executable proof\n- `schemas/canonical-span.schema.json` — `caveman.span.v1`, the normalized\n  evidence-plane span shared by trace intelligence and replay\n- `schemas/agent-run-receipt.schema.json` — `caveman.agent.run-receipt.v1`,\n  content-blind per-run SDK economic receipt. It is a shared wire shape, not an\n  anonymous CLI event and not a signed verified-savings receipt. Retry events\n  carry reserved spend, measured spend, and measurement basis\n- `schemas/continuous-improvement-report.schema.json` — versioned end-to-end\n  improvement report; the current generated result is E3 `inferred` /\n  `symbolic_counterfactual`. E4 `replayed_counterfactual` remains a reserved\n  actual recorded-output/sandbox shape and never means verified savings\n- `schemas/middleware-*.schema.json` + `openapi/middleware.openapi.json` — the\n  framework middleware protocol. Normative prose (negotiation, status/error and\n  reason catalogs, N-1 policy) is `docs/technical/middleware-protocol.md`;\n  executable vectors are `packages/sdk/parity/middleware-v1_1.fixtures.json`.\n  Additive changes keep `schema_version` 1; unknown `limits` keys must stay\n  positive safe integers because SDK 1.1.0 rejects anything else\n- `scripts/validate-schemas.mjs` — compiles every schema with AJV, validates\n  adapter fixtures, middleware fixture examples, `$id` = raw GitHub URL of the\n  file at tag `contracts-v<package.json version>` (bump both together),\n  relative cross-schema `$ref`s only, and every OpenAPI `$ref`\n- `package.json` — `@caveman-ai/contracts`; build/lint/test run both validators (no compiled output)\n\n## Key schema fields\n\n- `capabilities` — `{ compress?, cache_hints?, routing? }` booleans, `additionalProperties: false`; the authoritative description of what a project optimizes. An unordered SET, not a stage — the three compose\n- `pass_through` — boolean; forbids every transform. Outranks `capabilities`, which are retained so operator toggles keep their remembered positions\n- `runtime_mode` — `record | recommend | shadow | canary | active | compress`; **a DERIVED display string, not an input**. Still `required` for the compat window (removal 2026-10-01) because `projects.runtime_mode` and the telemetry write validator read it. A doc carrying `capabilities` is authoritative and its `runtime_mode` derives from them; a legacy doc carrying only `runtime_mode` derives the capability set\n- `fail_policy` — `fail_open | fail_closed`; unknown values fail closed (honesty rule)\n- `limits.monthly_usd` — `{ soft, hard }` spend caps per tenant\n- `telemetry.sample_rate` — float 0–1, validated by schema minimum/maximum\n- `runtime_policies` — OPTIONAL array (additive, nothing new required): the per-task-family runtime policies delivered to SDK callers through the policy overlay and served by the gateway's `GET /sdk/v1/runtime-policy`. Guard ops are a closed set (`eq|ne|gt|gte|lt|lte|in`, AND-ed); `budget` is an advisory cap the caller enforces, never a savings figure; the renderer drops a structurally invalid policy WHOLE rather than stripping the broken guard or experiment, because either strip would widen who the policy applies to\n- `additionalProperties: true` — intentional; services and optimizers may extend the object\n\n## Conventions\n\n- Schema version is an integer; `policy_schema_version` returned by `cloud/control-api/internal/httpapi/server.go:372`\n- Adding a new required field is a **breaking change** — bump `version` and coordinate across control-api, SDKs, web\n- No TypeScript types generated from schema yet; the web dashboard manually mirrors the shape\n- `scripts/check-contract-compat.mjs` **at the REPO ROOT** (`make check-contract-compat`), not this package's `scripts/`, is a bounded 2020-12 compatibility gate: it proves common narrowing constraints (types, enums/consts, bounds, object/array applicators, and supported combinators/refs) and treats changed unknown assertion/applicator keywords or unresolved reference semantics as major-version review items. Metadata annotations remain non-breaking. It is intentionally not a complete JSON Schema subsumption prover; add a focused fixture before relying on a new keyword.\n\n## Gotchas\n\n- `fail_policy: fail_closed` aligns with repo honesty rule: unknown enum cases must fail closed, not pass through\n- `runtime_mode: record` is always pass-through (byte-safe rule); never treat it as an optimizer mode\n- The six `runtime_mode` values are **not ordered**. Code comparing them for rank (`mode >= \"canary\"`) is a bug; the four middle values are behaviourally identical, and `shadow`/`canary` are display labels rather than traffic splits. The derivation lives in exactly one place, `cloud/internal/capability`, because the gateway, control-api, and the worker must never disagree about what a document means\n- Build script validates schemas and static fixtures; it emits no artifact and\n  does not prove executable cross-harness parity\n\nSee ../../../CLAUDE.md (root)\n\n<!-- File: packages/shared/contracts/openapi/middleware.openapi.json (Tokens: ~3850 | Category: anthropic-skill) -->\n{\n  \"openapi\": \"3.1.0\",\n  \"jsonSchemaDialect\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"info\": {\n    \"title\": \"Caveman middleware runtime protocol\",\n    \"version\": \"1.1.0\",\n    \"summary\": \"Compression-only runtime used by Caveman framework middleware. Normative prose: docs/technical/middleware-protocol.md.\",\n    \"description\": \"Status codes listed are the protocol 1.1 set, used only when the request carries the http_status_v2 feature in Caveman-Middleware-Features. Without it the runtime answers with the protocol 1.0 (legacy) mapping documented in the prose spec. Clients derive outcomes from error.code, never from the status alone. The referenced JSON Schemas are closed: they describe exactly what a protocol 1.1 server emits and are what the conformance kit checks servers against. Clients MUST NOT validate responses against them; clients ignore unknown response fields so later minor versions can add fields (spec §4, §16). Unknown routes and unlisted methods answer 404 not_found before any quota, queue or body handling.\",\n    \"license\": {\n      \"name\": \"Apache-2.0\",\n      \"identifier\": \"Apache-2.0\"\n    }\n  },\n  \"servers\": [\n    {\n      \"url\": \"http://127.0.0.1:8787\",\n      \"description\": \"Local runtime. A deployment may add a path prefix; clients preserve it.\"\n    }\n  ],\n  \"security\": [\n    {\n      \"bearer\": []\n    },\n    {\n      \"mtls\": []\n    }\n  ],\n  \"paths\": {\n    \"/caveman/v1/middleware/capabilities\": {\n      \"get\": {\n        \"operationId\": \"getCapabilities\",\n        \"summary\": \"Discover transforms, limits, features and protocol range.\",\n        \"description\": \"Without Caveman-Middleware-Features the runtime omits transforms a protocol 1.0 client would reject. Served from memory before any quota or queue, so it never answers 429; 403 is forbidden_origin only, and 503 means the runtime has no usable middleware store.\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/Features\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Client\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Traceparent\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Tracestate\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Capabilities for the authenticated principal.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"../schemas/middleware-capabilities.schema.json\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"$ref\": \"#/components/responses/E401\"\n          },\n          \"403\": {\n            \"$ref\": \"#/components/responses/E403\"\n          },\n          \"503\": {\n            \"$ref\": \"#/components/responses/E503\"\n          }\n        }\n      }\n    },\n    \"/caveman/v1/middleware/optimize\": {\n      \"post\": {\n        \"operationId\": \"optimize\",\n        \"summary\": \"Plan replacements for candidate segments.\",\n        \"description\": \"Every decision, including bypasses (not_smaller, cache_state_unavailable, recovery_unavailable, capacity), is a 200 plan under http_status_v2.\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/Features\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Client\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Traceparent\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Tracestate\"\n          }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"../schemas/middleware-optimize.schema.json\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Replacement plan or bypass decision.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"../schemas/middleware-plan.schema.json\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"$ref\": \"#/components/responses/E400\"\n          },\n          \"401\": {\n            \"$ref\": \"#/components/responses/E401\"\n          },\n          \"403\": {\n            \"$ref\": \"#/components/responses/E403\"\n          },\n          \"408\": {\n            \"$ref\": \"#/components/responses/E408\"\n          },\n          \"409\": {\n            \"$ref\": \"#/components/responses/E409\"\n          },\n          \"410\": {\n            \"$ref\": \"#/components/responses/E410\"\n          },\n          \"413\": {\n            \"$ref\": \"#/components/responses/E413\"\n          },\n          \"429\": {\n            \"$ref\": \"#/components/responses/E429\"\n          },\n          \"503\": {\n            \"$ref\": \"#/components/responses/E503\"\n          },\n          \"504\": {\n            \"$ref\": \"#/components/responses/E504\"\n          }\n        }\n      }\n    },\n    \"/caveman/v1/middleware/retrieve\": {\n      \"post\": {\n        \"operationId\": \"retrieve\",\n        \"summary\": \"Read a page or excerpt of an original by recovery handle.\",\n        \"description\": \"Has its own deadline (limits.retrieve_deadline_ms) and queue (limits.retrieve_queue_depth).\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/Features\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Client\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Traceparent\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Tracestate\"\n          }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"../schemas/middleware-retrieve.schema.json\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"One page of the original.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"../schemas/middleware-page.schema.json\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"$ref\": \"#/components/responses/E400\"\n          },\n          \"401\": {\n            \"$ref\": \"#/components/responses/E401\"\n          },\n          \"403\": {\n            \"$ref\": \"#/components/responses/E403\"\n          },\n          \"404\": {\n            \"$ref\": \"#/components/responses/E404\"\n          },\n          \"408\": {\n            \"$ref\": \"#/components/responses/E408\"\n          },\n          \"410\": {\n            \"$ref\": \"#/components/responses/E410\"\n          },\n          \"413\": {\n            \"$ref\": \"#/components/responses/E413\"\n          },\n          \"429\": {\n            \"$ref\": \"#/components/responses/E429\"\n          },\n          \"503\": {\n            \"$ref\": \"#/components/responses/E503\"\n          },\n          \"504\": {\n            \"$ref\": \"#/components/responses/E504\"\n          }\n        }\n      }\n    },\n    \"/caveman/v1/middleware/receipts\": {\n      \"post\": {\n        \"operationId\": \"recordReceipt\",\n        \"summary\": \"Record a content-free call observation.\",\n        \"description\": \"Aggregated or retention-bounded; never creates rows that outlive retention_seconds. Deduplicated on (logical_call_id, attempt_id, event_kind): the first write wins, and a retry, even with a different body, gets the same 200. A scope revoked by sessions/delete answers 410 deleted.\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/Features\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Client\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Traceparent\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Tracestate\"\n          }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"../schemas/middleware-receipt.schema.json\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Observation accepted.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"../schemas/middleware-receipt-response.schema.json\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"$ref\": \"#/components/responses/E400\"\n          },\n          \"401\": {\n            \"$ref\": \"#/components/responses/E401\"\n          },\n          \"403\": {\n            \"$ref\": \"#/components/responses/E403\"\n          },\n          \"408\": {\n            \"$ref\": \"#/components/responses/E408\"\n          },\n          \"409\": {\n            \"$ref\": \"#/components/responses/E409\"\n          },\n          \"410\": {\n            \"$ref\": \"#/components/responses/E410\"\n          },\n          \"413\": {\n            \"$ref\": \"#/components/responses/E413\"\n          },\n          \"429\": {\n            \"$ref\": \"#/components/responses/E429\"\n          },\n          \"503\": {\n            \"$ref\": \"#/components/responses/E503\"\n          },\n          \"504\": {\n            \"$ref\": \"#/components/responses/E504\"\n          }\n        }\n      }\n    },\n    \"/caveman/v1/middleware/sessions/delete\": {\n      \"post\": {\n        \"operationId\": \"deleteSession\",\n        \"summary\": \"Revoke a scope and delete every original it owns.\",\n        \"description\": \"Idempotent. An unknown scope answers 200 with zero counts. Runs on the retrieve queue and limits.retrieve_deadline_ms. The revocation commits first, so later use of the scope is 410 deleted at once; the scope's content is then deleted in bounded batches. A delete that fails or runs out of time (including a Postgres write conflict) answers 503 runtime_unavailable or 504 deadline, never 200 and never 409; retrying resumes where it stopped, and the retry's counts cover only what it removed.\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/Features\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Client\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Traceparent\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/Tracestate\"\n          }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"../schemas/middleware-session-delete.schema.json\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Scope revoked.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"../schemas/middleware-session-delete-response.schema.json\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"$ref\": \"#/components/responses/E400\"\n          },\n          \"401\": {\n            \"$ref\": \"#/components/responses/E401\"\n          },\n          \"403\": {\n            \"$ref\": \"#/components/responses/E403\"\n          },\n          \"408\": {\n            \"$ref\": \"#/components/responses/E408\"\n          },\n          \"413\": {\n            \"$ref\": \"#/components/responses/E413\"\n          },\n          \"429\": {\n            \"$ref\": \"#/components/responses/E429\"\n          },\n          \"503\": {\n            \"$ref\": \"#/components/responses/E503\"\n          },\n          \"504\": {\n            \"$ref\": \"#/components/responses/E504\"\n          }\n        }\n      }\n    }\n  },\n  \"components\": {\n    \"securitySchemes\": {\n      \"bearer\": {\n        \"type\": \"http\",\n        \"scheme\": \"bearer\",\n        \"description\": \"Runtime credential (static token or OIDC JWT). Never a model provider API key.\"\n      },\n      \"mtls\": {\n        \"type\": \"mutualTLS\",\n        \"description\": \"Client certificate; the principal comes from the certificate SAN.\"\n      }\n    },\n    \"parameters\": {\n      \"Features\": {\n        \"name\": \"Caveman-Middleware-Features\",\n        \"in\": \"header\",\n        \"required\": false,\n        \"description\": \"Comma-separated client features (for example `http_status_v2, revision_tolerant`). Absent means a protocol 1.0 client.\",\n        \"schema\": {\n          \"type\": \"string\",\n          \"maxLength\": 1024\n        }\n      },\n      \"Client\": {\n        \"name\": \"Caveman-Middleware-Client\",\n        \"in\": \"header\",\n        \"required\": false,\n        \"description\": \"Space-separated product tokens, for example `caveman-sdk-typescript/1.2.0`. Informational only.\",\n        \"schema\": {\n          \"type\": \"string\",\n          \"maxLength\": 256\n        }\n      },\n      \"Traceparent\": {\n        \"name\": \"traceparent\",\n        \"in\": \"header\",\n        \"required\": false,\n        \"description\": \"W3C Trace Context.\",\n        \"schema\": {\n          \"type\": \"string\"\n        }\n      },\n      \"Tracestate\": {\n        \"name\": \"tracestate\",\n        \"in\": \"header\",\n        \"required\": false,\n        \"description\": \"W3C Trace Context.\",\n        \"schema\": {\n          \"type\": \"string\"\n        }\n      }\n    },\n    \"headers\": {\n      \"RetryAfter\": {\n        \"description\": \"Delay in whole seconds (>= 1).\",\n        \"schema\": {\n          \"type\": \"integer\",\n          \"minimum\": 1\n        }\n      }\n    },\n    \"responses\": {\n      \"E400\": {\n        \"description\": \"invalid_request, unsupported_version, unknown_capability or invalid_range (legacy clients get 413 payload_limit for an out-of-range retrieve limit).\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      },\n      \"E401\": {\n        \"description\": \"unauthorized: missing or unknown runtime credential.\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      },\n      \"E403\": {\n        \"description\": \"forbidden_origin (browser request) or forbidden_namespace (principal may not use scope.namespace).\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      },\n      \"E404\": {\n        \"description\": \"not_found: unknown or foreign recovery handle.\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      },\n      \"E408\": {\n        \"description\": \"request_timeout: the request body did not arrive before the deadline. Legacy clients: 413 payload_limit.\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      },\n      \"E409\": {\n        \"description\": \"epoch_changed (optimize only, protocol 1.0 clients only: the context manifest is not an extension of the stored one, or sequence went backwards) or identity_conflict (retryable: an optimize replayed under the same idempotency_key with a different body, or a concurrent-writer conflict such as a Postgres deadlock or serialization failure on optimize or receipts).\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      },\n      \"E410\": {\n        \"description\": \"deleted (scope revoked by sessions/delete; receipts included) or expired (scope past retention or max retention).\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      },\n      \"E413\": {\n        \"description\": \"payload_limit: the request body exceeds a size limit.\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      },\n      \"E429\": {\n        \"description\": \"capacity (queue saturated) or quota_exceeded (per-principal quota_requests_per_minute). Legacy clients: a queue wait past the deadline is 504 deadline, and quota_exceeded is 503 capacity.\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        },\n        \"headers\": {\n          \"Retry-After\": {\n            \"$ref\": \"#/components/headers/RetryAfter\"\n          }\n        }\n      },\n      \"E503\": {\n        \"description\": \"runtime_unavailable (storage or dependency outage, a failed sessions/delete, or no middleware store mounted). retrieve also uses recovery_unavailable. Legacy clients also get 503 for optimize decisions (not_smaller, cache_state_unavailable, recovery_unavailable, capacity) and for quota_exceeded (as capacity).\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        },\n        \"headers\": {\n          \"Retry-After\": {\n            \"$ref\": \"#/components/headers/RetryAfter\"\n          }\n        }\n      },\n      \"E504\": {\n        \"description\": \"deadline: the runtime could not finish before its deadline.\",\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"../schemas/middleware-error.schema.json\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n\n<!-- File: plugins/caveman/skills/cavecrew/SKILL.md (Tokens: ~960 | Category: anthropic-skill) -->\n---\nname: cavecrew\ndescription: >\n  When to delegate to `cavecrew-investigator` (locate code), `cavecrew-builder`\n  (1-2 file edit) or `cavecrew-reviewer` (diff review) instead of working inline\n  or using `Explore`. Their output is compressed, so main context lasts longer.\n---\n\nCavecrew = three subagent presets that answer in ultracave voice (tool runs: one status line in, one out; nothing between routine calls). Same job as Anthropic defaults (`Explore`, edit-style agents, reviewer); difference is the tool-result they return is compressed, so main context shrinks per delegation.\n\n## When to use cavecrew vs alternatives\n\n| Task | Use |\n|---|---|\n| \"Where is X defined / what calls Y / list uses of Z\" | `cavecrew-investigator` |\n| Same but you also want suggestions/architecture commentary | `Explore` (vanilla) |\n| Surgical edit, ≤2 files, scope obvious | `cavecrew-builder` |\n| New feature / 3+ files / cross-cutting refactor | Main thread, or `feature-dev:code-architect` if installed |\n| Review diff, branch, or file for bugs | `cavecrew-reviewer` |\n| Deep code review with rationale + alternatives | `Code Reviewer` (vanilla) if installed, else main thread |\n| One-line answer you already know | Main thread, no subagent |\n\nRule of thumb: **if you'd want the subagent's output in 1/3 the tokens, pick cavecrew. If you'd want prose, pick vanilla.**\n\n## Why this exists (the real win)\n\nSubagent tool results get injected into main context verbatim. A vanilla `Explore` that returns 2k tokens of prose costs 2k tokens of main-context budget every time. The same finding from `cavecrew-investigator` comes back as a compressed `path:line` table instead. Across 20 delegations in one session that's the difference between context exhaustion and finishing the task.\n\n## Output contracts\n\nWhat main thread can rely on per agent:\n\n**`cavecrew-investigator`**\n```\n<Header>:\n- path:line — `symbol` — short note\ntotals: <counts>.\n```\nOr `No match.` Always file-path-first, line-number-attached, backticked symbols. Safe to grep with `path:\\d+`.\n\n**`cavecrew-builder`**\n```\n<path:line-range> — <change ≤10 words>.\nverified: <re-read OK | mismatch @ path:line>.\n```\nOr one of: `too-big.` / `needs-confirm.` / `ambiguous.` / `regressed.` (terminal first token).\n\n**`cavecrew-reviewer`**\n```\npath:line: <emoji> <severity>: <problem>. <fix>.\ntotals: N🔴 N🟡 N🔵 N❓\n```\nOr `No issues.` Findings sorted file → line ascending.\n\n## Chaining patterns\n\n**Locate → fix → verify** (most common):\n1. `cavecrew-investigator` returns site list.\n2. Main thread picks 1-2 sites, hands paths to `cavecrew-builder`.\n3. `cavecrew-reviewer` audits the diff.\n\n**Parallel scout** (when investigation is broad):\nSpawn 2-3 `cavecrew-investigator` calls in one message (different angles: defs vs callers vs tests). Aggregate in main thread.\n\n**Single-shot edit** (when site is already known):\nSkip investigator. Hand exact path:line to `cavecrew-builder` directly.\n\n## What NOT to do\n\n- Don't use `cavecrew-builder` when you don't already know the file. Spawn investigator first or main thread will eat tokens passing context.\n- Don't chain `cavecrew-investigator → cavecrew-builder` for a 5-file refactor. Builder will return `too-big.` and you'll have wasted a turn.\n- Don't ask `cavecrew-reviewer` for \"general feedback\" — it returns findings only, no architecture opinions. Use `Code Reviewer` for that when installed.\n- Don't expect prose. Cavecrew output is structured, sometimes terse to the point of cryptic. If a human will read it directly, paraphrase.\n\n## Auto-clarity (inherited)\n\nSubagents drop caveman → normal English for security warnings, irreversible-action confirmations, and any output where fragment ambiguity could be misread. Resume caveman after.\n\n<!-- File: plugins/caveman/skills/caveman-compress/SKILL.md (Tokens: ~1330 | Category: anthropic-skill) -->\n---\nname: caveman-compress\ndescription: >\n  Compress a memory file such as CLAUDE.md or a todo list into caveman format\n  to save input tokens, keeping a readable backup. Sends the file's text to the\n  configured model provider. Trigger: /caveman-compress.\n---\n\n# Caveman Compress\n\n## Purpose\n\n> **Data leaves the machine.** Compression sends the file's full text to the model provider that `scripts/compress.py` is configured for: the `claude` CLI, the Anthropic API, or an OpenAI-compatible endpoint. Files whose names look like secrets (`.env`, `credentials`, `secrets`, `.netrc`, …) are refused before they are read. Don't compress a file the provider shouldn't see.\n\nCompress natural language files (CLAUDE.md, todos, preferences) into caveman-speak to reduce input tokens. Compressed version overwrites original. Human-readable backup saved as `<filename>.original.md`, but NOT beside the source file — it lives in an out-of-tree data dir (`$XDG_DATA_HOME/caveman-compress/backups/<parent-dir-name>/`, or `%LOCALAPPDATA%\\caveman-compress\\backups\\<parent-dir-name>\\` on Windows) so skill auto-loaders don't re-ingest it as a live file.\n\n## Trigger\n\n`/caveman-compress <filepath>` or when user asks to compress a memory file.\n\n**Requires an explicit filepath.** If the user pastes raw text instead of naming a file, do NOT improvise an inline compression — ask them for the filepath to compress, or offer to save the pasted text to a file first. Writing your own reasoning/commentary about the request into the compressed output (e.g. \"User passed text directly, not a filepath...\") is the exact failure this guards against — never let that leak into a saved file.\n\n## Process\n\n1. The compression scripts live in `scripts/` (adjacent to this SKILL.md). If the path is not immediately available, search for `scripts/__main__.py` next to this SKILL.md.\n\n2. From the directory containing this SKILL.md, run:\n\npython3 -m scripts <absolute_filepath>\n\n3. The CLI will:\n- detect file type (no tokens)\n- call Claude to compress\n- validate output (no tokens)\n- if errors: cherry-pick fix with Claude (targeted fixes only, no recompression)\n- retry up to 2 times\n- if still failing after 2 retries: report error to user, leave original file untouched\n\n4. Return result to user\n\n## Compression Rules\n\n### Remove\n- Articles: a, an, the\n- Filler: just, really, basically, actually, simply, essentially, generally\n- Pleasantries: \"sure\", \"certainly\", \"of course\", \"happy to\", \"I'd recommend\"\n- Hedging: \"it might be worth\", \"you could consider\", \"it would be good to\"\n- Redundant phrasing: \"in order to\" → \"to\", \"make sure to\" → \"ensure\", \"the reason is because\" → \"because\"\n- Connective fluff: \"however\", \"furthermore\", \"additionally\", \"in addition\"\n\n### Preserve EXACTLY (never modify)\n- Code blocks (fenced ``` and indented)\n- Inline code (`backtick content`)\n- URLs and links (full URLs, markdown links)\n- File paths (`/src/components/...`, `./config.yaml`)\n- Commands (`npm install`, `git commit`, `docker build`)\n- Technical terms (library names, API names, protocols, algorithms)\n- Proper nouns (project names, people, companies)\n- Dates, version numbers, numeric values\n- Environment variables (`$HOME`, `NODE_ENV`)\n\n### Preserve Structure\n- All markdown headings (keep exact heading text, compress body below)\n- Bullet point hierarchy (keep nesting level)\n- Numbered lists (keep numbering)\n- Tables (compress cell text, keep structure)\n- Frontmatter/YAML headers in markdown files\n\n### Compress\n- Use short synonyms: \"big\" not \"extensive\", \"fix\" not \"implement a solution for\", \"use\" not \"utilize\"\n- Fragments OK: \"Run tests before commit\" not \"You should always run tests before committing\"\n- Drop \"you should\", \"make sure to\", \"remember to\" — just state the action\n- Merge redundant bullets that say the same thing differently\n- Keep one example where multiple examples show the same pattern\n\nCRITICAL RULE:\nAnything inside ``` ... ``` must be copied EXACTLY.\nDo not:\n- remove comments\n- remove spacing\n- reorder lines\n- shorten commands\n- simplify anything\n\nInline code (`...`) must be preserved EXACTLY.\nDo not modify anything inside backticks.\n\nIf file contains code blocks:\n- Treat code blocks as read-only regions\n- Only compress text outside them\n- Do not merge sections around code\n\n## Pattern\n\nOriginal:\n> You should always make sure to run the test suite before pushing any changes to the main branch. This is important because it helps catch bugs early and prevents broken builds from being deployed to production.\n\nCompressed:\n> Run tests before push to main. Catch bugs early, prevent broken prod deploys.\n\nOriginal:\n> The application uses a microservices architecture with the following components. The API gateway handles all incoming requests and routes them to the appropriate service. The authentication service is responsible for managing user sessions and JWT tokens.\n\nCompressed:\n> Microservices architecture. API gateway route all requests to services. Auth service manage user sessions + JWT tokens.\n\n## Boundaries\n\n- ONLY compress natural language files (.md, .txt, .typ, .typst, .tex, extensionless)\n- NEVER modify: .py, .js, .ts, .json, .yaml, .yml, .toml, .env, .lock, .css, .html, .xml, .sql, .sh\n- If file has mixed content (prose + code), compress ONLY the prose sections\n- If unsure whether something is code or prose, leave it unchanged\n- Original file is backed up as FILE.original.md before overwriting — in the out-of-tree backup data dir (see Purpose), not beside the source file\n- Never compress FILE.original.md (skip it)\n\n<!-- ... +69 more modular skills available via /api/skills/JuliusBrussee/caveman?path={path} -->\n</yakaai_skills>"}