Skip to content
Merged
416 changes: 214 additions & 202 deletions .agents/skills/backlog-burndown/SKILL.md

Large diffs are not rendered by default.

130 changes: 64 additions & 66 deletions .agents/skills/drive-pr/SKILL.md

Large diffs are not rendered by default.

196 changes: 99 additions & 97 deletions .agents/skills/pr-review-conduct/SKILL.md

Large diffs are not rendered by default.

51 changes: 26 additions & 25 deletions .agents/skills/python-codestyle/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ description: >-
This is the Python-specific half of the fleet's code style guide, kept in one place instead of
re-derived per repo or per session. CODESTYLE.md's General section still owns the rules every
language shares (clean-compile verification as a concept, the suppression-scope order, tooling
casing in prose), this Skill is everything specific to a Python project on top of that: the two
casing in prose). This Skill is everything specific to a Python project on top of that: the two
profiles, the toolchain, layout, and the language-level conventions.

## Two profiles
Expand Down Expand Up @@ -65,26 +65,27 @@ declaration, and versioning.
| [pytest][docs-link] | test runner (build profile only, lint-only uses `unittest`) | `pyproject.toml` `[tool.pytest.ini_options]` |

**Type checking targets strongly typed, deterministic code.** pyright in strict mode is the
default baseline on first-party code (a repo may instead run mypy with its strict flags in CI
and keep pyright editor-only via Pylance, per the next paragraph): `[tool.pyright]`
`strict = ["src"]`, or the integration package for a Home Assistant repo, with tests run in
standard mode. pyright is the anchor because Pylance embeds it, so where CI runs pyright, the
default baseline on first-party code. That baseline is `[tool.pyright]` `strict = ["src"]`, or the
integration package for a Home Assistant repo, with tests run in standard mode. A repo may instead
run mypy with its strict flags in CI and keep pyright editor-only via Pylance, per the next
paragraph. pyright is the anchor because Pylance embeds it, so where CI runs pyright, the
editor and the CLI/CI run the same engine. A pyright the requirements pin can lag the one Pylance
bundles, though. The CLI/CI form is `uv run pyright` in the uv form. In the pip form a declared
directory runs the environment's own pyright, or `uvx pyright@latest` pointed at the environment
where it installs none.
The standalone `ms-pyright.pyright` extension stays in
`unwantedRecommendations` because Pylance covers it. Relax strictness on third-party code only
when a dependency has no usable types and no alternative (e.g. `pandas`): a targeted, commented
`# pyright: ignore[...]` or a scoped `[tool.pyright]` override, never a blanket relaxation.
when a dependency has no usable types and no alternative (e.g. `pandas`). The relaxation is a
targeted, commented `# pyright: ignore[...]` or a scoped `[tool.pyright]` override, never a blanket
one.

**mypy is allowed, and required where the ecosystem demands it, it is not banned.** Running more
than one checker is normal when each serves a purpose (the .NET side pairs CSharpier and
`dotnet format` the same way), and pyright's inference and mypy's plugin ecosystem (e.g.
`dotnet format` the same way). pyright's inference and mypy's plugin ecosystem (e.g.
`pydantic.mypy`) catch different classes of error. A Home Assistant integration runs
`mypy --strict` because the platinum `strict-typing` quality-scale tier requires it, and a
pydantic-heavy library may opt in for the plugin. When a repo uses mypy it runs in CI and the
editor (the `ms-python.mypy-type-checker` extension) so the two stay consistent, and its mypy
editor (the `ms-python.mypy-type-checker` extension) so the two stay consistent. That repo's mypy
command joins the clean-compile. The hub validator runs at most one checker in each Python
directory, mypy where both are configured. A repo enforcing pyright beside mypy runs pyright from
its own `.github/actions/validate/action.yml` hook. That hook starts from a bare checkout, so it
Expand All @@ -97,9 +98,10 @@ engine.

From inside a **build**-profile Python project directory in its uv form. A pip-form directory runs
neither `uv sync` nor `uv run`, since either writes a `uv.lock` that, once committed, makes the
directory the uv form, and builds its environment as the pip-form block below shows. A
**lint-only** Scripts profile has no `uv.lock` to sync and no pytest to run, substitute `uvx` per
tool and `unittest` per the Two Profiles section above:
directory the uv form. A pip-form directory builds its environment as the pip-form block below
shows. A
**lint-only** Scripts profile has no `uv.lock` to sync and no pytest to run. For it, substitute
`uvx` per tool and `unittest` per the Two Profiles section above:

