diff --git a/.github/workflows/setup-repo-tests.yml b/.github/workflows/setup-repo-tests.yml index 2222d0f..5c245e3 100644 --- a/.github/workflows/setup-repo-tests.yml +++ b/.github/workflows/setup-repo-tests.yml @@ -31,4 +31,5 @@ jobs: bash tests/setup-repo/test-python-assets.sh bash tests/setup-repo/test-ci-assets.sh bash tests/setup-repo/test-node-assets.sh + bash tests/setup-repo/test-precommit-assets.sh bash tests/setup-repo/test-scaffold.sh diff --git a/plugins/core/.claude-plugin/plugin.json b/plugins/core/.claude-plugin/plugin.json index 68f07ce..62fa199 100644 --- a/plugins/core/.claude-plugin/plugin.json +++ b/plugins/core/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "core", - "version": "0.4.0", + "version": "0.5.0", "description": "Core Offworld Labs skills, commands, agents, and hooks shared across all repos.", "author": { "name": "Offworld Labs" diff --git a/plugins/core/skills/setup-repo/SKILL.md b/plugins/core/skills/setup-repo/SKILL.md index df217ae..506a232 100644 --- a/plugins/core/skills/setup-repo/SKILL.md +++ b/plugins/core/skills/setup-repo/SKILL.md @@ -36,14 +36,20 @@ interactive parts and the report. - **ts-frontend / ts-backend:** run `npm install` (this generates `package-lock.json` — remind the user to commit it, since the CI workflow uses `npm ci`). + - **pre-commit (python & ts stacks):** after the stack deps are installed, + register the git hook so the scaffolded `.pre-commit-config.yaml` runs on + every commit: `uvx pre-commit install` (or `pipx run pre-commit install`, or + `pip install pre-commit && pre-commit install`). Skip with a note if + pre-commit/uv is unavailable. Report the command and result; if the toolchain is unavailable, skip and note it rather than failing. 5. **Flesh out CLAUDE.md.** The stub was just written. Ask the user for a one-or-two-line description of what this repo does, then fill in the `Project Overview`, `Build & Test Commands`, and `Local Architecture` - sections from their answer plus what was scaffolded (stack, `ruff check .`, - `ruff format --check .`, `pytest`). If they skip, leave the stub as-is. Keep + sections from their answer plus what was scaffolded (stack; lint/format via + `pre-commit run --all-files`; tests via `pytest` or `npm test`). If they skip, + leave the stub as-is. Keep CLAUDE.md under the 200-line ceiling noted in the template. 6. **Report and follow-ups.** Summarise files written vs skipped, then list the diff --git a/plugins/core/skills/setup-repo/assets/ci/ci-node.yml b/plugins/core/skills/setup-repo/assets/ci/ci-node.yml index 67ff7ff..70cfc89 100644 --- a/plugins/core/skills/setup-repo/assets/ci/ci-node.yml +++ b/plugins/core/skills/setup-repo/assets/ci/ci-node.yml @@ -21,11 +21,19 @@ jobs: - name: Install dependencies run: npm ci + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Pre-commit + run: uvx pre-commit run --all-files --show-diff-on-failure + - name: Typecheck run: npm run typecheck - - name: Lint - run: npm run lint - - name: Test run: npm test diff --git a/plugins/core/skills/setup-repo/assets/ci/ci-python.yml b/plugins/core/skills/setup-repo/assets/ci/ci-python.yml index 18424b7..c2c233b 100644 --- a/plugins/core/skills/setup-repo/assets/ci/ci-python.yml +++ b/plugins/core/skills/setup-repo/assets/ci/ci-python.yml @@ -23,11 +23,8 @@ jobs: - name: Install dependencies run: uv pip install --system -r requirements.txt -r requirements-dev.txt - - name: Ruff lint - run: ruff check . - - - name: Ruff format check - run: ruff format --check . + - name: Pre-commit + run: uvx pre-commit run --all-files --show-diff-on-failure - name: Pytest run: pytest diff --git a/plugins/core/skills/setup-repo/assets/precommit/python.yaml b/plugins/core/skills/setup-repo/assets/precommit/python.yaml new file mode 100644 index 0000000..9a6478d --- /dev/null +++ b/plugins/core/skills/setup-repo/assets/precommit/python.yaml @@ -0,0 +1,16 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-json + - id: check-merge-conflict + - id: check-added-large-files + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.0 + hooks: + - id: ruff + args: [--fix] + - id: ruff-format diff --git a/plugins/core/skills/setup-repo/assets/precommit/ts.yaml b/plugins/core/skills/setup-repo/assets/precommit/ts.yaml new file mode 100644 index 0000000..f257608 --- /dev/null +++ b/plugins/core/skills/setup-repo/assets/precommit/ts.yaml @@ -0,0 +1,18 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-json + - id: check-merge-conflict + - id: check-added-large-files + - repo: local + hooks: + - id: eslint + name: eslint + entry: npx eslint --fix + language: system + types_or: [ts, tsx] + pass_filenames: true diff --git a/plugins/core/skills/setup-repo/scripts/scaffold-repo.sh b/plugins/core/skills/setup-repo/scripts/scaffold-repo.sh index a96594f..669d199 100755 --- a/plugins/core/skills/setup-repo/scripts/scaffold-repo.sh +++ b/plugins/core/skills/setup-repo/scripts/scaffold-repo.sh @@ -50,6 +50,7 @@ case "$STACK" in copy "$ASSETS/stack/python/gitignore" "$TARGET/.gitignore" copy "$ASSETS/stack/python/tests/.gitkeep" "$TARGET/tests/.gitkeep" copy "$ASSETS/ci/ci-python.yml" "$TARGET/.github/workflows/ci.yml" + copy "$ASSETS/precommit/python.yaml" "$TARGET/.pre-commit-config.yaml" ;; ts-frontend) copy "$ASSETS/stack/ts-frontend/package.json" "$TARGET/package.json" @@ -62,6 +63,7 @@ case "$STACK" in copy "$ASSETS/stack/ts-frontend/src/index.test.ts" "$TARGET/src/index.test.ts" copy "$ASSETS/stack/ts-frontend/src/main.tsx" "$TARGET/src/main.tsx" copy "$ASSETS/ci/ci-node.yml" "$TARGET/.github/workflows/ci.yml" + copy "$ASSETS/precommit/ts.yaml" "$TARGET/.pre-commit-config.yaml" ;; ts-backend) copy "$ASSETS/stack/ts-backend/package.json" "$TARGET/package.json" @@ -72,6 +74,7 @@ case "$STACK" in copy "$ASSETS/stack/ts-backend/src/index.ts" "$TARGET/src/index.ts" copy "$ASSETS/stack/ts-backend/src/index.test.ts" "$TARGET/src/index.test.ts" copy "$ASSETS/ci/ci-node.yml" "$TARGET/.github/workflows/ci.yml" + copy "$ASSETS/precommit/ts.yaml" "$TARGET/.pre-commit-config.yaml" ;; none) ;; esac diff --git a/tests/setup-repo/test-ci-assets.sh b/tests/setup-repo/test-ci-assets.sh index 36faf01..efb2958 100755 --- a/tests/setup-repo/test-ci-assets.sh +++ b/tests/setup-repo/test-ci-assets.sh @@ -18,8 +18,9 @@ assert on["push"]["branches"] == ["main"], on assert "pull_request" in on, on steps = doc["jobs"]["lint-and-test"]["steps"] runs = "\n".join(s.get("run", "") for s in steps) -assert "ruff check ." in runs, runs -assert "ruff format --check ." in runs, runs +assert "pre-commit run --all-files" in runs, runs +assert "ruff check ." not in runs, runs # ruff now runs via pre-commit, not a direct CI step +assert "ruff format --check ." not in runs, runs assert "pytest" in runs, runs assert "requirements.txt" in runs and "requirements-dev.txt" in runs, runs # org standard: install with uv, not pip diff --git a/tests/setup-repo/test-node-assets.sh b/tests/setup-repo/test-node-assets.sh index e79bd41..a5ae267 100755 --- a/tests/setup-repo/test-node-assets.sh +++ b/tests/setup-repo/test-node-assets.sh @@ -66,10 +66,14 @@ setup_node = [s for s in steps if str(s.get("uses", "")).startswith("actions/set assert setup_node, uses assert setup_node[0]["with"]["node-version"] == "20", setup_node assert setup_node[0]["with"]["cache"] == "npm", setup_node +setup_py = [s for s in steps if str(s.get("uses", "")).startswith("actions/setup-python")] +assert setup_py and setup_py[0]["with"]["python-version"] == "3.12", setup_py +assert any(u.startswith("astral-sh/setup-uv") for u in uses), uses runs = "\n".join(s.get("run", "") for s in steps) assert "npm ci" in runs, runs assert "npm run typecheck" in runs, runs -assert "npm run lint" in runs, runs +assert "pre-commit run --all-files" in runs, runs +assert "npm run lint" not in runs, runs # eslint now runs via pre-commit assert "npm test" in runs, runs print("ci-node.yml OK") EOF diff --git a/tests/setup-repo/test-precommit-assets.sh b/tests/setup-repo/test-precommit-assets.sh new file mode 100755 index 0000000..188cd73 --- /dev/null +++ b/tests/setup-repo/test-precommit-assets.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +set -euo pipefail +ROOT="$(git rev-parse --show-toplevel)" +PC="$ROOT/plugins/core/skills/setup-repo/assets/precommit" + +python3 - "$PC/python.yaml" "$PC/ts.yaml" <<'EOF' +import sys +try: + import yaml +except ModuleNotFoundError: + print("pyyaml missing; skipping precommit YAML parse"); sys.exit(0) +py = yaml.safe_load(open(sys.argv[1])) +ts = yaml.safe_load(open(sys.argv[2])) + +def by_repo(doc): + return {r.get("repo"): r for r in doc["repos"]} + +HYGIENE = "https://github.com/pre-commit/pre-commit-hooks" +RUFF = "https://github.com/astral-sh/ruff-pre-commit" +need_hygiene = {"trailing-whitespace", "end-of-file-fixer", "check-yaml", + "check-json", "check-merge-conflict", "check-added-large-files"} + +pr = by_repo(py) +assert HYGIENE in pr, pr.keys() +assert RUFF in pr, pr.keys() +assert pr[HYGIENE]["rev"] == "v5.0.0", pr[HYGIENE].get("rev") +assert pr[RUFF]["rev"] == "v0.8.0", pr[RUFF].get("rev") +assert need_hygiene <= {h["id"] for h in pr[HYGIENE]["hooks"]}, pr[HYGIENE] +ruff_hooks = {h["id"]: h for h in pr[RUFF]["hooks"]} +assert {"ruff", "ruff-format"} <= ruff_hooks.keys(), ruff_hooks +assert ruff_hooks["ruff"].get("args") == ["--fix"], ruff_hooks["ruff"] +print("python.yaml OK") + +tr = by_repo(ts) +assert HYGIENE in tr, tr.keys() +assert tr[HYGIENE]["rev"] == "v5.0.0", tr[HYGIENE].get("rev") +assert need_hygiene <= {h["id"] for h in tr[HYGIENE]["hooks"]}, tr[HYGIENE] +assert "local" in tr, tr.keys() +local_hooks = tr["local"]["hooks"] +eslint = next((h for h in local_hooks if h.get("id") == "eslint"), None) +assert eslint is not None, local_hooks +assert "npx eslint --fix" in eslint.get("entry", ""), eslint +assert eslint.get("language") == "system", eslint +assert eslint.get("types_or") == ["ts", "tsx"], eslint +assert eslint.get("pass_filenames") is True, eslint +print("ts.yaml OK") +EOF +echo "precommit assets OK" diff --git a/tests/setup-repo/test-scaffold.sh b/tests/setup-repo/test-scaffold.sh index 9de14cc..1bb23bc 100755 --- a/tests/setup-repo/test-scaffold.sh +++ b/tests/setup-repo/test-scaffold.sh @@ -12,7 +12,7 @@ for f in .claude/settings.json CLAUDE.md .claude/rules/security.md \ .claude/rules/code-style.md .github/workflows/claude.yml \ .github/workflows/claude-code-review.yml .github/workflows/ci.yml \ .editorconfig pyproject.toml requirements.txt requirements-dev.txt \ - .gitignore tests/.gitkeep; do + .gitignore tests/.gitkeep .pre-commit-config.yaml; do test -e "$TMP/$f" || { echo "MISSING: $f" >&2; exit 1; } done @@ -41,6 +41,7 @@ for f in pyproject.toml requirements.txt requirements-dev.txt .gitignore \ tests/.gitkeep .github/workflows/ci.yml; do test -e "$TMP_NONE/$f" && { echo "UNEXPECTED (none stack): $f" >&2; exit 1; } done +test -e "$TMP_NONE/.pre-commit-config.yaml" && { echo "UNEXPECTED (none stack): .pre-commit-config.yaml" >&2; exit 1; } # scaffolded python defaults are internally consistent mkdir -p "$TMP/src" @@ -84,7 +85,7 @@ bash "$ENGINE" "$TMP_TSF" ts-frontend for f in .claude/settings.json CLAUDE.md .editorconfig \ package.json tsconfig.json eslint.config.js vite.config.ts .gitignore \ index.html src/index.ts src/index.test.ts src/main.tsx .github/workflows/ci.yml \ - .github/workflows/claude.yml; do + .github/workflows/claude.yml .pre-commit-config.yaml; do test -e "$TMP_TSF/$f" || { echo "MISSING (ts-frontend): $f" >&2; exit 1; } done for f in pyproject.toml requirements.txt requirements-dev.txt; do @@ -100,7 +101,7 @@ trap 'rm -rf "$TMP" "$TMP_NONE" "$BOGUS" "$TMP_TSF" "$TMP_TSB"' EXIT git -C "$TMP_TSB" init -q bash "$ENGINE" "$TMP_TSB" ts-backend for f in package.json tsconfig.json eslint.config.js vitest.config.ts .gitignore \ - src/index.ts .github/workflows/ci.yml; do + src/index.ts .github/workflows/ci.yml .pre-commit-config.yaml; do test -e "$TMP_TSB/$f" || { echo "MISSING (ts-backend): $f" >&2; exit 1; } done grep -q '"react"' "$TMP_TSB/package.json" && { echo "ts-backend should not have react" >&2; exit 1; }