$npx -y skills add PolicyEngine/policyengine-claude --skill policyengine-standardsPolicyEngine coding standards, environment setup, formatters, changelog, and CI requirements across every repo — what makes a PR pass and merge cleanly. Covers uv + .venv, ruff, bun, towncrier changelog fragments, the "wait for CI" PR pattern, and the repo-rename checklist. Trigg
| 1 | # PolicyEngine development standards |
| 2 | |
| 3 | Load this before committing to, reviewing, or setting up any PolicyEngine repository. It |
| 4 | encodes the org conventions that CI enforces — the things a frontier model would otherwise |
| 5 | guess wrong. For prose style (blog posts, PR bodies, docs) see the policyengine-writing skill. |
| 6 | |
| 7 | ## Environment: uv + a local .venv |
| 8 | |
| 9 | All PolicyEngine Python work uses [`uv`](https://docs.astral.sh/uv/) with a `.venv` at the |
| 10 | repository root. Never use bare `pip`, and never run bare `python`/`pytest`. |
| 11 | |
| 12 | ```bash |
| 13 | uv venv # creates ./.venv (uv picks the interpreter from pyproject.toml) |
| 14 | uv pip install -e ".[dev]" # editable install with dev extras |
| 15 | uv run pytest # run anything through uv so it uses ./.venv |
| 16 | ``` |
| 17 | |
| 18 | - **Python version is per-repo — check `pyproject.toml`** (`requires-python`). The org spans |
| 19 | the 3.13/3.14 era; there is no single global version. Do not downgrade a repo to satisfy a |
| 20 | stale assumption, and do not hardcode a version across repos. |
| 21 | - `uv run <cmd>` guarantees the command runs in the repo's `.venv` with its locked deps. Bare |
| 22 | `python script.py` / `pytest` is the most common cause of "works locally, fails in CI". |
| 23 | - Verifying "latest": if asked for the latest version of a package, read PyPI |
| 24 | (`https://pypi.org/pypi/<pkg>/json` → `info.version`) immediately before pinning. Never |
| 25 | infer "latest" from a lockfile, a local install, or search snippets. |
| 26 | |
| 27 | ## Documentation builds |
| 28 | |
| 29 | Python docs use **Jupyter Book 2.0 (MyST-NB)** — build with `myst build docs`, never `jb build` |
| 30 | (that is Jupyter Book 1.x). Use MyST markdown syntax. |
| 31 | |
| 32 | ## Before committing — checklist |
| 33 | |
| 34 | 1. Write the test first (TDD, below). |
| 35 | 2. Format: `make format` (or `uv run ruff format .` for Python, `bun run lint -- --fix && bunx prettier --write .` for JS). |
| 36 | 3. Run tests: `make test` (or `uv run pytest`). |
| 37 | 4. Add a changelog fragment (towncrier, below). |
| 38 | 5. Reference the issue in the PR: "Fixes #123". |
| 39 | |
| 40 | ## Creating pull requests |
| 41 | |
| 42 | ### The CI waiting problem |
| 43 | |
| 44 | **Common failure pattern:** |
| 45 | ``` |
| 46 | User: "Create a PR and mark it ready when CI passes" |
| 47 | Claude: "I've created the PR as draft. CI will take a while, I'll check back later..." |
| 48 | [Chat ends - Claude never checks back] |
| 49 | Result: PR stays in draft, user has to manually check CI and mark ready |
| 50 | ``` |
| 51 | |
| 52 | ### Solution: use the /create-pr command |
| 53 | |
| 54 | When creating PRs, use the `/create-pr` command. It creates the PR as draft, actually waits |
| 55 | for CI (polls every 15 seconds), marks it ready when CI passes, and reports failures with |
| 56 | details. It works because the command carries explicit polling logic that Claude executes, so |
| 57 | it waits instead of giving up. |
| 58 | |
| 59 | ### If /create-pr is not available |
| 60 | |
| 61 | Implement the polling pattern directly: |
| 62 | |
| 63 | ```bash |
| 64 | # 1. Create PR as draft. Create the branch ON the PolicyEngine repo, not a fork. |
| 65 | gh pr create --repo PolicyEngine/policyengine-us --draft --title "Title" --body "Body" |
| 66 | PR_NUMBER=$(gh pr view --json number --jq '.number') |
| 67 | |
| 68 | # 2. Wait for CI (ACTUALLY WAIT - don't give up!) |
| 69 | POLL_INTERVAL=15 |
| 70 | ELAPSED=0 |
| 71 | |
| 72 | while true; do # No timeout - wait as long as needed |
| 73 | CHECKS=$(gh pr checks $PR_NUMBER --json status,conclusion) |
| 74 | TOTAL=$(echo "$CHECKS" | jq '. | length') |
| 75 | COMPLETED=$(echo "$CHECKS" | jq '[.[] | select(.status == "COMPLETED")] | length') |
| 76 | |
| 77 | echo "[$ELAPSED s] CI: $COMPLETED/$TOTAL completed" |
| 78 | |
| 79 | if [ "$COMPLETED" -eq "$TOTAL" ] && [ "$TOTAL" -gt 0 ]; then |
| 80 | FAILED=$(echo "$CHECKS" | jq '[.[] | select(.conclusion == "FAILURE")] | length') |
| 81 | if [ "$FAILED" -eq 0 ]; then |
| 82 | echo "All CI passed. Marking ready..." |
| 83 | gh pr ready $PR_NUMBER |
| 84 | break |
| 85 | else |
| 86 | echo "CI failed. PR remains draft." |
| 87 | gh pr checks $PR_NUMBER |
| 88 | break |
| 89 | fi |
| 90 | fi |
| 91 | |
| 92 | sleep $POLL_INTERVAL |
| 93 | ELAPSED=$((ELAPSED + POLL_INTERVAL)) |
| 94 | done |
| 95 | ``` |
| 96 | |
| 97 | **Never say "I'll check back later"** — the chat session ends. Default to draft PRs; mark ready |
| 98 | only when the user asks or CI is already verified green. Merge only after every check passes |
| 99 | and the PR is `MERGEABLE`. |
| 100 | |
| 101 | ## Test-driven development |
| 102 | |
| 103 | PolicyEngine follows TDD: write the test (RED), implement (GREEN), refactor. In multi-agent |
| 104 | encoding workflows, test authoring and rule implementation proceed independently from the same |
| 105 | regulations so the test is not shaped to the implementation. |
| 106 | |
| 107 | **Country models test with YAML integration cases**, not hand-built Python situations. A case |
| 108 | declares inputs and expe |