Skip to content
99 changes: 39 additions & 60 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,82 +2,61 @@

## Purpose

This file provides **project-specific guidance for AI agents** (and other automated tools) working on the `commitizen` repository.
Follow these instructions in addition to any higher-level system or tool rules.
This file is the auto-loaded entry point for AI agents working on the `commitizen` repository. It holds the rules an agent needs in **every** session. Deeper guidance lives in:

## Project Overview
- [Contributing](docs/contributing/contributing.md) — setup, dev workflow, PR lifecycle.
- [Contributing TL;DR](docs/contributing/contributing_tldr.md) — poe command cheat sheet.
- [Pull Request Guidelines](docs/contributing/pull_request.md) — PR etiquette and AI-assisted policy.
- [Architecture Overview](docs/contributing/architecture.md) — codebase topology and extension points.
- [For AI Agents](docs/contributing/agents/index.md) — agent-shaped recipes, validation map, playbooks.

- **Project**: `commitizen` - a tool to help enforce and automate conventional commits, version bumps, and changelog generation.
- **Primary language**: Python (library + CLI).
- **Cross-platform**: Tests run on Linux, macOS, and Windows. Avoid POSIX-only assumptions in code (paths, subprocesses, line endings).
- **Key entrypoints**:
- `commitizen/cli.py` - main CLI implementation.
- `commitizen/commands/` - subcommands such as `bump`, `commit`, `changelog`, `check`, etc.
- `commitizen/config/` - configuration discovery and loading.
- `commitizen/providers/` - version providers (e.g., `pep621`, `poetry`, `npm`, `uv`).
- **Config sources**: `pyproject.toml` (project config, poe tasks, ruff, mypy), `.pre-commit-config.yaml` (hooks), `.github/workflows/` (CI).

## General Expectations

