google/align-recipe-pyproject
> Aligns a Python recipe's pyproject.toml with the repo's standards enforced by .github/workflows/python-validate-recipe.yml, plus one critical [build-system] presence check. Scope is pyproject.toml only — standalone ruff.toml / .ruff.toml files (also forbidden in recipes) are caught by the --dry-run that reports what needs alignment, and an apply mode that rewrites pyproject.toml (and optionally manifest.yaml) using comment-preserving TOML/YAML editors. Use when the user wants to "align the recipe's pyproject.toml", "fix pyproject to match the repo standard", "check what needs changing in a recipe's pyproject", or clean up a recipe before submitting a PR.
npx skills add https://github.com/google/adk-samples --skill align-recipe-pyproject
Use this skill to bring a Python recipe's pyproject.toml (and, when it disagrees, its manifest.yaml description) into conformance with the repo standard enforced by .github/workflows/python-validate-recipe.yml.
Scope: pyproject.toml only. Standalone ruff.toml / .ruff.toml files are also forbidden in recipes but are enforced by the CI workflow (Check 7 in python-validate-recipe.yml) — outside this skill's concern.
Runs scripts/align_pyproject.py against a recipe directory. Eight rules:
| Rule ID | What it checks | Auto-fix |
|---|---|---|
| no-local-ruff-config | Recipe pyproject.toml must not declare any [tool.ruff*] table. Ruff config is centralized in the root pyproject.toml. | Yes — removes the tables. |
| python-version-floor | [project].requires-python must accept Python 3.11 exactly — it must neither permit anything below (loose floors like >=3.10) nor exclude 3.11 by requiring higher (>=3.12, ~=3.12, etc.). Per AGENTS.md "Minimum python version: 3.11" and CI in .github/workflows/python-dependency-policy.yml, which pins Python 3.11 and would otherwise emit a misleading "lockfile is out of date" error whose real cause is the interpreter mismatch. | Yes — rewrites the specifier so its lower bound is >=3.11 while preserving every upper bound, exclusion, compatible-release (~=) ceiling, and pin (only pure >=/> are dropped or replaced). Applies to BOTH failure modes (loose floors AND higher-than-min floors). If the result would still exclude 3.11 (e.g. >=3.10,!=3.11 → >=3.11,!=3.11, or ~=3.12 → >=3.11,~=3.12 == >=3.12,<4), refuses to apply and returns needs_input for a human to resolve (typically: relax the ceiling, or raise the recipe with the maintainers to update CI's pinned interpreter). |
| project-name-matches-folder | [project].name must equal the recipe folder basename. | Yes — sets it. |
| description-matches-manifest | If [project].description is set, it must equal manifest.description. Field is optional; skipped when absent. | Only with --description-source={pyproject,manifest,delete}. Refuses to touch description otherwise. |
| build-system-present | [build-system] must have both requires and build-backend. Without it, uv build and pip install . fail. | No — backend choice is editorial. Reported for the human to fix. |
| default-pypi-index | [[tool.uv.index]] must have an entry with default = true pointing at public PyPI (https://pypi.org/simple[/]). Required so uv sync works on Google corp workstations without corp Airlock auth — see the block comment in the root pyproject.toml for the full rationale. | Yes when the block is entirely missing — appends it. No when a default entry exists but points elsewhere (custom private index, TestPyPI, mirror) — reported for the human to reconcile, since the divergence may be intentional. |
| stale-python-version-refs | Scans every text file in the recipe for references to a Python version below the 3.11 floor. Raising requires-python is not a self-contained edit: the version is typically repeated in prose (README.md, SKILL.md) and in executable setup code. | No — report-only. Which references are stale and how to reword them is editorial. |
| runnability-test-in-testpaths | If [tool.pytest.ini_options].testpaths is set, at least one entry must collect tests/test_runnability.py (which .github/policy.yml requires every Python recipe to have). | No — report-only. Broadening testpaths changes what CI collects. |
stale-python-version-refs existsA bootstrap script that picks an interpreter from an allowlist still
containing the old floor will happily build a venv the recipe then refuses to
install into:
for py in python3.13 python3.12 python3.11 python3.10 python3; do
# ...
# ERROR: Package requires a different Python: 3.10.x not in '>=3.11'
The scan requires a Python-ish context (python 3.10, Python :: 3.10,
>=3.10) rather than matching bare digits, so a model name like
gemini-3.5-flash is not a false positive. It skips lockfiles, virtualenvs,
caches, and binary files, and ignores requires-python in pyproject.toml
itself (owned by python-version-floor, and still pre-rewrite in memory when
this check runs). Hits are capped at 40 in details.hits, but
details.files always lists every affected file.
tomlkit — comments, blank lines, and unrelated tables in pyproject.toml are preserved.--description-source=pyproject overwrites manifest.description) go through ruamel.yaml — comments in manifest.yaml are preserved.pyproject.toml, and other recipes are safe.pyproject.toml or manifest.yaml to perform these changes. The script exists specifically so edits are style-preserving and reviewable via one report.core/python/<name>/, contrib/python/<name>/, or skills/<vertical>/<solution>/.--dry-run unless the user has explicitly said "apply", "fix it", "just do it", or equivalent. Show them what would change before doing it.status != "ok".description-matches-manifest returns needs_input, the descriptions in pyproject.toml and manifest.yaml disagree and the script cannot pick a winner. Present the two values side by side and ask the user to choose one of the three resolution options. Do not re-run the script until they choose.build-system-present returns report_only, [build-system] is missing or incomplete. Tell the user the recipe cannot be built as a package until this is fixed, and offer them a hatchling or uv_build template snippet. Do not silently pick one.default-pypi-index returns report_only, the recipe declares a default index that is NOT public PyPI (e.g. a private mirror, TestPyPI). The skill will not overwrite an intentional choice. Show the user the current url from details.current_url and ask whether it's deliberate. If yes, they can # noqa-comment it or update the repo standard; if no, they should change the URL to https://pypi.org/simple/. Do not auto-rewrite.stale-python-version-refs returns report_only, list the affected files from details.files and call out any executable ones first (.sh, .py, Makefile, CI YAML) — a stale interpreter allowlist in a bootstrap script is a live bug, not a docs nit, and it produces a confusing requires a different Python failure at install time. Prose files (README.md, SKILL.md) are lower priority but still inconsistent. Offer to update them; do not rewrite without asking, since some references are legitimately historical ("dropped 3.10 support in v2").runnability-test-in-testpaths returns report_only, the recipe's testpaths excludes tests/test_runnability.py, so a bare uv run pytest never runs it — the recipe looks tested while its import-smoke test silently never executes. Show details.testpaths and suggest adding "tests". Do not auto-rewrite.10. After apply mode succeeds, remind the user to run uv sync in the recipe directory if the pyproject changes touched dependencies (the script emits this in notes when relevant).
11. Do not commit any changes. Show the diff or file contents; let the user commit.
| Field | Required | Description |
|---|---|---|
| --recipe-dir | Yes | Path to the recipe root (e.g. core/python/cross-session-memory, contrib/python/my-recipe, skills/retail/store-ops). |
| --dry-run | No | Report what would change without modifying any files. |
| --description-source | Only when resolving a description-matches-manifest mismatch. Values: pyproject, manifest, delete. See below. | Chooses how to reconcile a description mismatch. |
--description-source semantics| Value | Effect |
|---|---|
| pyproject | Overwrite manifest.description with the value from [project].description. Use when pyproject is authoritative (e.g. it was updated more recently). |
| manifest | Overwrite [project].description with the value from manifest.description. Use when the manifest is authoritative. |
| delete | Remove [project].description from pyproject.toml entirely, so manifest.description becomes the single source of truth. Use when the recipe doesn't need a wheel-metadata description. |
If the descriptions already match, this flag is ignored.
uv run --no-project --with tomlkit --with 'ruamel.yaml' --with packaging \
python .agents/skills/align-recipe-pyproject/scripts/align_pyproject.py \
--recipe-dir <RECIPE_DIR> --dry-run
Output: JSON on stdout. Every check produces one entry with id, status (ok / would_fix / needs_input / report_only / error), a human-readable message, and structured details. Exit code is always 0 in dry-run.
uv run --no-project --with tomlkit --with 'ruamel.yaml' --with packaging \
python .agents/skills/align-recipe-pyproject/scripts/align_pyproject.py \
--recipe-dir <RECIPE_DIR>
Fixes everything the script can safely fix. Exits 0 if nothing is left unresolved, 1 otherwise (typically because description-matches-manifest needs --description-source or build-system-present needs a human).
uv run --no-project --with tomlkit --with 'ruamel.yaml' --with packaging \
python .agents/skills/align-recipe-pyproject/scripts/align_pyproject.py \
--recipe-dir <RECIPE_DIR> --description-source={pyproject|manifest|delete}
--description-source)--description-source combines with --dry-run: the script reports the
would_fix outcome of the chosen resolution without writing any files.
Use this to show the user exactly what a given choice will do before you
apply it — the two flags are not mutually exclusive.
uv run --no-project --with tomlkit --with 'ruamel.yaml' --with packaging \
python .agents/skills/align-recipe-pyproject/scripts/align_pyproject.py \
--recipe-dir <RECIPE_DIR> --dry-run \
--description-source={pyproject|manifest|delete}
Filter the report to checks with status != "ok". Never list what's fine — the user is here to see what needs doing.
Reply with a single line, no table, no bullets. Something like:
> ✓ <RECIPE_DIR>'s pyproject.toml is fully aligned with the repo standard. No changes needed.
Then stop. Do not run any further tools.
Name the recipe directory once, then render a single Markdown table with these three columns:
| Rule | Status | Details |
|---|---|---|
id, in backticks (e.g. no-local-ruff-config ).would_fix, fixed, needs_input, report_only, error). Do not add emoji unless the user has asked for them.from → to values from details when present.Include exactly one row per non-ok check. Do not include ok rows.
Status-specific guidance for what to put in the Details cell:
would_fix (dry-run) — describe the current-state problem, then say what apply would do. Include the from → to or the list of tables to be removed.fixed (apply) — one-liner confirming the change (new value or list of removed tables).needs_input (only description-matches-manifest) — the Details cell says something like "descriptions differ — needs --description-source={pyproject,manifest,delete}". Do not put the two long descriptions inside the table. See "Follow-up content" below.report_only (four rules can hit this: build-system-present, default-pypi-index when a non-PyPI default is declared, stale-python-version-refs, and runnability-test-in-testpaths) — the Details cell names what's missing or non-conforming (e.g. "[build-system] missing; recipe cannot be built as a package" or "default index is TestPyPI, not public PyPI"). Follow-up content goes below the table (see next section).error — the Details cell shows the message verbatim; if it's very long, truncate with … and put the full text below.Only for statuses that need extra context. Order: table first, then this content, then closing action.
needs_input follow-up — present pyproject_description and manifest_description from details side by side (a small nested table works well), then ask the user to pick one of pyproject, manifest, or delete. Do not re-run until they answer.report_only follow-up — explain the impact (recipe cannot be built as a package) and paste both templates so the user can choose: # Hatchling (traditional Python packaging)
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["app"]
# ---- OR ----
# uv_build (Astral's uv-native backend)
[build-system]
requires = ["uv_build>=0.8.14,<0.9.0"]
build-backend = "uv_build"
[tool.uv.build-backend]
module-root = ""
module-name = "app"
error follow-up — if the message was truncated in the table, print it verbatim in full below. Do not attempt to work around it.would_fix row — offer to apply the fixes yourself. Do NOT paste the raw command as a copy-and-paste snippet for the user. Instead ask something like "Want me to apply these fixes?" (a yes/no question is fine; a question with clear options is nicer UX). If the user agrees, run apply mode yourself and render the result as another table (with fixed rows instead of would_fix). If the user declines, stop.needs_input for description-matches-manifest — after the table, show the two descriptions side by side and ask the user to pick one of pyproject, manifest, or delete. When they choose, run apply yourself with --description-source=<their choice> and render the resulting table. Do not offer generic "apply" — the flag is required.report_only (and no would_fix rows) — after the table, address the specific case:build-system-present: show the two [build-system] template snippets and stop. This is a manual edit; the skill does not auto-fix it.default-pypi-index: quote details.current_url, explain that this is not public PyPI, and ask whether it's intentional. If not, tell the user to change the URL to https://pypi.org/simple/. Do not auto-rewrite.stale-python-version-refs: list details.files, executable files first, and offer to update them.runnability-test-in-testpaths: quote details.testpaths and suggest adding "tests".error rows — do not offer to apply. Errors mean the script bailed before it could compute a fix; the user has to resolve the underlying issue first.would_fix + a needs_input) — offer to apply the fixable ones. The apply run will fix those and leave the others as-is; render the resulting table and then address the remaining statuses per the rules above.fixed row — end with the "Next steps" reminder (see below).Do NOT commit any changes yourself. Ever.
If any check has status: fixed, remind the user:
Next steps:
cd <RECIPE_DIR> && uv sync # if dependencies changed
git diff # review the edits before committing
Then stop. Do not commit. Do not run any further tools. End your turn.
Take google/align-recipe-pyproject from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference pip.
Without those the skill loads but fails at the first command.