```sh
uv sync # creates .venv, installs deps + dev group
Expand All @@ -124,7 +126,7 @@ the same way. Run the clean-compile, plus `uv run pytest`, before committing.
A **build**-profile directory in its pip form builds its environment the way CI does, through uv's
pip interface, which writes no `uv.lock`. It installs every `requirements*.txt` in one command, since
separate installs let one file's pins downgrade what another resolved. Where its `pyproject.toml`
carries a `[project]` table it then installs the directory itself as editable, without which a
carries a `[project]` table it then installs the directory itself as editable. Without that, a
`src`-layout package is not importable and pytest fails locally where CI passes:

```sh
Expand Down Expand Up @@ -156,9 +158,8 @@ clean-compile, run with pytest before committing.

A **lint-only** profile's clean-compile substitutes its `uvx` and `unittest` equivalents, per Two
Profiles above, and has no such command to run before committing beyond those. These are documented
commands, and the hub's `vscode-tasks-python.json` snippet carries the VS Code tasks mirror of the
uv form that the fleet baseline expects, with its header naming the pip-form and lint-only
adaptations. Every command-executing task in it is `type: process`, and every aggregator is
commands. The hub's `vscode-tasks-python.json` snippet carries the VS Code tasks mirror of the uv
form that the fleet baseline expects. Its header names the pip-form and lint-only adaptations. Every command-executing task in it is `type: process`, and every aggregator is
`dependsOn`-only. Neither chains with `&&`, so the mirror runs the same on any task shell.

CI runs these same commands as the authoritative backstop in every directory the hub validator's
Expand All @@ -167,15 +168,15 @@ a root `pyproject.toml`, per `WORKFLOW.md` D1.2. That undeclared root is gated m
reads its type-checker configuration only from the `[tool.mypy]` or `[tool.pyright]` section of its
`pyproject.toml`. It skips the type check where neither is there,
whatever the form. In its pip form CI installs nothing for that check and runs a bare
`uvx <checker>@latest`, so the local check against the environment is the stricter one. And CI runs
`uvx <checker>@latest`. So the local check against the environment is the stricter one. And CI runs
its tests only where a pytest suite in `tests/` sits beside a `uv.lock` or `requirements*.txt`.