- **Preserve public behavior and CLI UX** — no breaking changes to APIs, CLI flags, or exit codes unless explicitly requested.
- **Update or add tests/docs** when you change user-facing behavior.
- **Commit messages** must follow [Conventional Commits](https://www.conventionalcommits.org/) (enforced by commitizen itself).
- **Pull requests** must follow the [Pull Request Guidelines](docs/contributing/pull_request.md) and the template in `.github/pull_request_template.md`.

## Setup and Validation

> Full contributor guidelines (prerequisites, workflow, PR process): [`docs/contributing/contributing.md`](docs/contributing/contributing.md).

### Bootstrap

```bash
uv sync --frozen --group base --group test --group linters
uv run poe setup-pre-commit # install git hooks (uses prek, a pre-commit runner)
```

### Local commands

- **Format**: `uv run poe format` (runs `ruff check --fix` then `ruff format`)
- **Lint**: `uv run poe lint` (runs `ruff check` then `mypy`)
- **Test**: `uv run poe test` (runs `pytest -n auto`)
- **CI-equivalent**: `uv run poe ci` (commit check + pre-commit hooks via `prek` + test with coverage)
- **Full local check**: `uv run poe all` (format + lint + check-commit + coverage)
Follow these instructions in addition to any higher-level tool or system rules.

Always run at least `uv run ruff check --fix . && uv run ruff format .` before pushing. CI will fail if the formatter modifies any files.
## Project at a glance

### CI pipeline

- CI runs `poe ci` on a matrix of Python 3.10–3.14 × ubuntu/macos/windows.
- Pre-commit hooks are defined in `.pre-commit-config.yaml` and run via [`prek`](https://github.com/j178/prek) (a `pre-commit` compatible runner).
- The matrix is **fail-fast**: inspect the earliest failing job that completed; others are cancelled.

### Common CI failure patterns

- **"Format Python code...Failed"**: Run `uv run poe format` and commit the result.
- **mypy `[arg-type]` on TypedDict**: Dynamically-constructed dicts (e.g., from `pytest.mark.parametrize`) passed to TypedDict-typed params need `# type: ignore[arg-type]`.
- **"pathspec 'vX.Y.Z' did not match"**: `.pre-commit-config.yaml` pins a tag of this repo. Rebase onto master to pick up the tag.
- **`VersionProtocol` + `issubclass`**: This Protocol has non-method members (properties), so `issubclass()` raises `TypeError`. Use `hasattr` checks for runtime validation.
- **Project**: `commitizen` — Python CLI for enforcing Conventional Commits, automating version bumps, and generating changelogs.
- **Library + CLI**: code is reachable both via `cz` and `import commitizen`.
- **Cross-platform**: tests run on Linux/macOS/Windows × Python 3.10–3.14. Avoid POSIX-only assumptions (paths, subprocesses, line endings).
- **Key entrypoints**:
- `commitizen/cli.py` — CLI definition (decli + argparse).
- `commitizen/commands/` — one module per `cz` subcommand.
- `commitizen/config/` — configuration discovery and parsing.
- `commitizen/providers/` — version providers.
- `commitizen/changelog_formats/` — changelog file formats.

## What to Read Before Changing
## Read before changing

| Changing... | Read first |
|---|---|
| CLI flags/arguments | `commitizen/cli.py`, `docs/commands/<cmd>.md`, `tests/test_cli/` |
| Bump logic | `commitizen/bump.py`, `commitizen/commands/bump.py`, `docs/commands/bump.md` |
| Changelog generation | `commitizen/changelog.py`, `commitizen/changelog_formats/`, `docs/commands/changelog.md` |
| Version schemes | `commitizen/version_schemes.py`, `tests/test_version_schemes.py` |
| Version providers | `commitizen/providers/`, `tests/test_providers.py`, `docs/config/version_provider.md` |
| Version providers | `commitizen/providers/`, `tests/providers/`, `docs/config/version_provider.md` |
| Config resolution | `commitizen/config/`, `tests/test_conf.py`, `docs/config/` |
| Tag handling | `commitizen/tags.py`, `tests/test_tags.py` |
| Pre-commit / CI | `.pre-commit-config.yaml`, `.github/workflows/`, `pyproject.toml` (poe tasks) |

## Coding Guidelines
For recurring task types (add a provider, deprecate an API, regenerate snapshots, ...), use the matching playbook in [For AI Agents § Playbooks](docs/contributing/agents/index.md#playbooks) instead of reinventing the workflow.

## Do not touch

These files are regenerated by Commitizen-specific tooling, so hand-edits get reverted on the next release or doc rebuild:

- `CHANGELOG.md` — produced by `cz changelog`. Hand-edits will be overwritten on the next release.
- `commitizen/__version__.py` — bumped by `cz bump` via the configured version provider.
- `.pre-commit-config.yaml:rev:` lines under the `Commitizen` repo — bumped by `cz bump` (`version_files` in `pyproject.toml`).
- `docs/images/cli_help/*.svg` and `docs/images/cli_interactive/*.gif` — regenerated by `uv run poe doc:screenshots`. See the [update-snapshots playbook](docs/contributing/agents/playbooks/update-snapshots.md).
- `tests/**/*` snapshot files used by `pytest-regressions` — regenerated via `uv run poe test:regen`.

## Mandatory PR reminders

These are easy to miss when working from an agent and are required by the PR template:

- **Types**: Preserve or improve existing type hints.
- **Errors**: Prefer `commitizen/exceptions.py` error types; keep messages clear for CLI users.
- **Output**: Use `commitizen/out.py`; do not add noisy logging.
1. **Complete the AI disclosure**. Check "Was generative AI tooling used to co-author this PR?" and fill in the `Generated-by:` trailer with the tool name. Details: [Pull Request Guidelines § AI-Assisted Contributions](docs/contributing/pull_request.md#ai-assisted-contributions).
2. **Run `uv run poe all` before pushing**. This is the command named in the PR template; it auto-formats then runs the same lint/check/test pipeline as CI. To mirror CI exactly afterwards, run `uv run poe ci` (uses `prek`, does not auto-format).
3. **Fill in "Steps to Test This Pull Request"** with the exact commands you ran locally — the maintainers re-run them.

## When Unsure
## When unsure

- Prefer **reading tests and documentation first** to understand the expected behavior.
- When behavior is ambiguous, **assume backward compatibility** with current tests and docs is required.
When behavior is ambiguous, assume **backward compatibility with current tests and docs** is required. Add a deprecation window instead of breaking it; see the [deprecate-public-api playbook](docs/contributing/agents/playbooks/deprecate-public-api.md).
38 changes: 38 additions & 0 deletions docs/contributing/agents/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# For AI Agents

These pages are written for AI agents contributing to Commitizen. Human contributors may also find them useful as a quick reference. They **complement** the existing human-facing contributor docs rather than replace them — anything covered by the human docs is linked, not restated.

> If you are an AI agent looking to **use** Commitizen as a tool (validate
> commit messages, bump versions in a downstream project), see the skill
> definition at `.agents/skills/commitizen/SKILL.md` in the repo root.
> The pages here are for working **on** Commitizen itself.

## When to read what

| You want to... | Read |
|---|---|
| Set up a local dev environment | [Contributing](../contributing.md#prerequisites-setup) |
| Look up a poe command | [Contributing TL;DR](../contributing_tldr.md#command-cheat-sheet) |
| Understand the codebase layout and extension points | [Architecture Overview](../architecture.md) |
| Open a pull request | [Pull Request Guidelines](../pull_request.md) and the [PR template](https://github.com/commitizen-tools/commitizen/blob/master/.github/pull_request_template.md) |
| Pick the right test selector for a change | [Validation Guide](validation.md#targeted-test-map) |
| Recover from a CI failure | [Validation Guide](validation.md#ci-failure-recipes) |
| Implement a recurring task type | [Playbooks](#playbooks) |

The repo-root [`AGENTS.md`](https://github.com/commitizen-tools/commitizen/blob/master/AGENTS.md) is the auto-loaded entry point for most agent tools. It holds the rules an agent needs in every session; this page is the deeper reference.

## Playbooks

Recipes for recurring task types. Each playbook is self-contained: trigger, files to read first, ordered steps, verification commands, and known pitfalls. They link out to the human-facing concept docs rather than restating concepts.

- [Add a version provider](playbooks/add-version-provider.md)
- [Add a changelog format](playbooks/add-changelog-format.md)
- [Add or modify a CLI flag](playbooks/add-cli-flag.md)
- [Deprecate a public API](playbooks/deprecate-public-api.md)
- [Update generated snapshots and screenshots](playbooks/update-snapshots.md)

If no playbook matches, read the [Architecture Overview](../architecture.md) for the relevant subsystem and follow 1-2 existing examples in the same directory before changing code.

## Updating these pages

Treat these pages like any other code change: open a PR, follow the template, run `uv run poe doc:build` to verify the mkdocs build, and check internal links. If you find yourself restating something that already lives in a human-facing doc, link to it instead and shorten the agent doc.
67 changes: 67 additions & 0 deletions docs/contributing/agents/playbooks/add-changelog-format.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Playbook: Add a Changelog Format

A changelog format handles parsing and rendering a `CHANGELOG.<ext>` file in a specific markup language. End-user documentation: [Changelog command](../../../commands/changelog.md). Built-ins are `markdown` (default), `asciidoc`, `textile`, `restructuredtext`.

Architectural context: [Architecture § Extension points](../../architecture.md#extension-points).

## Trigger

- "Support `<markup>` changelogs."
- A user wants `cz changelog` to emit something other than Markdown.
- An incremental-changelog use case fails because the user's existing `CHANGELOG` file is not Markdown.

## Read first

- `commitizen/changelog_formats/__init__.py` — `ChangelogFormat` Protocol, entry-point group `commitizen.changelog_format`, `KNOWN_CHANGELOG_FORMATS` registry, `_guess_changelog_format` extension-based fallback.
- `commitizen/changelog_formats/base.py:BaseFormat` — abstract implementation; you only need to override `parse_version_from_title` and `parse_title_level`.
- A close-match existing format:
- Heading-prefix-based: `commitizen/changelog_formats/markdown.py` (uses `#`, `##` prefixes).
- Underline-based: `commitizen/changelog_formats/restructuredtext.py` (uses `===`, `---` lines).
- `commitizen/templates/` — Jinja2 templates named `CHANGELOG.<ext>.j2` control rendering.
- `tests/test_changelog_format_<name>.py` — every format has parity tests; copy the closest one.

## Steps

1. **Create the format module** at `commitizen/changelog_formats/<name>.py`. Subclass `BaseFormat`. Set the class attributes:
- `extension: ClassVar[str]` — primary file extension (no dot).
- `alternative_extensions: ClassVar[set[str]]` — other accepted extensions for the same format.
2. **Implement two methods**:
- `parse_version_from_title(line: str) -> VersionTag | None` — given one line, return a `VersionTag` if the line is a release heading.
- `parse_title_level(line: str) -> int | None` — return the heading level (1, 2, 3, ...) if the line is a heading. The base class `BaseFormat.get_metadata_from_file` walks the file once and calls both methods per line.
3. **Add the Jinja2 template** at `commitizen/templates/CHANGELOG.<ext>.j2`. Mirror the structure of `CHANGELOG.md.j2` — same blocks, different markup. Make sure the loops over `tree`, `entries`, and `change_type` match.
4. **Register the built-in** in `pyproject.toml` under `[project.entry-points."commitizen.changelog_format"]`:

```toml
<name> = "commitizen.changelog_formats.<name>:<Name>"
```

5. **Add tests** at `tests/test_changelog_format_<name>.py`. Copy the closest existing test file and adapt the fixtures.
6. **Update the cross-format suite** `tests/test_changelog_formats.py` if it parametrizes over all formats — add the new one to its lists.
7. **Update user docs** at `docs/commands/changelog.md` and `docs/customization/changelog_template.md` — list the new format and show how to opt in via `changelog_format`.
8. **Re-run the install** so the entry point registers:

```bash
uv sync --frozen --group base --group test --group linters
```

## Validate

```bash
uv run pytest tests/test_changelog_format_<name>.py tests/test_changelog_formats.py tests/test_changelog.py tests/test_incremental_build.py -n auto
uv run poe lint
uv run poe doc:build # if docs changed
uv run poe all # final pre-push
```

## Pitfalls

- **`KNOWN_CHANGELOG_FORMATS` is populated at import time** from entry points, so you must re-run `uv sync` after editing `pyproject.toml` before tests can see your new format.
- **Forgetting `alternative_extensions`** — `_guess_changelog_format` uses both `extension` and `alternative_extensions` when the user does not set `changelog_format` explicitly. If a user has `CHANGELOG.<alt-ext>`, your format will not auto-detect without it.
- **Template encoding** — Jinja2 reads templates with the active encoding; keep them ASCII-safe or test with non-UTF-8 `encoding` settings.
- **Heading regex anchoring** — match the whole line (`^...$`) when the markup is line-anchored (Markdown headings); a substring match will pick up non-heading lines that mention `unreleased`.
- **Snapshot updates** — many changelog tests use `pytest-regressions`. See the [update-snapshots playbook](update-snapshots.md) when output intentionally changes.

## Stop and ask if

- The target format requires structured metadata that does not fit the `parse_title_*` Protocol (e.g., front-matter in YAML).
- The format implies a fundamentally different rendering tree (e.g., one file per release) — that is a bigger change than a format addition.
68 changes: 68 additions & 0 deletions docs/contributing/agents/playbooks/add-cli-flag.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Playbook: Add or Modify a CLI Flag

Commitizen's CLI is built declaratively with [decli](https://github.com/woile/decli) and `argparse` in `commitizen/cli.py`. Flags are dicts inside a `data["subcommands"]` list. End-user documentation: [Commands](../../../commands/init.md).

## Trigger

- "Add a `--<name>` flag to `cz <subcommand>`."
- "Make `--<name>` configurable via the config file."
- "Change the default of `--<flag>`."

## Read first

- `commitizen/cli.py` — the entire CLI schema. Search for the subcommand name in the `subcommands` block to find where its `arguments` list lives.
- `commitizen/commands/<subcommand>.py` — the command class that receives the parsed arguments via `self.arguments`.
- `commitizen/defaults.py:Settings` — TypedDict of all settings; required if your flag should also be config-file-readable.
- `tests/test_cli.py` and `tests/test_cli/` — flag-parsing tests.
- `tests/commands/test_<subcommand>_command.py` — behavior tests.
- `docs/commands/<subcommand>.md` — user-facing reference for the subcommand.
- `scripts/gen_cli_help_screenshots.py` — regenerates `--help` SVGs.

## Steps

1. **Add the flag** in `commitizen/cli.py` inside the relevant subcommand's `arguments` list. Follow the existing dict shape:

```python
{
"name": ["--my-flag", "-m"], # or just "--my-flag" if no short
"action": "store_true", # or "store", "store_false", ParseKwargs, ...
"default": False, # only when not store_true
"help": "<one-line description, period at end>",
}
```

Look at neighboring flags in the same subcommand to match style (option grouping, help-text tone).
2. **Consume the flag** in `commitizen/commands/<subcommand>.py`. It will arrive as `self.arguments["my_flag"]` (dashes become underscores).
3. **Config-file support (optional)**. If the flag should also be settable in the user's config file:
- Add the key to `commitizen/defaults.py:Settings` (and to `DEFAULT_SETTINGS` if there is a non-`None` default).
- In the command, fall back to `self.config.settings["my_flag"]` when the CLI value is `None`.
- Document the setting in the relevant `docs/config/<area>.md` page.
4. **Add tests**:
- CLI parsing: extend `tests/test_cli/` or `tests/test_cli.py` with a case that invokes `cz <subcommand> --my-flag` and asserts the parsed namespace.
- Behavior: extend `tests/commands/test_<subcommand>_command.py`.
5. **Update user docs** at `docs/commands/<subcommand>.md`. If the flag has a corresponding config setting, also update `docs/config/<area>.md`.
6. **Regenerate the help SVGs** so the new flag appears in the rendered docs. See the [update-snapshots playbook](update-snapshots.md) for the `poe doc:screenshots` workflow.

## Validate

```bash
uv run pytest tests/test_cli/ tests/test_cli.py tests/commands/test_<subcommand>_command.py -n auto
uv run poe lint
uv run poe doc:build
uv run poe all # final pre-push
```

## Pitfalls

- **Underscores vs dashes** — argparse converts `--my-flag` to `my_flag` in the namespace, but `decli` accepts both. Be consistent with neighboring flags.
- **`store_true` with explicit `default`** — argparse uses `False` as the implicit default for `store_true`; do not set `default` unless you need `None` to detect "user did not pass the flag" (which matters for config-file fallback).
- **Mutually exclusive flags** — argparse does not enforce mutual exclusion through the `decli` dict schema; validate in the command class and raise `commitizen.exceptions.InvalidCommandArgumentError` with a clear message.
- **Forgetting the `Settings` TypedDict** when adding a config-file key — `read_cfg` will accept the value but `mypy` will flag every read of `self.config.settings["my_flag"]`.
- **Breaking flag removals** — see the [deprecate-public-api playbook](deprecate-public-api.md). A flag is user-facing surface; do not remove it without a deprecation window.
- **Stale `--help` screenshots** — CI does not regenerate them. Run `uv run poe doc:screenshots` after any flag change and commit the result.

## Stop and ask if

- The flag would change the **exit code** of an existing success path — that breaks scripts that depend on exit codes. See [Exit Codes](../../../exit_codes.md).
- The flag's behavior overlaps with an existing flag with subtly different semantics — propose a deprecation plan first.
- The flag controls something that is currently determined by config precedence rules (CLI > env > config); make the precedence explicit in the issue.
Loading
Loading