headroom/CONTRIBUTING.md
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

5.7 KiB

Contributing to Headroom

Thanks for contributing! Please skim this before opening a PR : the policies exist because we've been burned skipping them, not because we love paperwork.

By participating, you agree to our Code of Conduct.

Where does my contribution go?

Type What to do
🐛 Bug or small fix Open a PR (with repro + test)
New feature / architectural change Open an issue or ask in Discord first.
🧹 Refactor-only Don't. Only if a maintainer asked, as part of a concrete fix.
🧪 Test/CI-only PR chasing a known main failure Don't. We're tracking it.
📦 New dep or version bump PR with written justification.
Question Ask in Discord #help

Open PR cap: 10 per author. Get existing ones merged before opening more.

Guiding principles

  • Verification is the author's job, not the reviewer's.
  • Supply chain is a real threat. Dependency changes get human review, every time.

Bug fixes

Every bug-fix PR must include:

  1. A reproduction — minimal code, failing test, or steps.
  2. A test that fails before your fix and passes after (unit, integration, or e2e).

If you genuinely can't write a test, say so explicitly and explain how you verified.

"Real behavior proof" — required on every external PR

We can't merge what we can't verify. Include a Real behavior proof section in the PR body covering:

  • Setup you tested on (OS, Python, config, provider/model)
  • Exact command or steps you ran after the patch
  • After-fix evidence + observed result
  • What you did not test

Counts: screenshots, recordings, terminal output, copied live output, linked artifacts, redacted runtime logs. Does not count alone: unit tests, mocks, snapshots, lint, typechecks, green CI. Have them too — but they prove the test passes, not that the feature works.

PRs missing this may be autoclosed.

New features

Before writing code:

  1. Open a feature-request issue (or raise in Discord).
  2. Get a 👍 from a core maintainer before implementing.
  3. Include a short spec covering:
    • API surface (public functions, config, CLI flags)
    • Changes to existing behavior
    • User stories — Given / When / Then, golden path + one edge case
    • Failure modes
    • Recovery / resilience
    • Security considerations

Short and concrete beats long.

Dependencies & supply chain

A human maintainer reviews every dep change. PRs that add or bump a package must justify:

  • Why this package (vs. doing it ourselves / using existing deps)
  • Who maintains it (activity, release cadence, security history)
  • Install surface (transitive deps, native code, install/runtime network)
  • Why this version — permitted reasons: bug fix, security patch, required new functionality. Cosmetic bumps will be closed.

PR workflow

  1. Fork, branch from main.
  2. Install Node 18+ and run uv sync --extra dev then make install-git-hooks — installs repo pre-commit checks on every commit, commitlint on every commit message, and ci-precheck on every push.
  3. One logical change per PR.
  4. Add tests.
  5. uv run pytest · uv run ruff check . · uv run ruff format .
  6. Do not edit CHANGELOG.md — release-please generates it from your Conventional Commit PR title, so a clear fix(...)/feat(...) title is your changelog entry. A CI guard rejects manual edits.
  7. Open the PR with a clear description + Real behavior proof + any spec/justification required, and keep the PR in draft until the Review Readiness boxes are complete.

Title format (conventional commits): feat:, fix:, docs:, test:, refactor:.

Commit message format is enforced locally by the repo's commit-msg hook and again in CI.

Review: CI green, one maintainer review, coverage held/improved.

Development setup

git clone https://github.com/chopratejas/headroom.git
cd headroom
python -m venv .venv && source .venv/bin/activate
node --version  # Node 18+ required for commitlint hooks
python -m pip install --upgrade pip
python -m pip install -e ".[dev,relevance,proxy]"
python -m pytest

Headroom uses a pyproject.toml/maturin build backend. Older pip versions may fail editable installs by looking for setup.py; upgrade pip first or use uv sync --extra dev.

Dev Containers

Two configs ship for VS Code / Codespaces:

  • .devcontainer/devcontainer.json — Python 3.12, uv, Node.js, gh.
  • .devcontainer/memory-stack/devcontainer.json — adds Qdrant + Neo4j sidecars (use qdrant:6333, neo4j://neo4j:7687).

Inside, use: uv run ruff check ., uv run pytest, etc.

Optional automated review

This repository includes .github/copilot-instructions.md so maintainers can opt into GitHub Copilot code review without adding workflow billing noise to every PR.

Enable or disable automatic Copilot review in Settings → Rules → Rulesets → Automatically request Copilot code review. Keep it off unless maintainers explicitly want the extra review traffic.

Coding standards

  • Ruff for lint + format, line length 100, PEP 8.
  • Type hints on public functions; Google-style docstrings.
  • Cover new behavior + edge cases; aim >80% coverage on new code.
  • Python 3.10+. Optional features go behind extras.

Architecture principles

Safety first: never drop user/assistant content, never break tool call/response pairing, malformed content passes through unchanged, prefer false negatives.

Performance: transforms <50ms at P99, lazy-load optional deps, profile before optimizing.

Contributors are credited in CHANGELOG, the GitHub contributors page, and release notes. Thanks again. 💚