Commit graph

220 commits

Author SHA1 Message Date
Tejas Chopra
9fd5ae3d53
chore: release main (#2679)
🤖 I have created a release *beep* *boop*
---


<details><summary>0.34.0</summary>

##
[0.34.0](https://github.com/headroomlabs-ai/headroom/compare/v0.33.0...v0.34.0)
(2026-08-05)


### Features

* **claude:** support Claude Code in VS Code
([#2752](https://github.com/headroomlabs-ai/headroom/issues/2752))
([13a310a](13a310a00d))
* **code:** add PHP support to CodeAwareCompressor
([#2423](https://github.com/headroomlabs-ai/headroom/issues/2423))
([6d5516d](6d5516dcb8))
* **compress:** accept config.frozen_message_count on /v1/compress
([#2718](https://github.com/headroomlabs-ai/headroom/issues/2718))
([2797099](2797099bec))
* **compress:** reach the lossless provider seam on the general path and
default /v1/compress to marker-free output
([#2691](https://github.com/headroomlabs-ai/headroom/issues/2691))
([f2c48e2](f2c48e26c6))
* **copilot:** proxy VS Code models transparently
([#2687](https://github.com/headroomlabs-ai/headroom/issues/2687))
([007446c](007446c73a))


### Bug Fixes

* **ccr:** stop persisting retrieval markers as original content
([#2694](https://github.com/headroomlabs-ai/headroom/issues/2694))
([#2703](https://github.com/headroomlabs-ai/headroom/issues/2703))
([3e348f3](3e348f327f))
* **ci:** restrict Codecov shard uploads
([#2745](https://github.com/headroomlabs-ai/headroom/issues/2745))
([3f2ca99](3f2ca99fe1))
* **compression:** honor qualified CCR names across integrations
([#2698](https://github.com/headroomlabs-ai/headroom/issues/2698))
([dcb674b](dcb674b5e4))
* **compress:** resolve the /v1/compress tokenizer per model, and
document the real contract
([#2743](https://github.com/headroomlabs-ai/headroom/issues/2743))
([6422a80](6422a80a58))
* **cost:** send litellm the total prompt so --budget stops seeing $0
([#2757](https://github.com/headroomlabs-ai/headroom/issues/2757))
([a033ac4](a033ac4176))
* **deps:** bump aiohttp and cryptography to clear the CVEs blocking
0.34.0
([#2753](https://github.com/headroomlabs-ai/headroom/issues/2753))
([0221e7f](0221e7f240))
* **kompress:** let orgs run Kompress on their own inference stack
([#2736](https://github.com/headroomlabs-ai/headroom/issues/2736))
([3d23d76](3d23d76248))
* **kompress:** load merged.pt for the v2 checkpoint instead of the
unmerged PEFT safetensors
([#2716](https://github.com/headroomlabs-ai/headroom/issues/2716))
([46da91b](46da91b2f1))
* **kompress:** reject artifacts that fail at run, and prefetch model
files at startup
([#2740](https://github.com/headroomlabs-ai/headroom/issues/2740))
([224578e](224578e80b))
* **learn:** filter ambient user-role scaffolding
([#2275](https://github.com/headroomlabs-ai/headroom/issues/2275))
([3eb0122](3eb0122068))
* **learn:** run project discovery off the event loop
([#2731](https://github.com/headroomlabs-ai/headroom/issues/2731))
([a70e5ff](a70e5ff78d))
* normalize /p/&lt;project&gt; prefix on WebSocket upgrades so the
Responses WS route is not rejected with 403
([#2379](https://github.com/headroomlabs-ai/headroom/issues/2379))
([789a4f3](789a4f3060))
* **providers:** give every model exactly one tokenizer
([#2761](https://github.com/headroomlabs-ai/headroom/issues/2761))
([cd92ed5](cd92ed52ff))
* **providers:** stop a shorter model family shadowing a longer one
([#2762](https://github.com/headroomlabs-ai/headroom/issues/2762))
([0cb72f4](0cb72f45b2))
* **providers:** stop pricing modern content blocks at zero
([#2760](https://github.com/headroomlabs-ai/headroom/issues/2760))
([06add9e](06add9e9d8))
* **proxy/cost:** mark estimated-basis budget records and add an
enforcement policy
([#2713](https://github.com/headroomlabs-ai/headroom/issues/2713))
([#2725](https://github.com/headroomlabs-ai/headroom/issues/2725))
([01df245](01df245252))
* **proxy/debug:** reconcile Kompress warmup state in /debug/warmup
([#2711](https://github.com/headroomlabs-ai/headroom/issues/2711))
([3a27c4d](3a27c4dacb))
* **proxy/openai:** run tool-description compaction on chat-completions
([#2741](https://github.com/headroomlabs-ai/headroom/issues/2741))
([f9db5b5](f9db5b5060))
* **proxy:** route Codex Live voice through a dedicated /v1/live
transport
([#2709](https://github.com/headroomlabs-ai/headroom/issues/2709))
([232fb49](232fb49c73))
* **proxy:** skip OpenAI tool_search deferral for Codex client
([#2729](https://github.com/headroomlabs-ai/headroom/issues/2729))
([56b3e4c](56b3e4c1b1))
* **proxy:** stop toggling headroom_retrieve in the Anthropic tools
array ([#2672](https://github.com/headroomlabs-ai/headroom/issues/2672))
([08fce29](08fce29b47))
* remove rtk and lean-ctx CLI context tools
([#2677](https://github.com/headroomlabs-ai/headroom/issues/2677))
([e0ce4b1](e0ce4b1d48))
* **router:** stop counting an image's base64 payload as suffix tokens
([#2778](https://github.com/headroomlabs-ai/headroom/issues/2778))
([f03cc6d](f03cc6d88b))
* **savings:** surface request growth the tok_saved clamp swallows
([#2708](https://github.com/headroomlabs-ai/headroom/issues/2708))
([184146b](184146b688))
* **stats:** report one "Tokens Saved" headline across every harness
([#2737](https://github.com/headroomlabs-ai/headroom/issues/2737))
([8262a4a](8262a4a321))
* **telemetry:** anonymous compression stats — no prompts, no data
([#2728](https://github.com/headroomlabs-ai/headroom/issues/2728))
([9cfb008](9cfb00838a))
* **telemetry:** stop mixing tokenizer scales in RequestOutcome, and fix
the overhead framing
([#2756](https://github.com/headroomlabs-ai/headroom/issues/2756))
([04e1517](04e1517ede))
* **tokenizers:** count HuggingFace chat templates, and resolve gpt-5 /
gateway-wrapped names
([#2758](https://github.com/headroomlabs-ai/headroom/issues/2758))
([0ed306b](0ed306b22b))
* **tokenizers:** resolve gpt-5 and mixed-case model names to the right
encoding
([#2776](https://github.com/headroomlabs-ai/headroom/issues/2776))
([fc4680b](fc4680b37a))
* **transforms:** stop ContentRouter recompressing headroom_retrieve
results
([#2654](https://github.com/headroomlabs-ai/headroom/issues/2654))
([677e097](677e09735a))
* **wrap/serena:** stop creating serena_config.yml, unbricking Serena on
fresh installs
([#2676](https://github.com/headroomlabs-ai/headroom/issues/2676))
([759209c](759209cff3))


### Code Refactoring

* **pricing:** make LiteLLM the source of truth, not the hardcoded table
([#2779](https://github.com/headroomlabs-ai/headroom/issues/2779))
([0e1d6bf](0e1d6bfa79))
* remove the dead headroom/prediction module
([#2692](https://github.com/headroomlabs-ai/headroom/issues/2692))
([b7a79ac](b7a79ac31a))
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-04 19:39:34 -07:00
Tejas Chopra
3c10e8ff00
chore(docs): one documentation site, not two (#2784)
## Description

The repo published **two** documentation sites from two source trees:

```
docs/  -> Next.js/Fumadocs -> headroom-docs.vercel.app      <- canonical
wiki/  -> MkDocs -> gh-pages branch -> github.io/headroom    <- orphan
```

The Vercel site is what the README badge and **every** README deep link
point at, and what `pyproject.toml` names as both `Homepage` and
`Documentation`. The Pages site is referenced from **nowhere** in the
repo — not README, not `pyproject`, not `CLAUDE.md`, not any docs page.
I grepped for `github.io` and `gh-pages` across all of them and got zero
hits.

So it was costing work and causing breakage while nobody was reading it:

- **Every documented change had to be written twice.** This session I
wrote the same configuration content into
`docs/content/docs/configuration.mdx` *and* `wiki/configuration.md`.
That's the tax, and it compounds silently — the two drift and no one
notices which is stale.
- **It broke the Vercel deployment.** Each Pages deploy runs `mkdocs
gh-deploy --force`, force-pushing `gh-pages`. Vercel's Git integration
then tries to build that branch with Root Directory `docs`, which fails:
*"The specified Root Directory `docs` does not exist"* — because
`gh-pages` holds only the rendered site (`.nojekyll`, `404.html`, …).
Timing was exact:

  ```text
  23:06:44  main       a9a2fbd7  ci(docs): ... (#2746)
23:07:51 gh-pages 9cd8775c Deployed a9a2fbd7 with MkDocs <- 67s later
  ```

- The same workflow also carried the `deploy-vercel` job that failed 30
times on main without ever deploying (removed in #2746).

## Changes Made

- Removed the `validate-mkdocs` and `deploy-github-pages` jobs, and
`mkdocs.yml`.
- What remains is one workflow that **only validates** the Next.js build
on pull requests. Vercel owns deployment — duplicating that in Actions
is exactly what produced the dead `deploy-vercel` job.
- Dropped the `push` trigger entirely (nothing deploys from Actions now)
and the `wiki/**` / `mkdocs.yml` path filters.
- Renamed the workflow `Deploy Documentation` → `Validate Docs`, since
it no longer deploys anything. It isn't a required check, so the rename
is safe.
- Repointed `configuration.mdx`'s Filesystem Contract link from the
`wiki/` blob on GitHub to `/docs/filesystem-contract`, which already
existed.

## `wiki/` deliberately stays — please read this bit

I did **not** delete `wiki/`, even though the ask was to get rid of it.
~14 of its topics have no `docs/` equivalent, and one is significant:

```text
912L  wiki/cli.md               <- full CLI reference; docs/ has NO cli page
718L  wiki/macos-deployment.md
409L  wiki/compression.md
370L  wiki/transforms.md
345L  wiki/integration-guide.md
335L  wiki/api.md
292L  wiki/sdk.md
231L  wiki/learn.md
...
```

`docs/` mentions CLI commands across 27 pages but has no reference page
for them. Deleting `wiki/` today would drop 912 lines of CLI
documentation on the floor.

After this PR `wiki/` is **unpublished markdown**: nothing builds it,
nothing deploys it, so **it needs no syncing**. It's a migration
backlog, not a parallel site. That gets you the outcome you wanted — one
site, one tree to edit — without losing content.

## Type of Change

- [x] Code refactoring (no functional changes)

## Testing

- [x] Linting passes — workflow YAML parses and resolves to the intended
shape:

```text
name:      Validate Docs
jobs:      ['validate-nextjs']
triggers:  ['pull_request', 'workflow_dispatch']
pr paths:  ['docs/**', '.github/workflows/docs.yml']
```

Verified nothing else depends on the MkDocs pipeline: the only remaining
`mkdocs` references in the repo are `CHANGELOG.md` (history) and one
unrelated comment in `content_router.py:3329` ("Measured on
mkdocs.yml"). No `docs/` page links to a `wiki/` blob any more.

CI's `Validate Next.js build` is the real check that the surviving job
works.

## Two follow-ups this does NOT do

1. **Disable GitHub Pages and delete the `gh-pages` branch.** Pages is
currently enabled (`source: gh-pages`, status `building`) at
`https://headroomlabs-ai.github.io/headroom/`. After this PR nothing
updates it, so it freezes rather than breaks — and the Vercel `gh-pages`
build failure stops recurring because no further force-pushes happen.
Actually taking the site down and deleting the branch is a destructive,
outward-facing change; I'd rather do that as an explicit step than
bundle it here. Nothing in the repo links to it, so the only risk is
externally-indexed URLs 404ing.
2. **Migrate `wiki/cli.md` into `docs/` as a CLI reference page**, then
the smaller unique topics, then delete `wiki/`. That's content work
deserving its own review.
2026-08-04 16:42:49 -07:00
Tejas Chopra
a9a2fbd74f
ci(docs): deploy Pages on wiki changes, drop the never-working Vercel job (#2746)
## Description

Two independent bugs in `.github/workflows/docs.yml`.

**1. Pages went stale because the push filter watched the wrong
directory.**

`mkdocs.yml` sets `docs_dir: wiki`, but the push filter listed `docs/**`
and not `wiki/**`. So a merge touching only `wiki/` never triggered the
workflow and the published Pages site silently went stale, while a
`docs/**`-only change triggered a Pages rebuild whose sources mkdocs
doesn't even read.

Push now filters on `wiki/**` + `mkdocs.yml`. **Pull requests keep
`docs/**`**, so `validate-nextjs` still catches a broken MDX change
before merge.

**2. `deploy-vercel` never worked — it wasn't a working path that went
stale.**

It ran `npx vercel deploy --prod --token=${{ secrets.VERCEL_TOKEN }}`,
but those secrets don't exist:

```text
$ gh api repos/headroomlabs-ai/headroom/actions/secrets --jq '.secrets[].name' | grep -i vercel
(nothing)
$ gh api orgs/headroomlabs-ai/actions/secrets  --jq '.secrets[].name' | grep -i vercel
(nothing)
```

So it invoked the CLI with an empty `--token=` and exited 1, every time:

```text
30 failed runs on main, 2026-07-14 .. 2026-08-04

most recent:
  30874173333  13a310a0  failure
  30845343100  6422a80a  failure
  30810614546  007446c7  failure

per-job on 30874173333:
  failure  Deploy Vercel Docs
  success  Deploy GitHub Pages     <- same run
  skipped  Validate mkdocs build
  skipped  Validate Next.js build
```

`deploy-github-pages` succeeded in those same runs, so this job
contributed nothing but a red X on every merge to main.

The Next.js site is published by **Vercel's own Git integration**, which
is what has actually been deploying it. Removing this job leaves exactly
one deploy path per site: Pages via mkdocs here, Vercel via its Git
integration.

## Why this PR previously said the opposite

I opened this as "drop the redundant Vercel job", then converted it to
draft and posted a correction saying the deletion was **wrong** —
because at that point the Vercel site had been stale since mid-June and
this looked like the only deploy path. That correction was itself based
on a wrong assumption: with no `VERCEL_TOKEN` configured, this job could
never have deployed anything. It wasn't the deploy path; it was a job
that had always failed. The site was stale because *nothing* was
publishing it until the Git integration was connected.

So: original intent right, first correction wrong, and the evidence
above is what settles it. Recording that rather than quietly re-flipping
the description.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)

## Changes Made

- Push filter: `docs/**` → `wiki/**` (what mkdocs actually reads).
- Removed the `deploy-vercel` job.
- Replaced it with a comment recording *why* there is no Vercel job
here, so nobody re-adds one.

## Testing

- [x] Linting passes — workflow YAML parses, and the resulting shape is
what's intended:

```text
jobs:       ['validate-mkdocs', 'validate-nextjs', 'deploy-github-pages']
push paths: ['wiki/**', 'mkdocs.yml', '.github/workflows/docs.yml']
pr paths:   ['docs/**', 'wiki/**', 'mkdocs.yml', '.github/workflows/docs.yml']
```

CI is the real check for a workflow change. The observable proof after
merge is that the next push to `main` no longer reports a failing
`Deploy Vercel Docs`, and that a `wiki/**`-only change triggers a Pages
deploy (it currently does not).

## Real Behavior Proof

- **Environment:** GitHub Actions on `headroomlabs-ai/headroom`, branch
`main`.
- **Exact command / steps:** `gh run list --workflow "Deploy
Documentation" --branch main`; `gh api .../actions/secrets`; `gh run
view <id> --json jobs`.
- **Observed result:** 30 consecutive `Deploy Documentation` failures on
main attributable solely to `Deploy Vercel Docs`; no Vercel secrets
configured at repo or org level; `Deploy GitHub Pages` green throughout.

## What this does NOT fix

This is unrelated to the failing **`Vercel`** check currently showing on
~18 open PRs. That one comes from the Vercel GitHub App with
`Authorization required to deploy.` — Vercel asking each PR author to
authorize its app — and is a Vercel-side setting, not a workflow file.
Two different mechanisms that both say "Vercel":

| | mechanism | where it fails | fixed here? |
|---|---|---|---|
| `Deploy Vercel Docs` | Actions job in this file | pushes to `main` |
**yes** |
| `Vercel` / `Vercel Preview Comments` | Vercel GitHub App | contributor
PRs | no — dashboard setting |

For the record: `Vercel` is not a required status check (`template`,
`label`, `merge-conflicts`, `no-manual-changelog`, `Secret scan
(gitleaks)` are), so it has never actually blocked a merge.
2026-08-04 16:06:44 -07:00
Raúl
3f2ca99fe1
fix(ci): restrict Codecov shard uploads (#2745)
## Description

Closes #2744

Restrict each Codecov Action v5 matrix upload to its declared
`coverage-${{ matrix.shard }}.xml` report. This prevents automatic
discovery
from uploading the unsharded `coverage.xml` alongside every shard.

## Type of Change

- [x] Bug fix (non-breaking change fixes an issue)

## Changes Made

- Set Codecov Action `disable_search: true` for Python shard uploads.
- Add a CI workflow contract test that protects the explicit-report-only
setup.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check`)
- [x] Type checking passes (`mypy headroom`) (not applicable:
workflow/test-only change)
- [x] New tests added new functionality
- [x] Manual testing performed (not applicable: GitHub Actions will
execute the workflow)

### Test Output

```text
$ uv run --with ruff ruff format --check scripts/tests/test_ci_workflow.py
1 file already formatted

$ uv run --with ruff ruff check scripts/tests/test_ci_workflow.py
All checks passed!

$ uv run --with pytest pytest scripts/tests/test_ci_workflow.py -q
2 passed
```

## Real Behavior Proof

- Environment: GitHub Actions Ubuntu runner using Python 3.12.13;
Codecov Action v5.
- Exact command / steps: Run the CI Python test matrix, which writes
`coverage-${{ matrix.shard }}.xml`, then runs the Codecov Action upload
step. Inspect the uploader's discovered/uploaded report list.
- Observed result: Before this change, raw CI logs showed the Action
explicitly uploading `coverage-2.xml` and additionally
discovering/uploading `coverage.xml`. This PR configures
`disable_search: true`; the workflow contract test confirms the explicit
report setting and search disablement. Runtime upload evidence will be
added from this draft PR's CI run.
- Not tested: Codecov's final cross-shard patch calculation; that
depends on Codecov processing the reports after CI completes.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR ready for human review

## Checklist

- [x] My code follows project's style guidelines
- [x] I have performed a self-review
- [x] I commented my code, particularly in hard-to-understand areas
- [x] I made corresponding changes documentation (not applicable)
- [x] My changes generate no new warnings
- [x] I added tests prove my fix is effective or feature works
- [x] New and existing unit tests pass locally changes
- [x] I did **not** edit `CHANGELOG.md` — generated by release-please
from Conventional Commit PR title (a CI guard enforces this)

## Additional Notes

This is intentionally limited to the Codecov upload configuration and
its
workflow contract test. It does not include the unrelated Copilot
Keychain fix.
2026-08-03 14:20:17 -07:00
Tejas Chopra
e0ce4b1d48
fix: remove rtk and lean-ctx CLI context tools (#2677)
## Description

Removes both third-party CLI context tools — **rtk** and **lean-ctx** —
and with them the context-tool selector itself. Headroom no longer
downloads, installs or configures either one, and there is no
replacement.

The previous pass (#2344) gated only three entry points inside
`headroom/cli/wrap.py`. That left the feature reachable in practice:

| Gap | Effect |
|---|---|
| `scripts/install.sh:1544`, `install.ps1:1681` | Ran `rtk init --global
--auto-patch` from bash/PowerShell, **bypassing the Python gate
entirely** — `curl \| sh` still wrote a Claude Code `PreToolUse` hook
regardless of `HEADROOM_RTK` |
| `wrap.py` `_setup_context_tool_for_agent` | **`wrap openhands` was
broken by default**: `rtk_required=True` met a gate returning `None` →
`SystemExit(1)`. Invisible because all 8 openhands tests patched
`_ensure_rtk_binary` to a fake path |
| `proxy/helpers.py`, `subscription/tracker.py` | Proxy shelled out to
`rtk gain` from `/stats`, the dashboard and `headroom perf`; the tracker
polled it per contribution (`_RTK_WIRING_DEFAULT = "enabled"`) |
| No cleanup path | Nothing removed artifacts an earlier default had
installed, so a machine that once ran the old default kept rtk in the
loop forever (#1669, #1955) |

Also worth noting: the rtk binary download had **no SHA or signature
verification** — only `rtk --version` as a smoke test.

## Type of Change

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

## Changes Made

**Removed** — `headroom/rtk/` and `headroom/lean_ctx/` packages,
`headroom/cli/wrap_rtk_metrics.py`, `_selected_context_tool` /
`_setup_context_tool_for_agent` / `_VALID_CONTEXT_TOOLS`, the `--rtk` /
`--no-rtk` / `--no-project-rtk` / `--keep-rtk` flags across all 18 wrap
subcommands, `HEADROOM_RTK*`, the proxy-side `rtk gain` polling, the
dashboard CLI-filtering panel (rows + all 8 `cliFiltering*` Alpine
getters), `paths.rtk_path()` / `lean_ctx_path()`, the SDK path helpers,
`benchmarks/rtk_loop_learn_eval.py`, and the `headroom/rtk/**` CI path
filters.

**Fails loudly, not silently** — `--context-tool` / `--no-context-tool`
/ `HEADROOM_CONTEXT_TOOL` are kept solely to error out. They live in
shell profiles, aliases and CI jobs, and accepting them as a no-op would
read as Headroom having quietly stopped working. The installers reject
them too, which matters more than it looks: their arg parsers forward
the first unknown flag **and everything after it** to the wrapped tool,
so a leftover `--no-rtk` would have silently swallowed a following
`--port` and then been ignored downstream.

**New `headroom/context_tool_cleanup.py`** — deleting the code cannot
help a machine that already ran the old default, since the hooks,
binaries and injected guidance are durable on disk.
`purge_context_tool_artifacts()` runs once per `wrap`/`unwrap` and
removes the registered hook entries, the generated hook scripts, the
Headroom-managed `~/.local/bin` symlinks, the vendored
`~/.headroom/bin/{rtk,lean-ctx}` binaries, the `lean-ctx` MCP server
entry and the marker-fenced instruction blocks. Deliberately
conservative: idempotent, **skips** a malformed config rather than
overwriting it, and only unlinks a symlink resolving inside Headroom's
own bin dir so a user's own build is untouched. It reports on
**stderr**, because `wrap/unwrap openclaw --prepare-only` emit
machine-readable JSON on stdout as their entire contract. Skipped for
`wrap selfheal` (runs from a SessionStart hook; must not race Claude
Code's writer for `~/.claude.json`) and for `--help`, which must stay
read-only.

**Client-config hardening** (discovered while investigating a "corrupted
Serena settings file" report) — `wrap.py` reset a settings file to `{}`
when an existing file would not parse, then wrote that back. One
hand-edited typo or a transient `EACCES`/`EINTR` on a valid file
destroyed the user's `permissions`, `env` and `hooks`, on **every
`headroom wrap claude`**. It now refuses to write. Separately,
`fsutil.write_text` is now atomic (temp file + `fsync` + `os.replace`),
fixing all 14 non-atomic client-config writes at once; it follows
symlinks rather than replacing them (dotfile managers) and preserves an
existing file's mode.

**Deliberately kept** — `rtk` stays in the wrapper-peel list in
`transforms/content_router.py`. It sits beside `sudo`/`env`/`timeout` as
shell-command grammar, so `rtk cat f` is still classified as a file read
for anyone running their own rtk install, which the purge intentionally
leaves alone.

## 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
$ ruff check headroom/ tests/ e2e/ --exclude headroom/dashboard/templates
All checks passed!

$ ruff format --check headroom/ tests/ e2e/ --exclude headroom/dashboard/templates
1255 files already formatted

$ mypy headroom/
Success: no issues found in 508 source files

$ pytest tests/test_context_tool_cleanup.py -q
11 passed

$ pytest tests/test_fsutil.py -q
12 passed

$ pytest tests/test_cli/test_wrap_codex.py -q            # 89 tests
89 passed in 431.68s
$ pytest tests/test_cli/test_wrap_opencode.py -q
39 passed in 257.46s
$ pytest tests/test_cli/test_wrap_helpers.py -q
45 passed
$ pytest tests/test_paths.py -q
75 passed
$ pytest tests/test_cli/test_unwrap_claude.py -q
14 passed
$ pytest tests/test_proxy_savings_history.py -q
39 passed
$ pytest tests/test_cli/test_wrap_copilot.py -q
27 passed
$ pytest tests/test_cli/test_wrap_zcode.py -q
20 passed
$ pytest tests/test_subscription_tracker.py -q
9 passed
$ pytest tests/test_proxy_dashboard_stats_cache.py -q
5 passed, 1 skipped
```

Repo-wide grep for 14 removed symbols (`headroom.rtk`,
`headroom.lean_ctx`, `_ensure_rtk_binary`, `_selected_context_tool`,
`_get_context_tool_stats`, `rtk_path`, `lean_ctx_path`,
`wrap_rtk_metrics`, `HEADROOM_RTK`, `cli_tokens_avoided`,
`tokens_saved_rtk`, …) across `*.py`, `*.ts`, `*.sh`, `*.ps1`, `*.yml`,
`*.html`: **zero hits**.

Notable test changes: `test_wrap_openhands.py` no longer patches
`_ensure_rtk_binary` and asserts `wrap openhands --prepare-only` exits 0
unpatched — the regression that was previously masked.
`test_wrap_continue.py` and `test_wrap_hintfile_agents.py` were removed
(every test drove RTK instruction injection). A new
`test_subscription_tracker.py::test_load_state_written_before_cli_context_tools_were_removed`
proves a pre-removal `subscription_state.json` still loads.

## Real Behavior Proof

- **Environment:** macOS 15.4 (darwin 25.4.0), Python 3.12.6, Headroom @
this branch, real `~/.headroom` and `~/.claude` on the dev machine.
- **Exact command / steps and observed result:**

```text
# 1. Retired flag fails loudly instead of silently no-op'ing
$ headroom wrap codex --prepare-only --context-tool rtk
Error: CLI context tools (rtk, lean-ctx) have been removed from Headroom: they
rewrote shell commands through a third-party binary Headroom no longer manages.
Drop --context-tool / --no-context-tool and unset HEADROOM_CONTEXT_TOOL;
`headroom wrap` uninstalls what they left behind on first run.

$ HEADROOM_CONTEXT_TOOL=lean-ctx headroom wrap codex --prepare-only
Error: CLI context tools (rtk, lean-ctx) have been removed from Headroom: ...

# 2. install.sh rejects the retired flags (extracted parse_wrap_args harness)
['--no-rtk', '--port', '9999']   rc=1  ERROR: CLI context tools ... Drop --no-rtk
['--context-tool=rtk']           rc=1  ERROR: CLI context tools ... Drop --context-tool
$ bash -n scripts/install.sh   # syntax OK

# 3. Purge ran against the real machine, which had all the orphaned artifacts
$ python -c "from headroom.context_tool_cleanup import purge_context_tool_artifacts; ..."
  removed ~/.headroom/bin/lean-ctx        (51 MB)
  removed ~/.headroom/bin/rtk             (7.7 MB)
  removed ~/.local/bin/rtk                (symlink into ~/.headroom/bin)
  removed ~/.claude/hooks/rtk-rewrite.sh
  removed 8 lean-ctx-* hook scripts
# ~/.claude.json afterwards: 90 top-level keys, 19 projects, mcpServers unchanged
# → ~59 MB reclaimed, no unrelated key touched

# 4. stdout stays machine-readable while the purge reports (planted a fake artifact)
$ headroom wrap openclaw --prepare-only --gateway-provider-id codex >out 2>err
$ cat out
{"enabled":true,"config":{"proxyPort":8787,...}}     # parses as JSON
$ cat err
Retired CLI context tool cleanup: removed /Users/tcms/.headroom/bin/rtk

# 5. --help is inert (planted artifact survives), a real run purges
$ headroom wrap codex --help   → artifact survived: CORRECT
$ headroom wrap openclaw --prepare-only → purged: CORRECT

# 6. MCP purge dry-run against a copy of the real 82 KB ~/.claude.json
top-level keys 90 -> 90;  projects 19 -> 19;  LOST keys: none
all content outside mcpServers byte-identical: True
```

Dashboard rendered via the Playwright test after the panel removal:
"Token Savings" shows only `Proxy 0 (0.0%)` / `Of total wire: 36.86%`,
and "Token Usage" reads Before Compression → Proxy Removed → After
Compression with no "Filtered (this session)" row. Nothing below the
removed panel broke.

- **Not tested:** Windows and Linux (macOS only) — `install.ps1` is
verified by brace-balance and inspection, not executed, since no `pwsh`
is available locally. The wrap e2e suite (`e2e/wrap/run.py`) was updated
but not run; it needs the Docker e2e image. `serena project index`
interaction is exercised in the stacked base PR.

## 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 did **not** edit `CHANGELOG.md` — it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)

## Additional Notes

**Stacked on #2676** (`tejas/serena-config-bootstrap`) — please merge
that first; this PR's base should then be retargeted to `main`, or it
will read as containing that fix too.

**Breaking-change migration for users:**
- Drop `--rtk`, `--no-rtk`, `--no-project-rtk`, `--keep-rtk`,
`--context-tool`, `--no-context-tool` from any alias, script or CI job,
and unset `HEADROOM_RTK*` / `HEADROOM_CONTEXT_TOOL`. They now error
rather than being ignored, so the failure is immediate and
self-explaining.
- Previously-installed artifacts are purged automatically on the next
`wrap`/`unwrap`; no manual cleanup needed.
- `headroom perf --json` no longer carries a `cli_filtering` key, and
`/stats` no longer returns a `context_tool` section.

**Docs:** `docs/rtk-architecture.md` deleted; RTK/lean-ctx removed from
`README.md`,
`docs/content/docs/{configuration,opencode,grok-build,docker-install,filesystem-contract}.mdx`,
`docs/observability.md` and the matching `wiki/` pages.
`REALIGNMENT/09-phase-G-rtk-observability.md` is marked SUPERSEDED
rather than deleted, to keep the planning record.

**Follow-ups not in scope:** `_emit_wrap_interrupted` was deleted as
dead code — its only caller was the `except KeyboardInterrupt` guarding
the binary download, so with no download there is nothing slow left to
interrupt.
2026-07-30 22:59:41 -07:00
Tejas Chopra
28aa53dc7c
chore: release main (#2339)
🤖 I have created a release *beep* *boop*
---


<details><summary>0.33.0</summary>

##
[0.33.0](https://github.com/headroomlabs-ai/headroom/compare/v0.32.0...v0.33.0)
(2026-07-29)


### Features

* **lossless:** factor shared directory prefix in the grep search fold
([#2547](https://github.com/headroomlabs-ai/headroom/issues/2547))
([7dc9a97](7dc9a978ca))
* **metrics:** record per-extension token savings
([#2371](https://github.com/headroomlabs-ai/headroom/issues/2371))
([02eb90f](02eb90f243))
* **opencode:** ship the transport plugin in pip installs
([#2601](https://github.com/headroomlabs-ai/headroom/issues/2601))
([f54f04f](f54f04f5bf))
* **opencode:** support Copilot subscription backend for headroom models
([#2441](https://github.com/headroomlabs-ai/headroom/issues/2441))
([#2445](https://github.com/headroomlabs-ai/headroom/issues/2445))
([9089e7f](9089e7f7d3))
* **proxy/hooks:** run fold-only (stream-safe) turn hooks on streaming
OpenAI chat
([#2549](https://github.com/headroomlabs-ai/headroom/issues/2549))
([a6d4921](a6d4921e82))
* **proxy/savings:** aggregate tool-schema savings into Metrics + all
reporting sinks
([#2546](https://github.com/headroomlabs-ai/headroom/issues/2546))
([9f1ffef](9f1ffefe83))
* **proxy:** label GitHub Copilot traffic as "copilot" in the outcome…
([#2377](https://github.com/headroomlabs-ai/headroom/issues/2377))
([d7a8cdb](d7a8cdbee1))
* **proxy:** make /v1/compress usable as a gateway/Kong sidecar
([#2458](https://github.com/headroomlabs-ai/headroom/issues/2458))
([1329ed7](1329ed7f1a))
* **proxy:** model-aware cold-prefix hook — reasoning compaction
(Kimi/GLM) + cold recompaction (CC)
([#2555](https://github.com/headroomlabs-ai/headroom/issues/2555))
([cb8f4b6](cb8f4b6436))
* **proxy:** route selected external compressors through the content
router
([#2388](https://github.com/headroomlabs-ai/headroom/issues/2388))
([e3c7964](e3c7964038))
* **proxy:** select built-in compressors via --compressor + registry
inventory
([#2373](https://github.com/headroomlabs-ai/headroom/issues/2373))
([56c7d4a](56c7d4a59e))
* **rust:** add structured prose offload plumbing
([#334](https://github.com/headroomlabs-ai/headroom/issues/334))
([#2378](https://github.com/headroomlabs-ai/headroom/issues/2378))
([9e07785](9e0778553f))
* **rust:** port CodeCompressor AST compressor to Rust (parity-only)
([#1154](https://github.com/headroomlabs-ai/headroom/issues/1154))
([e530de5](e530de5ad2))
* **rust:** port Kompress ML prose compressor to Rust (parity-only)
([#1153](https://github.com/headroomlabs-ai/headroom/issues/1153))
([83e27e5](83e27e5036))
* **telemetry:** record provider cache read/write/uncached tokens per
request
([#2450](https://github.com/headroomlabs-ai/headroom/issues/2450))
([bec4cce](bec4cce8a9))
* **transforms:** add compressed signal + dispatch code_aware/html/diff
via registry
([#2400](https://github.com/headroomlabs-ai/headroom/issues/2400))
([7ebda67](7ebda67ef6))
* **transforms:** add pluggable compressor registry +
headroom.compressor entry point
([#2370](https://github.com/headroomlabs-ai/headroom/issues/2370))
([a02073e](a02073e332))
* **transforms:** dispatch kompress/text via the compressor registry +
forward question
([#2411](https://github.com/headroomlabs-ai/headroom/issues/2411))
([446ec26](446ec26003))
* **transforms:** dispatch smart_crusher via the compressor registry
(defer kompress/text ML boundary)
([#2404](https://github.com/headroomlabs-ai/headroom/issues/2404))
([7c7bf43](7c7bf43057))
* **transforms:** make built-in compressors real Compressor
implementations (adapters)
([#2391](https://github.com/headroomlabs-ai/headroom/issues/2391))
([981616c](981616c60e))
* **wrap:** boost Serena — symbol-first guidance, wrap-time pre-index,
repo-language scoping
([#2425](https://github.com/headroomlabs-ai/headroom/issues/2425))
([fd0e1a8](fd0e1a8afe))
* **wrap:** default code-memory to Serena (dashboard browser off) behind
unified --code-memory
([#2413](https://github.com/headroomlabs-ai/headroom/issues/2413))
([6e4425a](6e4425a6bd))
* **wrap:** reduce-at-source — SAFE quiet-CLI env defaults for the
launched agent
([#2548](https://github.com/headroomlabs-ai/headroom/issues/2548))
([c990cfb](c990cfb803))


### Bug Fixes

* **backends/litellm:** guard None completion_tokens in usage mapping
([#2322](https://github.com/headroomlabs-ai/headroom/issues/2322))
([44a174f](44a174fef4))
* **backends:** don't crash the OpenAI-&gt;Anthropic converter on empty
choices
([#2484](https://github.com/headroomlabs-ai/headroom/issues/2484))
([43a7b57](43a7b578a1))
* **cache:** preserve cache_control ttl when re-anchoring a breakpoint
([#2651](https://github.com/headroomlabs-ai/headroom/issues/2651))
([e0d2cd0](e0d2cd0c5a))
* **cache:** preserve client cache_control ttl when consolidating
breakpoints
([#2382](https://github.com/headroomlabs-ai/headroom/issues/2382))
([8906d3a](8906d3a676))
* **ccr:** guard empty/malformed OpenAI choices in
_extract_assistant_message
([#2389](https://github.com/headroomlabs-ai/headroom/issues/2389))
([89319fb](89319fbcad))
* **ccr:** sliding idle-window TTL with max-lifetime ceiling in the Rust
core backends
([#2604](https://github.com/headroomlabs-ai/headroom/issues/2604))
([#2631](https://github.com/headroomlabs-ai/headroom/issues/2631))
([e825588](e825588bfb))
* **ci:** align Ruff tooling versions
([#2406](https://github.com/headroomlabs-ai/headroom/issues/2406))
([2bb14d1](2bb14d1ab2))
* **cli:** warn when Headroom proxy URL leaks into the shell after
unwrap claude
([#2238](https://github.com/headroomlabs-ai/headroom/issues/2238))
([#2571](https://github.com/headroomlabs-ai/headroom/issues/2571))
([904bc67](904bc675b3))
* **codex:** detect keyring-backed ChatGPT auth
([#2478](https://github.com/headroomlabs-ai/headroom/issues/2478))
([46293f4](46293f4daf))
* **compression:** report source-line span in CCR compression marker
([#2597](https://github.com/headroomlabs-ai/headroom/issues/2597))
([18e1c3c](18e1c3c9ba))
* **copilot:** derive GHE credential host from API URL
([#800](https://github.com/headroomlabs-ai/headroom/issues/800))
([#2511](https://github.com/headroomlabs-ai/headroom/issues/2511))
([4a8157f](4a8157fa0a))
* **copilot:** normalize subscription API routing
([#2441](https://github.com/headroomlabs-ai/headroom/issues/2441))
([#2455](https://github.com/headroomlabs-ai/headroom/issues/2455))
([2eca5ee](2eca5ee114))
* **copilot:** preserve /v1 for the Anthropic /v1/messages endpoint
([#2409](https://github.com/headroomlabs-ai/headroom/issues/2409))
([#2414](https://github.com/headroomlabs-ai/headroom/issues/2414))
([c400f90](c400f90810))
* **deps:** bump mcp to 1.28.1 to clear 3 high-severity CVEs
([#2348](https://github.com/headroomlabs-ai/headroom/issues/2348))
([a90be94](a90be94e32))
* **grok:** preserve business-seat auth while routing only inference
([#2514](https://github.com/headroomlabs-ai/headroom/issues/2514))
([e4076bb](e4076bbe99))
* **image:** reuse image models instead of rebuilding them per request
([#2513](https://github.com/headroomlabs-ai/headroom/issues/2513))
([#2536](https://github.com/headroomlabs-ai/headroom/issues/2536))
([2a63ec7](2a63ec70b6))
* **install:** carry upstream-routing env overrides into supervised
deployments
([#2429](https://github.com/headroomlabs-ai/headroom/issues/2429))
([170b04a](170b04a74d))
* **install:** default to cache mode, matching `headroom proxy`
([#1893](https://github.com/headroomlabs-ai/headroom/issues/1893)
follow-up)
([#2563](https://github.com/headroomlabs-ai/headroom/issues/2563))
([b121223](b121223ec9))
* **install:** migrate deployments off the retired chopratejas image
repo ([#2427](https://github.com/headroomlabs-ai/headroom/issues/2427))
([17ff13c](17ff13ccbe))
* **install:** use CREATE_NO_WINDOW instead of DETACHED_PROCESS on
Windows
([#2527](https://github.com/headroomlabs-ai/headroom/issues/2527))
([045f3df](045f3dfe6f))
* **kompress:** raise the default execution-slot wait
([#2456](https://github.com/headroomlabs-ai/headroom/issues/2456))
([5bd2266](5bd2266f16))
* **learn:** detect the active OpenCode database
([#2587](https://github.com/headroomlabs-ai/headroom/issues/2587))
([f74d874](f74d874777))
* **learn:** keep traceback tail in tool-error digest preview
([#2596](https://github.com/headroomlabs-ai/headroom/issues/2596))
([85e8699](85e8699451))
* **learn:** treat unreadable candidate paths as absent in project
decode
([#2446](https://github.com/headroomlabs-ai/headroom/issues/2446))
([a09ba6c](a09ba6c087))
* **mcp:** pin mcp dependency to &lt;2.0.0 to prevent server startup
crash ([#2642](https://github.com/headroomlabs-ai/headroom/issues/2642))
([b3f016b](b3f016b866))
* **proxy/cost:** count Gemini thinking tokens in output usage
([#2639](https://github.com/headroomlabs-ai/headroom/issues/2639))
([22b707f](22b707fd31))
* **proxy/cost:** record each request's savings exactly once (drop 3
double-counts)
([#2545](https://github.com/headroomlabs-ai/headroom/issues/2545))
([0845b26](0845b26ee6))
* **proxy/cost:** warn once per model when pricing lookup fails
([#2504](https://github.com/headroomlabs-ai/headroom/issues/2504))
([#2535](https://github.com/headroomlabs-ai/headroom/issues/2535))
([fa47637](fa4763761b))
* **proxy/gemini:** None-guard token counts from usageMetadata
([#2347](https://github.com/headroomlabs-ai/headroom/issues/2347))
([f64aac9](f64aac9733))
* **proxy/gemini:** tolerate malformed parts on the compression path
([#2486](https://github.com/headroomlabs-ai/headroom/issues/2486))
([07cf547](07cf547607))
* **proxy/metrics:** move the savings-ledger append off the event loop
([#2439](https://github.com/headroomlabs-ai/headroom/issues/2439))
([4aac068](4aac068814))
* **proxy/openai:** cache under looked-up messages
([#2420](https://github.com/headroomlabs-ai/headroom/issues/2420))
([7052d52](7052d52dcb))
* **proxy/openai:** don't record Codex WS savings without input
accounting
([#2493](https://github.com/headroomlabs-ai/headroom/issues/2493))
([2195ba7](2195ba7d91))
* **proxy/openai:** feed chat/completions traffic into the traffic
learner
([#2333](https://github.com/headroomlabs-ai/headroom/issues/2333))
([6cdfd3f](6cdfd3f64d))
* **proxy/openai:** None-guard usage token counts on the chat path
([#2431](https://github.com/headroomlabs-ai/headroom/issues/2431))
([313c290](313c290df9))
* **proxy/openai:** replay incremental events in buffered Responses SSE
([#2410](https://github.com/headroomlabs-ai/headroom/issues/2410))
([#2415](https://github.com/headroomlabs-ai/headroom/issues/2415))
([0cbc0e8](0cbc0e8e54))
* **proxy/output-shaping:** tolerate a non-string system block text in
steering
([#2435](https://github.com/headroomlabs-ai/headroom/issues/2435))
([3e97671](3e976712e7))
* **proxy/perf:** count turn-hook message folds in token accounting
([#2520](https://github.com/headroomlabs-ai/headroom/issues/2520))
([c371d5a](c371d5ad60))
* **proxy/perf:** tokenizer-consistent token accounting + surface
tool-schema savings
([#2542](https://github.com/headroomlabs-ai/headroom/issues/2542))
([1cc53c9](1cc53c9c92))
* **proxy/streaming:** tolerate malformed content in _response_to_sse
([#2481](https://github.com/headroomlabs-ai/headroom/issues/2481))
([77b26c0](77b26c093c))
* **proxy:** keep buffered CCR streams alive
([#2479](https://github.com/headroomlabs-ai/headroom/issues/2479))
([a2e42fb](a2e42fb877))
* **proxy:** keep core tools and the client's ToolSearch resident for
PascalCase clients
([#2647](https://github.com/headroomlabs-ai/headroom/issues/2647))
([1d29738](1d29738818))
* **proxy:** offload OpenAI and Gemini tokenizer counting off the event
loop ([#2498](https://github.com/headroomlabs-ai/headroom/issues/2498))
([806d2e4](806d2e468a))
* **proxy:** promote Kompress health after runtime load
([#2402](https://github.com/headroomlabs-ai/headroom/issues/2402))
([54526bc](54526bc858))
* **proxy:** reassemble server_tool_use.input from streamed partial_json
([#2449](https://github.com/headroomlabs-ai/headroom/issues/2449))
([8c8fae0](8c8fae0d0b))
* **proxy:** report deferred Kompress status and promote health from
cache ([#2564](https://github.com/headroomlabs-ai/headroom/issues/2564))
([d50cfab](d50cfabedc))
* **proxy:** skip max_tokens rename for backend-routed openai chat
([#2401](https://github.com/headroomlabs-ai/headroom/issues/2401))
([d6a1af4](d6a1af40d5))
* **release:** publish Windows wheel + sdist (disable PyPI attestations,
[#112](https://github.com/headroomlabs-ai/headroom/issues/112))
([#2405](https://github.com/headroomlabs-ai/headroom/issues/2405))
([f9cbdd6](f9cbdd6e39))
* **release:** sync generated version metadata on the release branch
([#2659](https://github.com/headroomlabs-ai/headroom/issues/2659))
([5383c6b](5383c6bf2f))
* **rust:** port CJK-aware relevance-query matching to CodeCompressor
([#2634](https://github.com/headroomlabs-ai/headroom/issues/2634))
([e86c639](e86c6390ce))
* **security:** exclude compromised ast-grep-cli 0.44.1 (supply-chain
trojan)
([#2342](https://github.com/headroomlabs-ai/headroom/issues/2342))
([494fb5a](494fb5a60e))
* **tokenizers:** price Claude against a real BPE (tiktoken o200k) not a
char estimate
([#2543](https://github.com/headroomlabs-ai/headroom/issues/2543))
([285176b](285176be54))
* **transforms/cross-turn-dedup:** don't renumber-fold zero-padded line
prefixes
([#2369](https://github.com/headroomlabs-ai/headroom/issues/2369))
([f4070c4](f4070c44cb))
* **transforms/kompress-remote:** keep compress fail-open on malformed
200 ([#2320](https://github.com/headroomlabs-ai/headroom/issues/2320))
([b759990](b75999017f))
* **wrap:** emit bare dotted keys for Codex --config overrides
([#2383](https://github.com/headroomlabs-ai/headroom/issues/2383))
([f57e959](f57e959a50))
* **wrap:** make RTK opt-in (off by default) across wrap subcommands
([#2344](https://github.com/headroomlabs-ai/headroom/issues/2344))
([44136ed](44136ed042))
* **wrap:** skip Serena project setup outside real project roots
([#2574](https://github.com/headroomlabs-ai/headroom/issues/2574))
([0994ea0](0994ea04c8))
* **wrap:** stop same-port persistent routing during claude unwrap
([#2340](https://github.com/headroomlabs-ai/headroom/issues/2340))
([#2350](https://github.com/headroomlabs-ai/headroom/issues/2350))
([cf5fa64](cf5fa644b6))


### Performance Improvements

* **content_router:** dedupe content detection
([#2419](https://github.com/headroomlabs-ai/headroom/issues/2419))
([9b016f2](9b016f2b64))


### Dependencies

* bump the cargo-minor-patch group with 10 updates
([#2284](https://github.com/headroomlabs-ai/headroom/issues/2284))
([3266ed7](3266ed7641))
* bump the npm-minor-patch group across 3 directories with 7 updates
([#2276](https://github.com/headroomlabs-ai/headroom/issues/2276))
([961866b](961866ba7c))


### Code Refactoring

* **transforms:** dispatch simple built-in strategies via the compressor
registry
([#2399](https://github.com/headroomlabs-ai/headroom/issues/2399))
([fc9c63f](fc9c63f18c))
* **wrap:** retire tokensave; Serena is the code-memory MCP
([#2499](https://github.com/headroomlabs-ai/headroom/issues/2499))
([5d23a0a](5d23a0aec2))
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-29 15:54:23 -07:00
Tejas Chopra
5383c6bf2f
fix(release): sync generated version metadata on the release branch (#2659)
## Description

The 0.33.0 release PR (#2339) has sat in `changes-requested` since
2026-07-17. Root cause: **release-please only rewrites `pyproject.toml`
and its configured `extra-files`**, but other tracked files also carry
the version — and `server.json` is asserted byte-for-byte against
`render_server_json()`, which derives its version from `pyproject.toml`.
So the bump alone fails
`tests/test_mcp_registry/test_server_json.py::test_root_server_json_matches_builder`
(the `test (2)` shard) on every regenerated release PR.

Nothing in the repo regenerated `server.json` at all, so it fell behind
every release.

Unblocks #2339.

### Why the release *build* passes but the release PR does not

`release.yml` already runs `scripts/version-sync.py` immediately before
its own `verify-versions.py` gate (lines 145 and 278). That is why
`build` and `build-wheels` are green on #2339 despite the drift — it
syncs in the workspace, uncommitted. The regular CI test job does
**not** sync, so the fix has to be committed to the branch.

This also explains why reviewers kept seeing `verify-versions.py` fail
locally while CI's build jobs passed: the verifier is never run
un-synced inside `release.yml`.

## 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

- **`scripts/version-sync.py`**: also write `server.json`. It was the
one version-carrying file with no writer anywhere. Values are rewritten
in place so key order and formatting keep matching the builder's
byte-for-byte output (verified: the file is pure ASCII and round-trips
exactly through `json.dumps(..., indent=2) + "\n"`).
- **`.github/workflows/release-metadata-sync.yml`** (new): on a push to
`release-please--branches--**`, run version-sync → gate on
verify-versions → commit if changed.
- **Keyed off the branch push** because release-please force-regenerates
that branch on every merge to main. That is precisely what wiped the
hand-pushed metadata fixes on #2339 (`2a86c8ff`, `d5ea4dc5`) — a push
trigger re-heals after every regeneration instead of being lost.
- **Uses the same PAT as `release-please.yml`**: a `GITHUB_TOKEN` push
does not trigger workflows, so the release PR's checks would never
re-run against the synced commit and would stay red.
- **Idempotent**: the self-triggered rerun finds no diff and exits
before pushing, so the loop terminates after one no-op run.
- **Corrected pre-existing drift on `main`**: the agent-hooks plugin
manifests, both marketplace manifests, and `.releasemetadata` were
stranded at **0.31.0** — never bumped for 0.32.0 either.
`verify-versions.py` now passes on `main`.

### Why not more `extra-files` entries

That would need ~13 jsonpath entries restating what `version-sync.py`
already knows, and a jsonpath that fails to match **fails silently** —
the same class of failure this PR removes, discoverable only after a
real release PR regenerates. There is also no precedent for nested
jsonpath (`$.packages[0].version`, `$.metadata.version`) in the config
today; both existing entries are plain `$.version`. Running the script
keeps one source of truth, and files added to it later are covered with
no change here.

## Testing

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

### Test Output

```text
$ python -m pytest scripts/tests/ tests/test_release_workflows.py tests/test_mcp_registry/ -q
207 passed in 2.69s

$ ruff check scripts/version-sync.py scripts/tests/test_version_sync.py tests/test_release_workflows.py
All checks passed!
$ ruff format --check <same>
3 files already formatted

$ actionlint .github/workflows/release-metadata-sync.yml
(clean)
```

New tests:
- `test_server_json_version_is_synchronized` — version-sync moves both
`server.json` version fields and preserves the other keys.
- `test_release_metadata_sync_runs_on_release_please_branch` — asserts
the trigger, the sync→verify→commit ordering, the no-op guard, and the
PAT.
- `test_version_sync_covers_every_file_the_verifier_gates` — guards
`version-sync.py` and `verify-versions.py` against drifting apart again,
which is the root cause here.

## Real Behavior Proof

- **Environment:** macOS (Darwin arm64), Python 3.12, repo venv.
- **Exact command / steps:** reproduced the CI failure locally by
simulating release-please's partial bump, then applying the fix.

**Reproducing the exact `test (2)` failure** — set `pyproject` to 0.33.0
while `server.json` stays at 0.32.0, as release-please leaves it:

```text
$ python -m pytest tests/test_mcp_registry/test_server_json.py -q
FAILED tests/test_mcp_registry/test_server_json.py::test_root_server_json_matches_builder
1 failed, 3 passed
```

**After `version-sync.py`:**

```text
$ python scripts/version-sync.py && python -m pytest tests/test_mcp_registry/test_server_json.py -q
4 passed
```

**Both gates green on a simulated 0.33.0 bump:**

```text
$ python scripts/version-sync.py --version 0.33.0
Version synchronized to 0.33.0
$ python scripts/verify-versions.py
All versions aligned at 0.33.0
$ python -m pytest tests/test_mcp_registry/test_server_json.py -q
4 passed
```

**Idempotency** (the property the workflow's loop-termination relies
on): re-running against an already-synced tree leaves `pyproject.toml`,
`server.json`, `openclaw`, and `sdk/typescript` untouched.

- **Not tested:** the workflow has not executed on a real release-please
branch regeneration — that can only be exercised once this is on `main`
and release-please next updates #2339. The PAT push path and the
self-trigger no-op are reasoned from `release-please.yml`'s existing
token comment and from local idempotency, not observed in CI.

## 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
- [ ] 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 did **not** edit `CHANGELOG.md`

## Additional Notes

**Context on the v0.32.0 release failure, since it is easy to misread as
"images never build".** Every artifact built for v0.32.0 — all 5 wheel
platforms including Windows, all 16 Docker builds + 8 manifests +
`promote-latest`, npm, and GitHub Packages. Only `publish-pypi` failed
(PyPI attestations, already fixed by `f9cbdd6e` / #2405), and
`create-release` was skipped because it depends on it. That is why the
release looked like it produced nothing.

**Separate, approaching blocker — not addressed here.** PyPI is at
**9.69 GB of its 10 GB project cap (96.9%)**, leaving ~305 MB against
~68 MB per release, so roughly 4 more releases fit. The `0.21.x` series
alone holds **6.58 GB across 31 releases**, from the old
every-push-is-a-release era; pruning it would reclaim two thirds of the
quota. Worth a separate issue.

**`.releasemetadata` is written but never read** by anything outside
`version-sync.py` and its test. It is kept in sync here for internal
consistency, but it may be a deletion candidate.
2026-07-29 15:12:04 -07:00
Tejas Chopra
a30305bc4c
ci: require ONNX Runtime >= 1.24 and fail fast when it is missing or too old (#2591)
CI installed 'onnxruntime>=1.16.0' for the Rust ort runtime. That floor is 8
minor versions too low, and the failure mode below it is a silent hang.

Why 1.24: ort-sys computes ORT_API_VERSION = 17 + one per enabled api-N feature,
and Cargo features are additive across the graph. fastembed 5.17.3 enables
api-24, so the constant resolves to 24 and ort rejects any lower runtime.

Why it hangs instead of failing: on rejection ort calls Error::new() from inside
load_dylib_from_path, which already runs inside the Once that setup_api() is
initialising. Building the error re-enters that Once, and std::sync::Once blocks
forever on re-entry. Reproduced in isolation with a bare Session::builder() and
onnxruntime 1.21.1 - killed after >1h at 0% CPU, no output; ORT_DYLIB_PATH makes
no difference. With 1.24.4 the same call returns in 1.2s and the kompress parity
fixtures pass 21/21.

Per @RubenAAA this is not limited to old runtimes: a box that resolves no
libonnxruntime at all hangs identically (0.0% CPU, threads in futex_wait_queue,
nothing onnx-shaped in /proc/<pid>/maps). Any failure inside
load_dylib_from_path re-enters the Once, so a pin alone cannot close it.

So this adds a pre-flight to the dylib step asserting, before any test runs,
that onnxruntime imports, that its minor is >= 24, and that a libonnxruntime
object exists under capi/. Each failure exits 1 with an ::error:: annotation
naming the cause, instead of burning the 30-minute timeout with an empty log.

Verified all three branches locally (absent -> rc=1, 1.21.1 -> rc=1,
1.24.4 -> rc=0) and in CI, where it resolved onnxruntime 1.28.0 and exported
the .so path.

pyproject.toml is deliberately untouched: bumping the floor there makes
headroom-ai[all] unsatisfiable via a pillow chain (onnxruntime>=1.24 forces
pillow>=10.3.0,<12.0 while [all] requires pillow>=12.3.0). The user-facing
hazard via headroom/_ort.py remains open and needs its own change.
2026-07-27 08:04:53 -07:00
Tejas Chopra
e562d007d8
ci(rust): gate jobs with if: instead of a workflow-level paths filter (#2580)
Prerequisite for making `parity` a required status check on main.

A workflow skipped by a top-level `paths:` filter never creates its check runs
at all, so a required check sourced from it sits pending forever on any PR that
misses those paths and the PR can never merge. A job skipped by `if:` still
creates a check run, reports skipped, and GitHub counts skipped as success.

Moves the seven path patterns verbatim from the `on:` block into a new
`rust-changes` job (dorny/paths-filter) and gates all five existing jobs on
`needs.rust-changes.outputs.rust == 'true'`. Named rust-changes, not changes,
to avoid colliding with ci.yml's existing check. Job `name:` fields unchanged,
so no check names move. schedule/workflow_dispatch force rust=true, preserving
the nightly full-suite behaviour.

CI spend unchanged: same jobs on the same PRs, plus a ~15s gate job on PRs that
previously skipped the workflow outright.

Verified by the PR itself — it edits rust.yml, which is in the path list, so it
exercises the rust=true branch: rust-changes and parity both SUCCESS.

Follow-up before adding parity to required checks: confirm a non-Rust PR reports
parity as skipped rather than absent. ci.yml has the mirror-image problem
(paths-ignore on docs) and is deliberately not addressed here.
2026-07-27 07:34:00 -07:00
gglucass
f54f04f5bf
feat(opencode): ship the transport plugin in pip installs (#2601)
## Description

The OpenCode transport plugin - the piece that gives `wrap opencode`
all-provider routing by tagging each request with `x-headroom-base-url`
- only exists in repo checkouts today. `headroom_opencode_plugin_path()`
resolves `plugins/opencode/dist/entry.opencode.js`, which pip wheels do
not ship, so every pip install silently degrades to the two-provider
(anthropic/openai) baseURL fallback. The function's own docstring
documents the gap ("a pip-only install that does not ship `plugins/`").

Shipping the existing build output is not enough: the regular tsup build
leaves `headroom-ai` and `@opencode-ai/plugin` as bare external imports,
which only resolve next to the checkout's `node_modules`. Copied into
site-packages, the file fails to load. This PR ships a self-contained
bundle inside the wheel instead.

Closes #

## 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

- `plugins/opencode/tsup.standalone.config.ts` + `npm run
build:standalone`: a second build of the loader entry with `noExternal:
[/.*/]` and `splitting: false` - a single self-contained file whose only
imports are node builtins.
- `headroom/providers/opencode/_dist/entry.opencode.js`: the committed
standalone bundle (452 KB). It sits inside the package directory, so
maturin's `python-source = "."` packaging picks it up into the wheel
with no build-system changes.
- `headroom_opencode_plugin_path()`: falls back to the packaged bundle.
Precedence otherwise unchanged: `HEADROOM_OPENCODE_PLUGIN_PATH` env
override, then a repo-checkout build (fresher during development), then
the packaged bundle.
- CI (`opencode-plugin.yml`): rebuilds the standalone bundle and fails
the run if the committed artifact drifted from source, with a one-line
fix instruction; workflow path triggers extended to
`headroom/providers/opencode/_dist/**`.
- `tests/test_providers_opencode_plugin_path.py`: packaged bundle exists
and is self-contained (no bare npm imports), env override wins, fallback
resolution order.

## 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
$ uv run --frozen --extra dev pytest tests/test_providers_opencode_plugin_path.py \
    tests/test_providers_opencode_config.py tests/test_providers_opencode_install.py
============================== 49 passed in 0.31s ==============================

$ uvx ruff check headroom/providers/opencode/runtime.py tests/test_providers_opencode_plugin_path.py
All checks passed!
$ uvx ruff format --check headroom/providers/opencode/runtime.py tests/test_providers_opencode_plugin_path.py
2 files already formatted
$ uv run --frozen --extra dev mypy headroom/providers/opencode/runtime.py
Success: no issues found in 1 source file

$ cd plugins/opencode && npm run build:standalone
ESM dist-standalone/entry.opencode.js 452.28 KB
ESM Build success in 28ms
```

## Real Behavior Proof

- Environment: macOS 15 (arm64), opencode 1.18.5 (Homebrew), node 22 /
npm 10, isolated `XDG_*` dirs so no real user config was touched.
- Exact command / steps:
  1. `npm run build:standalone` in `plugins/opencode`.
2. Started a local header-logging HTTP listener on `127.0.0.1:9977`
(stands in for the proxy; logs method, path, headers, returns 401).
3. Registered the standalone bundle by absolute path in a scratch
`opencode.json` (`"plugin":
["<abs>/dist-standalone/entry.opencode.js"]`) with a `google` provider
entry and a fake API key. Note: the bundle's directory has **no**
`node_modules` - this is exactly the site-packages situation.
4. `HEADROOM_PROXY_URL=http://127.0.0.1:9977 opencode run -m
google/gemini-2.5-flash "say hi"`.
- Observed result: the listener received `POST
/v1beta/models/gemini-2.5-flash:streamGenerateContent?alt=sse` with
`User-Agent: opencode/1.18.5 ...` - i.e. the plugin loaded standalone
and rerouted a provider that the baseURL fallback cannot cover (native
Gemini wire format) to the proxy URL from `HEADROOM_PROXY_URL`.
- Not tested: Windows path resolution (pure `pathlib`, no platform
branches); wheel-build byte-determinism of the tsup output across OSes
(the CI drift check will surface it on the first divergent build).

## 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
- [ ] 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 did **not** edit `CHANGELOG.md` — it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)

## Screenshots (if applicable)

Not applicable - CLI/packaging change.

## Additional Notes

- Documentation checklist item: unchecked because the only doc surface I
found is the `headroom_opencode_plugin_path()` docstring, which this PR
rewrites to describe the three-step resolution order. Happy to add a
line to `docs/content/docs/` if there is a preferred page.
- A committed build artifact is not free: the CI drift check keeps it
honest, and the byte-compare relies on tsup/esbuild determinism under
`npm ci` (pinned lockfile). If you'd rather avoid the committed artifact
entirely, the alternative is publishing `headroom-opencode` to npm (its
`package.json` is publish-ready) and registering the plugin by package
name - happy to rework in that direction; the wheel-bundled path has the
advantage of version-locking the plugin to the backend it ships with.
- Downstream motivation: Headroom Desktop manages a long-lived shared
proxy (no `wrap` launcher) and wants to register this plugin from the
installed wheel path so OpenCode users get all-provider routing there
too.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 06:40:43 -07:00
Tejas Chopra
c15e557da1
ci(parity): make the parity harness a real per-PR gate (#2567)
Three hardening steps on the Rust-vs-Python parity harness:

- Drop the dead maturin/venv step. headroom-parity has no pyo3 dependency, so
  the venv requirement, the `maturin develop` rebuild, and the CI job's Python
  toolchain were all overhead. Verified no-op: identical report, exit 0.
- Register a text_crusher comparator. 6 recorded fixtures were invisible because
  the transform was missing from builtin_comparators(); parity-run only walks
  directories it has a comparator for. All 6 match on the first run.
- Promote parity to a blocking per-PR gate. Safe to harden now because
  parity-run exits non-zero only on a Diff, so the 65 still-stubbed fixtures
  report Skipped and cannot turn it red.

Harness: total=176 matched=111 skipped=65 diffed=0, exit 0.

Deliberately not widening the path filter to Python paths: the fixtures are
frozen recordings of Python output and the harness never invokes Python, so it
measures Rust-vs-snapshot and a Python edit cannot move the result.
2026-07-26 11:00:14 -07:00
Tejas Chopra
f9cbdd6e39
fix(release): publish Windows wheel + sdist (disable PyPI attestations, #112) (#2405)
Automated by Headroom + Kimi (Fireworks) in a Modal Sandbox.

Request:
Make ONLY this one change, nothing else: in
.github/workflows/release.yml, in the publish-pypi job's step that uses
pypa/gh-action-pypi-publish, add exactly one line 'attestations: false'
immediately after the 'skip-existing: true' line in its with: block
(same indentation). Do NOT edit any other file, do NOT investigate the
codebase, do NOT make any other change. Then finish.

Co-authored-by: Headroom Kimi <kimi@headroom.dev>
2026-07-18 20:56:31 -07:00
Floze
2bb14d1ab2
fix(ci): align Ruff tooling versions (#2406)
## Description

Ruff currently has three independent versions: `uv.lock` resolves
`0.14.14`, pre-commit runs `0.9.4`, and CI installs `0.15.17`.
Contributors can therefore pass one formatter path and fail another.

Make the exact Ruff pin in `pyproject.toml` the source of truth, align
the lockfile and pre-commit hook to it, and make CI read that pin
through a deterministic consistency verifier instead of carrying another
hardcoded version.

Closes #2398

## 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 causes existing functionality
to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- Pin the existing `[dev]` Ruff dependency to `0.15.17`, the formatter
baseline already used by CI.
- Refresh only Ruff in `uv.lock` with `uv 0.11.29`.
- Align `ruff-pre-commit` to `v0.15.17`.
- Add `scripts/verify-ruff-version.py` and run it from pre-commit and
CI.
- Make CI install the verified version read from `pyproject.toml` rather
than a separate literal.

## Testing

- [ ] Unit tests pass (`pytest`) — not run; no runtime source or test
behavior changed.
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New deterministic guard proves the configuration fix
- [x] Manual testing performed

### Test Output

```text
# Before: run the verifier with the patched pyproject pin but base-branch
# uv.lock, pre-commit config, and workflow.
Ruff version mismatch detected:
  uv.lock uses Ruff 0.14.14, expected 0.15.17
  .pre-commit-config.yaml uses Ruff 0.9.4, expected 0.15.17
  ci.yml does not run 'python scripts/verify-ruff-version.py --print-version'
  ci.yml does not install Ruff from 'steps.ruff-version.outputs.version'

$ python3 scripts/verify-ruff-version.py
Ruff versions aligned at 0.15.17

$ uvx uv@0.11.29 lock --check
Resolved 269 packages

$ uvx uv@0.11.29 tree --locked --package ruff
ruff v0.15.17

$ uvx ruff@0.15.17 check .
All checks passed!

$ uvx ruff@0.15.17 format --check .
1322 files already formatted

$ uvx mypy@1.20.2 headroom --ignore-missing-imports
Success: no issues found in 505 source files

$ uvx --with tomli mypy@1.20.2 scripts/verify-ruff-version.py --ignore-missing-imports
Success: no issues found in 1 source file

$ uvx pre-commit run ruff --all-files
Passed
$ uvx pre-commit run ruff-format --all-files
Passed
$ uvx pre-commit run verify-ruff-version --all-files
Passed
```

## Real Behavior Proof

- Environment: macOS 26.5, Python 3.14.6 locally; Python 3.10 fallback
also exercised; `uv 0.11.29`, Ruff `0.15.17`, mypy `1.20.2`.
- Exact command / steps: reproduced the mismatch using the base branch's
real `uv.lock`, `.pre-commit-config.yaml`, and
`.github/workflows/ci.yml`; then ran the verifier, locked Ruff tree,
full Ruff check/format, mypy, and actual pre-commit hooks after the
patch.
- Observed result: the base state fails with all four drift points
listed; the patched state reports one aligned Ruff version (`0.15.17`)
and every formatter path passes.
- Not tested: runtime proxy behavior and the pytest suite, because the
change is limited to development-tool configuration, lock metadata,
pre-commit, and CI wiring.

## Dependency / Supply-Chain Justification

- Ruff is an existing development-only formatter maintained by Astral;
this PR adds no new package.
- `0.15.17` is required to fix local/CI reproducibility and has already
been the repository's CI formatter baseline since #1295.
- Install surface is limited to the `[dev]` extra, lint CI job, and
pre-commit environment. Production/runtime dependencies are unchanged.
- The `uv.lock` refresh updates only Ruff; no unrelated dependency
upgrades are included.

## 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 the non-obvious consistency checks
- [x] Documentation changes are N/A; contributor commands are unchanged
- [x] My changes generate no new warnings
- [x] The guard fails on the real base-state mismatch and passes after
the fix
- [ ] New and existing unit tests pass locally — not run; no runtime
code changed
- [x] I did not edit `CHANGELOG.md`; release-please will use the
conventional PR title

## Additional Notes

No formatter-driven source changes are included. AI assistance was used
to inspect configuration, implement the verifier, and run validation.
2026-07-18 20:55:20 -07:00
Tejas Chopra
e7340aee65
ci(changelog): stop the CHANGELOG cascade — release-please owns it (#2329)
## Description

Stops the recurring **CHANGELOG cascade** where several concurrent PRs
all edit `CHANGELOG.md`'s `## Unreleased` section, so the first to merge
turns the rest `DIRTY` — forcing a serial rebase-per-merge grind.

Root cause: **release-please already generates `CHANGELOG.md`** from
Conventional Commit titles (see `.release-please-config.json`
`changelog-sections`), so the hand-written `## Unreleased` entries are
*both redundant with release-please and the sole source of the
conflicts*. The PR template and CONTRIBUTING were actively telling
contributors to keep hand-editing it. `.gitattributes merge=union`
(#2138) does not help — GitHub squash-merge ignores merge drivers.

Fix: make release-please the only author of the changelog and stop
hand-edits at the source.

Closes #

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue) — CI/process

## Changes Made

- **`.github/workflows/changelog-guard.yml`** (new): fails any PR that
modifies `CHANGELOG.md`, except release-please's own release PR (head
branch `release-please--*`). Uses the preinstalled `gh` CLI — no
third-party action to pin.
- **`.github/PULL_REQUEST_TEMPLATE.md`**: flips the "I have updated the
CHANGELOG.md" checkbox to "I did **not** edit CHANGELOG.md —
release-please generates it from my Conventional Commit PR title."
- **`CONTRIBUTING.md`**: replaces step 6 ("Update `CHANGELOG.md`") with
the release-please policy.

## Testing

- [x] `actionlint .github/workflows/changelog-guard.yml` — clean
- [x] YAML parses (`yaml.safe_load`)
- [x] Guard match logic unit-checked locally: fires on root
`CHANGELOG.md`, ignores nested paths (`docs/CHANGELOG.md`) and PRs that
don't touch it.

### Test Output

```text
YAML OK
actionlint OK
root CHANGELOG.md         -> FIRES
nested docs/CHANGELOG.md  -> pass
no changelog              -> pass
```

## Real Behavior Proof

- Environment: local clone + `actionlint`.
- Exact steps: created the workflow, validated with actionlint + a YAML
parse, and exercised the `grep -qx 'CHANGELOG.md'` decision against
representative changed-file lists.
- Observed result: guard fires only on a root-level `CHANGELOG.md` edit;
the release-please branch is exempted via the job-level `if`.
- Not tested: a live PR run in Actions (will exercise on this PR itself
— note this PR does **not** touch `CHANGELOG.md`, so the guard should
pass green here).

## Additional Notes

- To *enforce* (block merge, not just show red), add **Changelog Guard /
no-manual-changelog** to the branch's required status checks.
- Follow-up: the ~10 currently-open PRs that hand-edit `CHANGELOG.md`
will need that edit removed (a clean deletion now, not a conflict
resolution) before they merge — after which the cascade is gone for
good.
2026-07-16 14:28:57 -07:00
Ashish
46d4378cf7
feat(evals): weekly HotpotQA answer-recall report on the prose path (#1188)
## Description

Follow-up to **#1187** (the offline fidelity gate). That gate is
hermetic and **structured-only** (JSON tool outputs via Rust
compressors) so it can block every PR with zero setup. This PR adds the
genuinely-uncovered piece: **prose answer-recall on a real dataset
(HotpotQA)** in the **model-allowed weekly job**, where compression
routes through Kompress (ModernBERT).

> **Stacked on #1187.** Until that merges, this PR's diff shows its
commit too; it reduces to just `c71cc0cb` once #1187 lands. Please
review/merge #1187 first.

Closes #

## 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

- **`CompressionOnlyRunner.evaluate_dataset_recall(suite)`**: for each
QA case, compress the supporting `context` via the production routing
path (`ContentRouter`) and check the `ground_truth` answer survives
(`compute_information_recall`). Counts only **probeable** cases — answer
literally present in the context and non-trivial (skips `yes/no`,
too-short) — so the aggregate is meaningful rather than inflated by
un-measurable cases.
- **`.github/workflows/eval.yml`**: a non-blocking step in the existing
`weekly-suite` job (schedule/manual only) drives it with
`load_hotpotqa(n=50)`. Defensive: a dataset download or model failure
emits `:⚠️:` and `|| true`, never failing the job.
- **Hermetic unit test** (`tests/test_dataset_recall_runner.py`):
exercises the method with synthetic JSON-array contexts (SmartCrusher /
Rust — no model, no network), so it runs in the standard `[dev]` shard.

### Scope notes

- **Prose path only.** BFCL / tool-schema integrity is already covered
by the existing `evaluate_tool_schema_compaction` eval (which runs in
the PR smoke-test), so this targets the previously-uncovered prose
recall path. NQ is an easy further extension using the same method +
`load_natural_questions`.
- **Why weekly, not per-PR.** Real datasets need a network download +
the ModernBERT model. The `weekly-suite` job already installs `[all]`
and genuinely runs every Monday (verified: 5 consecutive successful
scheduled runs), so it's the correct home — keeping PR CI fast and
hermetic.

## 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
$ HF_HUB_OFFLINE=1 python -m pytest tests/test_dataset_recall_runner.py -q
..                                                                       [100%]
2 passed in 0.20s
```

## Real Behavior Proof

- Environment: local checkout of `feat/weekly-dataset-recall`, `pip
install -e ".[dev]"`, `HF_HUB_OFFLINE=1` (proves the unit tests need no
model/network)
- Exact command / steps: `HF_HUB_OFFLINE=1 python -m pytest
tests/test_dataset_recall_runner.py -q` -> `6 passed in 0.36s`; coverage
JSON confirms the runner's per-case exception handler and both
`warm_kompress_model` outcomes are exercised
- Observed result: with a synthetic suite of 3 cases (one probeable
answer in an error row, one trivial `yes`, one absent answer),
`evaluate_dataset_recall` counts only the 1 probeable case (`passed=1`,
`accuracy_rate=1.0`, `benchmark="dataset_recall:synthetic"`); a
monkeypatched compressor crash records the error and counts the case
failed instead of aborting; the new weekly-suite YAML step parses via
`yaml.safe_load` and sits under the `schedule || workflow_dispatch`
guard
- Not tested: the live HotpotQA download + ModernBERT compression --
exercised only by the weekly job (or `workflow_dispatch`), by design

## 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
- [ ] I have updated the CHANGELOG.md if applicable

## Additional Notes

- CHANGELOG/version intentionally untouched: repo uses
**release-please**.
- The weekly job can be triggered on demand via **workflow_dispatch** to
see the HotpotQA recall numbers without waiting for Monday.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: JD Davis <mxjerrett@gmail.com>
2026-07-15 21:40:55 +00:00
ninosat00
5709291914
chore(release): harden local artifact smokes (#1824)
## Description

Harden the release artifact workflow so maintainers can reproduce npm
and Python release checks locally and so OpenClaw packaging does not
depend on a not-yet-published SDK version. This follow-up also keeps the
OpenClaw loader export contract explicit and updates the PR after the
branch was merged with current `headroomlabs/main`.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [x] New feature (non-breaking change that adds functionality)
- [x] Documentation update

## Changes Made

- Added reusable local release smoke scripts for npm assets, Python
wheel/sdist artifacts, and the combined release gate.
- Switched the release workflow npm asset build to the reusable npm
builder.
- Made OpenClaw release asset building install against the just-built
local SDK tarball before rewriting packed metadata to the release
dependency range.
- Kept the OpenClaw source dependency registry-installable at
`headroom-ai@^0.22.3` while the packed artifact still ships
`^<release-version>`.
- Added `registerHeadroomPlugin` as a named export while preserving the
default `{ register }` OpenClaw loader contract.
- Added Windows development bootstrap docs/script and regression tests
for version sync, npm asset ordering, OpenClaw source installability,
and Python wheel smoke import isolation.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] New tests added for new functionality
- [x] Manual testing performed

### Test Output

```text
uv run --with pytest python -m pytest tests/test_release_workflows.py scripts/tests/test_version_sync.py -q
44 passed, 1 warning

node --check scripts/build_npm_release_assets.mjs
node --check scripts/verify_npm_release_assets.mjs

python -m py_compile scripts/build_python_release_smoke.py scripts/release_smoke_all.py scripts/version-sync.py
python scripts/verify-versions.py
All versions aligned at 0.31.0

npm ci (plugins/openclaw)
added 88 packages, audited 89 packages, found 0 vulnerabilities

python scripts/release_smoke_all.py --out release-assets-local/all-pr1824-postmerge-fixed-20260710-104739
Verified npm release assets for 0.31.0
wheel metadata OK: headroom_ai-0.31.0-cp310-abi3-win_amd64.whl contains headroom/_core.pyd
sdist License-File metadata OK: ['LICENSE', 'NOTICE']
smoke-import OK: version=0.31.0 hello=headroom-core
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.14.3, Node v24.14.0, npm 11.9.0, uv
0.11.15, Rust/Cargo already installed in the local development
environment.
- Exact command / steps: Ran `python scripts/release_smoke_all.py --out
release-assets-local/all-pr1824-postmerge-fixed-20260710-104739` after
syncing this branch with current `headroomlabs/main`.
- Observed result: The command built `headroom-ai-0.31.0.tgz`,
`headroom-openclaw-0.31.0.tgz`,
`headroom_ai-0.31.0-cp310-abi3-win_amd64.whl`, and
`headroom_ai-0.31.0.tar.gz`; npm verifier passed; the wheel installed
into a fresh venv and imported `headroom._core`.
- Not tested: Full GitHub Actions release matrix and publish jobs with
real PyPI/npm/GitHub Packages credentials.

## 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
- [ ] 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
- [ ] I have updated the CHANGELOG.md if applicable

## Screenshots (if applicable)

N/A.

## Additional Notes

The post-merge full smoke initially failed because the OpenClaw source
package depended on `headroom-ai@^0.31.0`, which is not available on
public npm yet. The builder now installs OpenClaw against the just-built
local SDK tarball before build/pack, and then rewrites packed metadata
to `^0.31.0`.

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-07-14 16:07:34 -04:00
Ashish Patel
7ab83c5107
fix(router): stop protecting passing build/test output as error traces (#1740)
## Description

![Error output protection false-positive
fix](https://raw.githubusercontent.com/ashishpatel26/headroom/fix/1696-error-protection-false-positive/.github/pr-images/issue-1696-error-protection-fix.svg)

`content_has_strong_error_indicators()`
(`headroom/transforms/error_detection.py`) protects any
message/content-block from compression when it contains 2+ distinct
indicator keywords (`error`, `fail`, `exception`, `traceback`, `fatal`,
`panic`, `crash`). That heuristic false-positives on **passing**
build/test tool output: `tsc`'s `"Found 0 errors"` plus a passing test
run's `"0 failures"` trips both `error` and `fail` — 2 distinct hits —
despite nothing failing. In a long JS/TS coding session this fired on
nearly every request (confirmed against the `stats.json` attached to
#1696), permanently protecting legitimate tool output from ever being
compressed and explaining the reported 0.3% savings vs. the advertised
60-95%.

Closes #1696

## 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

- `headroom/transforms/error_detection.py`: strip common zero-result
phrases before the 2-keyword scan in
`content_has_strong_error_indicators()` — both `"N word"`/`"word N"`
forms (`"0 errors"`, `"no failures"`) and
`"label:value"`/`"label=value"` forms (`"Failures: 0"`, `"errors=0"`),
covering `error(s)` and `fail`/`failed`/`failing`/`failure(s)` (the scan
matches `fail` by substring, so all inflections needed covering).
- `tests/test_error_detection.py` (new file — no prior coverage
existed): 7 tests covering real error/traceback detection,
single-keyword safety, `tsc`/`eslint` passing summaries, the `"0
failed"` regression a reviewer caught, label:value formats, and that a
genuine second indicator elsewhere in the same blob still triggers
protection.
- `.github/pr-images/issue-1696-error-protection-fix.svg`: diagram
explaining the mechanism (embedded above).

## Testing

- [x] Unit tests pass (`pytest`)
- [ ] Linting passes (`ruff check .`) — not run locally, CI `lint` check
is green
- [ ] Type checking passes (`mypy headroom`) — not run locally, CI is
green
- [x] New tests added for new functionality
- [x] Manual testing performed (see Real Behavior Proof)

### Test Output

```text
$ .venv/Scripts/python -m pytest tests/test_error_detection.py tests/test_transforms/test_content_router.py tests/test_transforms_content_router.py -q
tests\test_error_detection.py .......                                    [  7%]
tests\test_transforms\test_content_router.py ........................... [ 34%]
...........................                                              [ 62%]
tests\test_transforms_content_router.py ................................ [ 94%]
.....                                                                    [100%]
============================= 98 passed in 1.21s ==============================
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.14.5, local venv, `headroom._core`
built via `maturin develop --release` (not prebuilt in a fresh checkout)
- Exact command / steps: ran the reporter's exact scenario patterns
(`"Found 0 errors\nTests: 0 failures, 42 passed"`, eslint's `"0 problems
(0 errors, 0 warnings)"`) through
`content_has_strong_error_indicators()` directly, before and after the
fix
- Observed result: before → `True` (wrongly protected); after → `False`
(correctly compressible). Real failure text (`Traceback... fatal error`)
still returns `True` after the fix.
- Not tested: have not reproduced the full KiloCode/proxy session
end-to-end locally (no access to the reporter's actual traffic) — root
cause was confirmed via the `stats.json` they attached to the issue,
which shows `router:protected:error_output` firing on nearly every
request in their session.

## 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
- [ ] I have made corresponding changes to the documentation (N/A —
internal heuristic, no user-facing docs reference it)
- [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
- [ ] I have updated the CHANGELOG.md if applicable (release-please
generates this automatically from commit messages)

## Screenshots (if applicable)

See the diagram embedded in Description above.

## Additional Notes

**Investigation trail**: ruled out `protect_recent_reads_fraction`
(proxy already overrides its `0.0` dataclass default to `0.3` in token
mode, the default) before confirming the error-protection false-positive
via the reporter's `stats.json`.

**Response to review comments** (@AbelVM, @sparkbugz): prose mentions
like `"Fix the errors in the code."` or `"console.error(...)"` contain
only 1 distinct indicator keyword and were already safe under the
pre-existing 2-keyword threshold — not something this PR changes. The
broader concern about other CI summary formats is addressed above
(label:value forms). A case like genuine prose that happens to mention
*two* distinct keywords together (e.g. "there are errors and it failed")
is a known limitation of a keyword-substring heuristic in general,
predates this PR, and is out of scope here — downstream compressors
(LogCompressor) still preserve real error lines even when a block isn't
gate-protected, so the failure mode there is "slightly stricter than
ideal," not data loss.

**Response to @JerrettDavis's CHANGES_REQUESTED**: fixed in the
follow-up commit — `"0 failed"` is now stripped (previously only
`failing`/`failure(s)` were), with a regression test for the exact
reproduction given.
2026-07-14 13:25:49 -04:00
Parideboy
c46cd8f950
fix(core): load ONNX Runtime dynamically so headroom._core imports on non-AVX2 x86-64 (#1715)
## Description

`import headroom._core` dies with SIGILL (`Illegal instruction`) on
x86-64 CPUs without AVX2 (Pentium N4200, Celeron N4500, AMD FX 8350 —
all reported on the issue). The repo sets no `RUSTFLAGS`/`target-cpu`
anywhere, so first-party Rust code is baseline x86-64; the AVX2 code
comes from Microsoft's prebuilt ONNX Runtime, statically linked into the
extension by fastembed's `ort-download-binaries-rustls-tls` feature on
non-Windows targets. Because it is statically linked, its code is mapped
and initialized when the extension module loads — **before** the runtime
AVX2 guard from #1162 can run, which is why that fix helped Magika init
but not the import-time crash.

Fix, mirroring what Windows already does for its own reasons (DirectML
link libs): build with `ort-load-dynamic` on every platform, so ONNX
Runtime is only `dlopen`'d at first use, where the #1162 AVX2 guard
falls back to the non-ONNX detection tiers on unsupported CPUs. Since
both target blocks became identical, they are collapsed into one
platform-independent `fastembed` dependency.

To keep Magika/fastembed working out of the box on Linux/macOS, the
existing `ORT_DYLIB_PATH` auto-pin (`headroom/_ort.py`, previously
Windows-only) now resolves the pip `onnxruntime` package's shared
library on all platforms (`onnxruntime.dll` / `libonnxruntime.so*` /
`libonnxruntime*.dylib`). The pip `onnxruntime` CPU wheels use runtime
CPU dispatch, so they also work on pre-AVX2 machines — non-AVX2 users
get working ML detection instead of a crash. Without the `onnxruntime`
package, ML detection degrades gracefully to the non-ONNX tiers exactly
as it already does on Windows.

Fixes #1278

## 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)
- [x] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- `crates/headroom-core/Cargo.toml`: replaced the per-target `fastembed`
blocks (`ort-download-binaries-rustls-tls` on non-Windows,
`ort-load-dynamic` on Windows) with a single platform-independent
dependency on `ort-load-dynamic`, with a comment documenting both the
DirectML and the AVX2/#1278 rationale.
- `Cargo.lock`: regenerated — `ort-sys` drops its static-download
dependencies (`hmac-sha256`, `lzma-rust2`, `ureq`); no version bumps.
- `headroom/_ort.py`: `ORT_DYLIB_PATH` auto-pin extended from
Windows-only to all platforms via a small `_find_dylib` helper that
resolves the platform's shared-library name inside the pip `onnxruntime`
package.
- `tests/test_transforms/test_ort_dylib.py`: replaced the obsolete
`test_noop_on_non_windows` with Linux (versioned `.so`) and macOS
(`.dylib`) pin tests; module docstring updated.
- `docs/content/docs/configuration.mdx`: `ORT_DYLIB_PATH` row updated
from Windows-only wording to the cross-platform behavior.

## 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
$ cargo fmt --all -- --check && cargo clippy --workspace -- -D warnings && cargo test -p headroom-core --lib
clean
test result: 844 passed; 0 failed; 1 ignored

$ python -m pytest tests/test_transforms/test_ort_dylib.py -q
8 passed

$ ruff check headroom/_ort.py tests/test_transforms/test_ort_dylib.py
All checks passed!
```

## Real Behavior Proof

- Environment: Windows 11 (AVX2-capable — the SIGILL itself is not
reproducible on this machine), Python 3.13, Rust 1.95.0, local checkout
branched from `upstream/main` (9fbd47ba).
- Exact command / steps: `cargo check -p headroom-core` after the
feature switch; inspected the `Cargo.lock` diff; rebuilt and ran `python
-c "import headroom; from headroom._core import detect_content_type;
print(detect_content_type('hello world'))"`; ran the ort-pin test suite
with monkeypatched `linux`/`darwin` platforms.
- Observed result: build succeeds with `ort-load-dynamic`; the lockfile
shows `ort-sys` no longer pulls the binary-download machinery
(`hmac-sha256`, `lzma-rust2`, `ureq` removed), confirming the
statically-linked prebuilt ORT is gone; import + content detection works
with `ORT_DYLIB_PATH` auto-pinned to the pip onnxruntime library; all 8
pin tests pass including the new Linux/macOS branches.
- Not tested: actual pre-AVX2 x86-64 hardware (none available — the fix
removes AVX2 code from the import path by construction, and the issue
reporters on #1278 can verify); Linux/macOS wheel runtime behavior
beyond CI's ubuntu/macOS wheel-build jobs; embedding quality/performance
under a pip-provided ORT version differing from the previously vendored
one.

## Review Readiness

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-14 13:25:41 -04:00
Gaurav Yadav
b0fa84e84d
fix: add Vercel deploy config and workflow for docs site (#1739)
## Description

The Vercel docs site at headroom-docs.vercel.app had no automated
deployment pipeline, so newly added pages (persistent-installs, savings)
return 404 despite existing in the repo and building correctly locally.

Closes #1730

## Type of Change

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

## Changes Made

- Add docs/vercel.json with explicit Next.js project config (framework,
build/install commands)
- Add deploy-vercel job to .github/workflows/docs.yml to auto-deploy on
pushes to main touching docs/**

## Testing

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

### Test Output

```
Local build verification:
cd docs && npm ci && npm run build
Build succeeded - persistent-installs and savings pages
generated at .next/server/app/docs/persistent-installs.html
and .next/server/app/docs/savings.html
```

## Real Behavior Proof

- Environment: Linux x86_64, Node.js 20
- Exact command / steps:
  1. cd docs && npm ci && npm run build
  2. Checked .next/server/app/docs/ for generated HTML artifacts
3. Verified source.getPage(["persistent-installs"]) returns page object
- Observed result: Both pages build and render correctly locally
- Not tested: Live Vercel deployment requires maintainer secrets
(VERCEL_TOKEN, VERCEL_ORG_ID, VERCEL_PROJECT_ID)

## Review Readiness

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

## Additional Notes

Requires three repo secrets: VERCEL_TOKEN, VERCEL_ORG_ID,
VERCEL_PROJECT_ID.
2026-07-14 13:25:18 -04:00
JD Davis
fce93bf39a
fix(ci/deps): clear audit and release smoke failures (#2190)
## Summary
- add a uv constraint floor for `setuptools>=83.0.0` to address
`PYSEC-2026-3447`
- refresh `uv.lock` so the production audit export resolves with
`setuptools 83.0.0`
- harden the Release wheel smoke-import gate by retrying
Ubuntu-container `apt-get` operations and using `--fix-missing`
- keep the generated `requirements-prod.txt` uncommitted; it is produced
by the security workflow

## Why
This clears the new dependency audit alert that made PRs red:

- `setuptools 80.10.2`
- `PYSEC-2026-3447`
- fixed in `83.0.0`

While validating the queue, the same PR class also hit a Release
smoke-import failure in the Ubuntu 22.04 ARM container due apt mirror
skew:

`E: Failed to fetch ... python3-httplib2_0.20.2-2ubuntu0.1_all.deb 404
Not Found`

The smoke gate should still fail for broken wheels, but transient apt
mirror skew should not make unrelated PRs red.

## Lockfile impact
- `setuptools 80.10.2 -> 83.0.0`
- `torch 2.12.1 -> 2.13.0`, required for pip resolver compatibility with
`setuptools 83.0.0` in the exported audit set
- `cuda-toolkit 13.0.2 -> 13.0.3.0`, pulled by the torch lock refresh
- uv also refreshed the existing project metadata for the sandbox extra
so `uv lock --check` passes

## Validation
- `uv lock --check`
- `uv export --frozen --no-dev --no-emit-project --no-hashes --extra all
--format requirements-txt > requirements-prod.txt`
- confirmed generated `requirements-prod.txt` contains
`setuptools==83.0.0`, `torch==2.13.0`, `cuda-toolkit==13.0.3.0`
- `uvx pip-audit -r requirements-prod.txt` -> No known vulnerabilities
found
- `python -m pytest tests/test_release_workflows.py -q` -> 32 passed
- `uvx ruff@0.15.17 check tests/test_release_workflows.py` -> All checks
passed
- `git diff --check`
2026-07-14 11:43:23 -04:00
Tejas Chopra
58d3445d23
ci: scope native/wheel/dashboard jobs to relevant paths (unstarve the queue) (#2155)
## Problem
CI's `changes` paths-filter is too coarse: both `code` and `e2e` include
the catch-all `headroom/**`, so **any** Python change fires the full
matrix — including the scarce-runner hogs: `build-wheel-windows`,
`macos-native-wrapper`, `windows-native-wrapper`, `docker-native-e2e`.
macOS runners cap at 5 on the Free plan, so a trivial Python PR burns
the scarcest resource. `init-e2e`/`wrap-e2e` trigger on `headroom/**`
too.

## Change (CI config only — no product code)
- **ci.yml:** split `changes` into `code` / `native` / `dashboard` /
`packaging`. Re-gate:
  | job | before | after |
  |-----|--------|-------|
  | `build-wheel-windows` | code | `packaging` |
  | `test-dashboard-ui` | code | `dashboard` |
  | `docker-native-e2e` | e2e | `native` |
  | `windows-native-wrapper` | e2e | `native` |
  | `macos-native-wrapper` | e2e | `native` |
- **init-e2e / wrap-e2e:** scoped off `headroom/**` onto the
`cli`/`install`/`providers`/`rtk` subpaths the flows exercise (mirrors
the existing `*-native-e2e` workflows).

## Safety
- Verified no job `needs:` any of the re-gated jobs, so no dependency
chain breaks. `test-dashboard-ui` still gets `build-wheel` (dashboard
files are under `headroom/` ⟹ `code` too).
- Job-level `if:` skips resolve as **neutral** (not failing), so this is
safe even if branch protection is added.
- Full matrix still runs on any `.github/workflows/**` change and on
push to `main` (post-merge safety net).

## Net
A pure-Python logic PR skips ~7-9 jobs incl. all 3 native platform
builds — freeing the macOS/Windows caps that were starving the queue.
2026-07-13 23:50:26 -04:00
Tejas Chopra
6efd01f707
ci: concurrency-cancel Docker + Merge Conflicts to stop merge-spree pileups (#2138)
## Description
`docker.yml` builds a full multi-arch image on **every push to `main`**
with **no concurrency group**, so a merge spree stacks one build per
commit. Against the Free-plan **20 concurrent-job cap**, those long
builds hog the runner pool and starve the `CI` (`test`/`lint`/`build`)
jobs that PR merges actually depend on. This adds concurrency-cancel so
only the latest build per ref runs — `cancel-in-progress` scoped to
`main` so a release tag's publish (its own ref) is never killed. Same
fix for the per-PR **Merge Conflicts** check.

## Type of Change
- [x] Repo infrastructure / CI (no functional change)

## Changes Made
- `.github/workflows/docker.yml`: `concurrency: docker-${{ github.ref
}}`, cancel-in-progress on `main` only.
- `.github/workflows/merge-conflicts.yml`: per-PR concurrency-cancel.

## Testing
- [x] YAML validated (`yaml.safe_load` on both) — release/tag builds
unaffected (separate ref group).

## Real Behavior Proof
- Before: 24 merges → 24 stacked Docker builds queued against 20 slots.
- After: only the latest `main` Docker build runs; older superseded ones
auto-cancel; release publishes untouched.
2026-07-13 13:47:59 -04:00
JD Davis
2c9eb7c5f1
feat(simulators): add provider simulator service (#2014)
## Description  
Adds a Rust-only `headroom-simulators` workspace crate: a deterministic
local upstream simulator service for Headroom proxy and pipeline
validation. It supplies configurable stubs plus bottled provider-shaped
responses for supported provider/path surfaces without calling real
LLMs.

## Type of Change  
- [x] 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)
- [x] Documentation update  
- [ ] Performance improvement  
- [ ] Code refactoring (no functional changes)

## Changes Made  
- Added `crates/headroom-simulators` Rust crate with library and
`headroom-simulators` binary.
- Added clean domain classification for supported surfaces: Anthropic
`/v1/messages`, OpenAI chat/responses/conversations, Bedrock
invoke/stream routes, Vertex raw/stream predict, health, and generic
fallback.
- Added JSON-configured stub matching by method, path, body substring,
and JSON pointer.
- Added bottled provider-shaped JSON, SSE, and Bedrock EventStream
responses for unconfigured requests.
- Added a container `Dockerfile` and README for local/GitHub Actions
usage.
- Added unit and HTTP integration tests for defaults, configured stubs,
SSE, Vertex, and Bedrock EventStream behavior.
- Added proxy-level simulator-backed E2E tests that run Headroom against
the simulator across Anthropic, OpenAI Chat, OpenAI Responses, OpenAI
Conversations, Bedrock invoke/converse/streaming, Vertex raw/stream
predict, and upstream health.
- Added simulator-backed provider error-path E2E coverage for OpenAI
429, Anthropic 529, Bedrock 502, and Vertex 503 responses flowing
through Headroom unchanged.
- Added Headroom-owned preflight error E2E coverage proving Bedrock
missing credentials and invalid Vertex envelopes stop inside the proxy
instead of silently falling through to the simulator/provider.
- Fixed direct Rust `headroom-core` binaries/tests on Windows so Magika
initializes ONNX Runtime via `ort::init_from` from an explicit pip
`onnxruntime` library path, with fail-fast fallback only when no safe
runtime is discoverable.
- Added a Rust CI `simulator-e2e` matrix for `ubuntu-latest`,
`macos-latest`, and `windows-latest` that runs `cargo test -p
headroom-proxy --test e2e_simulators`.
- Gated dynamic Magika `Path`/`PathBuf` imports to Windows and x86_64
macOS so Linux clippy does not see unused dynamic-ORT-only imports.

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

### Test Output  
cargo fmt --all -- --check  
# passed  

cargo clippy --workspace -- -D warnings  
# passed  

$env:ORT_DYLIB_PATH=$null  
cargo test -p headroom-core transforms::magika_detector::tests:: --lib  
# 17 passed, 0 failed; Magika initialized from discovered pip
onnxruntime DLL

$env:ORT_DYLIB_PATH=$null  
cargo test --workspace  
# passed  

gitleaks protect --staged --no-banner --redact  
# no leaks found  

gitleaks git --log-opts="headroomlabs/main..HEAD" --no-banner --redact  
# 5 commits scanned; no leaks found

## Real Behavior Proof  
- **Environment:** Windows PowerShell, Rust toolchain `1.95.0`, clean
worktree from `headroomlabs/main` at `9bacf481`.
- **Exact simulator command / steps:**  
  - `cargo run -p headroom-simulators -- --listen 127.0.0.1:8789`  
- Point Headroom proxy upstream at `http://127.0.0.1:8789` for local
deterministic provider responses.
- Use optional `--config path/to/simulator.json` to bind exact request
fixtures.
- **Observed simulator result:**  
  - OpenAI chat default returns `chat.completion` shape.  
  - OpenAI Responses stream returns named SSE events.  
  - Vertex raw predict returns Anthropic message shape.  
- Bedrock stream can return binary `application/vnd.amazon.eventstream`
bytes.
  - Configured stubs override bottled defaults.  
- **Observed Magika result:**  
- Direct Rust `headroom-core` tests pass with `ORT_DYLIB_PATH` unset.
- Magika discovers the installed pip `onnxruntime.dll`, loads it via
`ort::init_from`, and only falls back if no safe runtime is available.
- **Not tested:**  
- No live provider calls; simulator behavior is intentionally offline
and deterministic.

## 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  
- [ ] I have updated the CHANGELOG.md if applicable

## Screenshots (if applicable)  
N/A

## Additional Notes  
No CHANGELOG entry was added because this introduces a developer/CI
simulator crate plus a Windows direct-Rust Magika runtime fix, without
changing shipped Python package behavior. The simulator intentionally
does not include a lightweight fallback LLM in this slice; unbound
inputs receive deterministic bottled responses so tests stay
reproducible and offline.
2026-07-11 09:41:49 -07:00
sgfdhgfnjgmhgmnfd657-sys
48f06caca7
ci: add Windows wheel build job (win_amd64) (#1086)
### Summary

Adds `build-wheel-windows` job to the CI pipeline that compiles the Rust
extension on `windows-latest` and uploads the resulting `.whl` as a
separate artifact (`headroom-wheel-windows`).

This addresses the long-standing missing Windows wheel.

### Changes

- New job `build-wheel-windows`: mirrors the existing `build-wheel`
(Linux) job
- Uses `dtolnay/rust-toolchain@stable` for Rust setup on Windows
- Uses `Swatinem/rust-cache` for dependency caching
- Builds with CI cargo profile for speed
- 45-minute timeout (Windows Rust builds are slower)
- Uploads wheel as `headroom-wheel-windows` artifact

### Testing

 **Local compilation verified**: built v0.26.0 from source on Windows
10 (Python 3.12.10, Rust 1.96.0, MSVC Build Tools 2022). The wheel
installed and ran successfully.

### Notes

Only the CI-profile build is added here. The release-wheel publish
(`release.yml`) can be updated in a follow-up PR once this basic Windows
build is proven in CI.

Co-authored-by: Win He <win-he@users.noreply.github.com>
2026-07-10 23:05:03 -05:00
JD Davis
98ff203f98
ci: allow PyPI deps during CPU torch install (#1930)
## Description

Fixes the CI failure exposed on the `8527b910` push run:

- Run:
https://github.com/headroomlabs-ai/headroom/actions/runs/29063619700
- Failed job: `test (3)` / job `86271743483`
- Failed step: `Install (CPU torch + prebuilt wheel + dev deps, no cargo
rebuild)`

The failing command used the PyTorch CPU index as the only package
index:

```bash
pip install torch --index-url https://download.pytorch.org/whl/cpu
```

That index does not provide transitive dependencies such as
`typing-extensions`, so pip backtracked across torch CPU wheels and
failed before tests ran. This PR keeps the PyTorch CPU index primary for
`torch` while allowing PyPI for dependencies.

## 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

- Adds `--extra-index-url https://pypi.org/simple` to each CI `pip
install torch --index-url https://download.pytorch.org/whl/cpu` command.
- Leaves the CPU torch index as the primary index so CI still installs
CPU torch wheels.

## Testing

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

### Test Output

```text
git diff --check
# passed

rg -n "pip install torch" .github/workflows/ci.yml
# all four torch install commands now include --extra-index-url https://pypi.org/simple
```

## Real Behavior Proof

- Environment: Local worktree `C:\git\headroom-ci-fix`, branch
`jd/fix-ci-torch-install-index`.
- Exact command / steps: Inspected failed GitHub Actions log for run
`29063619700` job `86271743483`, then updated matching torch install
commands in `.github/workflows/ci.yml`.
- Observed result: The workflow no longer uses the PyTorch CPU index as
the only index for torch installs; PyPI is available for transitive
dependencies such as `typing-extensions`.
- Not tested: Full CI run locally; GitHub Actions is the authoritative
verification for the affected install path.

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

## Screenshots (if applicable)

N/A.

## Additional Notes

This is a fix-forward PR. Reverting `8527b910` would reintroduce the
Ruff failure it fixed; the failed job did not reach tests and failed
only during dependency installation.
2026-07-09 19:36:26 -07:00
JerrettDavis
2d418335a1 ci: preserve merge labels while state is unknown 2026-07-09 19:51:41 -05:00
JerrettDavis
595b709a5b ci: keep ready label off changes-requested PRs 2026-07-09 19:49:55 -05:00
Tejas Chopra
49a1a1405b
ci(opencode): compile + test the OpenCode plugin on changes (#1750)
The OpenCode plugin (`plugins/opencode`) is the routing shim that
carries `headroom wrap opencode` traffic through the proxy — but **no CI
job ever compiled it**. It has `typecheck`/`build`/`test` scripts that
only ran locally, so:
- TypeScript 6.x (#1687) and @types/node 26 (#1688) major-bump PRs had
**zero build evidence** (why they're held).
- Any source edit to the plugin could silently break the build.

This adds a path-gated workflow that runs `npm ci → typecheck → build →
test` whenever `plugins/opencode/**` (or this workflow) changes. Matches
repo conventions (`setup-node@v6`, node 20, npm cache).

**Verified green locally on main:** `tsc --noEmit` clean, `tsup` build
ok, 13 vitest tests pass.

Unblocks safe evaluation of the held dependency-bump PRs and protects
the routing plugin going forward.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-09 16:55:51 -05:00
Mohammad Hussian
662b7bc00e
fix(release): sync all package versions to v0.31.0 (#1882)
## Description

Current `main` has advanced the core package versions to `0.31.0`, but
the plugin marketplace manifests and hook plugin manifests were still
left at `0.30.0`. This PR now keeps the original version-sync intent
while updating the remaining metadata to the current release line.

It also preserves the previously-added `lxml-html-clean>=0.4.5` security
floor in `pyproject.toml` / `uv.lock` so the security audit remains
unblocked.

Closes #1872

## 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

- Synced `pyproject.toml`, SDK/package manifests, plugin manifests,
marketplaces, `.release-please-manifest.json`, and editable package lock
metadata to `0.31.0`.
- Updated the OpenClaw plugin dependency on `headroom-ai` to `^0.31.0`.
- Merged current `main` and resolved the version metadata conflicts in
favor of current `0.31.0` alignment.

## Testing

- [x] Unit tests pass (`pytest tests/test_plugin_manifests.py
scripts/tests/test_version_sync.py -q`)
- [ ] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [ ] New tests added for new functionality
- [x] Manual testing performed (`python scripts/verify-versions.py`)

### Test Output

```text
python scripts/verify-versions.py
All versions aligned at 0.31.0

pytest tests/test_plugin_manifests.py scripts/tests/test_version_sync.py -q
10 passed in 0.42s
```

## Real Behavior Proof

- Environment: Windows, Python 3.13.13, PR head after merging current
`main`.
- Exact command / steps: Ran `python scripts/verify-versions.py` and
`pytest tests/test_plugin_manifests.py
scripts/tests/test_version_sync.py -q`.
- Observed result: Version verification exits successfully with `All
versions aligned at 0.31.0`; focused manifest/version-sync tests pass.
- Not tested: Full wheel/build matrix; this is metadata-only version
alignment and CI will cover the broader matrix.

## 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] My changes generate no new warnings
- [x] New and existing focused tests pass locally with my changes
- [ ] I have made corresponding changes to the documentation
- [ ] I have updated the CHANGELOG.md if applicable

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-09 16:53:55 -05:00
dependabot[bot]
ebd23152d5
ci: bump actions/cache from 5 to 6 (#1413)
Bumps [actions/cache](https://github.com/actions/cache) from 5 to 6.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/cache/releases">actions/cache's
releases</a>.</em></p>
<blockquote>
<h2>v6.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update packages, migrate to ESM by <a
href="https://github.com/Samirat"><code>@​Samirat</code></a> in <a
href="https://redirect.github.com/actions/cache/pull/1760">actions/cache#1760</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/cache/compare/v5...v6.0.0">https://github.com/actions/cache/compare/v5...v6.0.0</a></p>
<h2>v5.1.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Bump <code>@​actions/cache</code> to v5.1.0 - handle read-only cache
access by <a
href="https://github.com/jasongin"><code>@​jasongin</code></a> in <a
href="https://redirect.github.com/actions/cache/pull/1775">actions/cache#1775</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/cache/compare/v5...v5.1.0">https://github.com/actions/cache/compare/v5...v5.1.0</a></p>
<h2>v5.0.5</h2>
<h2>What's Changed</h2>
<ul>
<li>Update ts-http-runtime dependency by <a
href="https://github.com/yacaovsnc"><code>@​yacaovsnc</code></a> in <a
href="https://redirect.github.com/actions/cache/pull/1747">actions/cache#1747</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/cache/compare/v5...v5.0.5">https://github.com/actions/cache/compare/v5...v5.0.5</a></p>
<h2>v5.0.4</h2>
<h2>What's Changed</h2>
<ul>
<li>Add release instructions and update maintainer docs by <a
href="https://github.com/Link"><code>@​Link</code></a>- in <a
href="https://redirect.github.com/actions/cache/pull/1696">actions/cache#1696</a></li>
<li>Potential fix for code scanning alert no. 52: Workflow does not
contain permissions by <a
href="https://github.com/Link"><code>@​Link</code></a>- in <a
href="https://redirect.github.com/actions/cache/pull/1697">actions/cache#1697</a></li>
<li>Fix workflow permissions and cleanup workflow names / formatting by
<a href="https://github.com/Link"><code>@​Link</code></a>- in <a
href="https://redirect.github.com/actions/cache/pull/1699">actions/cache#1699</a></li>
<li>docs: Update examples to use the latest version by <a
href="https://github.com/XZTDean"><code>@​XZTDean</code></a> in <a
href="https://redirect.github.com/actions/cache/pull/1690">actions/cache#1690</a></li>
<li>Fix proxy integration tests by <a
href="https://github.com/Link"><code>@​Link</code></a>- in <a
href="https://redirect.github.com/actions/cache/pull/1701">actions/cache#1701</a></li>
<li>Fix cache key in examples.md for bun.lock by <a
href="https://github.com/RyPeck"><code>@​RyPeck</code></a> in <a
href="https://redirect.github.com/actions/cache/pull/1722">actions/cache#1722</a></li>
<li>Update dependencies &amp; patch security vulnerabilities by <a
href="https://github.com/Link"><code>@​Link</code></a>- in <a
href="https://redirect.github.com/actions/cache/pull/1738">actions/cache#1738</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/XZTDean"><code>@​XZTDean</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/cache/pull/1690">actions/cache#1690</a></li>
<li><a href="https://github.com/RyPeck"><code>@​RyPeck</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/cache/pull/1722">actions/cache#1722</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/cache/compare/v5...v5.0.4">https://github.com/actions/cache/compare/v5...v5.0.4</a></p>
<h2>v5.0.3</h2>
<h2>What's Changed</h2>
<ul>
<li>Bump <code>@actions/cache</code> to v5.0.5 (Resolves: <a
href="https://github.com/actions/cache/security/dependabot/33">https://github.com/actions/cache/security/dependabot/33</a>)</li>
<li>Bump <code>@actions/core</code> to v2.0.3</li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/cache/compare/v5...v5.0.3">https://github.com/actions/cache/compare/v5...v5.0.3</a></p>
<h2>v.5.0.2</h2>
<h1>v5.0.2</h1>
<h2>What's Changed</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/actions/cache/blob/main/RELEASES.md">actions/cache's
changelog</a>.</em></p>
<blockquote>
<h1>Releases</h1>
<h2>How to prepare a release</h2>
<blockquote>
<p>[!NOTE]
Relevant for maintainers with write access only.</p>
</blockquote>
<ol>
<li>Switch to a new branch from <code>main</code>.</li>
<li>Run <code>npm test</code> to ensure all tests are passing.</li>
<li>Update the version in <a
href="https://github.com/actions/cache/blob/main/package.json"><code>https://github.com/actions/cache/blob/main/package.json</code></a>.</li>
<li>Run <code>npm run build</code> to update the compiled files.</li>
<li>Update this <a
href="https://github.com/actions/cache/blob/main/RELEASES.md"><code>https://github.com/actions/cache/blob/main/RELEASES.md</code></a>
with the new version and changes in the <code>## Changelog</code>
section.</li>
<li>Run <code>licensed cache</code> to update the license report.</li>
<li>Run <code>licensed status</code> and resolve any warnings by
updating the <a
href="https://github.com/actions/cache/blob/main/.licensed.yml"><code>https://github.com/actions/cache/blob/main/.licensed.yml</code></a>
file with the exceptions.</li>
<li>Commit your changes and push your branch upstream.</li>
<li>Open a pull request against <code>main</code> and get it reviewed
and merged.</li>
<li>Draft a new release <a
href="https://github.com/actions/cache/releases">https://github.com/actions/cache/releases</a>
use the same version number used in <code>package.json</code>
<ol>
<li>Create a new tag with the version number.</li>
<li>Auto generate release notes and update them to match the changes you
made in <code>RELEASES.md</code>.</li>
<li>Toggle the set as the latest release option.</li>
<li>Publish the release.</li>
</ol>
</li>
<li>Navigate to <a
href="https://github.com/actions/cache/actions/workflows/release-new-action-version.yml">https://github.com/actions/cache/actions/workflows/release-new-action-version.yml</a>
<ol>
<li>There should be a workflow run queued with the same version
number.</li>
<li>Approve the run to publish the new version and update the major tags
for this action.</li>
</ol>
</li>
</ol>
<h2>Changelog</h2>
<h3>6.1.0</h3>
<ul>
<li>Bump <code>@actions/cache</code> to v6.1.0 to pick up <a
href="https://redirect.github.com/actions/toolkit/pull/2435">actions/toolkit#2435
Handle cache write error due to read-only token</a></li>
<li>Switch redundant &quot;Cache save failed&quot; warning to debug log
in save-only</li>
</ul>
<h3>6.0.0</h3>
<ul>
<li>Updated <code>@actions/cache</code> to ^6.0.1,
<code>@actions/core</code> to ^3.0.1, <code>@actions/exec</code> to
^3.0.0, <code>@actions/io</code> to ^3.0.2</li>
<li>Migrated to ESM module system</li>
<li>Upgraded Jest to v30 and test infrastructure to be ESM
compatible</li>
</ul>
<h3>5.0.4</h3>
<ul>
<li>Bump <code>minimatch</code> to v3.1.5 (fixes ReDoS via globstar
patterns)</li>
<li>Bump <code>undici</code> to v6.24.1 (WebSocket decompression bomb
protection, header validation fixes)</li>
<li>Bump <code>fast-xml-parser</code> to v5.5.6</li>
</ul>
<h3>5.0.3</h3>
<ul>
<li>Bump <code>@actions/cache</code> to v5.0.5 (Resolves: <a
href="https://github.com/actions/cache/security/dependabot/33">https://github.com/actions/cache/security/dependabot/33</a>)</li>
<li>Bump <code>@actions/core</code> to v2.0.3</li>
</ul>
<h3>5.0.2</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="55cc834586"><code>55cc834</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/cache/issues/1768">#1768</a>
from jasongin/readonly-cache</li>
<li><a
href="d8cd72f230"><code>d8cd72f</code></a>
Bump <code>@​actions/cache</code> to v6.1.0 - handle cache write error
due to RO token</li>
<li><a
href="2c8a9bd745"><code>2c8a9bd</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/cache/issues/1760">#1760</a>
from actions/samirat/esm_migration_and_package_update</li>
<li><a
href="e9b91fdc3f"><code>e9b91fd</code></a>
Prettier fixes</li>
<li><a
href="e4884b8ff7"><code>e4884b8</code></a>
Rebuild dist</li>
<li><a
href="10baf0191a"><code>10baf01</code></a>
Fixed licenses</li>
<li><a
href="e39b386c90"><code>e39b386</code></a>
Fix test mock return order</li>
<li><a
href="b692820337"><code>b692820</code></a>
PR feedback</li>
<li><a
href="60749128a4"><code>6074912</code></a>
Rebuild dist bundles as ESM to match type:module</li>
<li><a
href="5a912e8b4a"><code>5a912e8</code></a>
Fix lint and jest issues</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/cache/compare/v5...v6">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-09 13:32:20 -05:00
dependabot[bot]
bfe4b4d21f
ci: bump actions/checkout from 4 to 7 (#1414)
Some checks failed
CI / test (4) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, codex) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, copilot) (push) Has been cancelled
Install Native E2E / install-native (macos-latest) (push) Has been cancelled
rust / wheels (aarch64-apple-darwin) (push) Has been cancelled
rust / wheels (x86_64-apple-darwin) (push) Has been cancelled
Wrap Native E2E / wrap-native (macos-latest) (push) Has been cancelled
CI / test-extras (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / build-wheel (push) Has been cancelled
CI / prefetch-model (push) Has been cancelled
CI / test (1) (push) Has been cancelled
CI / test (2) (push) Has been cancelled
CI / test (3) (push) Has been cancelled
CI / test-agno (push) Has been cancelled
CI / test-dashboard-ui (push) Has been cancelled
CI / build (push) Has been cancelled
CI / workflow-validation (push) Has been cancelled
CI / docker-native-e2e (push) Has been cancelled
CI / windows-native-wrapper (push) Has been cancelled
CI / macos-native-wrapper (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / promote-latest (push) Has been cancelled
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to
7.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/releases">actions/checkout's
releases</a>.</em></p>
<blockquote>
<h2>v7.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>block checking out fork pr for pull_request_target and workflow_run
by <a href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2454">actions/checkout#2454</a></li>
<li>Bump actions/publish-immutable-action from 0.0.3 to 0.0.4 in the
minor-actions-dependencies group across 1 directory by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2458">actions/checkout#2458</a></li>
<li>Bump flatted from 3.3.1 to 3.4.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2460">actions/checkout#2460</a></li>
<li>Bump js-yaml from 4.1.0 to 4.2.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2461">actions/checkout#2461</a></li>
<li>Bump <code>@​actions/core</code> and
<code>@​actions/tool-cache</code> and Remove uuid by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2459">actions/checkout#2459</a></li>
<li>upgrade module to esm and update dependencies by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2463">actions/checkout#2463</a></li>
<li>Bump the minor-npm-dependencies group across 1 directory with 3
updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2462">actions/checkout#2462</a></li>
<li>getting ready for checkout v7 release by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2464">actions/checkout#2464</a></li>
<li>update error wording by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2467">actions/checkout#2467</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/2454">actions/checkout#2454</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v6.0.3...v7.0.0">https://github.com/actions/checkout/compare/v6.0.3...v7.0.0</a></p>
<h2>v6.0.3</h2>
<h2>What's Changed</h2>
<ul>
<li>Update changelog by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2357">actions/checkout#2357</a></li>
<li>fix: expand merge commit SHA regex and add SHA-256 test cases by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
<li>Fix checkout init for SHA-256 repositories by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2439">actions/checkout#2439</a></li>
<li>Update changelog for v6.0.3 by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2446">actions/checkout#2446</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/yaananth"><code>@​yaananth</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v6...v6.0.3">https://github.com/actions/checkout/compare/v6...v6.0.3</a></p>
<h2>v6.0.2</h2>
<h2>What's Changed</h2>
<ul>
<li>Add orchestration_id to git user-agent when ACTIONS_ORCHESTRATION_ID
is set by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2355">actions/checkout#2355</a></li>
<li>Fix tag handling: preserve annotations and explicit fetch-tags by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2356">actions/checkout#2356</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v6.0.1...v6.0.2">https://github.com/actions/checkout/compare/v6.0.1...v6.0.2</a></p>
<h2>v6.0.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Update all references from v5 and v4 to v6 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2314">actions/checkout#2314</a></li>
<li>Add worktree support for persist-credentials includeIf by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2327">actions/checkout#2327</a></li>
<li>Clarify v6 README by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2328">actions/checkout#2328</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v6...v6.0.1">https://github.com/actions/checkout/compare/v6...v6.0.1</a></p>
<h2>v6.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update README to include Node.js 24 support details and requirements
by <a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2248">actions/checkout#2248</a></li>
<li>Persist creds to a separate file by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2286">actions/checkout#2286</a></li>
<li>v6-beta by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2298">actions/checkout#2298</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/blob/main/CHANGELOG.md">actions/checkout's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>v7.0.0</h2>
<ul>
<li>Block checking out fork PR for pull_request_target and workflow_run
by <a href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2454">actions/checkout#2454</a></li>
<li>Bump actions/publish-immutable-action from 0.0.3 to 0.0.4 in the
minor-actions-dependencies group across 1 directory by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2458">actions/checkout#2458</a></li>
<li>Bump flatted from 3.3.1 to 3.4.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2460">actions/checkout#2460</a></li>
<li>Bump js-yaml from 4.1.0 to 4.2.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2461">actions/checkout#2461</a></li>
<li>Bump <code>@​actions/core</code> and
<code>@​actions/tool-cache</code> and Remove uuid by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2459">actions/checkout#2459</a></li>
<li>upgrade module to esm and update dependencies by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2463">actions/checkout#2463</a></li>
<li>Bump the minor-npm-dependencies group across 1 directory with 3
updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2462">actions/checkout#2462</a></li>
</ul>
<h2>v6.0.3</h2>
<ul>
<li>Fix checkout init for SHA-256 repositories by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2439">actions/checkout#2439</a></li>
<li>fix: expand merge commit SHA regex and add SHA-256 test cases by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
</ul>
<h2>v6.0.2</h2>
<ul>
<li>Fix tag handling: preserve annotations and explicit fetch-tags by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2356">actions/checkout#2356</a></li>
</ul>
<h2>v6.0.1</h2>
<ul>
<li>Add worktree support for persist-credentials includeIf by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2327">actions/checkout#2327</a></li>
</ul>
<h2>v6.0.0</h2>
<ul>
<li>Persist creds to a separate file by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2286">actions/checkout#2286</a></li>
<li>Update README to include Node.js 24 support details and requirements
by <a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2248">actions/checkout#2248</a></li>
</ul>
<h2>v5.0.1</h2>
<ul>
<li>Port v6 cleanup to v5 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2301">actions/checkout#2301</a></li>
</ul>
<h2>v5.0.0</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
</ul>
<h2>v4.3.1</h2>
<ul>
<li>Port v6 cleanup to v4 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2305">actions/checkout#2305</a></li>
</ul>
<h2>v4.3.0</h2>
<ul>
<li>docs: update README.md by <a
href="https://github.com/motss"><code>@​motss</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li>Add internal repos for checking out multiple repositories by <a
href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li>Documentation update - add recommended permissions to Readme by <a
href="https://github.com/benwells"><code>@​benwells</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li>Adjust positioning of user email note and permissions heading by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2044">actions/checkout#2044</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li>Update CODEOWNERS for actions by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2224">actions/checkout#2224</a></li>
<li>Update package dependencies by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
</ul>
<h2>v4.2.2</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment
variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<h2>v4.2.1</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="9c091bb21b"><code>9c091bb</code></a>
update error wording (<a
href="https://redirect.github.com/actions/checkout/issues/2467">#2467</a>)</li>
<li><a
href="1044a6dea9"><code>1044a6d</code></a>
getting ready for checkout v7 release (<a
href="https://redirect.github.com/actions/checkout/issues/2464">#2464</a>)</li>
<li><a
href="f0282184c7"><code>f028218</code></a>
Bump the minor-npm-dependencies group across 1 directory with 3 updates
(<a
href="https://redirect.github.com/actions/checkout/issues/2462">#2462</a>)</li>
<li><a
href="d914b262ff"><code>d914b26</code></a>
upgrade module to esm and update dependencies (<a
href="https://redirect.github.com/actions/checkout/issues/2463">#2463</a>)</li>
<li><a
href="537c7ef99c"><code>537c7ef</code></a>
Bump <code>@​actions/core</code> and <code>@​actions/tool-cache</code>
and Remove uuid (<a
href="https://redirect.github.com/actions/checkout/issues/2459">#2459</a>)</li>
<li><a
href="130a169078"><code>130a169</code></a>
Bump js-yaml from 4.1.0 to 4.2.0 (<a
href="https://redirect.github.com/actions/checkout/issues/2461">#2461</a>)</li>
<li><a
href="7d09575332"><code>7d09575</code></a>
Bump flatted from 3.3.1 to 3.4.2 (<a
href="https://redirect.github.com/actions/checkout/issues/2460">#2460</a>)</li>
<li><a
href="0f9f3aa320"><code>0f9f3aa</code></a>
Bump actions/publish-immutable-action (<a
href="https://redirect.github.com/actions/checkout/issues/2458">#2458</a>)</li>
<li><a
href="f9e715a95f"><code>f9e715a</code></a>
block checking out fork pr for pull_request_target and workflow_run (<a
href="https://redirect.github.com/actions/checkout/issues/2454">#2454</a>)</li>
<li><a
href="df4cb1c069"><code>df4cb1c</code></a>
Update changelog for v6.0.3 (<a
href="https://redirect.github.com/actions/checkout/issues/2446">#2446</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/checkout/compare/v4...v7">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-09 11:09:14 -05:00
Tejas Chopra
807e5e3e6a
ci(release-please): use a PAT so releases trigger the publish workflows (#1920)
## Description

Makes the release pipeline hands-off by fixing the token release-please
uses.

### Why the current setup silently breaks publishing
release-please authenticates with `secrets.GITHUB_TOKEN`. **A
release/tag created by `GITHUB_TOKEN` does not emit events that trigger
other workflows** — this is GitHub's built-in recursion guard. So
`release.yml` (PyPI + npm) and `docker.yml`, which both fire on
`release: published`, **never ran off a bot-created release**. The
result: releases had to be cut by hand (`gh release create`, which runs
as a real user and *does* trigger them), and pip / npm / Docker drifted
out of sync (pip 0.30 vs Docker 0.27).

Proof: creating v0.31.0 manually (my user token) immediately kicked off
both `Release: v0.31.0` and `Docker: v0.31.0`; a `GITHUB_TOKEN`-created
release would not have.

### Change
Use `RELEASE_PLEASE_TOKEN` (a fine-grained PAT with `contents: write` +
`pull-requests: write`, treated by GitHub as a real user):
```yaml
token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}
```
- The release the bot creates now **does** trigger `release.yml` /
`docker.yml` → PyPI + npm + Docker publish automatically on merge of the
release PR.
- The PAT can also tag past branch/tag protection.
- Falls back to `GITHUB_TOKEN` if the secret is ever unset — the release
PR still opens; it just won't trigger downstream publishes (i.e. no
worse than today).

The `RELEASE_PLEASE_TOKEN` secret is already configured in repo
settings.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- `.github/workflows/release-please.yml`: swap `token: ${{
secrets.GITHUB_TOKEN }}` for `${{ secrets.RELEASE_PLEASE_TOKEN ||
secrets.GITHUB_TOKEN }}`, with a comment explaining the recursion-guard
reason.

## Testing

- [x] Manual testing performed (YAML validated; token expression
resolves)

### Test Output

```text
$ python -c "import yaml; ... token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}"
parse OK
```

## Real Behavior Proof

- Environment: local macOS; CI workflow YAML change only.
- Exact command / steps: changed the `token:` input on the
`release-please-action` step to the PAT (with GITHUB_TOKEN fallback);
validated the workflow YAML parses and the token expression is correct.
- Observed result: the workflow now authenticates release-please as a
real user via `RELEASE_PLEASE_TOKEN`, so releases it creates will emit
`release: published` and trigger `release.yml` + `docker.yml`. Verified
out-of-band that a user-token release does trigger those two workflows
(v0.31.0), whereas the bot token does not.
- Not tested: a full bot-driven release cycle end-to-end (only
observable when the next release PR merges with this token in place);
this PR is the enabling change for that.

## 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] My changes generate no new warnings
2026-07-09 09:07:19 -07:00
Tejas Chopra
904fa30586
ci(devcontainers): free runner disk on the default variant too (#1917)
## Description

The **Dev Containers → `validate (default)`** job failed on `main` with:

```
error: Failed to install: numpy-2.4.1-...whl
  Caused by: ... No space left on device (os error 28)
postCreateCommand from devcontainer.json failed with exit code 2
```

The failing step was `Start default` → the devcontainer's
`post-create.sh` runs `uv sync`, which fills the runner's disk
installing the ML wheel set (numpy et al.).

The workflow already has a `Free runner disk` step (reclaims ~14 GB),
but it was gated `if: matrix.name == 'memory-stack'` — so the
**default** variant had no disk cushion and tipped over. This drops the
gate so **both** variants free disk before the container build.

Closes #

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- Remove the `matrix.name == 'memory-stack'` gate on the `Free runner
disk` step in `.github/workflows/devcontainers.yml` so it runs for the
`default` variant too; reword the comment to note the default variant's
`uv sync` disk exhaustion.

## Testing

- [x] Linting passes (`ruff check .`) — n/a (workflow YAML)
- [x] Manual testing performed (YAML validated; step now ungated)

### Test Output

```text
$ python -c "import yaml; ... 'Free runner disk' steps: 1 | has if-gate: False"
Free runner disk steps: 1 | has if-gate: False
```

## Real Behavior Proof

- Environment: local macOS; edited CI workflow only.
- Exact command / steps: removed the `if: matrix.name == 'memory-stack'`
gate on the `Free runner disk` step; validated the YAML parses and the
step is now ungated.
- Observed result: `Free runner disk` runs for every `validate` matrix
variant, reclaiming ~14 GB before the devcontainer build so `uv sync` no
longer exhausts the disk on the default variant.
- Not tested: the live CI run (the fix's effect is only observable when
the Dev Containers workflow next runs on this PR / main). The `default`
failure was a runner disk-exhaustion flake, not a code issue.

## 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] My changes generate no new warnings
2026-07-09 07:44:48 -07:00
Tejas Chopra
2990b7457d
chore: sync version state to released 0.30.0 to unblock release-please (#1916)
## Description

**Fixes the Release Please pipeline**, which stopped opening the release
PR, leaving pip / Docker / npm out of sync.

### Root cause
Recent releases (0.28 → 0.30) were cut **out-of-band** (manual tag +
release), so Release Please's state drifted from reality:
- `.release-please-manifest.json` was frozen at **0.29.0**, and the
in-repo version files at **0.29.0** — even the `v0.30.0` tag commit has
`pyproject.toml = 0.29.0`. The plugin/marketplace manifests were frozen
even further back, at **0.22.3**.
- `v0.30.0` was tagged and published to PyPI (CI stamps the version from
the tag at build time via `version-sync.py`, which is why PyPI got
0.30.0 despite the committed 0.29.0).
- With the manifest at 0.29.0, RP kept computing the next version as
**0.30.0**, saw that tag already exists, and produced **no PR** — so
nothing new could ship, and Docker/npm fell behind.

### Fix
Realign the repo with the last real release (0.30.0) so RP can drive the
next one:
- `.release-please-manifest.json` → `0.30.0`
- `pyproject.toml`, `sdk/typescript/package.json`,
`plugins/openclaw/package.json`, both `plugin.json`, both
`marketplace.json` → `0.30.0` (via `scripts/version-sync.py --version
0.30.0`, which also fixes the 0.22.3 drift).

### What happens after merge
1. Release Please runs on `main`, sees manifest = 0.30.0 + 69 releasable
commits since `v0.30.0`, and opens a clean **`chore: release 0.31.0`**
PR (bumping every version file).
2. Merging that PR tags `v0.31.0` and fires `release: published`, which
publishes **PyPI + npm (SDK + openclaw) + Docker** at 0.31.0 in one shot
— bringing all three registries back in sync.

No behavior/code change — versions only.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- Advance the Release Please manifest to the last released version
(0.30.0).
- Sync all 7 version-tracked files to 0.30.0 with the repo's own
`version-sync.py`.

## Testing

- [x] Linting passes (`ruff check .`)
- [x] Manual testing performed (`scripts/verify-versions.py`)

### Test Output

```text
$ python scripts/version-sync.py --version 0.30.0
Version synchronized to 0.30.0

$ python scripts/verify-versions.py
All versions aligned at 0.30.0
Packages: pyproject.toml, plugins/openclaw/package.json, sdk/typescript/package.json,
  plugins/headroom-agent-hooks/.claude-plugin/plugin.json,
  plugins/headroom-agent-hooks/.github/plugin/plugin.json,
  .claude-plugin/marketplace.json, .github/plugin/marketplace.json
```

## Real Behavior Proof

- Environment: local macOS, project `.venv` (Python 3.12.6).
- Exact command / steps: ran `scripts/version-sync.py --version 0.30.0`,
set the RP manifest to 0.30.0, then `scripts/verify-versions.py`.
- Observed result: `verify-versions.py` reports all seven version
locations aligned at 0.30.0; `git diff` shows version-field changes only
(no code). Confirmed PyPI latest is 0.30.0 and a `v0.30.0` tag/release
exists, while the manifest was 0.29.0 — the drift this PR corrects.
- Not tested: the downstream release itself (that runs when the
follow-up `chore: release 0.31.0` PR is merged); npm registry state
could not be read from this environment (network), but the `publish-npm`
job in `release.yml` publishes both npm packages on release.

## 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] My changes generate no new warnings
- [x] New and existing unit tests pass locally with my changes
2026-07-09 07:08:06 -07:00
Rob Francis
32ce99e4b4
fix(build): enable Intel macOS pip installs via ort-load-dynamic (#1538)
## Description

Fix `pip install headroom-ai` on Intel Mac (`x86_64-apple-darwin`).
Source installs failed because `ort-sys 2.0.0-rc.12` (transitive via
`fastembed`) does not ship prebuilt ONNX Runtime binaries for that
target, causing maturin/cargo to exit during the wheel build.

This PR mirrors the existing Windows fix: build the Rust core with
`ort-load-dynamic`, pin `ORT_DYLIB_PATH` to the pip `onnxruntime` native
library at import time, and publish Intel macOS wheels from CI.

Closes #

## 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)
- [x] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- `crates/headroom-core/Cargo.toml`: use `ort-load-dynamic` for
`x86_64-apple-darwin` instead of `ort-download-binaries-rustls-tls`.
- `headroom/_ort.py`: extend the ORT dylib pin hook to Intel macOS
(`darwin` + `x86_64`), resolving `libonnxruntime*.dylib` from the pip
`onnxruntime` package.
- `.github/workflows/release.yml` and `.github/workflows/rust.yml`: add
`macos-15-intel` / `x86_64-apple-darwin` wheel matrix entries.
- `tests/test_release_workflows.py` and
`tests/test_transforms/test_ort_dylib.py`: update/add coverage for the
new target and dylib pin behavior.
- `README.md`: note that prebuilt wheels are published for Intel macOS.

## Testing

- [x] Unit tests pass (`pytest`)
- [ ] 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_transforms/test_ort_dylib.py \
    tests/test_release_workflows.py::test_fastembed_uses_dynamic_ort_on_windows \
    tests/test_release_workflows.py::test_build_wheels_matrix_includes_intel_macos_with_dynamic_ort -q

..........................                                           [100%]
10 passed in 0.18s

$ maturin build --release -o /tmp/headroom-dist
📦 Built wheel for abi3 Python ≥ 3.10 to /tmp/headroom-dist/headroom_ai-0.27.0-cp310-abi3-macosx_10_12_x86_64.whl

$ python3.11 -m venv /tmp/hr-venv && /tmp/hr-venv/bin/pip install .
Successfully built headroom-ai
Successfully installed headroom-ai-0.27.0

$ cd /tmp && /tmp/hr-venv/bin/python -c "import headroom; import headroom._core; print('ok')"
version 0.27.0
_core ok
```

## Real Behavior Proof

- Environment: macOS `x86_64-apple-darwin`, Python 3.11.5, Rust 1.95.0
- Exact command / steps: Reproduced the reported failure with `pip
install headroom-ai` (sdist build dies in `ort-sys` for
`x86_64-apple-darwin`); after this patch ran `maturin build --release`,
then `pip install .` in a clean venv, then `python -c "import
headroom._core"`.
- Observed result: Before fix, cargo/maturin exit 101 on missing ORT
prebuilts; after fix, wheel build succeeds and `headroom._core` imports
cleanly (`version 0.27.0`, `_core ok`).
- Not tested: `macos-15-intel` GitHub Actions wheel matrix row (will be
validated by CI after merge).

## 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
- [ ] 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
- [ ] I have updated the CHANGELOG.md if applicable

## Additional Notes

- ML features (magika detection, fastembed embeddings) still require
`onnxruntime` at runtime on Intel Mac. Users should install
`headroom-ai[proxy]` or `pip install onnxruntime`; `_ort.py` auto-pins
`ORT_DYLIB_PATH` when that package is present.
- Apple Silicon (`aarch64-apple-darwin`) behavior is unchanged: it
continues to bundle ORT via `ort-download-binaries-rustls-tls`.
- Lint/mypy not re-run locally in this pass; targeted pytest +
maturin/pip install proof covers the changed surface.

---------

Co-authored-by: Bor <you@example.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 18:33:34 -05:00
Tejas Chopra
715ed7d200
chore: add CODEOWNERS with maintainer catch-all (#1622)
Some checks failed
CI / build (push) Failing after 2s
CI / workflow-validation (push) Failing after 1s
CI / docker-native-e2e (push) Failing after 1s
Init E2E / docker-init-e2e (push) Failing after 3s
Init Native E2E / init-native (ubuntu-latest, claude) (push) Failing after 3s
Init Native E2E / init-native (ubuntu-latest, codex) (push) Failing after 4s
Init Native E2E / init-native (ubuntu-latest, copilot) (push) Failing after 3s
Install Native E2E / install-native (ubuntu-latest) (push) Failing after 3s
Merge Conflicts / merge-conflicts (push) Failing after 4s
Release Please / release-please (push) Failing after 3s
Security / Dependency audit (pip-audit) (push) Failing after 17s
Security / CodeQL (javascript-typescript) (push) Failing after 4s
Security / CodeQL (python) (push) Failing after 4s
Security / Secret scan (gitleaks) (push) Failing after 10s
Wrap E2E / docker-wrap-e2e (push) Failing after 3s
Wrap Native E2E / wrap-native (ubuntu-latest) (push) Failing after 4s
CI / test-extras (push) Failing after 1s
CI / test-agno (push) Failing after 0s
CI / test-dashboard-ui (push) Failing after 1s
CI / test (1) (push) Failing after 1s
CI / test (2) (push) Failing after 1s
CI / test (3) (push) Failing after 1s
CI / test (4) (push) Failing after 1s
CI / windows-native-wrapper (push) Has been cancelled
CI / macos-native-wrapper (push) Has been cancelled
Init Native E2E / init-native (macos-latest, claude) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, codex) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, copilot) (push) Has been cancelled
Install Native E2E / install-native (macos-latest) (push) Has been cancelled
Wrap Native E2E / wrap-native (macos-latest) (push) Has been cancelled
## Description

Adds `.github/CODEOWNERS` so pull requests auto-request review from the
repository maintainers. A single catch-all rule assigns all matching
paths to the three accounts that actually have write access — the only
accounts GitHub accepts as code owners.

Closes # N/A

## Type of Change

- [ ] 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)
- [x] Documentation update <!-- repo governance/config; closest category
-->
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- Add `.github/CODEOWNERS` with a catch-all: `* @chopratejas
@JerrettDavis @DevanshiVyas`.
- Header comment documents the semantics (auto-request, last-match-wins,
owners need write access).

## Testing

<!-- No code changed, so ruff/mypy/pytest are N/A. Validated the file
with GitHub's own CODEOWNERS checker instead. -->

- [ ] Unit tests pass (`pytest`) — N/A (no code)
- [ ] Linting passes (`ruff check`) — N/A (no Python)
- [ ] Type checking passes (`mypy`) — N/A (no Python)
- [ ] New tests added for new functionality — N/A
- [x] Manual testing performed (GitHub CODEOWNERS validation API)

### Test Output

```text
# All 3 listed owners have write access (required, else the line is ignored):
$ gh api 'repos/headroomlabs-ai/headroom/collaborators?permission=push' --jq '.[].login'
JerrettDavis
chopratejas
DevanshiVyas

# GitHub's authoritative CODEOWNERS validation on this branch — zero errors:
$ gh api 'repos/headroomlabs-ai/headroom/codeowners/errors?ref=chore/add-codeowners'
{"errors":[]}
```

## Real Behavior Proof

- **Environment:** GitHub repo `headroomlabs-ai/headroom`, branch
`chore/add-codeowners`.
- **Exact command / steps:** `gh api
repos/headroomlabs-ai/headroom/codeowners/errors?ref=chore/add-codeowners`.
- **Observed result:** `{"errors":[]}` — every owner in the file
resolves to a valid account with repo access, so the rule is live (no
silently-ignored lines).
- **Not tested:** Whether branch protection is set to *require*
code-owner review — that's a separate repo setting, not part of this
file. See Additional Notes.

## 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 (this file
is self-documenting via its header)
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my feature works — N/A (config file;
validated via GitHub API)
- [ ] New and existing unit tests pass locally with my changes — N/A (no
code)
- [ ] I have updated the CHANGELOG.md if applicable — N/A (`chore:` is
excluded from the Release Please changelog)

## Additional Notes

- **Owner selection:** only `@chopratejas`, `@JerrettDavis`,
`@DevanshiVyas` have write access, so they are the only valid code
owners. High-volume contributors without push access were intentionally
omitted — GitHub silently ignores owners that lack write access.
- **Why one catch-all with all three (not per-path, not solo):** any
single owner can satisfy a required code-owner review, and listing three
avoids the solo-maintainer deadlock (a PR author can't self-satisfy
their own code-owner review). No per-subsystem rules were added because
commit history doesn't cleanly map owners to subsystems — happy to add
them if you want to define that.
- **Enforcement is opt-in:** this file only *auto-requests* reviewers
today. It becomes blocking only if branch protection enables "Require
review from Code Owners." If you turn that on, the three-owner setup is
what keeps everyone's PRs mergeable.
2026-06-30 21:52:29 -07:00
Rick van Hattem
db0356b0e7
ci: guard against committed merge-conflict markers (#1505)
## Description

Prevent unresolved Git merge-conflict markers from ever being merged
again. `CHANGELOG.md` collected literal `<<<<<<<` / `=======` /
`>>>>>>>` lines from **two** separate merged PRs (removed in #1497) and
nothing caught them: no `check-merge-conflict` hook, no workflow runs
`pre-commit`, and `ruff`/`mypy`/`pytest` do not parse Markdown.

This adds two layers:

1. **Dedicated `merge-conflicts` workflow**
(`.github/workflows/merge-conflicts.yml`) — runs on every PR/push, greps
all tracked files for conflict markers, and fails with the offending
locations. It is a **standalone workflow on purpose**: `ci.yml` sets
`on.pull_request.paths-ignore: ['**/*.md', ...]`, so a
Markdown/CHANGELOG-only PR skips `ci.yml` entirely — and the markers
this guard exists to catch landed in `CHANGELOG.md`. The new workflow
has no `paths-ignore`, so it always runs. (Thanks to the automated
review for catching that an earlier revision put the job inside
`ci.yml`, which would have reproduced the gap.)
2. **`check-merge-conflict` pre-commit hook** (`--assume-in-merge`) — a
local early-catch for devs who install hooks. The workflow is the actual
enforcement since hooks are opt-in.

## 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

- `.github/workflows/merge-conflicts.yml` (new): a single
`merge-conflicts` job, **no `paths-ignore`**, that runs `git grep -nI -E
'^(<{7}|>{7}|\|{7})( |$)'` over all tracked files and fails if any
match. Matches conflict start/end (and diff3 `|||||||`) markers — every
real conflict has these — while deliberately not matching a bare
`=======`, to avoid false positives on Markdown setext headers.
- `.pre-commit-config.yaml`: add `pre-commit/pre-commit-hooks`
`check-merge-conflict` with `--assume-in-merge`.

## Testing

- [x] Linting passes (`actionlint`)
- [x] Manual testing performed
- [ ] Unit tests pass (`pytest`) — N/A, CI-config + pre-commit change
(no product code)
- [ ] Type checking passes (`mypy headroom`) — N/A
- [ ] New tests added for new functionality — N/A (the guard is itself
the test; verified below)

### Test Output

```text
# The guard's command catches real markers (run against the pre-#1497 tree that still had them):
$ git grep -nI -E '^(<{7}|>{7}|\|{7})( |$)' <markered-tree> -- .
CHANGELOG.md:11:<<<<<<< pr/503-proactive-expansion-xml-tag
CHANGELOG.md:19:>>>>>>> main
CHANGELOG.md:48:<<<<<<< fix/gemini-offload
CHANGELOG.md:53:>>>>>>> main
#  -> exit 0 (matches found) => workflow step exits 1 (fails)

# And passes on a clean tree (this branch):
$ git grep -nI -E '^(<{7}|>{7}|\|{7})( |$)' HEAD -- .
#  (no output) -> exit 1 (no matches) => prints "No merge-conflict markers found." and exits 0

$ actionlint .github/workflows/merge-conflicts.yml .github/workflows/ci.yml  -> (no output) exit 0
$ commitlint --from <base> --to HEAD                                          -> 0 problems, 0 warnings
# Both workflow YAMLs parse; merge-conflicts.yml has no `paths-ignore`, so it runs on Markdown-only PRs.
```

## Real Behavior Proof

- Environment: Linux; the repo at this branch
(`ci/guard-merge-conflict-markers`), with `actionlint 1.7.7` and
`commitlint` (`@commitlint/config-conventional`). The "real behavior"
under test is the workflow's grep command and the YAML/trigger config —
exercised directly, not mocked.
- Exact command / steps: ran the workflow's exact command (`git grep -nI
-E '^(<{7}|>{7}|\|{7})( |$)' -- .`) against a tree that contains markers
and against this clean branch; ran `actionlint` on both workflow files;
parsed `merge-conflicts.yml` and confirmed its `pull_request` trigger
has no `paths-ignore` (the fix for the Markdown-only gap).
- Observed result: against the markered tree the command matched the
conflict start/end lines and exited 0 → the job exits 1 (red); against
this branch it matched nothing and exited 1 → the job prints "No
merge-conflict markers found." and exits 0 (green). `actionlint`
reported no problems on either workflow, and `merge-conflicts.yml` is
confirmed to omit `paths-ignore`, so it runs on CHANGELOG-only PRs that
`ci.yml` skips.
- Not tested: I did not drive the `check-merge-conflict` pre-commit hook
through a real in-progress merge (it is a standard upstream hook); the
gated CI run (fork-PR approval) was not executed — the guard logic and
triggers are verified above.

## 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 (inline
comments explain the workflow + hook)
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works (the guard is self-verifying; see Real Behavior Proof)
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable (N/A — CI tooling,
no user-facing change)

## Additional Notes

- **Supply chain:** the only new dependency is the
`pre-commit/pre-commit-hooks` repo pinned at `v5.0.0`. It is the
canonical pre-commit hooks collection maintained by the pre-commit
author (Anthony Sottile), is dev-only (runs solely inside pre-commit),
contains no native code, and has no runtime/install-time footprint. The
repo already trusts `astral-sh/ruff-pre-commit` and
`pre-commit/mirrors-mypy` the same way.
- The workflow adds ~10s per run and needs no secrets, network, or
build. Rebased onto current `main` after #1497 merged, so this is now a
clean single commit (the changelog cleanup is already on `main`).
2026-06-30 14:20:25 -05:00
Tejas Chopra
a639540959
chore: remove committed node_modules + stray/internal markdown (repo hygiene) (#1528)
## Description

Repo hygiene for a public OSS project: removes committed `node_modules`,
stray/internal/draft markdown, and commercial-surface references —
keeping every real doc (the published docs site, the wiki guides, and
all component READMEs) intact. Every file was content-audited before
removal, and load-bearing files were verified against the code/CI and
kept.

Net: **1,695 files changed, +23 / −266,409** (the deletions are
dominated by a committed `node_modules` tree).

Closes # (no tracking issue)

## Type of Change

- [ ] 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)
- [x] Documentation update
- [x] Code refactoring (no functional changes)

## Changes Made

**Removed (verified to have no code/CI dependencies):**
- `examples/vercel-ai-sdk-pr/` — 1,649 committed `node_modules` files
(zero example source); `node_modules/` added to `.gitignore`.
- `docs/spec/` (23 draft "Living Specification" files — orphaned,
`1.0.0-draft`, drifted from the code), `docs/superpowers/` (2 agent
plans), `docs/proposals/` (2 internal/commercial memos).
- 6 orphan `docs/*.md` (auth-modes, bedrock,
claude-code-vertex-headroom, cortex-code, output-token-reduction-guide,
rtk-loop-weighting).
- `PR.md` (committed PR draft), `ENTERPRISE.md`, `.github/FUNDING.yml`.

**Content scrubs:**
- Removed unreleased "Headroom Cloud" / `api.headroom.ai` / `hr_`
references from `configuration.mdx`, `wiki/configuration.md`,
`wiki/typescript-sdk.md`, `sdk/typescript/README.md` (reworded to
neutral, accurate phrasing).
- Dropped a stale "awaiting maintainer before merge" line from
`plugins/headroom-oauth2/SPEC.md`; tidied `.gitignore` comments (kept
the protective `headroom-managed/` ignore rule).
- Fixed the now-dangling links into removed files (README
nav/`output-token-reduction` link, `scripts/README`, `wiki/vertex`).

**Explicitly KEPT (load-bearing — would orphan in-code citations if
removed):**
- `.changelog.md` — consumed by `.github/workflows/release.yml` (read as
the release-notes file).
- `REALIGNMENT/`, `docs/observability.md`, `docs/rtk-architecture.md`,
`wiki/plans/`, `TESTING-copilot-subscription.md` — referenced by the
Rust core / Python / tests as design docs.

## Testing

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

### Test Output

```text
# Docs/markdown + .gitignore only — no Python/Rust source changed, so the
# behavioral test suite is unaffected. Verified the cleanup did not orphan
# references or break the published docs site:

$ git ls-files 'docs/content/docs/*.mdx' | wc -l      # published site intact
42
$ # meta.json nav unchanged; no published page removed.

$ grep -rnI "Headroom Cloud|api.headroom.ai|'hr_" $(git ls-files '*.md' '*.mdx')
>>> none

$ # dangling refs to removed files (excl pre-existing P0/P2 spec stubs that
$ # never existed in git): none remaining.
```

## Real Behavior Proof

- Environment: macOS, local git clone of the repo (markdown/.gitignore
changes only — no runtime).
- Exact command / steps: 4 read-only content-audit agents classified
every `.md`/`.mdx` file; each removal candidate was cross-checked
against the codebase (`grep` for citations in `.rs`/`.py`/tests,
workflows, and configs); only files with no dependents were removed; the
tree was re-grepped after removal to confirm no new dangling references;
verified the published docs site page count (`git ls-files
'docs/content/docs/*.mdx' | wc -l` = 42, unchanged).
- Observed result: the 42-page published docs site and all wiki guides
are untouched; no source or workflow references a removed file;
`.changelog.md` (consumed by release.yml) and the code-cited design docs
were detected as dependencies and kept; the committed `node_modules`
tree is removed and `node_modules/` is gitignored so it can't be
re-committed; zero "Headroom Cloud"/`headroom.dev` references remain.
- Not tested: N/A — no executable code changed (only markdown, `.mdx`,
and `.gitignore`), so the behavioral test suite is unaffected.

## 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
- [ ] 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
- [ ] I have updated the CHANGELOG.md if applicable

## Additional Notes

- This branch deletes `.github/FUNDING.yml` while PR #1526 edits it —
the two will be sequenced at merge (delete wins).
- A follow-up option (not in this PR): also remove the internal design
docs that are currently cited by the code (`REALIGNMENT/`,
`docs/observability.md`, `docs/rtk-architecture.md`, `wiki/plans/`) —
that requires scrubbing ~15–20 in-code citations so nothing dangles, so
it's deliberately deferred.
- Untracked local working files (`benchmarks/hf_pilot/`,
`tools/copilot-test/`) are intentionally left out of git (not
committed).
2026-06-27 23:32:54 -07:00
Tejas Chopra
53be64ca12
chore(telemetry): remove Supabase anonymous beacon; fix contact domain to headroomlabs.ai (#1526)
## Description

Removes the anonymous-telemetry **beacon** — the only external,
third-party data flow Headroom ever initiated. When telemetry was opted
in, it POSTed aggregate `/stats` to a hardcoded **Supabase** REST
endpoint (with an embedded anon API key in the source). For
enterprise/on-prem deployments this is exactly the kind of
vendor-controlled data egress a security review flags, so it's gone
entirely — **zero "Supabase" references remain in the codebase.**

What stays (by design): the **local** telemetry collector + the
`HEADROOM_TELEMETRY` opt-in (it only feeds `/stats` and `/v1/telemetry`
— nothing leaves the process), **OpenTelemetry export**
(`HEADROOM_OTEL_METRICS_*`, so operators send operational metrics to
*their own* collector), and the license usage reporter (your own domain,
license-key-gated).

Also fixes the contact domain: `headroom.dev` → `headroomlabs.ai`
everywhere.

Closes # (no tracking issue)

## 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
- [x] Code refactoring (no functional changes)

> Non-breaking: `HEADROOM_TELEMETRY` is still accepted (now gates local
collection only). The only behavior change is that no telemetry is ever
sent externally.

## Changes Made

- **Deleted the Supabase beacon**: `TelemetryBeacon` class,
`_SUPABASE_URL`/`_SUPABASE_KEY`/`_TABLE`/`_ENDPOINT`, the JSONB
projection helper, the proxy-lifespan beacon wiring, the `SUPABASE_`
install env passthrough, and `tests/test_strategy_stats_supabase.py`.
- **Kept** the local opt-in predicate (`is_telemetry_enabled` etc.) in
`beacon.py` — still used by the local collector + CLI — reworded to
"local only".
- **Retained** the single-worker-owner file lock (the cc-switch
reconciler depends on it); updated its comments to drop the beacon
framing.
- `/stats` `anon_telemetry_shipping` is now always `False` (nothing
ships externally); startup log reworded to "Local telemetry".
- Reworded remaining "Supabase" comments in `collector.py`,
`context.py`, `prometheus_metrics.py`, and two test docstrings.
- Contact domain: `security@headroom.dev` → `security@headroomlabs.ai`,
`conduct@headroom.dev` → `conduct@headroomlabs.ai`, FUNDING.yml sponsor
URL.

## Testing

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

### Test Output

```text
$ grep -rniI "supabase" --include=*.py --include=*.md --include=*.mdx .   # (excl .venv/sbom)
>>> ZERO Supabase references

$ grep -rniI "headroom.dev" .
>>> ZERO headroom.dev references

$ ruff check <changed files>           -> All checks passed!
$ ruff format --check <changed files>  -> 10 files already formatted
$ mypy <changed telemetry files>       -> Success: no issues found

$ pytest tests/test_telemetry.py tests/test_telemetry_warning.py \
    tests/test_proxy_telemetry_env.py tests/test_compression_observability.py \
    tests/test_paths.py tests/test_paths_backward_compat.py -q
============================= 173 passed in 6.67s ==============================
```

## Real Behavior Proof

- **Environment:** macOS, Python 3.12 (`.venv`).
- **Exact command / steps:** repo-wide grep for
`supabase`/`headroom.dev`; `create_app(...)` driven through a full
`TestClient` lifespan (startup + shutdown) in
`test_proxy_telemetry_env.py`; `/stats` exercised in
`test_telemetry_warning.py`.
- **Observed result:** zero `supabase`/`headroom.dev` strings remain;
the proxy starts and shuts down cleanly with the beacon removed (the
worker-owner lock + reconciler still elect a single owner);
`/stats.anon_telemetry_shipping` is `False` even with
`HEADROOM_TELEMETRY=on`; local collector + OTEL paths unchanged.
- **Not tested:** no live network call was ever made (the point — the
external POST is gone). OTEL export and the license reporter were not
exercised (unchanged by this PR).

## 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
- [ ] 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
- [ ] I have updated the CHANGELOG.md if applicable

## Additional Notes

- The license usage reporter (`reporter.py` → `app.headroomlabs.ai`) is
intentionally **kept** — it's license-key-gated (dormant for
unlicensed/OSS deployments) and goes to your own domain, not a third
party.
- Docs/CHANGELOG left unchecked: a couple of docs mention the telemetry
beacon and may want a follow-up note that it now collects locally only;
happy to add.
2026-06-27 22:48:26 -07:00
Tejas Chopra
51a3b01174
feat(security): pilot hardening — stateless guarantee, model pinning, CI security gate (#1515)
## Description

Engineering hardening derived from the Box vendor security assessment.
Each change turns a "No/Partial" questionnaire answer into a genuine
"Yes" by making the product safer — not by editing the form. The
throughline is Headroom's core promise to enterprise pilots: **it runs
inside the customer's environment and never persists or leaks their
data.** These changes make that provable.

Three themes: (1) a complete **stateless write guarantee** (a stateless
proxy writes nothing to the workspace during serving), (2)
**data-at-rest** protection (no cleartext prompts written on errors),
and (3) **supply-chain integrity** (all model downloads pinned;
SCA/SAST/secret-scanning in CI).

Closes # (no tracking issue)

## Type of Change

- [x] 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)

> Note: two deliberate, reversible default changes (not breaking): the
upstream-error debug dump is now off by default
(`HEADROOM_DEBUG_DUMP=1`/`=full` to opt in), and model downloads are
pinned (`HEADROOM_HF_PIN=off` to bypass). All stateless plumbing is a
pure no-op when not stateless.

## Changes Made

- **Stateless writes** — savings tracker + ledger, TOIN (`toin.json`),
and the output-savings recorder now honor stateless (in-memory only);
persistent memory is disabled under stateless with a warning. Added a
process-wide flag `headroom.paths.process_is_stateless()` (also honors
`HEADROOM_STATELESS`).
- **Debug dump** — the Anthropic *and* OpenAI handlers wrote full
requests (cleartext prompts/tools/system) to
`~/.headroom/logs/debug_400/` on every ≥400, even stateless. Now OFF by
default, stateless-aware, with a redacted middle tier; helpers extracted
to `handlers/_debug_dump.py`.
- **Model pinning** — all model downloads pin an immutable commit SHA:
our repos, kompress, image router/SigLIP, the third-party Qdrant memory
embedder (centralized in `onnx_runtime`), and the fastembed relevance
model (via the `revision` kwarg fastembed forwards to
`snapshot_download`). `HEADROOM_HF_PIN=off` bypasses.
- **CI security gate** — new `security.yml`: dependency audit
(pip-audit, scoped to the CVE-free `[all]` set), CodeQL (Python +
JS/TS), and gitleaks secret scanning (binary, MIT-licensed; PR-diff
scoped). `.gitleaks.toml` allowlists SBOM/lockfiles.
- **Dependabot** — extended to Rust (cargo) and npm (TS SDK, plugins,
docs).

## 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
$ ruff check <10 changed source files>
All checks passed!

$ mypy <changed source files>
Success: no issues found in 7 source files   # + handlers/server: no issues (annotation-unchecked notes only)

$ pytest tests/test_stateless_writers.py tests/test_stateless_toin.py \
    tests/test_debug_dump_gating.py tests/test_hf_revision_pinning.py \
    tests/test_proxy_savings_history.py tests/test_observability_metrics.py \
    tests/test_toin.py tests/test_paths.py -q
================= 176 passed, 6 skipped, 2 warnings in 13.92s ==================
```

New tests (18): `tests/test_stateless_writers.py`,
`tests/test_stateless_toin.py`, `tests/test_debug_dump_gating.py`,
`tests/test_hf_revision_pinning.py`, plus stateless control assertions
in `tests/test_proxy_savings_history.py`. They include the non-stateless
control cases (savings/TOIN still persist) and a regression guard that
fails if any handler writes a debug dump without gating it.

## Real Behavior Proof

- **Environment:** macOS, Python 3.12 (`.venv`); CI on `ubuntu-latest`.
- **Exact command / steps:**
- Stateless guarantee: `SavingsRecorder(tmp/"output_savings.json")` +
`set_process_stateless(True)` → `flush()`; TOIN
`ToolIntelligenceNetwork(TOINConfig(storage_path=""))`;
`create_app(ProxyConfig(memory_enabled=True, stateless=True))`.
- Debug-dump gating: `_debug_dump_mode(SimpleNamespace(stateless=...))`
across env values.
- Model pinning: model SHAs fetched/verified against the live
HuggingFace API; `_resolve_revision` / `_pinned_revision` resolvers
tested.
- **Observed result:** under stateless, no `proxy_savings.json` /
`savings_events.jsonl` / `toin.json` / `output_savings.json` /
`memory.db` is created; `proxy.memory_handler is None`. With
`stateless=False` the control tests confirm each still persists. Debug
dump resolves to `off` by default and is forced off in stateless. CI:
dependency-audit, CodeQL (python + js/ts) pass; secret-scan now runs the
gitleaks binary.
- **Not tested:** `pip-audit` was not run on the local machine (broken
`ensurepip`); CI is the first real run (the committed all-extras grype
scan is clean). The fastembed download path is exercised by CI/runtime,
not in unit tests (the revision resolver is unit-tested).

## 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
- [ ] 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
- [ ] I have updated the CHANGELOG.md if applicable

## Additional Notes

- **Concurrency:** `stateless` is a per-process config flag, never
per-request/per-session. Many sessions share one proxy's setting; a
stateless and a stateful proxy are separate OS processes with isolated
state. The one in-process edge (two proxies, different settings —
essentially tests) fails closed to in-memory, so a stateless proxy can
never leak.
- **Memory under stateless** is *disabled* (not in-RAM): the memory
subsystem is multi-component (SQLite + vector + markdown bridge) and a
partial in-RAM mode would be risky; ephemeral containers and
cross-session learning are contradictory. An ephemeral in-RAM memory
mode is a possible follow-up.
- **Docs/CHANGELOG** left unchecked: the two new env vars
(`HEADROOM_DEBUG_DUMP`, `HEADROOM_HF_PIN`) and the stateless behavior
changes are documented in code comments; happy to add user docs + a
CHANGELOG entry if preferred.
- CI deprecation warnings (Node 20, CodeQL Action v3) are GitHub-side
and out of scope here.
2026-06-27 17:44:10 -07:00
Tejas Chopra
5771a8020e
fix(deps): remediate dependency CVEs and publish SBOM (#1509)
## Description

Supply-chain hardening: takes the **shipped** dependency surface from
**26 known CVEs to 0**. `pip install headroom-ai[all]` now resolves with
no known vulnerabilities (verified with Anchore syft + grype). Also
publishes a checked-in SBOM package (`sbom/`) so any user — especially
pilots running their own security review — can verify what's inside and
that we track it.

This addresses the Dependabot alerts on `main` (9 high / 4 moderate / 7
low at time of writing).

Closes #

## 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)
- [x] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

**Rust**
- `pyo3` 0.24 → 0.29 (GHSA-36hh-v3qg-5jq4 High, GHSA-chgr-c6px-7xpp
Med). Migrated `Python::allow_threads` → `Python::detach` (10 sites) and
added `from_py_object` to the `Clone`-deriving `#[pyclass]` types (both
required by the 0.25+ API).
- `pyo3-log` 0.12 → 0.13; `lru` 0.12 → 0.18 (GHSA-rhfx-m35p-ff5j).

**Python**
- `torch` → 2.12.1, `mem0ai` → 2.x.
- Floor-pinned transitive CVE deps via `[tool.uv]
constraint-dependencies`: `pygments>=2.20.0`,
`pydantic-settings>=2.14.2`, `gitpython>=3.1.50`, `langsmith>=0.9.0`.
- **Removed `benchmark` from the `[all]` aggregate** so the default
install is CVE-free. `lm-eval` is invoked as an external subprocess
(`python -m lm_eval`) and never imported, so it is not a true runtime
dep — it remains available via the opt-in `[benchmark]` extra. See
[Accepted Risks](#additional-notes).

**npm (build/test tooling — never shipped in the
wheel/container/published SDK)**
- `esbuild` override `>=0.28.1` in `sdk/typescript` + `plugins/openclaw`
(GHSA-g7r4-m6w7-qqqr).
- `docs/`: `@anthropic-ai/sdk` → `^0.106.0` (GHSA-p7fg-763f-g4gf),
`postcss` override to force Next.js's bundled copy ≥8.5.10
(GHSA-qx2v-qp2m-jg93); regenerated a stale `bun.lock` that carried a
**Critical** vitest/vite.

**CI**
- Pinned `pypa/gh-action-pypi-publish` `@release/v1` → `@v1.13.0`
(GHSA-vxmw-7h4f-hqxh) in `release.yml` + `publish.yml`.

**SBOM**
- New `sbom/` directory: CycloneDX 1.7 + SPDX 2.3 SBOMs, grype scan
evidence, 330-package license inventory, and a regeneration guide.

## Testing

- [ ] Unit tests pass (`pytest`) — N/A, no Python source changed
(deps/config only)
- [x] Linting passes — `cargo fmt --check` + `cargo clippy` clean on the
changed crate; 0 `.py` files changed so `ruff`/`mypy` scope is
unaffected
- [x] Type checking passes — `cargo check --workspace` (0 errors)
- [ ] New tests added — N/A (dependency bumps; covered by existing
suites)
- [x] Manual testing performed — see Real Behavior Proof

### Test Output

```text
# headroom-ai[all] product surface — the number that matters
$ grype sbom:sbom/headroom-sbom-all-extra.cdx.json
No vulnerabilities found

# full repo scan (universal lock incl. opt-in [benchmark] + dev)
$ grype sbom:sbom/headroom-sbom.cdx.json
NAME        INSTALLED  TYPE    VULNERABILITY        SEVERITY
sqlitedict  2.1.0      python  GHSA-g4r7-86gm-pgqc  High      # [benchmark]-only, unpatchable, accepted
nltk        3.9.4      python  GHSA-p4gq-832x-fm9v  High      # [benchmark]-only, unpatchable, accepted

# pyo3 0.29 migration — extension builds + imports + runs
$ cargo check --workspace
    Finished `dev` profile [unoptimized + debuginfo] target(s)
$ maturin develop && python -c "from headroom._core import DiffCompressor, SmartCrusher; ..."
extension OK — detach + from_py_object paths exercised

# lru 0.18 — eviction path
$ cargo test -p headroom-proxy --lib drift
14 passed, 213 filtered out

# per-ecosystem npm audits
$ (cd sdk/typescript && npm audit)         -> found 0 vulnerabilities
$ (cd plugins/openclaw && npm audit)       -> found 0 vulnerabilities
$ (cd docs && npm audit && bun audit)      -> found 0 vulnerabilities / No vulnerabilities found
```

## Real Behavior Proof

- Environment: macOS (darwin 25.4.0, arm64), Python 3.12 `.venv`, Rust
1.95 toolchain, syft 1.46.0, grype 0.115.0, bun 1.3.14, maturin 1.13.3.
- Exact command / steps: (1) `uv export --extra all --no-dev
--no-emit-project | syft → grype` for the product surface; (2) `cargo
check --workspace` + `maturin develop` + extension import/compress smoke
test; (3) `cargo test -p headroom-proxy --lib drift`; (4) `cargo fmt
--check` + `cargo clippy -p headroom-py`; (5) `npm audit` in
sdk/openclaw/docs + `bun audit` in docs.
- Observed result: `headroom-ai[all]` resolution scans clean — "No
vulnerabilities found" (179 pkgs); full/prod SBOM shows only the 2
documented accepted CVEs; pyo3 0.29 extension imports and runs (detach +
from_py_object paths exercised); drift tests 14/14 pass; cargo fmt +
clippy clean; all npm/bun audits report 0.
- Not tested: full `pytest` suite (no Python source changed);
release-profile wheel build (used dev-profile `maturin develop` for the
import proof — the extension is semantically identical).

## 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
(`sbom/README.md`)
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective — N/A
(dependency bumps; existing suites + scans cover it)
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md — N/A (Release Please
auto-generates from the conventional commit)

## Additional Notes

**Accepted risks (the 2 residual CVEs).** Both originate solely from the
EleutherAI `lm-evaluation-harness` under the **opt-in `[benchmark]`
extra**, which Headroom invokes as a subprocess (never imports):
- `sqlitedict` CVE-2024-35515 (High) — pickle deserialization; package
abandoned (last release 2021), **no upstream fix exists**.
- `nltk` CVE-2026-54293 (High) — path traversal in `nltk.data.load()`;
affects ≤3.9.4 (current latest), **no patched release**.

Neither is in `[all]`, the published wheel, or the container. They are
documented in `sbom/README.md` and will be picked up automatically once
upstream ships fixes.

**Release/CHANGELOG:** N/A items above are because this is a
dependency/security PR with no Python source changes; CHANGELOG is
Release-Please-managed via the conventional commit message.
2026-06-27 15:28:12 -07:00
JD Davis
adb793bee1
ci: harden PR governance and model cache checks (#1401)
## Description

Hardens two routine PR-review pain points from the recent open-PR sweep:

- PR Governance reruns could keep validating the stale
`pull_request_target` event body even after the live PR description had
been fixed.
- Main CI model-cache misses could surface as dozens of unrelated
memory-test failures instead of one clear cache-preflight failure.

This intentionally avoids PyPI/package-bloat and release/nightly
workflow changes so the PR stays scoped to review and CI stabilization.

## Type of Change

- [x] Bug fix
- [ ] New feature
- [ ] Documentation
- [x] Refactor
- [x] Tests only

## Changes Made

- Added `--body-file` support to `scripts/pr-governance.py` so workflows
can validate the current PR body rather than stale rerun payloads.
- Updated PR Governance to fetch the live PR body via the GitHub API
before validating template fields.
- Added a CI preflight script that loads the default
sentence-transformer model in offline mode and verifies the expected
embedding dimension.
- Wired that preflight into the sharded CI job before pytest starts,
turning missing/corrupt Hugging Face caches into one early, actionable
failure.
- Added workflow/script regression tests for the live-body override and
model-cache preflight placement.

## Testing

- [x] Unit tests
- [x] Lint/static checks
- [ ] Integration tests
- [ ] Manual testing

### Test Output

```text
uv run --with pytest --with pytest-asyncio python -m pytest scripts/tests/test_pr_governance.py scripts/tests/test_pr_health_workflow.py scripts/tests/test_ci_workflow.py -q
9 passed in 0.04s

uv run ruff check scripts/pr-governance.py scripts/ci/verify_hf_model_cache.py scripts/tests/test_pr_governance.py scripts/tests/test_pr_health_workflow.py scripts/tests/test_ci_workflow.py
All checks passed!

python -m py_compile scripts\ci\verify_hf_model_cache.py scripts\pr-governance.py
# passed
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.13.3, isolated worktree
`C:\git\headroom\.worktrees\stabilization-hardening`.
- Exact command / steps: Ran the focused governance/workflow tests, ruff
on touched Python files, and `py_compile` for the executable scripts.
- Observed result: Governance tests prove a stale event body can be
overridden by the live PR body; workflow tests prove CI validates live
PR body and runs the Hugging Face offline-cache preflight before pytest
shards.
- Not tested: Full GitHub CI before PR creation; that will run on this
PR. The new Hugging Face preflight itself is intentionally not run
locally because it depends on the CI-warmed offline model cache.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review
2026-06-26 21:34:34 -07:00
JD Davis
2e29c7223f
fix(ci): guarantee model present in test shards to end cache-miss flakiness (#1399)
## Description

Fixes intermittent (`~25-test`) failures in `test` shards caused by a
GitHub Actions cache race between the `prefetch-model` job and the four
parallel `test` shards.

Closes #<!-- no upstream issue number yet -->

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)

## Changes Made

- Added `id: restore-hfcache` to the "Restore HuggingFace model cache"
step in the `test` job so its cache-hit outcome is observable.
- Added a conditional "Fallback model download if cache missed" step
immediately after the restore, gated on
`steps.restore-hfcache.outputs.cache-hit != 'true'`. When the cache
misses it runs the same authenticated `snapshot_download` retry loop
that `prefetch-model` already uses (same
`snapshot_download('sentence-transformers/all-MiniLM-L6-v2')`, same
default `~/.cache/huggingface` cache root, same unpinned
`huggingface_hub` — byte-for-byte the warm path's mechanism), with
`HF_HUB_OFFLINE=0` / `TRANSFORMERS_OFFLINE=0` scoped to that step only,
so the model lands where pytest looks before pytest starts.
- `TRANSFORMERS_OFFLINE: "1"` on the actual `pytest` step is unchanged.
- The `prefetch-model` job and shared cache key remain the warm-path
optimisation.
- **Added `.github/workflows/**` to the `code` paths-filter group** (the
gate `test` / `prefetch-model` / `build-wheel` / `lint` read via
`needs.changes.outputs.code == 'true'`). Rationale: a change to *how the
tests run* must be validated by the test suite it governs. Without this,
a PR that only touches `ci.yml` matches only the separate `workflows`
filter, so `code=false` and every test job is **skipped** — a CI change
would merge on a hollow green having never executed the pipeline it
modifies. With this line **this PR is self-validating**: the four `test`
shards and `prefetch-model` actually run and exercise the new cache-miss
fallback path. The separate `workflows` filter is left unchanged.
- Polish: the fallback retry loop no longer sleeps after its final (6th)
attempt — it only backs off when another attempt will follow, saving up
to 30s of wasted runner time on a hard failure.

## Testing

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

### Test Output

```text
YAML validation: python -c "import yaml,sys; yaml.safe_load(open('.github/workflows/ci.yml')); print('YAML valid')"
→ YAML valid

pre-commit hooks (Sync plugin versions, ruff, ruff-format, mypy): all Passed/Skipped
```

## Real Behavior Proof

- **Root cause**: GitHub Actions cache is eventually-consistent. The
`prefetch-model` job saves the model under `Linux-models-allMiniLM-v2`.
The four `test` shards are independent runner VMs that restore from that
key concurrently. If a shard reaches the restore step before the cache
entry has propagated to the storage layer it gets a cache miss. With
`TRANSFORMERS_OFFLINE=1` on the runner and no model on disk, any test
that instantiates `LocalEmbedder` (≈25 tests) crashes with
`OSError`/`LocalEntryNotFoundError`. Since only some shards miss per run
the failure appears random.
- **Fix rationale**: The inline fallback approach (adding an `id` to the
restore step + a conditional download step) is the smallest possible
diff — two logical additions inside the existing `test` job, no new
jobs, no new artifacts, no changes to any other job. The alternative
(artifact-based sharing via `upload-artifact` / `download-artifact`)
would have been more reliable but required restructuring
`prefetch-model` and the `test` job more significantly. Given the
existing retry loop in `prefetch-model` already handles transient
HuggingFace failures, reusing it as a fallback is the right call.
- **Validated on this PR**: by adding `.github/workflows/**` to the
`code` filter, the `test` shards (×4) and `prefetch-model` execute on
this very PR and pass — so the modified pipeline is proven, not skipped.
- **Not tested**: a live cache miss is not deterministically
reproducible on-demand (it depends on Actions cache propagation timing);
the fallback is byte-for-byte the prefetch-model job's proven download
path, so its correctness rests on that parity.

## 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] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works (N/A — CI-only change, no Python source modified)
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable (N/A — CI
infrastructure fix)

## Additional Notes

The `prefetch-model` job is preserved as the warm-path optimisation: on
a typical run the cache hits and the fallback step is skipped entirely
(no extra cost). The fallback only fires on the rare cache-consistency
miss that was previously causing flakiness.
2026-06-24 21:41:50 -05:00
Lakshya Sharma
88e67edf03
ci(release): publish win_amd64 wheel so Windows installs need no Rust (#1328) (#1335)
## Description

We ship wheels for macOS arm64 and manylinux x86_64/aarch64, but there's
no `win_amd64` wheel on PyPI for any Python version. So on Windows,
pip/uv can't find a binary and try to build from the sdist with maturin,
which pulls the Rust toolchain from static.rust-lang.org and crates from
crates.io. On locked-down machines (corporate proxies, CI runners, the
GitHub Copilot CLI sandbox, anything air-gapped) those hosts aren't
reachable and the install just dies:

```
error: could not download file from 'https://static.rust-lang.org/dist/channel-rust-stable.toml.sha256'
error: failed to get pyo3-macros as a dependency of package pyo3 v0.24.2
  [28] Timeout was reached (Failed to connect to index.crates.io port 443)
```

This adds the Windows wheel to the release matrix so `pip install
headroom-ai` works on Windows without a local Rust install.

Closes #1328

## 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

- Added a `windows-latest` / `x86_64-pc-windows-msvc` row to the
`build-wheels` matrix. The runner already has MSVC and maturin-action
sets up Rust, so it produces `headroom_ai-*-win_amd64.whl` on every
release. I checked `crates/headroom-core/Cargo.toml` first — the Windows
ONNX path is already on `ort-load-dynamic` under `cfg(windows)`, so the
wheel loads ORT at runtime instead of linking the DirectML SDK libs.
Nothing else was needed on the Rust side.
- Added a matching `windows-latest` row to `smoke-import-wheels` so a
broken Windows wheel blocks publish like the other platforms do. Windows
needed its own step: the venv puts Python under `Scripts\` not `bin/`,
and the runner defaults to pwsh. I also pinned the shared script-staging
step to `shell: bash` since it uses a heredoc that pwsh can't run (Git
Bash is on the runner), and added a `setup-python` step to get the right
minor version.
- Updated the README install section so the "install Rust first"
workaround is clearly only for the sdist fallback (e.g. Intel macOS) now
that Windows/Linux/macOS-arm64 all have prebuilt wheels.

## Testing

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

This is a CI workflow + docs change, no Python runtime code. I leaned on
the existing `tests/test_release_workflows.py` structural gates plus a
YAML parse and matrix-shape sanity check.

### Test Output

```text
$ python -m pytest tests/test_release_workflows.py -q
28 passed, 1 skipped, 1 failed
# The one failure, test_no_native_tls_in_wheel_build_tree, shells out to cargo, which
# isn't installed here. I confirmed with `git stash` that it fails the same way on main
# without my changes, so it's pre-existing and unrelated.

$ python -c "import yaml; d=yaml.safe_load(open('.github/workflows/release.yml',encoding='utf-8')); \
  j=d['jobs']; print('build-wheels rows:', len(j['build-wheels']['strategy']['matrix']['include'])); \
  print('smoke rows:', len(j['smoke-import-wheels']['strategy']['matrix']['include']))"
build-wheels rows: 4
smoke rows: 6
```

## Real Behavior Proof

- Environment: Windows 11 local clone; CI runs on GitHub-hosted
`windows-latest`.
- Exact command / steps: edited the build-wheels and smoke-import-wheels
matrices in `.github/workflows/release.yml` and the README, then ran the
release-workflow tests and the YAML/matrix-shape check above.
- Observed result: tests pass, YAML parses, build matrix is now 4 rows
(Linux x64, Linux arm64, macOS arm64, Windows x64) and the smoke matrix
is 6 rows including the new native Windows row.
- Not tested: the actual win_amd64 build + PyPI publish. Those jobs only
run in the release workflow on a tag or workflow_dispatch, not on a
feature PR. The PR-time release dry-run will exercise the new rows once
a maintainer approves the workflow run. I couldn't run `maturin build
--target x86_64-pc-windows-msvc` end to end here.

## 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
- [ ] I have updated the CHANGELOG.md if applicable

## Additional Notes

- No new test file: the existing structural gates in
`tests/test_release_workflows.py`
(`test_build_wheels_matrix_excludes_intel_macos`,
`test_aarch64_wheel_uses_native_arm64_runner`, the smoke-import gate
test) already assert the matrix contract and still pass with the Windows
row added.
- I didn't touch CHANGELOG.md — release-please generates it from the
Conventional Commit subject, so the `ci(release):` commit gets picked up
automatically.
- The win_amd64 wheel actually shows up on PyPI on the next tagged
release.
2026-06-24 09:48:37 -05:00
Parideboy
d633e8172c
fix(windows): pin UTF-8 encoding on text-mode subprocess calls (#1311)
Fixes #1310.

## Description

On Windows, `headroom` startup crashes a subprocess reader thread:

```
UnicodeDecodeError: 'charmap' codec can't decode byte 0x8d in position 7894: character maps to <undefined>
  ... subprocess.py _readerthread -> buffer.append(fh.read())
  ... encodings/cp1252.py
```

Text-mode `subprocess` calls omit `encoding=`, so Python decodes child
output with the locale codec (**cp1252** on Windows). Children that emit
UTF-8 ??? `cbm index_repository` (indexing sources with chars like
`???`/`???`), `claude mcp get/add`, the memory-sync process ??? produce
bytes invalid in cp1252 and kill the reader thread. Linux/macOS default
to UTF-8, so it's invisible there.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)

## Changes Made

- Add `encoding="utf-8", errors="replace"` to every text-mode
(`text=True` / `universal_newlines=True`) subprocess call in the
`headroom/` package (~50 call sites; several already had it).
- `errors="replace"` (not `ignore`) so corrupt bytes surface as `???`
rather than vanishing from parsed output.
- Add `tests/test_cli/test_subprocess_utf8_encoding.py`: an AST guard
asserting every text-mode subprocess call pins `encoding=`. The runtime
crash can't reproduce on UTF-8 CI, so the invariant is enforced at the
source level instead.

## Testing

- [x] Unit tests pass (`pytest`)
- New guard test passes (validates 51 call sites).
- `tests/test_install`, `tests/test_cli/test_mcp.py`,
`tests/test_mcp_registry` pass.
(`test_runtime_start_lock_blocks_another_process` fails on this Windows
box, but it fails identically on unmodified `main` ??? a pre-existing
`msvcrt` lock flake, unrelated.)

### Test Output

```text
> python -m pytest tests/test_cli/test_subprocess_utf8_encoding.py -q
1 passed in 0.12s

> python -m pytest tests/test_install/ tests/test_cli/test_mcp.py tests/test_mcp_registry/ -q
133 passed, 2 skipped in 15.34s
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.13.13.
- Exact command / steps: Started `headroom` without `PYTHONUTF8=1` on a
repo with UTF-8 chars in indexable files. Observed the
`UnicodeDecodeError` crash. Applied the fix (pinning `encoding="utf-8"`
on all text-mode subprocess calls). Re-ran. No crash. The AST guard
enforces the invariant on CI (which runs UTF-8 locales and cannot
reproduce the cp1252 crash natively).
- Observed result: Subprocess reader threads no longer crash on UTF-8
output under cp1252 locale.
- Not tested: All third-party tools that `headroom` shells out to; each
was given `errors="replace"` as a safety net.

## Workaround for affected users (before fix is deployed)

`PYTHONUTF8=1` (PowerShell: `$env:PYTHONUTF8=1; headroom ...`).

## Review Readiness

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 12:52:49 -05:00
Rod Boev
ad7993bf15
fix(codex): stop pinning Codex memory MCP to one project db (#1269)
## Description

Stop `headroom wrap codex --memory` from pinning the global
`headroom_memory` MCP server to one absolute SQLite path. Today the
wrapper writes `--db <wrap-cwd>/.headroom/memory.db` into
`~/.codex/config.toml`, which makes later Codex sessions either reopen a
stale project-local DB or fail with `unable to open database file` when
that original path disappears. This change lets the MCP server use its
existing per-cwd default again, so each Codex session resolves
`.headroom/memory.db` from the active project instead of a serialized
past cwd. Closes #1147

The current Codex-memory config surface was shaped by
https://github.com/chopratejas/headroom/issues/462 and
https://github.com/chopratejas/headroom/issues/730; this PR keeps that
surface project-scoped again instead of globally pinning one DB.

## 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

- remove the injected `--db` argument from the global `headroom_memory`
Codex MCP block while keeping `--user` intact
- preserve the wrap-time local `.headroom/memory.db` setup and
Claude-memory import path for the current project
- treat only wrap-owned Codex markers as snapshot-suppression and
unwrap-cleanup signals, so pre-existing named MCP blocks still back up
and restore
- log a startup diagnostic from `headroom.memory.mcp_server` that
records the configured DB path, config source, cwd/project root,
resolved storage scope, path existence/readability, and whether the path
was static or cwd-derived
- add a shared MCP SDK test stub so both the memory MCP and CCR MCP test
surfaces still run in CI when `mcp` is absent
- make the shared MCP stub re-import target modules under the stubbed
dependency set and restore any pre-existing target module object plus
dotted parent-package attribute state after cleanup
- add focused regressions and guard coverage for the persisted Codex
config shape, named-MCP marker backup and restore, the no-backup
memory-only unwrap path, the wrap-memory-then-unwrap cleanup path, the
failed-wrap memory-only cleanup path, the startup-diagnostic path
classification, the shared-store CCR retrieval path, and the shared MCP
stub import lifecycle
- add a `CHANGELOG.md` entry for the user-visible Codex memory scoping
fix

## Testing

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

### Test Output

```text
uv run pytest tests/test_ccr_mcp_server.py tests/test_memory/test_mcp_server.py tests/test_cli/test_wrap_codex.py tests/test_mcp_stub.py
======================== 78 passed, 1 warning in 5.96s ========================
Pytest warning:
PytestConfigWarning: Unknown config option: asyncio_mode
Pytest post-success atexit noise:
PermissionError: [WinError 5] Access is denied: 'C:\Users\Rod\AppData\Local\Temp\pytest-of-Rod\pytest-current'

uv run ruff check headroom/cli/wrap.py headroom/memory/mcp_server.py tests/_mcp_stub.py tests/test_ccr_mcp_server.py tests/test_cli/test_wrap_codex.py tests/test_mcp_stub.py tests/test_memory/test_mcp_server.py
All checks passed!

uv run ruff format headroom/cli/wrap.py headroom/memory/mcp_server.py tests/_mcp_stub.py tests/test_ccr_mcp_server.py tests/test_cli/test_wrap_codex.py tests/test_mcp_stub.py tests/test_memory/test_mcp_server.py --check
7 files already formatted
```

## Real Behavior Proof

- Environment: isolated temp project directories, a temp Codex home, the
real `wrap codex` and `unwrap codex` CLI commands under pytest, a mocked
missing-`codex` launch path for the failed-wrap cleanup case, and shared
MCP-SDK stubs for the memory MCP and CCR MCP test modules so CI still
exercises those paths without a real `mcp` install.
- Exact command / steps: run `uv run pytest tests/test_ccr_mcp_server.py
tests/test_memory/test_mcp_server.py tests/test_cli/test_wrap_codex.py
tests/test_mcp_stub.py`; prove the persisted config shape with
`TestCodexMemoryMcpConfig::test_inject_omits_db_and_replaces_existing_memory_block`;
prove prepare-only wrap cleanup with
`test_wrap_codex_memory_prepare_only_unwrap_removes_memory_mcp_without_prior_config`;
prove failed-wrap cleanup with
`test_wrap_codex_memory_launch_failure_unwrap_cleans_memory_only_config`;
guard pre-existing named Codex MCP preservation with
`test_memory_only_wrap_restores_preexisting_named_mcp_block` and
`test_memory_only_wrap_without_backup_preserves_named_mcp_block`; prove
the startup diagnostic classifications with
`test_memory_mcp_startup_context_reports_dynamic_project_db` and
`test_memory_mcp_startup_context_reports_static_external_db`; prove the
shared-store CCR retrieval path with
`test_mcp_uses_shared_singleton_store` and
`test_mcp_retrieves_proxy_stored_content`; prove stub import cleanup
with `test_import_module_with_mcp_stub_imports_target_and_cleans_up`,
`test_import_module_with_mcp_stub_reimports_target_and_restores_originals`,
and
`test_import_module_with_mcp_stub_cleans_up_dotted_target_attribute`.
- Observed result: the persisted global `headroom_memory` block now
keeps `--user` but omits `--db`; prepare-only memory setup still
bootstraps the current project's `.headroom/memory.db`; `headroom unwrap
codex --no-stop-proxy` now removes both the prepare-only generated
config and the failed-wrap memory-only config instead of leaving
`[mcp_servers.headroom_memory]` behind; pre-existing named Codex MCP
blocks remain restorable across both normal and no-backup memory-only
unwrap paths because only wrap-owned markers suppress backups or trigger
named-block cleanup; the memory MCP server now logs whether its DB path
came from the cwd default or an explicit static path, along with the
resolved path and scope it will open; CI can exercise both MCP test
modules even when the `mcp` package is absent from the shard
environment, and the shared stub now re-imports target modules under the
stubbed SDK while restoring both dependency and dotted parent-package
target-module import state after cleanup.
- Not tested: full end-to-end interactive Codex CLI launch.

## 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
- [ ] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [ ] 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 the CHANGELOG.md if applicable

## Additional Notes

The code change stays narrowly scoped to Codex memory config
persistence, cleanup, and startup observability. It does not widen into
larger memory-routing redesign or startup-failure recovery logic.
2026-06-23 07:49:07 -05:00
Ashish
23d73ae070
test(evals): add offline fidelity regression gate (recall-based, zero-model) (#1187)
## Description

Headroom's lossy compression drops rows/lines using statistical
heuristics but **never checks that meaning survived** — a dropped `OOM
killed worker 3` line can silently flip a model's answer with no signal
that compression caused it. The repo already ships a quality-metric
toolkit (`headroom/evals/metrics.py`) and a `weekly-suite` eval job, but
neither gates the compression path on a PR.

This adds a **per-PR fidelity regression gate**: compress vendored
golden tool-outputs through SmartCrusher's lossy path and assert the
evidence that answers each case's question survives. It is the first of
a planned trio (this is the "offline gate" half of the fidelity work);
query-aware retention and a hard token-budget API are documented
follow-ups.

Closes #

## 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

- **Blocking gate** (`tests/test_compression_fidelity_regression.py`):
compresses each golden case via `smart_crush_tool_output(...,
with_compaction=False)` and scores with `compute_information_recall`.
Two assertions:
- **Per-case critical recall == 1.0** — every `answer_evidence` string
(placed in error/anomaly rows, the documented SmartCrusher retention
guarantee) must survive.
- **Aggregate recall ≥ committed baseline** (`baseline.json`, tol 0.02)
— catches softer regressions.
- **Vendored fixtures** (`tests/fixtures/fidelity_golden/`):
deterministic `_generate.py` emits `cases.json` (4 cases: OOM crash,
payment exception, latency anomaly, CI failure) + `baseline.json`.
- **Non-blocking weekly report** (`.github/workflows/eval.yml`): one
step in the existing `weekly-suite` job (schedule/manual only) reuses
the existing `evaluate_information_retention` runner for a recall report
on the production routing path.
- **Pure reuse**: scoring (`evals/metrics.py`), compressor
(`smart_crush_tool_output`), and the weekly runner
(`evaluate_information_retention`) all already existed.

### Design notes

- **Zero new CI setup.** The blocking gate runs in the existing `[dev]`
test shard — no new workflow, no new deps, **no model, no network, no
secrets** (verified under `HF_HUB_OFFLINE=1`). It deliberately uses
small hand-made structured fixtures rather than the repo's HuggingFace
dataset loaders, which would require a network download + ModernBERT and
don't belong in a fast PR gate.
- **Scope:** structured JSON tool-output (the dominant, deterministic,
model-free case). Real-dataset (HotpotQA/BFCL) recall — which needs
`[all]` + a local model — is a **documented follow-up PR**, and the
`weekly-suite` job (which genuinely runs every Monday) is its natural
home.

## 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
$ HF_HUB_OFFLINE=1 python -m pytest tests/test_compression_fidelity_regression.py -v
tests/test_compression_fidelity_regression.py::test_critical_evidence_survives_compression[logs_oom] PASSED
tests/test_compression_fidelity_regression.py::test_critical_evidence_survives_compression[payment_exception] PASSED
tests/test_compression_fidelity_regression.py::test_critical_evidence_survives_compression[latency_anomaly] PASSED
tests/test_compression_fidelity_regression.py::test_critical_evidence_survives_compression[ci_test_failures] PASSED
tests/test_compression_fidelity_regression.py::test_aggregate_recall_not_regressed PASSED
============================== 5 passed in 0.18s ===============================
```

## Real Behavior Proof

- **Environment:** local checkout of `feat/fidelity-regression-gate`,
`pip install -e ".[dev]"`, `HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1`
(proves no model/network).
- **Exact command / steps:** `HF_HUB_OFFLINE=1 python -m pytest
tests/test_compression_fidelity_regression.py -q` → `5 passed in 0.14s`.
- **Negative control (proves the gate has teeth):** compressing
`logs_oom` and probing for a benign row that compression legitimately
drops returns `recall = 0.00, lost = ['heartbeat ping 25']` — i.e. the
gate fires when critical evidence is dropped, so it is not trivially
green.
- **Weekly (non-blocking) step verified locally:**
  ```text
  Information retention: 50/50 cases >=0.9 recall, avg compression 65.7%
  ```
- **Not tested:** real-dataset (HotpotQA/BFCL) recall and
prose/ModernBERT compression — intentionally deferred to a follow-up PR
targeting the weekly job.

## 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
- [ ] I have updated the CHANGELOG.md if applicable

## Additional Notes

- CHANGELOG/version intentionally untouched: repo uses
**release-please**.
- **Follow-up PR (planned):** wire the real HotpotQA/BFCL loaders
(`headroom/evals/datasets.py`) into the `weekly-suite` job for genuine
benchmark-scale recall coverage (model-allowed, non-blocking). Further
follow-ups from the same design: a live per-request fidelity guardrail,
query-aware lossy retention, and a hard `target_tokens` budget API.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 22:53:59 -05:00
Parideboy
5194388b66
fix(ci): normalize Windows CRLF line endings in PR governance script (#1012)
## Description

The `CODE_BLOCK_RE` regex in `scripts/pr-governance.py` expects LF after
the opening fenced code block. PR bodies authored on Windows can arrive
with CRLF line endings, which leaves a `\r` before the `\n` and prevents
`has_test_output()` from detecting a valid Test Output block.

This normalizes CRLF to LF once when loading the pull request body,
before section extraction and code-block matching. A regression test now
verifies that a valid PR body with CRLF line endings still passes
governance.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [x] Tests only

## Changes Made

- Normalize Windows CRLF line endings in `scripts/pr-governance.py`
before regex-based validation runs.
- Added `test_validate_pull_request_accepts_crlf_test_output_code_block`
to prevent regressions.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check`)
- [x] Formatting passes (`ruff format --check`)
- [x] New tests added for new functionality

### Test Output

```text
python -m pytest scripts/tests/test_pr_governance.py scripts/tests/test_pr_health_labels.py scripts/tests/test_pr_health_workflow.py -q
8 passed in 0.06s

ruff check scripts/pr-governance.py scripts/tests/test_pr_governance.py scripts/tests/test_pr_health_labels.py scripts/tests/test_pr_health_workflow.py
All checks passed!

ruff format --check scripts/pr-governance.py scripts/tests/test_pr_governance.py scripts/tests/test_pr_health_labels.py scripts/tests/test_pr_health_workflow.py
4 files already formatted
```

## Real Behavior Proof

- Environment: local Windows 11 checkout, Python 3.13.13.
- Exact command / steps: Converted the known-valid governance test body
to CRLF line endings and passed it through `validate_pull_request` in
the new regression test.
- Observed result: The report is valid with no problems, proving the
fenced Test Output block is recognized after normalization.
- Not tested: GitHub-hosted Windows PR authoring path end to end; the
unit test covers the exact CRLF body shape consumed by the validator.

## Review Readiness

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

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-06-22 18:45:12 -05:00
Parideboy
487aa71a3c
ci: restore green lint (reformat for ruff 0.15.17, fix mypy no-any-return, pin linters) (#1295)
## Description

The CI lint job (`ruff check .` → `ruff format --check .` → `mypy
headroom`) was red on `main`
and therefore on every open PR, for two unrelated reasons that the early
ruff failure was masking:

1. **ruff**: the lint job installs `ruff` unpinned, and ruff 0.15.17
began enforcing import-block
sorting (`I001`) and formatting that older ruff accepted → `ruff check
.` / `ruff format --check .`
   fail on files nobody touched.
2. **mypy**: `headroom/providers/opencode/config.py` had two `return
json.loads(...)` statements in
a function declared `-> dict[str, Any]`; `json.loads` is typed `Any`, so
`mypy headroom` fails
with `no-any-return` (reproduced on mypy 1.20.2 — not a version-specific
quirk).

This restores a green lint baseline and pins both linters so a future
release can't silently break
CI again.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)

## Changes Made

- `.github/workflows/ci.yml`: pin `ruff==0.15.17` and `mypy==1.20.2` in
the lint job.
- Applied `ruff check --fix .` (6 `I001` import-sort fixes) and `ruff
format .` (10 files) across
  the repo — import ordering and whitespace only, no behavior change.
- `headroom/providers/opencode/config.py`: narrow both
`_parse_json_loose` return sites with an
`isinstance(parsed, dict)` guard, so the `dict[str, Any]` annotation is
true at runtime
(non-dict JSON falls back to `{}`) and mypy's `no-any-return` is
resolved.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`, `ruff format --check .`, `mypy
headroom --ignore-missing-imports`)

### Test Output

```text
$ python -m ruff check .
All checks passed!

$ python -m ruff format --check .
913 files already formatted

$ python -m mypy headroom/providers/opencode/config.py --ignore-missing-imports
Success: no issues found in 1 source file

$ python -m pytest tests/test_providers_opencode_config.py -q
37 passed
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.13, ruff 0.15.17, mypy 1.20.2,
branch ci/fix-ruff-lint off
  headroomlabs-ai/main
- Exact command / steps: reproduced the red lint (latest ruff: 6 `I001`
+ 10 unformatted files;
the mypy failure was read from the #1295 CI lint log —
`config.py:125,133 no-any-return`, and
re-confirmed locally on mypy 1.20.2). Applied the ruff auto-fix/format,
added the dict guard,
  pinned both linters, and re-ran each lint step.
- Observed result: `ruff check .` → "All checks passed!"; `ruff format
--check .` → "913 files
already formatted"; `mypy` on the fixed file → "Success: no issues
found"; full `mypy headroom`
reports only Unix `fcntl` attributes that don't exist on this Windows
box (present on the Linux
CI runner, where the prior run showed exactly the two now-fixed errors).
37 opencode-config
  tests pass.
- Not tested: did not run the full OS/Python test matrix — the change is
formatting + two CI
dependency pins + a two-line type-narrowing guard, with no runtime
behavior change for dict JSON.

## Review Readiness

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

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 15:14:40 -05:00
Ashish
d789a7c528
feat(transforms): tabular + spreadsheet (.xlsx/.xls) compression (#1128)
## Description

Adds a content-type-aware path for **tabular data** — CSV/TSV, markdown
tables, fixed-width text, and binary `.xlsx`/`.xls` spreadsheets — by
routing them through the existing, battle-tested `SmartCrusher` instead
of letting them fall through to `PLAIN_TEXT → Kompress`.

The pipeline already compressed tables losslessly when handed a JSON
array of records. This wires up the missing front door: detect tabular
text (and ingest binary spreadsheets), convert to JSON records, and
reuse `SmartCrusher.crush()`. No new compression algorithm.

Closes #

## 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

- **Detection** (`content_detector.py`): new `ContentType.TABULAR` +
`_try_detect_tabular()` for CSV/TSV, markdown tables, and fixed-width
columns. Ordered after search/log (which also look "delimited") and
before code, with a prose-rejection guard so it never steals
`file:line:content` search output, `key: value` logs, or sentences with
incidental commas. Rust backend returns `plain_text` for unknown types
and the router already falls back to the Python detector, so **no Rust
change**.
- **Bridge** (`tabular_ingest.py`): stdlib parsers + `to_records()` + a
`TabularCompressor` that parses → JSON records → `SmartCrusher`
(lossless `csv-schema` first; lossy row-drop with reversible
`<<ccr:HASH>>` markers stays SmartCrusher's built-in fallback). Only
adopts a result when it actually saves bytes.
- **Spreadsheets** (`spreadsheet_ingest.py`): `.xlsx`/`.xls` → per-sheet
CSV text at the SDK boundary. Optional deps (`pip install
headroom-ai[spreadsheet]`) fail loudly with an install hint, never
silently degrade.
- **Routing** (`content_router.py`): `CompressionStrategy.TABULAR`,
`enable_tabular_compressor` flag, lazy getter, apply branch, strategy
maps, Kompress fallback eligibility.
- **SDK** (`compress.py`): `compress_spreadsheet(path, ...)` helper (one
message per sheet).
- **Packaging** (`pyproject.toml`): new `[spreadsheet]` extra;
`openpyxl` added to `[dev]` so the xlsx path is exercised in CI.
- **Docs/demo**: `examples/tabular_compression_demo.py` + README entry.

### Design note: lossless-only

Compact, all-unique tables with no query yield ~0 savings — this is
correct, not a bug. SmartCrusher returns
`skip:unique_entities_no_signal` and won't drop unique rows without a
duplicate/relevance signal. Real wins come from verbose/redundant tables
and query-driven selection. A pressure-driven lossy row sampler was
considered and intentionally not added.

## 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
$ python -m pytest tests/test_transforms_tabular.py -q
collected 20 items
tests/test_transforms_tabular.py ....................                    [100%]
============================== 20 passed in 7.15s ==============================

$ ruff check headroom/transforms/tabular_ingest.py headroom/transforms/spreadsheet_ingest.py
All checks passed!

$ mypy headroom/transforms/tabular_ingest.py headroom/transforms/spreadsheet_ingest.py
Success: no issues found in 2 source files
```

`tests/test_transforms_tabular.py` (20 tests): detection true positives
+ no-misroute negatives (search/log/JSON/prose), parser units (incl.
fixed-width), the CSV→SmartCrusher bridge, router routing + disable
flag, and `.xlsx` ingestion (skipif openpyxl missing) + error paths.
`spreadsheet_ingest` 100% / `tabular_ingest` 90% line coverage.

## Real Behavior Proof

- **Environment:** local checkout of `feat/tabular-compression`, Python
3.x, `pip install -e ".[dev]"`.
- **Exact command / steps:** `python
examples/tabular_compression_demo.py` (no API key required).
- **Observed result:**
  ```text
  === Raw tabular text (ContentRouter, char-level) ===
compact unique CSV strat=tabular chars 1306 -> 1072 ( 17.9% saved)
redundant CSV strat=tabular chars 2661 -> 1350 ( 49.3% saved)
verbose markdown strat=tabular chars 2019 -> 1580 ( 21.7% saved)

  === Full pipeline (real tokenizer) ===
  redundant CSV            tokens       768 ->    394  ( 48.7% saved)

  === Binary spreadsheet (.xlsx) ===
  2-sheet workbook         tokens      1092 ->    683  ( 37.5% saved)
  ```
- **Not tested:** legacy `.xls` binary path (needs optional `xlrd` +
binary fixture; `# pragma: no cover`); base64-embedded `.xlsx` inside
multimodal blocks (out of scope, noted as a follow-up).

## 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
- [ ] I have updated the CHANGELOG.md if applicable

## Additional Notes

- CHANGELOG/version are intentionally untouched: this repo uses
**release-please**, which bumps the version and CHANGELOG via automated
`chore: release main` PRs, not per-feature PRs.
- The `.xls` path is `# pragma: no cover` (legacy, needs optional `xlrd`
+ a binary fixture).
- Follow-up (out of scope): base64-embedded `.xlsx` inside
tool-result/multimodal blocks; porting tabular parsers into the Rust
core for parity.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 11:30:20 -05:00