Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ed3130f
feat: add cross-session memory sample application with agent engine a…
happyhuman Jul 16, 2026
ee32c06
refactor: remove cross-session-memory sample project and update skill…
happyhuman Jul 16, 2026
aea541c
feat: scaffold data science multi-agent architecture with AlloyDB, Bi…
happyhuman Jul 16, 2026
c96e674
chore: remove data_science contribution and update python skill confi…
happyhuman Jul 16, 2026
2bfcd81
feat: add airflow-version-upgrade-agent sample with automated migrati…
happyhuman Jul 16, 2026
67632bd
feat: add local folder name validation script and integrate into SKIL…
happyhuman Jul 16, 2026
e21c74c
feat: implement model configuration via environment variables and pro…
happyhuman Jul 16, 2026
a07ad7b
chore: update dependencies in pyproject.toml
happyhuman Jul 16, 2026
a9a4df7
chore: add Gemini model environment variables and refactor style form…
happyhuman Jul 16, 2026
09ee2fe
fix: add retry logic to GitHub CLI commands in PR review workflow to …
happyhuman Jul 16, 2026
bc70d7c
fix: detect flat core/<recipe> recipes in CI and skip container dirs …
happyhuman Jul 16, 2026
f873576
refactor: rename and reorganize project directory to remove excessive…
happyhuman Jul 16, 2026
a0795ff
refactor: migrate airflow-version-upgrade-agent directory structure t…
happyhuman Jul 16, 2026
20ef938
fix: demote GOOGLE_CLOUD_PROJECT to notice; fix project name in pypro…
happyhuman Jul 17, 2026
1c2adad
fix: demote GOOGLE_CLOUD_LOCATION and MODEL_NAME to notices in .env.e…
happyhuman Jul 17, 2026
98aaf16
fix: suppress C901 complexity warning on extract_env_vars helper
happyhuman Jul 17, 2026
829ec06
refactor: fix manifest validation formatting and rename airflow agent…
happyhuman Jul 17, 2026
43d6b69
refactor: remove deprecated airflow-version-upgrade-agent sample project
happyhuman Jul 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/skills/align-recipe-pyproject/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Runs `scripts/align_pyproject.py` against a recipe directory. Six 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 not permit any Python version below 3.11 (per `AGENTS.md`). A recipe that requires Python 3.12+ is the author's choice and is left alone. | Yes — raises the lower bound to `>=3.11` while preserving every upper bound, exclusion, compatible-release (`~=`) ceiling, and pin (only pure `>=`/`>` are dropped). If the result would be self-contradictory because the recipe's own ceiling/pin/exclusion excludes 3.11 (e.g. `>=3.10,!=3.11` or `==3.10.*`), refuses to apply and returns `needs_input`. |
| `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. |
Expand Down
205 changes: 146 additions & 59 deletions .agents/skills/align-recipe-pyproject/scripts/align_pyproject.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,23 @@
(Standalone ruff.toml / .ruff.toml files are also forbidden but are
outside this skill's scope — see the workflow's Check 7.)
- python-version-floor
[project].requires-python must not permit any Python version below
3.11 (per AGENTS.md "Minimum python version: 3.11"). Recipes that
require Python 3.12+ are the author's choice and are left alone.
Auto-fix: raise the lower bound to >=3.11 while preserving every
upper bound, exclusion, compatible-release ceiling, and pin (only the
pure lower-bound operators >= and > are dropped). If the rewrite
would produce a self-contradictory result — because the recipe's own
ceiling/pin/exclusion excludes 3.11 (e.g. `>=3.10,!=3.11` or
`==3.10.*`) — refuse to apply and return NEEDS_INPUT.
[project].requires-python must ACCEPT Python 3.11 exactly — it must
neither permit anything below (loose floor like >=3.10) nor exclude
3.11 by requiring higher (>=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 confusing "lockfile
is out of date" error whose real cause is the interpreter mismatch.
Auto-fix: rewrite the specifier so its lower bound is >=3.11 while
preserving every upper bound, exclusion, compatible-release ceiling,
and pin (only the pure lower-bound operators >= and > are dropped
or replaced). Applies to BOTH failure modes — loose floors (>=3.10)
and higher-than-min floors (>=3.12). If the rewrite would produce a
self-contradictory result — because the recipe's own ceiling/pin/
exclusion still excludes 3.11 after lowering (e.g. `>=3.10,!=3.11`,
`==3.10.*`, or `~=3.12` where the compatible-release ceiling shuts
out 3.11) — refuse to apply and return 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.
Auto-fix: set it.
Expand Down Expand Up @@ -211,7 +219,7 @@ def _enumerate_ruff_subtables(
return names


# ---------- python-version-floor: requires-python floor must be >= 3.11 ----
# ---------- python-version-floor: requires-python must accept exactly 3.11 -


def _add_missing_python_floor(
Expand Down Expand Up @@ -239,14 +247,22 @@ def _validate_and_apply_python_floor_rewrite(
spec: SpecifierSet,
permits_older: list[Version],
apply: bool,
excludes_min: bool = False,
) -> Check:
"""Rewrite requires-python, validating the result is not degenerate."""
"""Rewrite requires-python, validating the result is not degenerate.

Two failure modes route here:
* ``permits_older``: the spec accepts a Python below MIN_PYTHON.
* ``excludes_min``: the spec rejects MIN_PYTHON by requiring higher
(e.g. ``>=3.12``). The rewrite lowers the floor to MIN_PYTHON.
"""
target = _rewrite_requires_python(spec)

# If the mechanical result no longer admits MIN_PYTHON (e.g.
# `>=3.10,!=3.11` -> `>=3.11,!=3.11`), the recipe author's exclusion
# collides with our required floor. Refuse to apply rather than emit a
# self-contradictory specifier.
# `>=3.10,!=3.11` -> `>=3.11,!=3.11`, or `~=3.12` -> `>=3.11,~=3.12`
# which normalizes back to `>=3.12,<4`), the recipe author's exclusion
# or ceiling collides with our required floor. Refuse to apply rather
# than emit a self-contradictory or ineffective specifier.
try:
new_spec = SpecifierSet(target)
except InvalidSpecifier as e:
Expand All @@ -258,26 +274,41 @@ def _validate_and_apply_python_floor_rewrite(
{"current": current, "attempted_rewrite": target},
)
if Version(MIN_PYTHON_STR) not in new_spec:
# Build a message that names the specific reason we're stuck.
if excludes_min and not permits_older:
explain = (
f"the recipe's compatible-release ceiling or pin "
f"(e.g. ~=, ==) still shuts out {MIN_PYTHON_STR} after "
"lowering the pure lower bound"
)
else:
explain = (
"the recipe's own upper bound, pin, or exclusion "
"contradicts the required floor"
)
return Check(
"python-version-floor",
NEEDS_INPUT,
f"[project].requires-python = '{current}' permits Python "
f"versions below {MIN_PYTHON_STR}, but a mechanical rewrite "
f"would produce '{target}' which excludes {MIN_PYTHON_STR} "
f"itself (the recipe's own upper bound, pin, or exclusion "
f"contradicts the required floor). Fix by hand.",
f"[project].requires-python = '{current}' is incompatible "
f"with Python {MIN_PYTHON_STR}, but a mechanical rewrite "
f"would produce '{target}' which still excludes "
f"{MIN_PYTHON_STR} itself ({explain}). Fix by hand.",
{"current": current, "attempted_rewrite": target},
)

# Prefer a real witness version for the message; fall back to a generic
# phrase rather than surfacing a synthetic `.9999` probe (which would only
# appear for a micro-version floor like `>=3.10.5`).
real = [v for v in permits_older if v.micro != _PROBE_MICRO]
reason = (
f"permits Python {real[0]}"
if real
else f"permits Python below {MIN_PYTHON_STR}"
)
# Build the "reason this violated" phrase for the log message.
if excludes_min and not permits_older:
reason = f"excludes Python {MIN_PYTHON_STR}"
else:
# Prefer a real witness version; fall back to a generic phrase rather
# than surfacing a synthetic `.9999` probe (which would only appear
# for a micro-version floor like `>=3.10.5`).
real = [v for v in permits_older if v.micro != _PROBE_MICRO]
reason = (
f"permits Python {real[0]}"
if real
else f"permits Python below {MIN_PYTHON_STR}"
)
verb = "Rewrote" if apply else "Would rewrite"
if apply:
project["requires-python"] = target
Expand All @@ -293,14 +324,31 @@ def _validate_and_apply_python_floor_rewrite(
def check_python_version_floor(
pyproject_path: Path, doc: tomlkit.TOMLDocument, apply: bool
) -> Check:
"""Ensure [project].requires-python does not permit versions < MIN_PYTHON.

Interpretation A (per AGENTS.md discussion): the repo standard is a FLOOR.
A recipe that requires Python 3.12+ is fine and is left untouched.
A recipe that permits Python < 3.11 is rewritten so its lower bound
becomes >=3.11 (upper bounds and exclusions preserved). If the rewrite
would produce a self-contradictory result (e.g. `>=3.10,!=3.11`), the
script refuses to apply and returns NEEDS_INPUT.
"""Ensure [project].requires-python is compatible with MIN_PYTHON.

Interpretation B (aligned with CI in .github/workflows/
python-dependency-policy.yml, which pins Python 3.11 and runs
`uv lock --check` against every recipe): every recipe MUST accept
MIN_PYTHON (3.11) as a valid interpreter. Two failure modes are rewritten:

1. **Permits versions below MIN_PYTHON** (e.g. `>=3.10`, `~=3.10`,
unpinned): floor raised to MIN_PYTHON while preserving upper
bounds/exclusions.
2. **Excludes MIN_PYTHON by requiring higher** (e.g. `>=3.12`,
`>=3.12,<3.14`, `~=3.12`): floor lowered to MIN_PYTHON while
preserving upper bounds. Previously (Interpretation A) this was
treated as "author's choice, leave alone" — that stance conflicts
with CI, which hardcodes Python 3.11 and produces a confusing
"lockfile is out of date" error whose real cause is the interpreter
version mismatch.

In either case, if the rewrite would produce a self-contradictory
specifier that excludes MIN_PYTHON itself (e.g. `>=3.10,!=3.11` or
`~=3.12` where the compatible-release ceiling still shuts out 3.11),
the script refuses to apply and returns NEEDS_INPUT for the human to
resolve. Recipes that genuinely need Python 3.12+ features must be
reported to the repo maintainer so CI can be updated in tandem — the
align skill will not silently allow a floor above MIN_PYTHON.
"""
project = doc.get("project")
current = None if project is None else project.get("requires-python")
Expand All @@ -324,20 +372,20 @@ def check_python_version_floor(
)

permits_older = [v for v in BELOW_MIN if v in spec]
excludes_min = Version(MIN_PYTHON_STR) not in spec

# Interpretation A: only rewrite when the spec permits versions below
# MIN_PYTHON. Higher lower bounds (e.g. `>=3.12`) are the recipe author's
# deliberate choice and are left alone.
if not permits_older:
# OK only when both invariants hold: 3.11 is accepted AND nothing below
# 3.11 is accepted.
if not permits_older and not excludes_min:
return Check(
"python-version-floor",
OK,
f"[project].requires-python lower bound is >= {MIN_PYTHON_STR} "
f"('{current}').",
f"[project].requires-python admits Python {MIN_PYTHON_STR} "
f"and rejects everything below it ('{current}').",
)

return _validate_and_apply_python_floor_rewrite(
project, current, spec, permits_older, apply
project, current, spec, permits_older, apply, excludes_min
)


Expand Down Expand Up @@ -720,9 +768,30 @@ def _find_pypi_entry(doc: tomlkit.TOMLDocument) -> Any:
def _append_default_pypi_index(doc: tomlkit.TOMLDocument) -> None:
"""Append a `[[tool.uv.index]]` block declaring public PyPI as default.

Placed under `[tool.uv]` (created if absent). Existing non-default
`[[tool.uv.index]]` entries are preserved and remain higher-priority
(per uv's index ordering).
Two code paths depending on whether any `[[tool.uv.index]]` AoT already
exists:

1. AoT already present (recipe has index entries but none marked
`default=true`): append a new entry into the existing AoT via
tomlkit. Placement is unambiguous — the new entry sits with its
siblings, which is exactly what the user wants.

2. AoT NOT present (nothing under `[tool.uv.index]` at all): DO NOT let
tomlkit place the new block. tomlkit places a newly-created nested
AoT immediately after its parent's last child table, and that
position can fall INSIDE a trailing comment block that visually
introduces the NEXT top-level table. In real recipes with a
`[tool.uv.build-backend]` sub-table followed by comments introducing
`[tool.agent-starter-pack]`, tomlkit will wedge the new
`[[tool.uv.index]]` between those comments and their target table,
silently reassigning comment ownership. To avoid this, defer the
block to a raw string append after `tomlkit.dumps(doc)` — see
`_persist_changes`. The string always lands at the end of the file,
which is unambiguous and preserves every comment's semantic
attribution.

Existing non-default `[[tool.uv.index]]` entries are preserved either
way and remain higher-priority (per uv's index ordering).

No leading comment is emitted — tomlkit attaches comments passed to a
table INSIDE the table (after its header), which reads awkwardly.
Expand All @@ -732,19 +801,26 @@ def _append_default_pypi_index(doc: tomlkit.TOMLDocument) -> None:
this auto-fix path just get the bare block. Both are equivalent
functionally.
"""
if "tool" not in doc:
doc["tool"] = tomlkit.table()
tool = doc["tool"]
if "uv" not in tool:
tool["uv"] = tomlkit.table()
uv = tool["uv"]
if "index" not in uv:
uv["index"] = tomlkit.aot()
tool = doc.get("tool")
uv = tool.get("uv") if tool is not None else None
existing_aot = uv.get("index") if uv is not None else None

if existing_aot is not None:
# Path 1: append into the existing AoT (safe placement).
entry = tomlkit.table()
entry["url"] = "https://pypi.org/simple/"
entry["default"] = True
existing_aot.append(entry)
return

entry = tomlkit.table()
entry["url"] = "https://pypi.org/simple/"
entry["default"] = True
uv["index"].append(entry)
# Path 2: no existing AoT. Defer the append to post-serialization so
# tomlkit's placement heuristic can't wedge the block inside a trailing
# comment group. `_persist_changes` will pick this up.
doc._pending_pypi_index_append = (
"\n[[tool.uv.index]]\n"
'url = "https://pypi.org/simple/"\n'
"default = true\n"
)


def check_default_pypi_index(doc: tomlkit.TOMLDocument, apply: bool) -> Check:
Expand Down Expand Up @@ -870,7 +946,18 @@ def _persist_changes(
"""
try:
with open(pyproject_path, "w") as f:
f.write(tomlkit.dumps(doc))
rendered = tomlkit.dumps(doc)
# `_append_default_pypi_index` stashes a raw block on the doc
# when it needs the [[tool.uv.index]] declaration to land at
# end-of-file rather than wherever tomlkit's placement heuristic
# would put a newly-created nested AoT — see the docstring
# there for the comment-adjacency bug this avoids.
pending = getattr(doc, "_pending_pypi_index_append", None)
if pending:
if not rendered.endswith("\n"):
rendered += "\n"
rendered += pending
f.write(rendered)
except OSError as e:
# Nothing has touched disk yet — the manifest write was deferred.
report.add(
Expand Down
27 changes: 25 additions & 2 deletions .agents/skills/extract-python-environment-variables/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ description: >
bootstrap lines. Pre-existing `os.environ.setdefault(...)` or
`os.getenv("VAR", "default")` calls that the recipe author wrote by
hand are LEFT UNTOUCHED — the skill is additive-only for Python files
(adds `load_dotenv()` bootstrap; replaces hardcoded model literals).
(adds `load_dotenv()` bootstrap; replaces hardcoded model literals;
appends `# noqa: E402` to trailing relative imports in `__init__.py`
when they'd otherwise trip Ruff after an env-bootstrap block).
Use when the user wants to "extract env vars", "update .env.example",
"add load_dotenv", "replace hardcoded model names", or "fix environment
variables" in a Python recipe.
Expand Down Expand Up @@ -70,7 +72,28 @@ Runs `scripts/extract_env_vars.py` against a recipe directory. The script:
load_dotenv()
```

If `load_dotenv` is already present the file is left unchanged.
If `load_dotenv` is already present the injection is skipped.

**Additionally — always, regardless of whether we injected** — appends
`# noqa: E402 -- must come after load_dotenv()` to any top-level relative
import (`from .x import y`) that sits AFTER a non-import module-level
statement. Two cases this covers:

- **Fresh injection.** The injected `load_dotenv()` call pushes
pre-existing trailing relative imports below a non-import statement, so
they'd trigger Ruff `E402` ("module-level import not at top of file")
when Phase 4 (ruff) of `prepare-python-recipe` runs.

- **Author-written bootstrap.** The recipe author already wrote
`load_dotenv()` + `os.environ.setdefault(...)` calls followed by a
trailing `from . import agent`, but never marked the trailing import.
The skill did NOT inject anything (load_dotenv was already present) but
still adds the noqa suffix so the file is lint-clean on the pipeline's
next ruff pass.

The suppression pass is precise — a relative import at the very TOP of
the file (before any non-import statement) is fine and left untouched.
Idempotent: a line that already carries `# noqa: E402` is skipped.

4. **Replaces hardcoded model names** in source (e.g. `model="gemini-3.5-flash"`
in `agent.py`) with **bare `os.getenv("MODEL_NAME")`** — no default argument.
Expand Down
Loading
Loading