Commit graph

8 commits

Author SHA1 Message Date
Focused Instability
ced75e4718
feat(learn): write per-project learnings to CLAUDE.local.md by default (#1115)
## Description

`headroom learn` wrote per-project learnings into the project's
`CLAUDE.md`, which Claude Code treats as team-shared and git-tracked.
That meant machine-specific absolute paths and tool-discovery byproducts
polluted the shared file for every teammate. This switches the default
to the personal, gitignored `CLAUDE.local.md`, adds a `--target`
override, and migrates any stale block out of `CLAUDE.md`.

Closes #1072.

## Type of Change

- [ ] Bug fix (non-breaking change that fixes an issue)
- [x] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- `ClaudeCodeWriter` now writes CONTEXT_FILE recommendations to
`CLAUDE.local.md` by default instead of `CLAUDE.md` (the home-directory
case still uses `~/.claude/CLAUDE.md`, which is personal global memory).
- Added a `--target` flag (Claude Code only) and `set_context_target()`
to override the destination — e.g. `--target CLAUDE.md` to opt back into
the shared file, or any relative/absolute path.
- On first run after upgrade, a stale Headroom block left in `CLAUDE.md`
is moved into `CLAUDE.local.md` and stripped from `CLAUDE.md`, with a
warning surfaced by the CLI. If `CLAUDE.md` held nothing but the block,
the empty file is removed.
- `WriteResult` carries `warnings`; the `learn` CLI prints them.
- Updated docs (`failure-learning.mdx`) and `CHANGELOG.md`.

This implements the maintainer's stated preference order from the issue
(default → `CLAUDE.local.md`, plus a `--target` flag), scoped to the
Claude writer only — `AGENTS.md`/`GEMINI.md` have no `.local` convention
and are untouched.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed

### Test Output

```text
$ pytest tests/test_learn/ tests/test_cli_learn.py -q
196 passed, 2 skipped in 17.80s

$ ruff check headroom/learn/writer.py headroom/cli/learn.py
All checks passed!

$ mypy headroom/learn/writer.py headroom/cli/learn.py
Success: no issues found in 2 source files
```

## Real Behavior Proof

- Environment: macOS (Darwin), Python 3.11, headroom on rebased
upstream/main
- Exact command / steps: ran ClaudeCodeWriter against a temp project
whose `CLAUDE.md` held hand-written content plus a legacy Headroom
block, then `writer.write([...], dry_run=False)`
- Observed result: `CLAUDE.md` kept its hand-written content with the
block removed; `CLAUDE.local.md` gained both the migrated `### Old`
section and the new `### Env` section; `result.warnings` contained the
"Moved Headroom learnings out of …" notice. A block-only `CLAUDE.md` was
deleted and a "Removed …" warning emitted.
- Not tested: live end-to-end `headroom learn --apply` against real LLM
analysis (writer + CLI plumbing covered by unit/CLI tests with mocked
analysis)

## Review Readiness

- [x] I have performed a self-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 my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated CHANGELOG.md if applicable

## Additional Notes

Scoped to the Claude Code writer per the issue. After migration,
`discover_projects` may briefly re-surface a section the LLM re-derives,
but the write-side merge dedups by section name so the file stays
correct.
2026-06-22 15:05:06 -05:00
jichaowang02-lang
6129808462
Fix headroom learn crashing/no-op on Windows from missing UTF-8 encoding (#1239)
## Description

Fixes #1202. On a Windows (cp1252) locale, `headroom learn` cannot
complete a
run: the whole pipeline opens transcript files and pipes analyzer
prompts
without `encoding="utf-8"`, so any non-ASCII byte (em-dashes, arrows —
ubiquitous
in code and prose) breaks it. Same bug class already fixed for `headroom
wrap`
(#65, #1126) and the dashboard (#533), never swept through `learn`.

Three independent failure points, each hidden behind the previous:

1. **Reading transcripts** — six bare `open()` calls in the learn
plugins. The
**Codex** JSONL scanner caught only `OSError`, so a `UnicodeDecodeError`
propagated and **aborted the whole cross-agent run**; the **Claude**
scanner
caught it and **silently dropped the session**. `analyzer.py` also read
the
   user's own CLAUDE.md/MEMORY.md with no encoding.
2. **Analyzer subprocess** — `subprocess.run`/`Popen(..., text=True)`
with no
encoding raised `UnicodeEncodeError` on the piped prompt; it was
swallowed,
   so the run produced **0 recommendations** with no obvious failure.
3. **`--apply` merge** — `writer.py` read the existing context file with
strict
   `encoding="utf-8"`, which aborts on a single stray legacy byte.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- `learn/plugins/{claude,codex,gemini}.py`: add `encoding="utf-8",
errors="replace"`
  to the six transcript `open()` calls.
- `learn/analyzer.py`: same on the `read_text` of the user's context
files and on
  both analyzer subprocess calls (`subprocess.run` and `Popen`).
- `learn/writer.py`: add `_read_text_tolerant` — decode the
to-be-rewritten
context file as UTF-8, falling back to UTF-8-with-replacement on a stray
byte
(a whole-file cp1252 fallback is wrong: it mojibakes genuine UTF-8
em-dashes);
  the subsequent `write_text(encoding="utf-8")` self-heals the file.
- `cli/learn.py`: wrap `plugin.scan_project` so one unreadable
agent/project is
  skipped with a warning instead of aborting the whole run.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed

### Test Output

```text
$ pytest tests/test_learn/test_writer.py tests/test_learn/test_plugin_encoding.py -q
22 passed

$ ruff check headroom/learn/plugins/*.py headroom/learn/analyzer.py \
    headroom/learn/writer.py headroom/cli/learn.py tests/test_learn/test_*.py
All checks passed!
```

New tests are **red on the old code, green with the fix**:
- `test_plugin_encoding.py` — a transcript with a stray `0x9d` byte
(undefined in
cp1252 *and* an invalid UTF-8 start byte, so a bare `open()` fails on
any
locale): the Codex scanner no longer raises, the Claude scanner now
recovers
  the session instead of dropping it.
- `test_writer.py::TestEncodingResilience` — `_read_text_tolerant`
preserves
valid UTF-8 (no mojibake) and `--apply` merges over a file with a stray
byte.

## Real Behavior Proof

- Environment: Windows 11, Python 3.10, against the real learn
plugins/writer
(no live LLM backend; the decode failures occur before any backend
call).
- Exact command / steps: write a Claude transcript and a Codex rollout
JSONL
containing a valid em-dash/arrow line plus a stray `0x9d` byte, then
call
`ClaudeCodePlugin._scan_session` / `CodexPlugin._scan_jsonl_session`;
for the
writer, `write_bytes` an `AGENTS.md` with a stray `0x97` and run
`_merge_into_file`.
- Observed result: **before** the fix →
`CodexPlugin._scan_jsonl_session` raises
`UnicodeDecodeError: 'utf-8' codec can't decode byte 0x9d` (aborts the
run) and
`ClaudeCodePlugin._scan_session` returns `None` (session dropped);
**after** →
Codex completes, Claude returns the `SessionData` (`total_input_tokens
== 5`),
  and `_merge_into_file` keeps `Notes — existing` with no mojibake.
- Not tested: a full end-to-end `headroom learn --apply` against live
agent
histories + a real LLM backend (verified at the plugin/writer level,
which is
  where the decode failures live).

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review
2026-06-21 10:37:37 -07:00
Tejas Chopra
5ceca13c65 fix: harden learn path handling across platforms 2026-05-09 15:45:26 -07:00
Garm
6d2aba8741 fix(learn): show prior patterns block to LLM to prevent dangling refs
When `headroom learn` re-surfaced a section heading that already existed
in CLAUDE.md / MEMORY.md, the writer replaced that section wholesale —
but the LLM never saw the prior block, so it emitted condensed bullets
like "X is *also* large — same rule as Y, Z" assuming Y and Z would
remain siblings. After replacement, Y and Z were gone and the "also"
dangled.

This threads the project's current `<!-- headroom:learn -->` block (from
both CLAUDE.md and MEMORY.md) into the digest as a "Prior Learned
Patterns" section, and extends the system prompt to make the re-emission
contract explicit: re-stating a section replaces it wholesale, so the
LLM must copy forward prior bullets it still agrees with. Prior sections
the LLM omits entirely are still carried forward by the writer (#231
behavior preserved as a safety net).

Changes:
- New `extract_marker_block(file_content)` helper in `learn.writer` that
  returns the raw marker block (delimiters included) or None.
- New `_build_prior_patterns_section(project)` in `learn.analyzer` reads
  `project.context_file` and `project.memory_file` via the new helper
  and formats a labeled section ahead of the per-session event stream.
- `_build_digest` emits the prior-patterns section when present; char
  budget accounting unchanged (prior blocks are small).
- `_SYSTEM_PROMPT` gains a "Prior Learned Patterns" rule block telling
  the LLM how to integrate prior bullets (preserve / revise / drop-only-
  if-contradicted) and warning against unresolved cross-references.
- Tests: 6 new `TestPriorPatternsInjection` cases (present/absent files,
  no-marker-block, both-files, end-to-end via mocked `_call_llm`); 4 new
  `TestExtractMarkerBlock` cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 12:59:02 +02:00
Garm
aad799d76e test(learn): cover _parse_prior_recommendations edge cases
Closes the codecov gap flagged on PR 232 (88.89% → near 100% on the patch):

- A file with no marker block returns no prior recommendations.
- A marker block with nothing between the markers yields an empty list
  (the re.split fast-path with zero sections).
- A stray `### ` with no heading text inside the block is silently
  skipped (the `if not heading: continue` branch, previously
  unexercised in tests) — a real section after it still parses cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 20:13:49 +02:00
Garm
72ae0a9e03 chore: apply ruff format + add CHANGELOG entry 2026-04-22 11:28:11 +02:00
Garm
0123e49939 fix(learn): preserve prior recommendations across runs (#231)
`headroom learn` built the marker block from only the current run's
recommendations and wholesale-replaced any prior block via
`_MARKER_PATTERN.sub`. Sections learned weeks earlier that didn't
re-surface in a later run were silently dropped.

Fix: in `_merge_into_file`, parse recommendations out of the prior
block and union them with the new run's recommendations. Sections
re-surfaced by the new run take precedence (latest analysis wins);
sections not re-surfaced are carried forward so learnings accumulate
instead of getting clobbered.

To fully rebuild the block, delete it manually and re-run.

Tests: existing wholesale-replace test rewritten as a carry-forward
assertion. Added tests for same-section override, MEMORY.md
carry-forward, and round-trip of sections without a tokens annotation.

Closes #231
2026-04-22 10:22:36 +02:00
chopratejas
17442c2dcc Add headroom learn: offline failure learning for coding agents
Analyzes past conversation history to find tool call failure patterns,
correlates each failure with what eventually succeeded, and writes
specific project-level learnings to CLAUDE.md and MEMORY.md.

Key design:
- Success correlation: extracts the diff between failed and successful
  inputs as the learning (not generic advice)
- Generic architecture: tool-agnostic ToolCall model with pluggable
  Scanner/Writer adapters (Claude Code first, extensible to Cursor/Codex)
- 5 analyzers: Environment, Structure, Commands, Retries, Cross-Session
- Dry-run by default, --apply to write, --all for all projects

Also fixes mypy errors in litellm_callback, asgi, langchain chat_model,
and anthropic provider (AsyncClient typing, ToolCall arg-type, int cast).
2026-02-27 21:19:03 -08:00