Skip to content

Contributing

Setup

git clone https://github.com/gregology/developer.git
cd developer
uv sync

Install pre-commit hooks:

uv run pre-commit install

This runs ruff, mypy, and trailing whitespace checks on every commit.

Code quality

All checks need to pass. CI runs them on every push.

uv run ruff check src/ tests/                        # lint
uv run ruff format src/ tests/                       # format
uv run mypy                                           # type check (strict mode)
uv run pytest                                         # test + coverage (minimum enforced by pytest-cov)
uv run bandit -r src/ -q -c pyproject.toml            # security
uv run vulture src/ --min-confidence 80               # dead code
uv run complexipy src/ --max-complexity-allowed 15    # cognitive complexity

Conventions

  • Python 3.11+, type annotations on all function signatures
  • mypy strict mode. Use dict[str, Any] not bare dict.
  • Click for the CLI. Each command module exports a Click command or group.
  • Config goes in developer.yaml (copy from example.developer.yaml). The Pydantic models in config.py (DeveloperConfig, RepoConfig) are the in-code representation.
  • Architectural decisions go in DECISIONS.md with context, rationale, and revisit triggers.

Adding a new repo

Add a block to your developer.yaml (copied from example.developer.yaml). See Configuration.

Adding a new audit type

  1. Create src/developer/templates/audit/<name>.md.j2 -- include _labels.md.j2 and _personality.md.j2, use {{ max_findings }} for the limit
  2. Add an AuditSpec to AUDITS in src/developer/services/audit.py: the template, the tool profile, a one-line help text, and a function that gathers the template's extra variables from the worktree. The developer audit <name> command and the API's /audit/<name> endpoint come from that entry.

Adding a new resolve stage

  1. Create src/developer/templates/resolve/<name>.md.j2
  2. Define the Stage in src/developer/pipelines/resolve.py, add it to STAGES, and add its name to STAGE_NAMES in src/developer/config.py
  3. Call it from the resolve() function where it belongs, with run.stages.ask() or run.stages.work()

Adding a new pipeline

Bigger lift, but the pattern is well-established:

  1. Create a pipeline file in src/developer/pipelines/
  2. Define its Stages and a STAGES tuple, and add their names to STAGE_NAMES in src/developer/config.py
  3. Write the pipeline as a function of its input and a Run, using ordinary control flow for its branches
  4. Do any git or GitHub work in that function, never through the agent
  5. Create Pydantic models in src/developer/models/ for structured outputs
  6. Create Jinja2 templates in src/developer/templates/<pipeline>/
  7. Add a Click command in src/developer/commands/ and register it in cli.py
  8. Add a service in src/developer/services/ that runs the pipeline and returns a typed result, and call it from the Click command
  9. Add a POST endpoint in src/developer/api/routes.py with a _run_* task function that calls the same service, and a request model in api/models.py

API conventions

The API lives in src/developer/api/. It mirrors the CLI — same pipelines, same building blocks — but returns structured JSON instead of terminal output.

  • Routes go in api/routes.py. Each pipeline endpoint has a _run_* task function that calls the pipeline's service in src/developer/services/, the same one the CLI uses.
  • Request/response models go in api/models.py.
  • Task execution uses the app's TaskStore (api/tasks.py). Task functions run in daemon threads, keyed by the repo and target they work on, and log progress with task.log_message(). Tests give the store an executor that runs tasks synchronously.
  • Put pipeline logic in the pipeline and its service, never in a Click command or an API route, so the CLI and the API can't drift apart.