A repo that keeps no .NET tool manifest declaring Husky.Net, or one that prefers the
`pre-commit` framework, wires its local hook from the canonical `catalog/snippets/pre-commit/`
directory, hub-local and not carried into every fleet repo. Any repo may instead wire an equivalent
A repo that keeps no .NET tool manifest declaring Husky.Net wires its local hook from the canonical
`catalog/snippets/pre-commit/` directory. So does one that prefers the `pre-commit` framework. That
directory is hub-local and not carried into every fleet repo. Any repo may instead wire an equivalent
hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath` and sourcing nothing. That
path and `.pre-commit-config.yaml` are the ones the audit reads. The runner is bounded by the
toolchain the repo already keeps rather than by the languages the hook checks, so a repo keeping a
toolchain the repo already keeps rather than by the languages the hook checks. So a repo keeping a
Husky.Net manifest may run these same Python checks from the `catalog/snippets/husky/` shape
instead. Each shape carries whichever language checks its own repo keeps. The `pre-commit`
directory's own README names the second file to copy alongside the config. GOVERNANCE.md's hub-only
Expand Down Expand Up @@ -231,7 +232,7 @@ For comments, docstrings, full type-hint rules, naming, imports, and all pattern
## Tests

`uv run pytest` for a build profile (`.venv/bin/python -m pytest` in its pip form, from the
environment the Local development loop builds), `unittest` for a lint-only Scripts profile (see Two
environment the Local development loop builds). `unittest` for a lint-only Scripts profile (see Two
Profiles above). One test file per module (`test_<module>.py`). A build profile prefers fixtures
over `unittest`'s `setUp`/`tearDown` lifecycle hooks. A lint-only profile uses those hooks directly,
since `unittest` has no fixture-injection mechanism of its own. Fakes over mocks either way. Test
Expand Down Expand Up @@ -263,9 +264,9 @@ Before pushing or opening a PR:
- The **build**-profile CI gate, in its uv form, runs the local loop's commands above, each from
where that loop runs it. Those are `uv run ruff check`, `uv run ruff format --check`, the repo's
type checker, and `uv run pytest`. CI invokes them as separate steps, not `&&`-chained, so the
runner shell is irrelevant. The pip form's
CI gate is its commands in the local loop above, and a **lint-only** profile's is its `uvx`
equivalents plus its `unittest` suite, each per `references/profiles.md`. The local loop names
runner shell is irrelevant. Per `references/profiles.md`, the pip form's CI gate is its commands
in the local loop above. Per the same file, a **lint-only** profile's is its `uvx` equivalents
plus its `unittest` suite. The local loop names
what CI relaxes for an undeclared root.
- Markdown in this directory follows CODESTYLE.md's repo-wide Markdown and Spelling rules,
packaged as the `comment-and-doc-style` Skill.
Expand Down
47 changes: 25 additions & 22 deletions .agents/skills/python-codestyle/references/profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ review. The axes that commonly vary:
pyright kept editor-only through Pylance), or both. A repo running both runs pyright itself,
per `SKILL.md` "Toolchain". The clean-compile runs every checker CI runs.
- **Dependency declaration**: the uv form declares the dev tools CI runs in the `dev` group of
`[dependency-groups]`, the one group a plain local `uv sync` or `uv run` installs, and which CI's
`uv sync --all-groups --frozen` installs too, since it takes every group and no extra. PEP 621
`[dependency-groups]`. The `dev` group is the one group a plain local `uv sync` or `uv run`
installs. CI's
`uv sync --all-groups --frozen` installs that group too, since the command takes every group and
no extra. PEP 621
`[project.optional-dependencies]` (installed with `uv sync --extra <group>`) suits only a tool CI
does not run. The pip form declares its dependencies in `requirements*.txt` files, installed
together in one resolve, per `SKILL.md` "Local development loop".
- **Versioning / publishing**: a published package (`_version.py` plus a version source,
`uv build`, and a PyPI publish step), or a source-only repo with a static `version` and no
publish step (see Versioning below).
- **Versioning / publishing**: a published package or a source-only repo (see Versioning below).
A published package has `_version.py` plus a version source, `uv build`, and a PyPI publish step.
A source-only repo has a static `version` and no publish step.

## Two profiles: full specification

Expand All @@ -29,15 +31,15 @@ pytest coverage) describe the Project shape (the `build` profile). Its uv projec
`uv run` rules describe that shape's uv form. The two differ by trait, whether the Python has
third-party runtime dependencies or is a deliverable. The fleet's audit reads the shape
that trait leaves rather than inspecting imports. A `[project]` or `[build-system]` table in
`pyproject.toml`, a committed `uv.lock`, or a `requirements*.txt` beside it marks the build profile,
and tool configuration alone marks the lint-only one:
`pyproject.toml`, a committed `uv.lock`, or a `requirements*.txt` beside it marks the build profile.
Tool configuration alone marks the lint-only one:

- **Project** (the `build` profile): the Python has third-party runtime dependencies, or is a
deliverable. It takes one of these forms. The uv form is a PEP 621 project: `[project]`
with `dependencies` (dev tools in `[dependency-groups]`, per the dependency-declaration axis
above), a `[build-system]`, and a committed `uv.lock` (pinned LF, per GOVERNANCE.md's "Line
Endings" section). CI runs `uv sync --all-groups --frozen` + `uv run <tool>`, so the lockfile pins
tool versions. That sync installs every `[dependency-groups]` group and no
with `dependencies`, a `[build-system]`, and a committed `uv.lock`. Its dev tools sit in
`[dependency-groups]`, per the dependency-declaration axis above. The `uv.lock` is pinned LF, per
GOVERNANCE.md's "Line Endings" section. CI runs `uv sync --all-groups --frozen` +
`uv run <tool>`, so the lockfile pins tool versions. That sync installs every `[dependency-groups]` group and no
`[project.optional-dependencies]` extra. The pip form is a `pyproject.toml` beside a
`requirements*.txt`, installed with pip, whether or not it carries a `[project]` table. A
committed `uv.lock` makes a directory the uv form even where a `requirements*.txt` sits beside it.
Expand All @@ -49,31 +51,32 @@ and tool configuration alone marks the lint-only one:
`uvx <checker>@latest`, with nothing installed.
- **Scripts** (the `lint-only` profile): stdlib-only utility scripts (e.g. a
Python tooling subtree of a `csharp` app). Run the tools with `uvx` (no project install,
no lockfile): the `pyproject.toml` carries only tool config (`[tool.ruff]`, `[tool.mypy]`, and
an optional `[tool.pyright]` editor block), with no `[project]`, no `[build-system]`, and no
no lockfile). The `pyproject.toml` carries only tool config (`[tool.ruff]`, `[tool.mypy]`, and
an optional `[tool.pyright]` editor block). There is no `[project]`, no `[build-system]`, and no
`uv.lock` (that metadata would misrepresent it as a shippable package). mypy is the type-check
gate (there is no first-party package for pyright strict to anchor on), and a `[tool.pyright]`
gate (there is no first-party package for pyright strict to anchor on). A `[tool.pyright]`
block in standard mode keeps Pylance quiet in the editor, the same mypy-gate/pyright-editor
split the build profile uses. There is no lockfile, and a `uvx <tool>@<ver>` pin in a `run:`
step is not something Dependabot tracks, so CI runs `uvx ruff@latest` / `uvx mypy@latest` rather
step is not something Dependabot tracks. So CI runs `uvx ruff@latest` / `uvx mypy@latest` rather
than a manual pin that would silently go stale. The fleet rule is to pin only what Dependabot
auto-updates (SHA-pinned actions, package deps) and otherwise run latest, so the VS Code tasks,
auto-updates (SHA-pinned actions, package deps) and otherwise run latest. So the VS Code tasks,
README, and CI all run the unpinned latest here. `.py` files follow the repo's LF line-ending
default (per GOVERNANCE.md's "Line Endings" section). There is no pytest suite, and `unittest` is
the runner instead. The directory still owes tests, written with the standard
library's `unittest` so they need no test runner installed. Each is a `test_<script>.py`
under a `tests/` directory beside the scripts it exercises and their `pyproject.toml`
(`<scripts-dir>/tests/`), where the validator looks for it. That directory stays apart so a
test never reads as a tool. Within the scripts directory the name carries the kind: a gate
that checks and exits non-zero on a finding takes a `_lint` or `_gate` suffix, and a utility
that checks and exits non-zero on a finding takes a `_lint` or `_gate` suffix. A utility there
that does work takes none. Every declared directory
owes the same gates whatever its profile: lint, format, a type check, a test suite, and a
coverage report to Codecov. The hub validator runs them in each directory the caller declares
in its `python-directories` input, which the registry's `pythonDirectories` mirrors. From inside
a lint-only directory it runs `uvx ruff@latest check`, `uvx ruff@latest format --check`,
`uvx mypy@latest`, and `uvx coverage@latest run -m unittest discover -s tests`, adding
`--with-requirements tests/requirements.txt` to that last where the directory carries one, then writes
`coverage.xml` and uploads it best-effort per `WORKFLOW.md` D1.6, with no threshold adopted. A
`uvx mypy@latest`, and `uvx coverage@latest run -m unittest discover -s tests`. It adds
`--with-requirements tests/requirements.txt` to that last where the directory carries one. It then
writes `coverage.xml` and uploads it best-effort per `WORKFLOW.md` D1.6, with no threshold
adopted. A
declared directory with no `tests/` fails that job.

A profile belongs to a declared directory, not to the repository. A repository declaring none has
Expand All @@ -95,10 +98,10 @@ their own and differ only by purpose, so they merge.

**Published packages.** `_version.py` ships with `__version__ = "0.0.0"` as a placeholder. Until
you wire `_version.py` to something that increments (the usual options are `hatch-vcs`, a
version.json bridge, or manual bumps), no new PyPI versions will land, and publishing with
version.json bridge, or manual bumps), no new PyPI versions land. Publishing with
`skip-existing: true` keeps a stuck placeholder version from failing the run.

**Source-only repos** (no PyPI publish, with a source-release on dispatch or no release at all) do
not need `_version.py`: keep a static `version` in `pyproject.toml` `[project]`, or let the
not need `_version.py`. Keep a static `version` in `pyproject.toml` `[project]`, or let the
release pipeline's version source (e.g. NBGV plus `version.json`) own the tag. There is no publish
step to guard, so `skip-existing` does not apply.
Loading
Loading