From afad8210087a5e23d9b62fdb588d97a208a4c65a Mon Sep 17 00:00:00 2001 From: Jonny Spicer Date: Mon, 20 Jul 2026 23:49:19 +0100 Subject: [PATCH 1/7] Add pre-commit config assets (python/ts) with validation + CI wiring --- .github/workflows/setup-repo-tests.yml | 1 + .../setup-repo/assets/precommit/python.yaml | 16 ++++++++ .../setup-repo/assets/precommit/ts.yaml | 18 +++++++++ tests/setup-repo/test-precommit-assets.sh | 38 +++++++++++++++++++ 4 files changed, 73 insertions(+) create mode 100644 plugins/core/skills/setup-repo/assets/precommit/python.yaml create mode 100644 plugins/core/skills/setup-repo/assets/precommit/ts.yaml create mode 100755 tests/setup-repo/test-precommit-assets.sh 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/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/tests/setup-repo/test-precommit-assets.sh b/tests/setup-repo/test-precommit-assets.sh new file mode 100755 index 0000000..ab643e5 --- /dev/null +++ b/tests/setup-repo/test-precommit-assets.sh @@ -0,0 +1,38 @@ +#!/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 need_hygiene <= {h["id"] for h in pr[HYGIENE]["hooks"]}, pr[HYGIENE] +assert {"ruff", "ruff-format"} <= {h["id"] for h in pr[RUFF]["hooks"]}, pr[RUFF] +print("python.yaml OK") + +tr = by_repo(ts) +assert HYGIENE in tr, tr.keys() +assert need_hygiene <= {h["id"] for h in tr[HYGIENE]["hooks"]}, tr[HYGIENE] +assert "local" in tr, tr.keys() +local_hooks = tr["local"]["hooks"] +assert any(h.get("id") == "eslint" and "eslint" in h.get("entry", "") for h in local_hooks), local_hooks +print("ts.yaml OK") +EOF +echo "precommit assets OK" From e57826a6e4afbdba483b5e34d41b267a9d277996 Mon Sep 17 00:00:00 2001 From: Jonny Spicer Date: Mon, 20 Jul 2026 23:53:15 +0100 Subject: [PATCH 2/7] Harden precommit validator: assert revs, ruff --fix, eslint hook fields --- tests/setup-repo/test-precommit-assets.sh | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/setup-repo/test-precommit-assets.sh b/tests/setup-repo/test-precommit-assets.sh index ab643e5..188cd73 100755 --- a/tests/setup-repo/test-precommit-assets.sh +++ b/tests/setup-repo/test-precommit-assets.sh @@ -23,16 +23,26 @@ need_hygiene = {"trailing-whitespace", "end-of-file-fixer", "check-yaml", 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] -assert {"ruff", "ruff-format"} <= {h["id"] for h in pr[RUFF]["hooks"]}, pr[RUFF] +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"] -assert any(h.get("id") == "eslint" and "eslint" in h.get("entry", "") for h in local_hooks), 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" From 420d9de0b0271e70d0a582167b68e2e13abeb4c5 Mon Sep 17 00:00:00 2001 From: Jonny Spicer Date: Mon, 20 Jul 2026 23:54:32 +0100 Subject: [PATCH 3/7] Python CI: run pre-commit (single source of truth) instead of direct ruff steps --- plugins/core/skills/setup-repo/assets/ci/ci-python.yml | 7 ++----- tests/setup-repo/test-ci-assets.sh | 5 +++-- 2 files changed, 5 insertions(+), 7 deletions(-) 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/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 From 5329f580e010639f81ab0aea77a8787822352261 Mon Sep 17 00:00:00 2001 From: Jonny Spicer Date: Mon, 20 Jul 2026 23:57:22 +0100 Subject: [PATCH 4/7] Node CI: run pre-commit (adds python/uv) instead of npm run lint --- .../core/skills/setup-repo/assets/ci/ci-node.yml | 14 +++++++++++--- tests/setup-repo/test-node-assets.sh | 6 +++++- 2 files changed, 16 insertions(+), 4 deletions(-) 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/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 From 6b4a456676d8891b21d1651186852294f0cb9ba7 Mon Sep 17 00:00:00 2001 From: Jonny Spicer Date: Tue, 21 Jul 2026 00:00:55 +0100 Subject: [PATCH 5/7] Scaffold engine: copy .pre-commit-config.yaml for python/ts stacks --- plugins/core/skills/setup-repo/scripts/scaffold-repo.sh | 3 +++ tests/setup-repo/test-scaffold.sh | 7 ++++--- 2 files changed, 7 insertions(+), 3 deletions(-) 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-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; } From 27d97bafc1e6b3f53bd051ef167feb864d2f10df Mon Sep 17 00:00:00 2001 From: Jonny Spicer Date: Tue, 21 Jul 2026 00:04:09 +0100 Subject: [PATCH 6/7] setup-repo: register pre-commit hook on install; bump core to 0.5.0 --- plugins/core/.claude-plugin/plugin.json | 2 +- plugins/core/skills/setup-repo/SKILL.md | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) 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..d870381 100644 --- a/plugins/core/skills/setup-repo/SKILL.md +++ b/plugins/core/skills/setup-repo/SKILL.md @@ -36,6 +36,11 @@ 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. From bc7d46d4457bf31474b8324e02e7006838334f3a Mon Sep 17 00:00:00 2001 From: Jonny Spicer Date: Tue, 21 Jul 2026 00:10:45 +0100 Subject: [PATCH 7/7] SKILL.md: reference pre-commit as the lint/format command in step 5 --- plugins/core/skills/setup-repo/SKILL.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/core/skills/setup-repo/SKILL.md b/plugins/core/skills/setup-repo/SKILL.md index d870381..506a232 100644 --- a/plugins/core/skills/setup-repo/SKILL.md +++ b/plugins/core/skills/setup-repo/SKILL.md @@ -47,8 +47,9 @@ interactive parts and the report. 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