diff --git a/.claude/rules/git.md b/.claude/rules/git.md new file mode 100644 index 0000000..c60a7c1 --- /dev/null +++ b/.claude/rules/git.md @@ -0,0 +1,42 @@ +--- +# Git & PR Discipline + +## Branch Naming +- `feat/` +- `fix/` +- `chore/` +- `docs/` +- `test/` + +Never work directly on `main`. + +## Commit Format — Conventional Commits + +``` +type(scope): description +``` + +Types: `feat`, `fix`, `chore`, `docs`, `test`, `refactor` + +- Atomic commits — one intention per commit. +- Never squash unless explicitly instructed. +- Never amend a commit that has already been pushed. + +## PR Workflow +1. Create feature branch. +2. Work and commit atomically. +3. Push: `git push -u origin ` +4. Open PR: `gh pr create` — title follows conventional commits. +5. **Stop. Do not merge. Sebastien reviews.** + +## Versioning +- Semver. Python and Rust share the same version number. +- Update `pyproject.toml` and `rust/Cargo.toml` version + git tag simultaneously. +- Tag format: `v..` + +## Hard Stops — Create BLOCKED.md and Halt +- Ambiguous spec on anything touching architecture. +- Test suite failing without an understood cause. +- Non-trivial merge conflict. +- Any operation touching credentials or `.env` files. +- Uncertainty about which branch to work on. diff --git a/.claude/rules/python.md b/.claude/rules/python.md new file mode 100644 index 0000000..6b6ff0d --- /dev/null +++ b/.claude/rules/python.md @@ -0,0 +1,37 @@ +--- +# Python Conventions + +## Runtime +- Python 3.11+ only. `tomllib` is stdlib — import directly (no third-party `tomli`). +- `pyproject.toml` sets `>=3.12` — do not lower this constraint. + +## HTTP +- `httpx` async for all HTTP. No `requests`. +- FastAPI handlers must be `async def`. +- Existing `requests` usage is tech debt — migrate if touching those files. + +## Typing +- Type hints on every function — no untyped functions. +- No `Any` without an inline comment explaining why. + +## Linting & Formatting +- `ruff` only — no black, no flake8, no isort. +- Run: `uv run ruff check src/ tests/` and `uv run ruff format src/ tests/` + +## Output +- All terminal output via `Rich` — no raw `print()` in command handlers. + +## Model Resolution +- Default: `claude-sonnet-4-6`. +- Always resolved via `resolve_model()` in `devbrief.core.credentials`. +- Chain: `DEVBRIEF_MODEL` env → `config.toml [anthropic] default_model` → `"claude-sonnet-4-6"`. +- Never hardcode a model string in any command file. + +## Credentials +- Never log or print credentials (even partially). +- Never commit `.env` or `config.toml` to the repo. +- Config file permissions: `600` on write. + +## Rust Extension +- If `devbrief_core` is unavailable at import time, fall back to Python implementation. +- Never hard-crash on a missing native extension. diff --git a/.claude/rules/rust.md b/.claude/rules/rust.md new file mode 100644 index 0000000..3b958e7 --- /dev/null +++ b/.claude/rules/rust.md @@ -0,0 +1,33 @@ +--- +# Rust Conventions + +## Quality +- `clippy` clean — zero warnings allowed. CI enforces this. +- No `unwrap()` in library code — use `?` or explicit error handling. + +## Build Setup (PyO3 / maturin) +- `Cargo.toml` must declare `crate-type = ["cdylib", "rlib"]`: + - `cdylib` — produces the Python extension module. + - `rlib` — lets the linker produce a test binary. +- `maturin develop` to build and install locally. + +## Running Tests + +``` +PYO3_BUILD_EXTENSION_MODULE=1 cargo test --manifest-path rust/Cargo.toml +``` + +- `PYO3_BUILD_EXTENSION_MODULE=1` tells PyO3 not to link against libpython + (may not be available as a shared lib on the host). +- Tests only call pure Rust functions — no live Python interpreter needed. +- The `rust-check` CI job sets this env var automatically. + +## Versioning +- Python and Rust share the same version number (semver). +- Update both `pyproject.toml` and `rust/Cargo.toml` versions simultaneously. +- Tag format: `v..` + +## Scope +- Rust is used only for `devbrief env` (gitignore audit, .env drift, secret scan). +- Published as `devbrief-core` on crates.io. +- If unavailable at runtime, Python falls back gracefully — never hard-crash. diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md new file mode 100644 index 0000000..2a7a790 --- /dev/null +++ b/.claude/rules/testing.md @@ -0,0 +1,40 @@ +--- +# Testing Conventions + +## Python — pytest + +- **Required:** tests for every new command and every credential resolution path. +- Current count: ~122 Python tests across 5 test files. +- Test files mirror `src/devbrief/` structure: + - `test_cache.py` — cache module + repo cache integration + - `test_credentials.py` — credential resolution + auth command + - `test_logs.py` — log parser, ring buffer, polling endpoints + - `test_github.py` — GitHub fetchers + - `test_display.py` — Rich display functions + +## Credential Mocking +- **Always** mock credential reads in tests — never use real API keys. +- Mock at the `devbrief.core.credentials` boundary, not deeper. + +## Running Python Tests + +``` +uv run pytest +``` + +## Rust — cargo test + +- Current count: 12 `#[cfg(test)]` tests in `rust/`. +- `dev-dependencies` must include `tempfile` for filesystem tests. + +## Running Rust Tests + +``` +PYO3_BUILD_EXTENSION_MODULE=1 cargo test --manifest-path rust/Cargo.toml +``` + +See `rust.md` for why this env var is required. + +## CI +- `ci.yml` runs lint + type-check + Python tests on every PR and push to `main`. +- `rust-check` CI job runs Rust tests with `PYO3_BUILD_EXTENSION_MODULE=1` set automatically. diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..f23a10d --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://claude.ai/claude-code/settings.json", + "permissions": { + "allow": [ + "Bash(uv:*)", + "Bash(cargo:*)", + "Bash(maturin:*)", + "Bash(pytest:*)", + "Bash(ruff:*)", + "Bash(git:*)", + "Bash(gh:*)", + "Bash(python3 -m json.tool *)" + ], + "deny": [] + } +} diff --git a/CLAUDE.md b/CLAUDE.md index 267a072..8951089 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,167 +1,78 @@ -# CLAUDE.md — DevBrief Agent Memory +# CLAUDE.md — DevBrief -## 1. Project Identity +## Project -DevBrief is a developer CLI tool for **project situational awareness**: given a GitHub repository URL (and later: log streams, API endpoints, infra configs, and PRs), it fetches structured data and generates a human-readable brief via Claude AI. The tool is designed for developers who need rapid context on any project without reading every file manually. +DevBrief: developer CLI for **project situational awareness** — fetches structured data from a GitHub repo and generates a human-readable brief via Claude AI. -**Tagline:** Project situational awareness **Distribution:** PyPI (`devbrief`) + crates.io (`devbrief-core`) +**Stack:** Python (uv) + Rust (maturin/PyO3). No React, no Docker, no webpack. --- -## 2. Tech Stack +## Commands -**Python layer:** -- `typer` — CLI framework (migration target from current `click`) -- `fastapi` + `jinja2` + HTMX — web UI for future `devbrief serve` -- SSE (Server-Sent Events) — streaming output in web UI -- `httpx` (async) — HTTP client (migration target from current `requests`) -- `boto3` — AWS integration for `devbrief logs` -- `anthropic` SDK — Claude AI integration -- `tomllib` (stdlib, Python 3.11+) — config file parsing -- `uv` — package manager and virtual env - -**Rust layer:** -- `maturin` + `PyO3` — Rust extensions callable from Python -- Enters via `devbrief env` subcommand -- Published as `devbrief-core` on crates.io - -**Not used:** React, Node, webpack, Docker (end users install via pip/cargo only) - ---- - -## 3. Project Structure - -``` -devbrief/ -├── src/ -│ └── devbrief/ -│ ├── __init__.py # Package init -│ ├── cli.py # Typer app — registers all subcommands -│ ├── commands/ -│ │ ├── repo.py # devbrief repo (cache-aware) -│ │ ├── auth.py # devbrief auth -│ │ └── logs.py # devbrief logs — FastAPI server, log parser, ring buffer -│ ├── core/ -│ │ ├── credentials.py # API key + model resolution chain -│ │ ├── config.py # Config file read/write (~/.config/devbrief/config.toml) -│ │ └── cache.py # Brief cache keyed by sha256(url+commit_sha) → ~/.cache/devbrief/ -│ ├── github.py # GitHub REST API fetchers (+ fetch_latest_commit_sha) -│ ├── brief.py # Claude prompt builder and generate_brief() -│ └── display.py # Rich terminal display functions -├── tests/ -│ ├── __init__.py -│ ├── test_cache.py # Cache module + repo cache integration tests -│ ├── test_credentials.py # Credential resolution + auth command tests -│ ├── test_logs.py # Log parser, ring buffer, polling endpoints -│ ├── test_github.py # Unit tests for GitHub fetchers -│ └── test_display.py # Unit tests for Rich display functions -├── dist/ # Built distributions (gitignored except .gitignore) -├── .github/ -│ └── workflows/ -│ ├── ci.yml # CI: lint + test on every PR and push to main -│ └── release.yml # Release: build + publish to PyPI on git tag v* -├── pyproject.toml # Project metadata, deps, build config (maturin) -├── uv.lock # Locked dependency tree -├── README.md # PyPI-ready README -├── assets/ -│ ├── devbrief-cache.gif # Demo GIF for devbrief repo (excluded from wheel) -│ ├── devbrief-env.gif # Demo GIF for devbrief env (excluded from wheel) -│ └── vhs/ -│ ├── devbrief-cache.tape # VHS tape source for devbrief-cache.gif -│ └── devbrief-env.tape # VHS tape source for devbrief-env.gif -├── LICENSE # MIT -├── CLAUDE.md # This file — agent persistent memory -└── .gitignore -``` - -**Assets policy:** `assets/` is excluded from the PyPI wheel via `[tool.maturin] exclude`. GIFs and tapes are repo-only. To regenerate: `vhs assets/vhs/devbrief-cache.tape` or `vhs assets/vhs/devbrief-env.tape`. +| Task | Command | +|------|---------| +| Install deps | `uv sync` | +| Run tests | `uv run pytest` | +| Lint | `uv run ruff check src/ tests/` | +| Format | `uv run ruff format src/ tests/` | +| Type check | `uv run mypy src/` | +| Rust tests | `PYO3_BUILD_EXTENSION_MODULE=1 cargo test --manifest-path rust/Cargo.toml` | +| Build wheel | `maturin develop` | --- -## 4. Subcommand Status - -| Subcommand | Status | Notes | -|-----------------|-------------|------------------------------------------------| -| devbrief repo | LIVE | v0.3.2, cache layer (SHA-keyed, ~/.cache/devbrief/), --no-cache/--refresh | -| devbrief auth | LIVE | v0.2.0, key validation, config write/read/clear, 600 perms | -| devbrief logs | LIVE | v0.3.0, FastAPI+HTMX polling dashboard, ring buffer, file (1s tail)/stdin | -| devbrief env | LIVE | v0.4.2, Rust active (maturin/PyO3), gitignore audit + .env drift + secret scan | -| devbrief api | PLANNED | | -| devbrief infra | PLANNED | | -| devbrief pr | PLANNED | | - +## Architecture — Do Not Change Without a Spec Card +- **Credential resolution:** env var → `.env` file → `~/.config/devbrief/config.toml` → keychain (future). Implemented in `devbrief.core.credentials`. +- **Cache key:** `sha256(url + commit_sha)` → `~/.cache/devbrief/` +- **Config file:** `~/.config/devbrief/config.toml`, permissions `600` on write. +- **Model:** always resolved via `resolve_model()` — never hardcoded in command files. +- **Rust extension:** `devbrief env` only. If unavailable at runtime, fall back to Python. +- **Assets:** `assets/` excluded from PyPI wheel via `[tool.maturin] exclude`. --- -## 5. Credential System - -**Layered resolution chain (highest priority first):** -1. Environment variable (e.g. `ANTHROPIC_API_KEY`, `GITHUB_TOKEN`) -2. `.env` file in working directory (loaded via `python-dotenv`) -3. `~/.config/devbrief/config.toml` — user-level config file -4. System keychain (future, not yet implemented) +## Subcommand Status -**Config file:** `~/.config/devbrief/config.toml` -**File permissions:** `600` (user read/write only — enforce on write) -**Rules:** -- Never log credentials -- Never print credentials (even partially) in normal output -- Never commit `.env` files or `config.toml` to the repo -- Tests must mock credential reads — never use real keys in tests +| Subcommand | Status | Notes | +|-----------------|----------|----------------------------------------------------------------| +| devbrief repo | LIVE | v0.3.2, SHA-keyed cache, --no-cache/--refresh | +| devbrief auth | LIVE | v0.2.0, key validation, config write/read/clear, 600 perms | +| devbrief logs | LIVE | v0.3.0, FastAPI+HTMX polling dashboard, ring buffer, file/stdin| +| devbrief env | LIVE | v0.4.2, gitignore audit + .env drift + secret scan (Rust) | +| devbrief api | PLANNED | | +| devbrief infra | PLANNED | | +| devbrief pr | PLANNED | | --- -## 6. CI/CD Rules +## Current Sprint -- **`ci.yml`**: Runs on every PR and push to `main`. Steps: lint (ruff), type-check, test (pytest). -- **`release.yml`**: Runs on git tag push matching `v*`. Steps: build wheels only (no sdist — Rust extension requires Rust to build from source), publish to PyPI via trusted publishing (OIDC). -- **Branch strategy:** `main` is protected. Feature branches: `feat/` or `feat/`. -- **Conventional commits:** `feat:`, `fix:`, `chore:`, `docs:`, `test:`, `refactor:` -- **Versioning:** semver. Python and Rust share the same version number. Update `pyproject.toml` version and tag simultaneously. +All subcommands through v0.4.2 are LIVE. **Next action:** await spec card before touching any subcommand. ---- - -## 7. Coding Rules (Enforce Always) - -- **Python 3.11+ only** — `tomllib` is stdlib, import it directly. (`pyproject.toml` currently sets `>=3.12`.) -- **Async-first:** Use `httpx` async for all HTTP. FastAPI handlers must be `async def`. (Current `requests` usage is tech debt to migrate.) -- **Ruff** for linting and formatting — no other linters, no black, no flake8. -- **Type hints everywhere** — no untyped functions, no `Any` without explanation. -- **Rust:** `clippy` clean, zero warnings allowed. -- **Rust testing:** `cargo test` requires two things: `crate-type = ["cdylib", "rlib"]` (rlib lets - the linker produce a test binary) and `PYO3_BUILD_EXTENSION_MODULE=1` (tells PyO3 not to link - against libpython, which may not be available as a shared lib on the host). Our tests only call - pure Rust functions so they do not need a live Python interpreter. Run as: - `PYO3_BUILD_EXTENSION_MODULE=1 cargo test --manifest-path rust/Cargo.toml`. - The `rust-check` CI job sets this env var automatically. -- **Tests required** for every new command and every credential resolution path. -- **Default model:** `claude-sonnet-4-6`. Never hardcode a model string in any command file. Model is always resolved via `resolve_model()` in `devbrief.core.credentials` (env var `DEVBRIEF_MODEL` → `config.toml [anthropic] default_model` → `"claude-sonnet-4-6"`). -- **Graceful degradation:** If Rust extension is unavailable, fall back to Python implementation. Never hard-crash on missing native extension. -- **Rich** for all terminal output — no raw `print()` in command handlers. +- **CI:** `ci.yml` runs lint + type-check + test on every PR and push to `main`. +- **Release:** `release.yml` on git tag `v*` — wheels only (no sdist), PyPI OIDC. +- **Versioning:** semver. Python and Rust share version. Update `pyproject.toml` + tag simultaneously. --- -## 8. Agent Boundaries +## What NOT to Touch -- **You implement. You do not decide architecture.** -- If a spec is ambiguous, stop and ask before writing code. -- If a decision would be hard to reverse (schema changes, public API shape, breaking changes), flag it before proceeding. -- When a task is complete, summarize: what was built, what files changed, what tests cover it. -- Read this file at the start of every session. If a subcommand ships or status changes, update the table in section 4. +- Do not push to `main` or merge PRs — Sebastien reviews. +- Do not hardcode model strings — always use `resolve_model()`. +- Do not use `print()` in command handlers — use Rich. +- Do not commit `.env` or `config.toml`. +- Do not build `sdist` — Rust extension requires Rust toolchain; wheels only. +- Do not implement new subcommands without a spec card. --- -## 9. Current Task Queue - -1. [x] Create CLAUDE.md -2. [x] Set up CI/CD pipeline (`ci.yml` + `release.yml`) — Rust steps present as commented stubs -3. [x] v0.2.0: CLI restructure (`devbrief repo`), `devbrief auth`, credential + model resolution -4. [x] v0.3.0: `devbrief logs` — FastAPI+HTMX polling dashboard, ring buffer, file/stdin -5. [x] v0.3.1: `devbrief repo` cache layer — SHA-keyed local cache, --no-cache/--refresh flags -6. [x] v0.3.2: `github.py` migrated from `requests` to `httpx` — closes HTTP client tech debt -7. [x] v0.4.0: `devbrief env` — gitignore audit, .env drift (Rust), secret scan (Rust), stub types -8. [x] Rust unit tests: `["cdylib","rlib"]`, `tempfile` dev-dep, 12 `#[cfg(test)]` tests, - `rust-check` CI job active, `PYO3_BUILD_EXTENSION_MODULE=1` for cargo test -9. [ ] Await spec card before touching any subcommand +## Conventions + +See `.claude/rules/` for enforced coding conventions: +- `python.md` — Python conventions, async, typing, Rich, model resolution +- `rust.md` — PyO3/maturin, clippy, cargo test setup +- `testing.md` — pytest structure, cargo test, credential mocking +- `git.md` — branch naming, conventional commits, PR discipline, hard stops