{"owner":"pyinfra-dev","repo":"pyinfra","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["CLAUDE.md","AGENTS.md"],"skills":{"CLAUDE.md":"# CLAUDE.md\n\nSee @AGENTS.md for guidance.\n","AGENTS.md":"# AGENTS.md\n\nThis file provides guidance to coding agents, like Claude Code (claude.ai/code), when working with \ncode in this repository.\n\n## About pyinfra\n\npyinfra turns Python code into shell commands and runs them on servers. Think Ansible, but Python\ninstead of YAML, and much faster. It supports SSH, local machine, Docker, and more via connectors.\n\n## Development Setup\n\n```bash\nuv sync  # Install all dependencies into managed venv\n```\n\n## Common Commands\n\n```bash\n# Run tests\nscripts/dev-test.sh\n# or directly:\nuv run pytest --cov --disable-warnings -m 'not end_to_end'\n\n# Run fixtures for a single operation or fact\nuv run pytest tests/test_operations.py -k \"apt.packages\"\nuv run pytest tests/test_facts.py -k \"LinuxHardware\"\n\n# End-to-end tests (require Docker/SSH/local targets)\nuv run pytest -m end_to_end_local\nuv run pytest -m end_to_end_docker\n\n# Lint and type check\nscripts/dev-lint.sh\n# Individually:\nuv run ruff check\nuv run ruff format --check\nuv run mypy\nuv run python scripts/lint_arguments_sync.py\n\n# Auto-format\nscripts/dev-format.sh\n```\n\n## Architecture\n\nThe repo has two top-level packages under `src/`:\n\n- **`pyinfra/`** — core library (operations, facts, connectors, API)\n- **`pyinfra_cli/`** — CLI wrapper using Click + gevent\n\n### Core Concepts\n\n**Operations** (`src/pyinfra/operations/`) — Declarative functions (e.g. `apt.packages`,\n`files.put`) that describe desired state. Each operation uses the `@operation` decorator from\n`api/operation.py`, generates a list of commands, and is idempotent. Operations call facts to\nread current state, then return commands to reach desired state.\n\n**Facts** (`src/pyinfra/facts/`) — Read system state (e.g. `AptPackages`, `LinuxHardware`). Each\nfact is a class extending `FactBase` with a `command` attribute and a `process()` method that\nparses command output. Facts are cached per-host per-run.\n\n**Connectors** (`src/pyinfra/connectors/`) — Abstractions for how to connect to and execute on a\ntarget (SSH, local, Docker, chroot, Terraform, Vagrant, etc.). New connectors should be separate\npackages, not added to this repo.\n\n**API** (`src/pyinfra/api/`) — The core engine:\n- `state.py` — Global deploy state, callbacks, host grouping\n- `host.py` — Per-host metadata and fact access\n- `inventory.py` — Collection of hosts and groups\n- `operation.py` — `@operation` decorator that wraps functions into deploy operations\n- `operations.py` — Execution engine that runs operations across hosts\n- `command.py` — Command types: `StringCommand`, `FileUploadCommand`, `RsyncCommand`,\n  `QuoteString`, `MaskString`, etc.\n- `facts.py` — Fact base classes and execution logic\n- `deploy.py` — `@deploy` decorator for grouping operations\n- `connect.py` — Connector lifecycle (connect/disconnect)\n- `config.py` — `Config` object with all configuration options\n- `arguments.py` / `arguments_typed.py` — Global operation arguments (e.g. `_sudo`, `_su_user`);\n  **these two files must stay in sync** — CI enforces this via `scripts/lint_arguments_sync.py`,\n  so touching one requires touching the other\n- `output.py` — Pluggable output functions (decoupled from Click for testability)\n\n**Context** (`src/pyinfra/context.py`) — Thread-local (gevent-safe) context objects: `host`,\n`state`, `config`, `inventory`. Operations access the current host via `pyinfra.context.host`\nrather than explicit passing.\n\n**Concurrency** — Uses gevent greenlets for parallel host execution. `pyinfra_cli/main.py`\nmonkey-patches stdlib at startup.\n\n### Adding Operations / Facts\n\nOperations and facts are auto-discovered from their respective directories. A new\n`src/pyinfra/operations/mytool.py` is immediately available as `from pyinfra.operations import\nmytool`.\n\n- Operations must be idempotent and use facts to check current state\n- Facts must implement `command` (shell command to run) and `process(output)` (parse result)\n- Both need corresponding tests (see fixture convention below)\n- Every operation/fact module must be registered in `pyinfra-metadata.toml` as a plugin with\n  tags; omitting this won't break tests but will break docs generation\n\n**Operation / fact tests are YAML or JSON fixtures, not Python tests.** Drop a file under\n`tests/operations/<module>.<op>/` or `tests/facts/<module>.<Fact>/` — it is auto-discovered by\nthe `testgen` metaclass. Prefer YAML for new fixtures. To cover a new code path, add a fixture —\ndo not write a new Python test.\n\nOperation fixture structure (`tests/operations/<module>.<op>/<name>.yaml`):\n\n```yaml\nargs:\n  - positional_arg\nkwargs:\n  param: value\nfacts:\n  module.FactClass: {}          # a dict of mock values keyed by object_id and attribute\ncommands:\n  - shell command that should be produced\n```\n\nOptional keys: `exception` (e.g. `{name: OperationError, message: \"...\"}`), `noop_description`.\n\nFact fixture structure (`tests/facts/<module>.<Fact>/<name>.yaml`):\n\n```yaml\ncommand: shell command the fact runs\nrequires_command: binary               # optional\noutput: |\n  raw stdout to parse\nfact:\n  item:\n    key: value                         # expected return value of process()\n```\n\n## Coding Conventions\n\n**Docstring format** — pyinfra uses `+ param: description` bullets (parsed by\n`scripts/generate_operations_docs.py`). Do not use Google/NumPy/Sphinx style — it will silently\nbreak docs generation.\n\n**Shell safety** — user-supplied values must be composed into shell commands using `StringCommand`\n+ `QuoteString` / `MaskString` from `pyinfra.api`. Do not use plain string formatting (e.g.\n`\"rm -f {}\".format(path)`) for user-controlled values. This applies to **every** user-controlled\nvalue regardless of type — wrap ports, integers, paths and other non-string args with\n`QuoteString` too; reviewers explicitly flag unquoted ints as injection risk.\n\n**Optional parameter defaults** — optional parameters must default to `None`, not `\"\"`. Older\noperations in the codebase use `\"\"` defaults; do not replicate this pattern. Type these as\n`T | None` (e.g. `path: str | None = None`); do not use `Optional[T]`.\n\n**Distinguish unset from empty** — when a parameter is `T | None`, branch on `if x is not None:`\nrather than truthy `if x:`. Empty strings, `0`, and empty containers are valid user input and a\ntruthy check silently drops them.\n\n**FactBase typing** — every `FactBase` subclass must annotate its `command()` and `process()`\nmethods: `def command(self, repo: str) -> str:` and `def process(self, output: list[str]):`. The\n`output` argument is **already a `list[str]`** — do not re-wrap it with `list(output)` or iterate\ninto a new list before indexing.\n\n**Fact \"show file or empty\" pattern** — prefer `! test -e PATH || cat PATH` over\n`cat PATH 2>/dev/null || true`. The first form only suppresses the missing-file case; the second\nswallows real `cat` errors and hides bugs.\n\n**Reuse existing helpers** — before adding chown/chmod/path/permission utilities, check\n`pyinfra.operations.util.file_utils` (and the rest of `operations.util/`). Reviewers consistently\nask for duplicated logic to be replaced with the existing helper.\n\n**No `assert` in `src/`** — `python -O` strips assertions, silently dropping the check. Raise an\nexplicit exception instead: `OperationError` for operation argument issues, `ValueError` /\n`TypeError` for library code. `assert` is fine in tests.\n\n**Type hints** — all new (non-test) code must be fully type hinted. Use modern Python 3.10+\nconventions: built-in generics (`list`, `set`, `dict`, `tuple`) instead of `typing` equivalents\n(`List`, `Set`, etc.), and avoid quoting class names unless a forward reference is strictly\nrequired.\n\n## Branch Strategy\n\nPRs target the latest major branch (`3.x`). One branch per major version exists (`2.x`, `1.x`,\netc.).\n\n## PR Checklist\n\n- Tests pass (`scripts/dev-test.sh`)\n- Lint/types pass (`scripts/dev-lint.sh`)\n- New operations/facts include tests and documentation\n- **Atomic scope** — one change per PR. Split unrelated `__init__.py` re-exports, drive-by\n  refactors, and unrelated test edits into separate PRs even when the change is correct.\n  Reviewers consistently ask for unrelated edits to be removed before merge.\n"},"files":{"CLAUDE.md":"# CLAUDE.md\n\nSee @AGENTS.md for guidance.\n","AGENTS.md":"# AGENTS.md\n\nThis file provides guidance to coding agents, like Claude Code (claude.ai/code), when working with \ncode in this repository.\n\n## About pyinfra\n\npyinfra turns Python code into shell commands and runs them on servers. Think Ansible, but Python\ninstead of YAML, and much faster. It supports SSH, local machine, Docker, and more via connectors.\n\n## Development Setup\n\n```bash\nuv sync  # Install all dependencies into managed venv\n```\n\n## Common Commands\n\n```bash\n# Run tests\nscripts/dev-test.sh\n# or directly:\nuv run pytest --cov --disable-warnings -m 'not end_to_end'\n\n# Run fixtures for a single operation or fact\nuv run pytest tests/test_operations.py -k \"apt.packages\"\nuv run pytest tests/test_facts.py -k \"LinuxHardware\"\n\n# End-to-end tests (require Docker/SSH/local targets)\nuv run pytest -m end_to_end_local\nuv run pytest -m end_to_end_docker\n\n# Lint and type check\nscripts/dev-lint.sh\n# Individually:\nuv run ruff check\nuv run ruff format --check\nuv run mypy\nuv run python scripts/lint_arguments_sync.py\n\n# Auto-format\nscripts/dev-format.sh\n```\n\n## Architecture\n\nThe repo has two top-level packages under `src/`:\n\n- **`pyinfra/`** — core library (operations, facts, connectors, API)\n- **`pyinfra_cli/`** — CLI wrapper using Click + gevent\n\n### Core Concepts\n\n**Operations** (`src/pyinfra/operations/`) — Declarative functions (e.g. `apt.packages`,\n`files.put`) that describe desired state. Each operation uses the `@operation` decorator from\n`api/operation.py`, generates a list of commands, and is idempotent. Operations call facts to\nread current state, then return commands to reach desired state.\n\n**Facts** (`src/pyinfra/facts/`) — Read system state (e.g. `AptPackages`, `LinuxHardware`). Each\nfact is a class extending `FactBase` with a `command` attribute and a `process()` method that\nparses command output. Facts are cached per-host per-run.\n\n**Connectors** (`src/pyinfra/connectors/`) — Abstractions for how to connect to and execute on a\ntarget (SSH, local, Docker, chroot, Terraform, Vagrant, etc.). New connectors should be separate\npackages, not added to this repo.\n\n**API** (`src/pyinfra/api/`) — The core engine:\n- `state.py` — Global deploy state, callbacks, host grouping\n- `host.py` — Per-host metadata and fact access\n- `inventory.py` — Collection of hosts and groups\n- `operation.py` — `@operation` decorator that wraps functions into deploy operations\n- `operations.py` — Execution engine that runs operations across hosts\n- `command.py` — Command types: `StringCommand`, `FileUploadCommand`, `RsyncCommand`,\n  `QuoteString`, `MaskString`, etc.\n- `facts.py` — Fact base classes and execution logic\n- `deploy.py` — `@deploy` decorator for grouping operations\n- `connect.py` — Connector lifecycle (connect/disconnect)\n- `config.py` — `Config` object with all configuration options\n- `arguments.py` / `arguments_typed.py` — Global operation arguments (e.g. `_sudo`, `_su_user`);\n  **these two files must stay in sync** — CI enforces this via `scripts/lint_arguments_sync.py`,\n  so touching one requires touching the other\n- `output.py` — Pluggable output functions (decoupled from Click for testability)\n\n**Context** (`src/pyinfra/context.py`) — Thread-local (gevent-safe) context objects: `host`,\n`state`, `config`, `inventory`. Operations access the current host via `pyinfra.context.host`\nrather than explicit passing.\n\n**Concurrency** — Uses gevent greenlets for parallel host execution. `pyinfra_cli/main.py`\nmonkey-patches stdlib at startup.\n\n### Adding Operations / Facts\n\nOperations and facts are auto-discovered from their respective directories. A new\n`src/pyinfra/operations/mytool.py` is immediately available as `from pyinfra.operations import\nmytool`.\n\n- Operations must be idempotent and use facts to check current state\n- Facts must implement `command` (shell command to run) and `process(output)` (parse result)\n- Both need corresponding tests (see fixture convention below)\n- Every operation/fact module must be registered in `pyinfra-metadata.toml` as a plugin with\n  tags; omitting this won't break tests but will break docs generation\n\n**Operation / fact tests are YAML or JSON fixtures, not Python tests.** Drop a file under\n`tests/operations/<module>.<op>/` or `tests/facts/<module>.<Fact>/` — it is auto-discovered by\nthe `testgen` metaclass. Prefer YAML for new fixtures. To cover a new code path, add a fixture —\ndo not write a new Python test.\n\nOperation fixture structure (`tests/operations/<module>.<op>/<name>.yaml`):\n\n```yaml\nargs:\n  - positional_arg\nkwargs:\n  param: value\nfacts:\n  module.FactClass: {}          # a dict of mock values keyed by object_id and attribute\ncommands:\n  - shell command that should be produced\n```\n\nOptional keys: `exception` (e.g. `{name: OperationError, message: \"...\"}`), `noop_description`.\n\nFact fixture structure (`tests/facts/<module>.<Fact>/<name>.yaml`):\n\n```yaml\ncommand: shell command the fact runs\nrequires_command: binary               # optional\noutput: |\n  raw stdout to parse\nfact:\n  item:\n    key: value                         # expected return value of process()\n```\n\n## Coding Conventions\n\n**Docstring format** — pyinfra uses `+ param: description` bullets (parsed by\n`scripts/generate_operations_docs.py`). Do not use Google/NumPy/Sphinx style — it will silently\nbreak docs generation.\n\n**Shell safety** — user-supplied values must be composed into shell commands using `StringCommand`\n+ `QuoteString` / `MaskString` from `pyinfra.api`. Do not use plain string formatting (e.g.\n`\"rm -f {}\".format(path)`) for user-controlled values. This applies to **every** user-controlled\nvalue regardless of type — wrap ports, integers, paths and other non-string args with\n`QuoteString` too; reviewers explicitly flag unquoted ints as injection risk.\n\n**Optional parameter defaults** — optional parameters must default to `None`, not `\"\"`. Older\noperations in the codebase use `\"\"` defaults; do not replicate this pattern. Type these as\n`T | None` (e.g. `path: str | None = None`); do not use `Optional[T]`.\n\n**Distinguish unset from empty** — when a parameter is `T | None`, branch on `if x is not None:`\nrather than truthy `if x:`. Empty strings, `0`, and empty containers are valid user input and a\ntruthy check silently drops them.\n\n**FactBase typing** — every `FactBase` subclass must annotate its `command()` and `process()`\nmethods: `def command(self, repo: str) -> str:` and `def process(self, output: list[str]):`. The\n`output` argument is **already a `list[str]`** — do not re-wrap it with `list(output)` or iterate\ninto a new list before indexing.\n\n**Fact \"show file or empty\" pattern** — prefer `! test -e PATH || cat PATH` over\n`cat PATH 2>/dev/null || true`. The first form only suppresses the missing-file case; the second\nswallows real `cat` errors and hides bugs.\n\n**Reuse existing helpers** — before adding chown/chmod/path/permission utilities, check\n`pyinfra.operations.util.file_utils` (and the rest of `operations.util/`). Reviewers consistently\nask for duplicated logic to be replaced with the existing helper.\n\n**No `assert` in `src/`** — `python -O` strips assertions, silently dropping the check. Raise an\nexplicit exception instead: `OperationError` for operation argument issues, `ValueError` /\n`TypeError` for library code. `assert` is fine in tests.\n\n**Type hints** — all new (non-test) code must be fully type hinted. Use modern Python 3.10+\nconventions: built-in generics (`list`, `set`, `dict`, `tuple`) instead of `typing` equivalents\n(`List`, `Set`, etc.), and avoid quoting class names unless a forward reference is strictly\nrequired.\n\n## Branch Strategy\n\nPRs target the latest major branch (`3.x`). One branch per major version exists (`2.x`, `1.x`,\netc.).\n\n## PR Checklist\n\n- Tests pass (`scripts/dev-test.sh`)\n- Lint/types pass (`scripts/dev-lint.sh`)\n- New operations/facts include tests and documentation\n- **Atomic scope** — one change per PR. Split unrelated `__init__.py` re-exports, drive-by\n  refactors, and unrelated test edits into separate PRs even when the change is correct.\n  Reviewers consistently ask for unrelated edits to be removed before merge.\n"},"items":[{"name":"CLAUDE.md","path":"CLAUDE.md","title":"CLAUDE.md","content":"# CLAUDE.md\n\nSee @AGENTS.md for guidance.\n","category":"root","tokens":11},{"name":"AGENTS.md","path":"AGENTS.md","title":"AGENTS.md","content":"# AGENTS.md\n\nThis file provides guidance to coding agents, like Claude Code (claude.ai/code), when working with \ncode in this repository.\n\n## About pyinfra\n\npyinfra turns Python code into shell commands and runs them on servers. Think Ansible, but Python\ninstead of YAML, and much faster. It supports SSH, local machine, Docker, and more via connectors.\n\n## Development Setup\n\n```bash\nuv sync  # Install all dependencies into managed venv\n```\n\n## Common Commands\n\n```bash\n# Run tests\nscripts/dev-test.sh\n# or directly:\nuv run pytest --cov --disable-warnings -m 'not end_to_end'\n\n# Run fixtures for a single operation or fact\nuv run pytest tests/test_operations.py -k \"apt.packages\"\nuv run pytest tests/test_facts.py -k \"LinuxHardware\"\n\n# End-to-end tests (require Docker/SSH/local targets)\nuv run pytest -m end_to_end_local\nuv run pytest -m end_to_end_docker\n\n# Lint and type check\nscripts/dev-lint.sh\n# Individually:\nuv run ruff check\nuv run ruff format --check\nuv run mypy\nuv run python scripts/lint_arguments_sync.py\n\n# Auto-format\nscripts/dev-format.sh\n```\n\n## Architecture\n\nThe repo has two top-level packages under `src/`:\n\n- **`pyinfra/`** — core library (operations, facts, connectors, API)\n- **`pyinfra_cli/`** — CLI wrapper using Click + gevent\n\n### Core Concepts\n\n**Operations** (`src/pyinfra/operations/`) — Declarative functions (e.g. `apt.packages`,\n`files.put`) that describe desired state. Each operation uses the `@operation` decorator from\n`api/operation.py`, generates a list of commands, and is idempotent. Operations call facts to\nread current state, then return commands to reach desired state.\n\n**Facts** (`src/pyinfra/facts/`) — Read system state (e.g. `AptPackages`, `LinuxHardware`). Each\nfact is a class extending `FactBase` with a `command` attribute and a `process()` method that\nparses command output. Facts are cached per-host per-run.\n\n**Connectors** (`src/pyinfra/connectors/`) — Abstractions for how to connect to and execute on a\ntarget (SSH, local, Docker, chroot, Terraform, Vagrant, etc.). New connectors should be separate\npackages, not added to this repo.\n\n**API** (`src/pyinfra/api/`) — The core engine:\n- `state.py` — Global deploy state, callbacks, host grouping\n- `host.py` — Per-host metadata and fact access\n- `inventory.py` — Collection of hosts and groups\n- `operation.py` — `@operation` decorator that wraps functions into deploy operations\n- `operations.py` — Execution engine that runs operations across hosts\n- `command.py` — Command types: `StringCommand`, `FileUploadCommand`, `RsyncCommand`,\n  `QuoteString`, `MaskString`, etc.\n- `facts.py` — Fact base classes and execution logic\n- `deploy.py` — `@deploy` decorator for grouping operations\n- `connect.py` — Connector lifecycle (connect/disconnect)\n- `config.py` — `Config` object with all configuration options\n- `arguments.py` / `arguments_typed.py` — Global operation arguments (e.g. `_sudo`, `_su_user`);\n  **these two files must stay in sync** — CI enforces this via `scripts/lint_arguments_sync.py`,\n  so touching one requires touching the other\n- `output.py` — Pluggable output functions (decoupled from Click for testability)\n\n**Context** (`src/pyinfra/context.py`) — Thread-local (gevent-safe) context objects: `host`,\n`state`, `config`, `inventory`. Operations access the current host via `pyinfra.context.host`\nrather than explicit passing.\n\n**Concurrency** — Uses gevent greenlets for parallel host execution. `pyinfra_cli/main.py`\nmonkey-patches stdlib at startup.\n\n### Adding Operations / Facts\n\nOperations and facts are auto-discovered from their respective directories. A new\n`src/pyinfra/operations/mytool.py` is immediately available as `from pyinfra.operations import\nmytool`.\n\n- Operations must be idempotent and use facts to check current state\n- Facts must implement `command` (shell command to run) and `process(output)` (parse result)\n- Both need corresponding tests (see fixture convention below)\n- Every operation/fact module must be registered in `pyinfra-metadata.toml` as a plugin with\n  tags; omitting this won't break tests but will break docs generation\n\n**Operation / fact tests are YAML or JSON fixtures, not Python tests.** Drop a file under\n`tests/operations/<module>.<op>/` or `tests/facts/<module>.<Fact>/` — it is auto-discovered by\nthe `testgen` metaclass. Prefer YAML for new fixtures. To cover a new code path, add a fixture —\ndo not write a new Python test.\n\nOperation fixture structure (`tests/operations/<module>.<op>/<name>.yaml`):\n\n```yaml\nargs:\n  - positional_arg\nkwargs:\n  param: value\nfacts:\n  module.FactClass: {}          # a dict of mock values keyed by object_id and attribute\ncommands:\n  - shell command that should be produced\n```\n\nOptional keys: `exception` (e.g. `{name: OperationError, message: \"...\"}`), `noop_description`.\n\nFact fixture structure (`tests/facts/<module>.<Fact>/<name>.yaml`):\n\n```yaml\ncommand: shell command the fact runs\nrequires_command: binary               # optional\noutput: |\n  raw stdout to parse\nfact:\n  item:\n    key: value                         # expected return value of process()\n```\n\n## Coding Conventions\n\n**Docstring format** — pyinfra uses `+ param: description` bullets (parsed by\n`scripts/generate_operations_docs.py`). Do not use Google/NumPy/Sphinx style — it will silently\nbreak docs generation.\n\n**Shell safety** — user-supplied values must be composed into shell commands using `StringCommand`\n+ `QuoteString` / `MaskString` from `pyinfra.api`. Do not use plain string formatting (e.g.\n`\"rm -f {}\".format(path)`) for user-controlled values. This applies to **every** user-controlled\nvalue regardless of type — wrap ports, integers, paths and other non-string args with\n`QuoteString` too; reviewers explicitly flag unquoted ints as injection risk.\n\n**Optional parameter defaults** — optional parameters must default to `None`, not `\"\"`. Older\noperations in the codebase use `\"\"` defaults; do not replicate this pattern. Type these as\n`T | None` (e.g. `path: str | None = None`); do not use `Optional[T]`.\n\n**Distinguish unset from empty** — when a parameter is `T | None`, branch on `if x is not None:`\nrather than truthy `if x:`. Empty strings, `0`, and empty containers are valid user input and a\ntruthy check silently drops them.\n\n**FactBase typing** — every `FactBase` subclass must annotate its `command()` and `process()`\nmethods: `def command(self, repo: str) -> str:` and `def process(self, output: list[str]):`. The\n`output` argument is **already a `list[str]`** — do not re-wrap it with `list(output)` or iterate\ninto a new list before indexing.\n\n**Fact \"show file or empty\" pattern** — prefer `! test -e PATH || cat PATH` over\n`cat PATH 2>/dev/null || true`. The first form only suppresses the missing-file case; the second\nswallows real `cat` errors and hides bugs.\n\n**Reuse existing helpers** — before adding chown/chmod/path/permission utilities, check\n`pyinfra.operations.util.file_utils` (and the rest of `operations.util/`). Reviewers consistently\nask for duplicated logic to be replaced with the existing helper.\n\n**No `assert` in `src/`** — `python -O` strips assertions, silently dropping the check. Raise an\nexplicit exception instead: `OperationError` for operation argument issues, `ValueError` /\n`TypeError` for library code. `assert` is fine in tests.\n\n**Type hints** — all new (non-test) code must be fully type hinted. Use modern Python 3.10+\nconventions: built-in generics (`list`, `set`, `dict`, `tuple`) instead of `typing` equivalents\n(`List`, `Set`, etc.), and avoid quoting class names unless a forward reference is strictly\nrequired.\n\n## Branch Strategy\n\nPRs target the latest major branch (`3.x`). One branch per major version exists (`2.x`, `1.x`,\netc.).\n\n## PR Checklist\n\n- Tests pass (`scripts/dev-test.sh`)\n- Lint/types pass (`scripts/dev-lint.sh`)\n- New operations/facts include tests and documentation\n- **Atomic scope** — one change per PR. Split unrelated `__init__.py` re-exports, drive-by\n  refactors, and unrelated test edits into separate PRs even when the change is correct.\n  Reviewers consistently ask for unrelated edits to be removed before merge.\n","category":"root","tokens":2027}]}