headroom/wiki/learn.md
JD Davis 3077ac81e8
feat: add deterministic runtime rollout controls (#1490)
## Description

Establish one centrally resolved, observable, deterministic, versioned
runtime rollout-control mechanism for Headroom. Runtime rollout controls
which behaviors an already-built artifact may expose; it does not select
or qualify a Headroom release/version.

## Type of Change

- [x] New feature (non-breaking change that adds functionality)
- [x] Bug fix (non-breaking change that fixes rollout enforcement
regressions)
- [x] Documentation update
- [x] Code refactoring (no functional changes)

## Changes Made

- Added `RolloutChannel`, `HEADROOM_ROLLOUT_CHANNEL`,
`--rollout-channel`, and a versioned immutable `RolloutSnapshot` shared
by Python configuration boundaries.
- Added schema/policy versions, canonical registry and snapshot SHA-256
identities, per-feature decision reasons, disable precedence, unsafe
qualification poisoning, strict CLI validation, and fail-closed
environment handling.
- Added `headroom rollout status --json`, Python `/stats.rollout`, and
Rust `/rollout/status` runtime provenance.
- Added equivalent Rust snapshot semantics and shared Python/Rust policy
vectors while retaining language-specific feature registries.
- Enforced rollout policy at alternate Python server composition roots
so `HEADROOM_READ_MATURATION=1` cannot bypass its beta gate.
- Preserved typed rollout snapshots across multi-worker serialization
with schema, policy, registry, snapshot-digest, type, and feature-name
validation.
- Made loopback runtime output-shaper updates replace the immutable
snapshot atomically for request readers, retain explicit request/disable
provenance, preserve channel and kill-switch precedence, invalidate
cached stats, and return the effective rollout decision.
- Made `headroom learn --verbosity --apply` report a channel-blocked
update instead of claiming the shaper is live.
- Made explicit CLI feature flags fail loudly when their current channel
blocks them.
- Made persistent interceptor installation select canary automatically,
or reject an explicitly insufficient channel unless the break-glass
override is set.
- Updated architecture, proxy, rollout, learn, and output-shaper
documentation with required channels and hot-reload semantics.

## Testing

- [x] Unit tests pass
- [x] Linting passes (`ruff check .` and `ruff format --check .`)
- [x] Type checking passes (`mypy headroom --ignore-missing-imports`)
- [x] New regression tests added for every corrected behavior
- [x] Rust tests and production-target Clippy pass
- [x] Documentation build passes

### Test Output

```text
Focused rollout coverage suite
57 passed; headroom.rollout + rollout CLI: 98% coverage

Affected proxy/rollout/transform/governance suites
222 passed; 0 failed

Final changed regression suites
100 passed; 0 failed

Cross-module hot-reload isolation regression
6 passed; 0 failed

cargo test -p headroom-core -p headroom-proxy --quiet
headroom-core: 924 passed; 1 ignored
headroom-proxy and integration suites: all passed

cargo clippy -p headroom-core -p headroom-proxy --lib --bins -- -D warnings
cargo fmt --all -- --check
ruff check .
ruff format --check .
mypy headroom --ignore-missing-imports
git diff --check
All passed

cd docs && npm run build
Compiled successfully; 164 static pages generated
```

The unsharded Windows-only CI selection exposed unrelated baseline
failures, principally the existing `sqlite:///C:\\...` URL parser
producing an invalid `\\C:\\...` path. At commit `8e793a80`, all 52
completed GitHub checks passed; the only other conclusions are expected
skips and superseded governance jobs.

## Real Behavior Proof

- **Environment:** Windows checkout on Python 3.13.3 and the current
Rust workspace, based on upstream `main` at `93f2d7a2`.
- **Exact command / steps:** Exercised canary and beta feature requests
through CLI status, Python `/stats.rollout`, Rust `/rollout/status`,
multi-worker payload round trips, loopback `/admin/runtime-env`, real
proxy request shaping before/after hot reload, installer manifest
generation, and shared Python/Rust policy vectors.
- **Observed result:** Stable blocks unstable requests; disable wins
over explicit/default/legacy/unsafe paths; unsafe state reports
`qualification_eligible=false`; worker handoff rejects tampering;
running output shaping changes only when the effective beta policy
permits it; explicit blocked flags fail with actionable diagnostics.
- **Not tested:** Live production traffic requiring provider
credentials, or future artifact qualification/promotion automation
(intentionally out of scope).

## Runtime Rollout Safety

- **Rollout-managed features:** Python `tool_result_interceptors`,
`proxy_output_shaper`, `read_maturation`; Rust `native_bedrock`,
`openai_responses_streaming`, `canary_probe`.
- **Minimum rollout channel:** Registry-defined per feature; process
default is `stable`.
- **Stable/default behavior changed:** No unstable feature becomes
enabled by default. Explicit blocked CLI flags now fail instead of
silently doing nothing.
- **Kill switch / disable path:**
`HEADROOM_DISABLE_FEATURES=<comma-separated feature names>`; explicit
disable has highest precedence, including over the unsafe override.
- **Unsafe override required:** No.
`HEADROOM_UNSAFE_ALLOW_UNSTABLE_FEATURES=1` is break-glass only and
makes qualification evidence ineligible.
- **Qualification impact:** Adds machine-readable policy/snapshot
identities and eligibility; does not implement qualification itself.
- **Rollback path:** Set the named disable list for operational
rollback, lower the channel, or revert this PR.

## Review Readiness

- [x] I have performed a full diff review
- [x] This PR is ready for human review

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented hard-to-understand areas
- [x] I have made corresponding documentation changes
- [x] My changes generate no new warnings
- [x] I added tests that reproduce and prevent every regression fixed
during review
- [x] New and existing affected tests pass locally
- [x] I did **not** edit `CHANGELOG.md`; release-please generates it
from the Conventional Commit PR title

## Additional Notes

Out of scope: artifact candidates, benchmark orchestration,
qualification manifests/gates, promotion automation, release branches,
publication guards, and release-risk classification. Those workflows can
consume the rollout registry digest, runtime snapshot digest, decision
reasons, and qualification eligibility through supported black-box
interfaces.

---------

Co-authored-by: JD Davis <jd@jds-macbook-air.tail2a279.ts.net>
Co-authored-by: JD Davis <jd@JDH-AIR-00.local>
2026-08-12 23:16:54 -05:00

232 lines
8.9 KiB
Markdown

# Headroom Learn
Offline failure learning for coding agents. Analyzes past conversations, finds what went wrong, correlates it with what eventually worked, and writes specific project-level learnings that prevent the same mistakes next session.
## Quick Start
```bash
# See recommendations for current project (dry-run, no changes)
headroom learn
# Write recommendations to CLAUDE.local.md (gitignored, personal default)
headroom learn --apply
# Write to the shared team file instead
headroom learn --apply --target CLAUDE.md
# Analyze a specific project
headroom learn --project ~/my-project --apply
# Analyze all projects
headroom learn --all --apply
```
## How It Works
```
Past Sessions → Plugin → Analyzer → Writer → Agent-native context file
│ │ │
│ │ └─ Writes marker-delimited sections
│ │ (replaced on re-run, not duplicated)
│ │
│ └─ LLM-based analysis: finds failure patterns,
│ success correlations, and actionable rules
└─ Plugin reads agent-specific logs:
• Claude Code: ~/.claude/projects/*.jsonl
• Codex: ~/.codex/sessions/*.json
• Gemini CLI: ~/.gemini/tmp/*/chats/session-*.json
```
### Success Correlation
The core innovation. Instead of cataloging failures ("Read failed 5 times"), Headroom finds what the model did to fix each failure:
- **Failed**: `Read axion-formats/src/main/java/.../FirstClassEntity.java`
- **Then succeeded**: `Read axion-scala-common/src/main/scala/.../FirstClassEntity.scala`
- **Learning**: "`FirstClassEntity` is at `axion-scala-common/`, not `axion-formats/`"
This produces specific, actionable corrections — not generic advice.
## What It Learns
### 1. Environment Facts → CLAUDE.md
Which runtime commands work vs fail.
```markdown
### Environment
- **Python**: use `uv run python` (not `python3` — modules not available outside venv)
```
### 2. File Path Corrections → CLAUDE.md
Wrong paths the model keeps guessing, with the correct locations.
```markdown
### File Path Corrections
- `axion-common/src/.../AxionSparkConstants.scala`
→ actually at `axion-spark-common/src/.../AxionSparkConstants.scala`
```
### 3. Search Scope → CLAUDE.md
Which directories to search in (narrow paths fail, broader ones work).
```markdown
### Search Scope
- Don't search `axion-model/` → use `axion/` (the repo root)
```
### 4. Command Patterns → CLAUDE.md
How commands should (and shouldn't) be run.
```markdown
### Command Patterns
- **user_prefers_manual**: User rejected gradle 18 times — show the command, don't execute
- **python_runtime**: Use `uv run python` not `python3` (ModuleNotFoundError)
```
### 5. Known Large Files → CLAUDE.md
Files that need `offset`/`limit` with Read.
```markdown
### Known Large Files
- `proxy/server.py` (~8000 lines) — always use offset/limit
```
### 6. Retry Prevention → MEMORY.md
Specific suggestions derived from actual corrections.
### 7. Permission Notes → MEMORY.md
Commands repeatedly rejected — model should suggest them to the user instead.
## Where Learnings Go
| Pattern | Claude Code | Codex | Gemini CLI |
|---------|-------------|-------|-----------|
| Environment, paths, commands | **CLAUDE.local.md** (default) or `CLAUDE.md` (with `--target CLAUDE.md`) | **AGENTS.md** | **GEMINI.md** |
| Retry patterns, permissions | **MEMORY.md** | **instructions.md** | **GEMINI.md** |
Output files are agent-native: Claude Code writes to `CLAUDE.local.md` by default (gitignored, personal); pass `--target CLAUDE.md` for the shared team file. Codex uses `AGENTS.md`, Gemini uses `GEMINI.md`. The same learnings, written to the format each agent reads.
## Marker-Based Updates
Headroom manages a clearly-delimited section in each file:
```markdown
<!-- headroom:learn:start -->
## Headroom Learned Patterns
*Auto-generated by `headroom learn` — do not edit manually*
...
<!-- headroom:learn:end -->
```
On re-run, only the content between markers is replaced. Your existing file content is preserved.
## Architecture (Plugin System)
Headroom Learn uses a plugin architecture where each agent is a self-contained plugin:
```
Plugin Registry (auto-discovered)
├── ClaudeCodePlugin → Analyzer (LLM) → ClaudeCodeWriter → CLAUDE.md / MEMORY.md
├── CodexPlugin → Analyzer (LLM) → CodexWriter → AGENTS.md / instructions.md
├── GeminiPlugin → Analyzer (LLM) → GeminiWriter → GEMINI.md
└── (your plugin) → Analyzer (LLM) → (your writer) → (your file)
```
**Plugins** bundle scanning, detection, and writing for one agent. Built-in plugins are auto-discovered from `headroom.learn.plugins.*`. External plugins register via the `headroom.learn_plugin` entry point.
**The Analyzer** is shared — it uses an LLM (Sonnet, GPT-4o, or Gemini Flash) to find patterns. Same analysis for any agent.
### Adding Support for a New Agent
1. Create `headroom/learn/plugins/myagent.py`
2. Implement `LearnPlugin` + `ConversationScanner` (scanner + writer + detection)
3. Add `plugin = MyAgentPlugin()` at module scope
4. Done — `headroom learn --agent myagent` works automatically
Or install an external plugin: `pip install headroom-learn-cursor` (registers via entry point).
## CLI Reference
```
headroom learn [OPTIONS]
Options:
--project PATH Project directory (default: current directory)
--all Analyze all discovered projects (mutually exclusive with --project)
--apply Write recommendations (default: dry-run)
--target TEXT Context file to write (default: CLAUDE.local.md for Claude Code)
--main-only Write only to the main context file, skip MEMORY.md
--agent [auto|claude|codex|gemini]
Which agent to analyze (default: auto-detect)
--model TEXT LLM for analysis (default: auto from API keys or CLI)
--workers / -j INTEGER Parallel analysis workers (min 1, default: auto)
--verbosity Analyze verbosity level instead of failure patterns
--llm-judge Use an LLM to score verbosity quality (requires --verbosity)
```
### Verbosity learning (`--verbosity`)
`headroom learn --verbosity` analyzes past sessions to infer the ideal output verbosity level for your project and writes a `verbosity.json` profile.
**Important**: the output shaper is **off by default** and requires the `beta`
runtime rollout channel. Running `--verbosity --apply` will either:
- Hot-enable the output shaper on an eligible running proxy (`POST /admin/runtime-env`), OR
- Print instructions to set `HEADROOM_ROLLOUT_CHANNEL=beta` and `HEADROOM_OUTPUT_SHAPER=1` before `headroom wrap ...`
To keep the shaper on across proxy restarts, export both variables before starting the proxy.
**Flag interactions**:
- `--all` and `--project` are mutually exclusive
- `--llm-judge` requires `--verbosity`
- `--verbosity --all --apply` is rejected (verbosity persists a single global level)
### Supported Agents
| Agent | Scanner | Writer | Output Files |
|-------|---------|--------|-------------|
| **Claude Code** | Reads `~/.claude/projects/*.jsonl` | ClaudeCodeWriter | CLAUDE.md, MEMORY.md |
| **OpenAI Codex** | Reads `~/.codex/sessions/*.json` | CodexWriter | AGENTS.md, instructions.md |
| **Gemini CLI** | Reads `~/.gemini/tmp/*/chats/session-*.json` | GeminiWriter | GEMINI.md |
## LLM Backend Selection
`headroom learn` needs an LLM to analyze your sessions. It picks one automatically using this priority:
| Priority | Source | Example |
|----------|--------|---------|
| 1 | `--model` flag | `headroom learn --model gpt-4o` |
| 2 | API key env var | `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY` |
| 3 | `HEADROOM_LEARN_CLI` env var | `export HEADROOM_LEARN_CLI=gemini` |
| 4 | Auto-detect installed CLIs | Checks PATH for `claude`, `gemini`, `codex` |
### Using without an API key
If you use Claude Code, Gemini CLI, or Codex via subscription (no raw API key), `headroom learn` can call them directly:
```bash
# Auto-detects claude in PATH — no API key needed
headroom learn
# Explicitly select a CLI backend
headroom learn --model gemini-cli
# Pin a CLI via environment variable
export HEADROOM_LEARN_CLI=codex
headroom learn
```
Valid values for `HEADROOM_LEARN_CLI`: `claude`, `gemini`, `codex`.
## Real-World Results
Tested on 67,583 tool calls across 23 projects:
| Metric | Value |
|--------|-------|
| Failure rate | 7.5% (5,066 failures) |
| Corrections extracted | 164 per project (avg) |
| Specific path corrections | 22 (axion project) |
| Search scope corrections | 24 (axion project) |
| Command patterns learned | 5 (axion project) |
| Estimated preventable waste | ~27 MB across corpus |