prepare-python-recipe
End-to-end orchestration to prepare or update a Python recipe under core/python/, contrib/python/, or plugins/<vertical>/<solution>/ so it passes every check in .github/workflows/python-validate-recipe.yml. Runs eight phases in order on an already-in-place recipe: manifest.yaml generation, environment-variable extraction, pyproject.toml alignment, ruff format+check, per-recipe `uv lock`, runnability-test generation, compile-and-run verification of the generated test file, and a final pass through the repo's own `validate manifest` / `validate structure` validators. Assumes the user has already done the manual prep (deactivated any venv, `git pull` and `uv sync` from the repo root, placed the recipe at its target path, renamed if needed). Delegates to the existing sub-skills (generate-manifest, extract-python-environment-variables, align-recipe-pyproject, generate-python-runnability-test) so the master never duplicates their logic. Pauses at fixed decision points (description mismatch, existing test regeneration) AND is free to interrupt for clarification any time a phase's output looks ambiguous, unexpected, or would benefit from a human judgment call — this is an interactive skill by design. Use when the user wants to "prepare a recipe", "update a recipe end to end", "run all the checks and fixes", "make this recipe PR-ready", or invokes it by name.
Warnings for the author
- "description" exceeds the recommended 1024 characters; it is preserved within the front-matter size limit
- Not listed or searched: a skill under a hidden directory is discoverable only when the repository has no visible skill.
Prepare Python Recipe
Master orchestration skill. Runs the other Python-recipe skills in the right order, with the right inputs, in a single pipeline. Use when the user wants a recipe brought fully up to standard in one go.
This is an interactive skill. It's expected to pause and ask questions when doing so genuinely improves the outcome — not just at the fixed checkpoints below, but any time a phase's output is ambiguous, surprising, or would benefit from a judgment call. See rule 5 (fixed checkpoints) and rule 6 (judgment-based interruptions) for the difference.
Canonical placeholder strings
Two ownership placeholders are written by generate-manifest and enforced by tools/validate_manifest.py. They must be the EXACT strings below — never invent, translate, or rephrase them:
OWNERSHIP_TEAM_PLACEHOLDER = "TODO: Replace with your team name"
OWNERSHIP_POC_PLACEHOLDER = "TODO: Replace with your GitHub user ID"
generate-manifest is the single source of truth for these values. This skill NEVER replaces them mid-pipeline — they are intentionally left in place so CI validation fails until a human fills them in. Replacing them lives in the user's post-pipeline TODO list (see the summary's "What you still need to do" section).
Prerequisites (manual, done by the user BEFORE invoking this skill)
The skill assumes the user has already:
- Deactivated any active Python virtual environment.
- Pulled latest from
origin(git pullat the repo root). - Synced repo root deps (
uv syncat the repo root). - Placed the recipe at its target path — either freshly scaffolded, moved from another location, or renamed to its final basename under
core/python/<name>/,contrib/python/<name>/, orplugins/<vertical>/<solution>/. - Committed the original recipe so
git diffshows what the skill changed.
If the user has NOT done these and asks you to run the skill anyway, tell them to complete the prerequisites first and stop. Do NOT run git pull, git commit, deactivate their venv, or move/rename directories on their behalf — those are deliberately out of scope.
What This Skill Does
Runs eight ordered phases against a target recipe. Each phase either invokes an existing sub-skill (or its underlying script) or runs a repo-standard command:
- Manifest — generate
manifest.yamlif missing. Ownership placeholders (ownership.team,ownership.poc) are LEFT AS-IS — never replaced mid-pipeline. See "Canonical placeholder strings" above. - Environment variables — extract env vars used by the recipe into
.env.example; ensureload_dotenv()is bootstrapped andpython-dotenvis a dep. - Align pyproject.toml — remove
[tool.ruff*], raiserequires-pythonfloor, ensure[project].namematches folder, reconcile description with manifest, ensure[[tool.uv.index]]declares public PyPI as default (needed to bypass corp Airlock), and report stale sub-3.11 version references plus atestpathsthat would exclude the runnability test. - Lint —
ruff format+ruff check --fixon the recipe (from the repo root, so the root ruff config wins). Must run AFTER Phase 3 — align removes any recipe-local[tool.ruff*]block, and that removal is what makes the root config the effective one. Running lint before align would check against the recipe's (often more permissive) local config and miss violations that CI will later catch. - Recipe
uv lock— regenerateuv.lockso it reflects the post-alignpyproject.toml. Does NOT install into.venv/— that's a heavier step the user runs after they've reviewed the diff.uv lockjust resolves and records;uv syncwould download and install every wheel, which is scope-creep for a "prepare" pipeline. - Runnability test — generate
tests/test_runnability.pyif missing (or ask before overwriting), plus atests/conftest.pypath shim when the recipe isn't installable. - Verify (compile + run) —
py_compilethe runnability test, then run it with pytest. The compile step is a syntax check; running it is what proves the test'simportcan actually resolve (--collect-onlywould not — the guarded test shape puts the import inside the test function). The test is side-effect-free by construction. - Validate (repo validators) — run
uv run validate manifestanduv run validate structureon the recipe. This is the phase that catches everything the seven build phases don't model: required files, required directories (tests/unit/for plugins), size limits, naming.
At the end, print a summary table and remind the user to git diff and commit — the skill never commits.
Why Phase 8 exists. The pipeline used to end at Phase 7 and report a clean run while uv run validate structure failed — the recipe passed every phase the skill modelled and was still rejected by CI. Reimplementing policy checks inside this skill would guarantee drift, so the pipeline defers to the repo's own validators as the last word.
Deployability is deliberately NOT a phase here. The serving files a container needs (Dockerfile, fast_api_app.py, app_utils/, the serving dependencies) are the job of a separate opt-in skill, make-python-recipe-deployable. Most recipes neither need nor want them, and adding them can require an ADK major migration the owner must do by hand — so it must never run unasked as part of "prepare". If the user wants a deployable recipe, finish this pipeline first, then invoke that skill.
Rules for the Agent
-
Ask for
--recipe-dirup front if the user hasn't given one. All eight phases operate on the same recipe. -
Confirm before starting. The pipeline touches many files. Show the user the plan (the eight phases + the target recipe path) and ask for a single "yes, go ahead" before Phase 1. Do NOT prompt again for each phase unless a decision is required (see rules 5 and 6).
-
Invoke sub-skill SCRIPTS directly (not the sub-skills' own agent-facing SKILL.md). Reason: sub-skills each have their own "want me to apply?" prompt. In master-orchestration mode the user has already opted into apply for the whole pipeline; individual prompts would be noise. Command lines for each sub-script are given in each phase below.
-
Exception for pure-instructions skills:
generate-manifesthas no script — it's a pure-instructions skill. For that one only, load its SKILL.md (via theskilltool) and follow it inline. -
Fixed checkpoints — always pause here:
- Description mismatch — if Phase 3 returns
needs_inputfordescription-matches-manifest, show both sides and ask the user to pickpyproject,manifest, ordelete. - Test file exists — before Phase 6, if
tests/test_runnability.pyalready exists, ask whether to regenerate (default: keep existing). Regeneration uses--overwrite. - Entry point not found (Phase 6) — if the runnability-test generator errors because no
agent.pywas found, surface the message and offer to re-run with--agent-file <path>once the user says where the entry point is. This is the oneerrorcase with a defined recovery instead of a hard stop. - Anything else a sub-script flags as
error— surface the message, stop the pipeline, do NOT retry (the entry-point case above is the only exception).
- Description mismatch — if Phase 3 returns
-
Judgment-based interruptions — pause when it genuinely helps. This skill is interactive by design. Beyond the fixed checkpoints in rule 5, feel free to interrupt any time doing so meaningfully improves the outcome. Some situations where a pause is appropriate:
- A sub-script returns unexpected detections (e.g. the runnability-test generator reports
has_root_agent: false— a legit recipe should always have one; something may be wrong). - The manifest generator inferred an
architecture.agent = "multi"where you counted only one agent, or vice versa. - The environment-variable extractor added an unusually large number of new vars (say, ≥ 10) — worth having the user glance at the list.
- The align script's proposed rewrite of
requires-pythondrops support for a version the recipe's README claims to support. uv lockin Phase 5 records a suspicious dependency inuv.lock(say, a package that renames or shadows a well-known one — reviewable viagit diff uv.lock).- The recipe has a non-standard layout the sub-skills don't recognise (multiple
agent.pyfiles, noapp/package, etc.) and you're unsure which to use. - Any time the "right answer" for a step depends on knowledge outside the recipe itself (project conventions, team decisions, downstream consumers).
Do NOT interrupt for:
- Progress updates ("Phase 3 done, moving to Phase 4?") — just move on.
- Cosmetic curiosity ("I noticed a TODO in agent.py, want to discuss?") — out of scope.
- "Just to make sure" prompts where the answer wouldn't change what you do next.
When you interrupt, present the specific concern, show the relevant data, and offer concrete options — don't just say "does this look OK?".
- A sub-script returns unexpected detections (e.g. the runnability-test generator reports
-
Halt on hard error. If any phase's script exits with a non-zero code that isn't
refused_overwrite(handled by rule 5), stop. Print the phase name, the error, and what's already been done. Do NOT continue past a hard error.Carve-out for Phase 3 (align). The align script exits
1whenever any check is left inreport_onlystatus (e.g. missing[build-system], a non-PyPI default index) — those are deferrals-by-design, NOT hard errors (see Phase 3b). Do NOT halt on Phase 3's exit code alone: decide from its JSON. Halt only if a check has statuserror(or an unexpectedneeds_input/would_fix); a non-zero exit whose only non-clean checks arereport_onlymeans "continue". -
Report progress compactly. After each phase, one line:
Phase N (<name>): <one-line outcome>. Do NOT dump raw JSON. Do NOT re-render each sub-skill's own table — the summary at the end covers it. Judgment-based interruptions from rule 6 are separate from progress lines and should be their own turn (question, then wait for the answer). -
Never commit. The skill is done when the summary is printed. Let the user
git diffand commit.
Input
| Field | Required | Description |
|---|---|---|
| Recipe directory | Yes | Path to the recipe root (e.g. core/python/cross-session-memory, contrib/python/my-recipe, plugins/retail/store-ops). Passed to every sub-script as --recipe-dir. |
If the user has not specified the recipe directory, ask for it before proceeding.
Pipeline
Phase 0 — plan + confirm (do this first, always)
Step 0a — Verify the recipe directory actually exists. A path typo shouldn't cost the user the whole plan-confirmation round-trip only to fail in Phase 1:
[ -d <RECIPE_DIR> ] || { echo "Recipe directory not found: <RECIPE_DIR>"; exit 1; }
If it isn't a directory, stop immediately with that message — do NOT show the plan or prompt.
Step 0b — Verify the recipe folder name matches CI's naming rules. python-validate-recipe.yml's Check 1 (folder-name regex + max length) rejects folders that don't match ^[a-z][a-z-]*$ or exceed .github/policy.yml recipe_naming.max_folder_name_length. Historically the pipeline was BLIND to this — it would run every phase against a folder named data_science or MyBadName, report success, and let CI reject the PR later (or worse: Phase 3's project-name-matches-folder would propagate the bad name into [project].name). This check catches it up front.
MAX_LEN=$(uv run --no-project --with pyyaml python3 .github/scripts/load_policy.py recipe_naming.max_folder_name_length)
uv run --no-project python3 .agents/skills/prepare-python-recipe/scripts/check_folder_name.py \
--recipe-dir <RECIPE_DIR> --max-length "$MAX_LEN"
The check exits 0 silently on a compliant name; on violation it exits 1 with the specific offending characters, the length overrun (if any), and a suggested compliant name derived from the current one (lowercase, _ → -, drop disallowed characters, truncate on a hyphen boundary). The suggestion is ADVISORY — the script never renames anything.
If it fails, HALT the pipeline before Phase 1. Print the script's stderr verbatim (it already includes the suggestion and the manual git mv command). Do NOT show the plan, do NOT prompt to proceed, do NOT ask "want me to rename?" — renaming a recipe directory is the user's decision, not the skill's. They rename by hand and re-invoke the skill.
Only proceed past this step if the folder-name check passed.
Step 0c — For a recipe under plugins/, check the required directories. .github/policy.yml required_dirs.by_root.plugins mandates a fixed shape for every vertical plugin — scripts/, assets/, references/, and tests/unit/. None of the eight phases creates these, so a missing one survives the whole pipeline and fails validate structure in Phase 8 (and CI). Surfacing it here means the user can create the directory before anything else runs, rather than reading about it in the final summary.
Skip this step entirely for core/ and contrib/ recipes — required_dirs.by_root is empty for both.
uv run --no-project --with pyyaml python3 -c "
import pathlib, sys, yaml
recipe = pathlib.Path('<RECIPE_DIR>')
policy = yaml.safe_load(open('.github/policy.yml'))
needed = policy.get('required_dirs', {}).get('by_root', {}).get('plugins', []) or []
missing = [d for d in needed if not (recipe / d).is_dir()]
print('MISSING_DIRS: ' + (', '.join(missing) if missing else '(none)'))
"
This is INFORMATIONAL, not a halt. An empty directory satisfies the check, and git cannot commit an empty directory, so the fix is a .gitkeep:
mkdir -p <RECIPE_DIR>/tests/unit && touch <RECIPE_DIR>/tests/unit/.gitkeep
Mention any missing directories in the Step 0d plan message and offer to create them with .gitkeep files as part of the run. If the user agrees, create them right after they confirm the plan and before Phase 1; record them in the summary's "Files created" list. If they decline, carry the item into the final TODO list. Do NOT create them unasked — an empty scaffold directory the user didn't want is still clutter.
Step 0d — Show the plan and get confirmation. Before composing the plan, glance at the recipe for anything non-standard (package not called app/, .env.example outside root, missing tests/, extra Python source dirs, deprecated model literals per AGENTS.md). If any will affect what the pipeline does, flag them briefly in the plan message so the user isn't surprised mid-pipeline. Skip the flags entirely for a standard recipe.
Then flag the assumptions the pipeline is making and show the user the plan. Do NOT frame these as "prerequisites" — they're a heads-up so the user can push back if any assumption is wrong, not a preflight checklist for the user to tick off:
A few things I'm assuming — say so if any aren't true:
- You've deactivated any active venv.
- You've run
git pullanduv syncat the repo root.<RECIPE_DIR>is already at its target path (and renamed to its final basename).I'll run the prepare-python-recipe pipeline on
<RECIPE_DIR>— 8 phases:
- Generate manifest.yaml (if missing)
- Extract env vars into .env.example
- Align pyproject.toml
- Ruff format + check --fix
- uv lock inside the recipe (regenerates uv.lock; does NOT install .venv/)
- Generate tests/test_runnability.py (if missing)
- Verify the runnability test compiles and runs
- Run the repo validators (
validate manifest,validate structure)Nothing gets committed — you'll
git diffat the end. Proceed?
If Step 0c found missing required directories, add one line before "Nothing gets committed":
<RECIPE_DIR>is a plugin and is missingtests/unit/, which.github/policy.ymlrequires. Want me to create it with a.gitkeep?
Get a yes-or-no. If no, stop.
Phase 1 — manifest.yaml
1a. Check whether manifest exists.
[ -f <RECIPE_DIR>/manifest.yaml ] && echo exists || echo missing
1b. If missing — load the generate-manifest skill (via the `skill