PolicyEngine coding standards, formatters, CI requirements, and development best practices. Triggers: "CI failing", "linting", "formatting", "before committing", "PR standards", "code style", "ruff formatter", "prettier", "pre-commit"
From essentialnpx claudepluginhub policyengine/policyengine-claude --plugin data-scienceThis skill uses the workspace's default tool permissions.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Use this skill to ensure code meets PolicyEngine's development standards and passes CI checks.
⚠️ MUST USE Python 3.13 - Do NOT downgrade to older versions
python --versionpyproject.toml to specify version requirements⚠️ ALWAYS use uv run for Python commands - Never use bare python or pytest
uv run python script.py, uv run pytest tests/python script.py, pytest tests/⚠️ MUST USE Jupyter Book 2.0 (MyST-NB) - NOT Jupyter Book 1.x
myst build docs (NOT jb build)make format or language-specific formattermake test to ensure all tests passCommon failure pattern:
User: "Create a PR and mark it ready when CI passes"
Claude: "I've created the PR as draft. CI will take a while, I'll check back later..."
[Chat ends - Claude never checks back]
Result: PR stays in draft, user has to manually check CI and mark ready
When creating PRs, use the /create-pr command:
/create-pr
This command:
Why this works: The command contains explicit polling logic that Claude executes, so it actually waits instead of giving up.
If the command isn't installed, implement the pattern directly:
# 1. Create PR as draft
# CRITICAL: Use --repo flag to create PR in upstream repo from fork
gh pr create --repo PolicyEngine/policyengine-us --draft --title "Title" --body "Body"
PR_NUMBER=$(gh pr view --json number --jq '.number')
# 2. Wait for CI (ACTUALLY WAIT - don't give up!)
POLL_INTERVAL=15
ELAPSED=0
while true; do # No timeout - wait as long as needed
CHECKS=$(gh pr checks $PR_NUMBER --json status,conclusion)
TOTAL=$(echo "$CHECKS" | jq '. | length')
COMPLETED=$(echo "$CHECKS" | jq '[.[] | select(.status == "COMPLETED")] | length')
echo "[$ELAPSED s] CI: $COMPLETED/$TOTAL completed"
if [ "$COMPLETED" -eq "$TOTAL" ] && [ "$TOTAL" -gt 0 ]; then
FAILED=$(echo "$CHECKS" | jq '[.[] | select(.conclusion == "FAILURE")] | length')
if [ "$FAILED" -eq 0 ]; then
echo "✅ All CI passed! Marking ready..."
gh pr ready $PR_NUMBER
break
else
echo "❌ CI failed. PR remains draft."
gh pr checks $PR_NUMBER
break
fi
fi
sleep $POLL_INTERVAL
ELAPSED=$((ELAPSED + POLL_INTERVAL))
done
# Important: No timeout! Population simulations can take 30+ minutes.
CRITICAL: Never say "I'll check back later" — the chat session ends. Always use the polling loop above to actually wait. Default to creating PRs as draft; only create as ready when user explicitly requests it or CI is already verified.
PolicyEngine follows TDD: write test first (RED), implement (GREEN), refactor. In multi-agent workflows, @test-creator and @rules-engineer work independently from the same regulations. See policyengine-core-skill for details.
Example test:
def test_ctc_for_two_children():
"""Test CTC calculation for married couple with 2 children."""
situation = create_married_couple(income_1=75000, income_2=50000, num_children=2, child_ages=[5, 8])
sim = Simulation(situation=situation)
ctc = sim.calculate("ctc", 2026)[0]
assert ctc == 4400, "CTC should be $2,200 per child"
# Python
make test # All tests
uv run pytest tests/ -v # With uv
uv run pytest tests/test_credits.py::test_ctc -v # Specific test
# React
make test # All tests
bun test -- --watch # Watch mode
make format or ruff format .ruff format --check .# Format all Python files
make format
# Check if formatting is needed (CI-style)
ruff format --check .
exceptbun run lint -- --fix && bunx prettier --write .bun run lint -- --max-warnings=0# Format all files
make format
# Or manually
bun run lint -- --fix
bunx prettier --write .
# Check if formatting is needed (CI-style)
bun run lint -- --max-warnings=0
src/config/environment.js pattern for env config (not REACT_APP_ env vars)CRITICAL: NEVER manually update CHANGELOG.md. Check which system the repo uses, then follow that system.
How to tell which system a repo uses:
changelog.d/ directory -- if yes, use towncrier (new system)changelog.d/ but the repo uses changelog_entry.yaml, use the legacy systemchangelog.d/ fragments)Used by: policyengine-skills, the generated policyengine-claude wrapper, and newer repos with a changelog.d/ directory.
echo "Description of change." > changelog.d/branch-name.added.md
Fragment filename format: {name}.{type}.md
Types: added (minor), changed (patch), fixed (patch), removed (minor), breaking (major)
GitHub Actions runs towncrier build on merge to compile fragments into CHANGELOG.md.
changelog_entry.yamlUsed by: policyengine-us, policyengine-uk, and other country model repos.
Create changelog_entry.yaml at repository root:
- bump: patch # or minor, major
changes:
added:
- Description of new feature
fixed:
- Description of bug fix
GitHub Actions automatically updates CHANGELOG.md and changelog.yaml on merge.
DO NOT (either system):
make changelog manually during PR creationCHANGELOG.md in your PRSee the parent PolicyEngine/CLAUDE.md for full git workflow, branch naming, commit message format, and common AI pitfalls (file versioning, formatter not run, env vars, wrong Python version). Key points:
make format before committingpolicyengine-package/
├── policyengine_package/
│ ├── __init__.py
│ ├── core/
│ ├── calculations/
│ └── utils/
├── tests/
│ ├── test_calculations.py
│ └── test_core.py
├── pyproject.toml
├── Makefile
├── CLAUDE.md
├── CHANGELOG.md
└── README.md
policyengine-app/
├── src/
│ ├── components/
│ ├── pages/
│ ├── config/
│ │ └── environment.js
│ └── App.jsx
├── public/
├── package.json
├── .eslintrc.json
├── .prettierrc
└── README.md
Standard commands across PolicyEngine repos:
make install # Install dependencies
make test # Run tests
make format # Format code
make changelog # Update changelog (automation only, not manual)
make debug # Start dev server (apps)
make build # Production build (apps)
See PolicyEngine/CLAUDE.md for full CI stability details (fork failures, rate limits, linting). Quick fixes: use make format before committing, use uv run pytest not bare pytest, create branches on PolicyEngine repos not forks.
When renaming a PolicyEngine repository, references to the old name are often hardcoded across the org. Follow this checklist to avoid broken links, builds, and embeds.
# Find every file in the org that mentions the old repo name
gh api "/search/code?q=org:PolicyEngine+OLD_REPO_NAME" --paginate | jq '.items[] | {repo: .repository.full_name, path: .path}'
Review every result -- some will be docs/changelogs (safe to update later), others will break builds if not updated before the rename.
| Location | What to look for | Example |
|---|---|---|
| GitHub Actions workflows | PUBLIC_URL, checkout paths, artifact names | PUBLIC_URL: https://policyengine.github.io/OLD_NAME |
| Iframe embeds in policyengine-app-v2 | src URLs in page components | app/src/pages/*.jsx referencing OLD_NAME.github.io |
| README badges and links | Shield.io badges, repo links |  |
| package.json / pyproject.toml | name, repository, homepage fields | "name": "old-name" |
| GitHub Pages URLs | Any URL containing policyengine.github.io/OLD_NAME | Links in docs, blog posts, other READMEs |
| CLAUDE.md | Repo-specific instructions that reference the old name | Paths, URLs, skill references |
| Import paths (Python) | Package name derived from repo name | from old_name import ... |
| Vercel / deployment configs | Project names, domain aliases | vercel.json, Vercel dashboard settings |
| policyengine-skills source | Skill files that reference the repo | Links in SKILL.md files across the canonical source repo |
If the renamed repo is embedded in another site (e.g., via iframe or GitHub Pages), both repos need updates:
PUBLIC_URL and any self-referencing URLs in workflows, configs, and docs.src URLs, links, and any CI that depends on the old name.policyengine.github.io/old-name will 404.gh api "/search/code?q=org:PolicyEngine+OLD_REPO_NAME" --paginate | jq '.total_count'
/PolicyEngine/CLAUDE.mdSee PolicyEngine repositories for examples of standard-compliant code: