From 1bfc96bb0562bde3ac532764cfa3e1a64512e475 Mon Sep 17 00:00:00 2001 From: Sollan Systems Date: Sat, 4 Jul 2026 15:53:39 -0400 Subject: [PATCH 1/4] =?UTF-8?q?feat(c1):=20composite=20GitHub=20Action=20?= =?UTF-8?q?=E2=80=94=20doctor=20hard=20gate=20+=20inspect=20scorecard,=20d?= =?UTF-8?q?ogfooded=20in=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 10 +++++ action.yml | 91 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 action.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c8920f..afc8720 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,3 +60,13 @@ jobs: run: python -m pip install --upgrade pip pyyaml pytest jsonschema langgraph - name: LangGraph recipe end-to-end run: python -B -m pytest -q -p no:cacheprovider scripts/test_langgraph_recipe.py + + action-dogfood: + name: action (dogfood on own contract) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./ + with: + path: "." + fail-under-score: "90" diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..390bc5b --- /dev/null +++ b/action.yml @@ -0,0 +1,91 @@ +name: "loop-engineer gate" +description: "Proof-of-done gate for agent-loop contracts: hard-fails on doctor, scores with inspect (warn-only by default)." +branding: + icon: "check-circle" + color: "green" + +inputs: + path: + description: "Workspace (or .loop dir) holding the loop contract" + required: false + default: "." + version: + description: "loop-engineer version to install from PyPI (e.g. 0.6.1). Empty installs from the action's own checkout." + required: false + default: "" + fail-under-score: + description: "Fail the job when the inspect score (0-100) is below this. 0 keeps inspect warn-only." + required: false + default: "0" + python-version: + description: "Python version for the gate" + required: false + default: "3.12" + github-token: + description: "Token for the optional PR scorecard comment. Empty skips the comment." + required: false + default: "" + +runs: + using: "composite" + steps: + - uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python-version }} + + - name: Install loop-engineer + shell: bash + run: | + if [ -n "${{ inputs.version }}" ]; then + python -m pip install --quiet "loop-engineer==${{ inputs.version }}" + else + python -m pip install --quiet "${{ github.action_path }}" + fi + + - name: loop doctor (hard gate) + shell: bash + run: loop doctor "${{ inputs.path }}" + + - name: loop inspect (scorecard) + shell: bash + run: | + set +e + loop inspect "${{ inputs.path }}" > "${RUNNER_TEMP}/inspect.json" + set -e + python - "${RUNNER_TEMP}/inspect.json" "${{ inputs.fail-under-score }}" <<'PY' + import json, os, sys + + report = json.load(open(sys.argv[1])) + fail_under = int(sys.argv[2]) + score, verdict = report.get("score", 0), report.get("verdict", "?") + lines = [ + "## loop-engineer scorecard", + "", + f"| metric | value |", + f"|---|---|", + f"| verdict | **{verdict}** |", + f"| score | {score}/100 |", + f"| gaps | {len(report.get('gaps', []))} |", + "", + ] + for gap in report.get("gaps", [])[:10]: + lines.append(f"- {gap}") + summary = "\n".join(lines) + "\n" + with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as fh: + fh.write(summary) + open(os.path.join(os.environ["RUNNER_TEMP"], "scorecard.md"), "w", encoding="utf-8").write(summary) + if verdict == "weak": + print(f"::warning::loop inspect verdict is weak (score {score}/100)") + if fail_under and score < fail_under: + print(f"::error::inspect score {score} < fail-under-score {fail_under}") + raise SystemExit(1) + PY + + - name: PR scorecard comment (optional) + if: ${{ inputs.github-token != '' && github.event_name == 'pull_request' }} + shell: bash + env: + GH_TOKEN: ${{ inputs.github-token }} + run: | + gh api "repos/${GITHUB_REPOSITORY}/issues/${{ github.event.pull_request.number }}/comments" \ + -f body="$(cat "${RUNNER_TEMP}/scorecard.md")" || echo "::warning::PR comment failed (non-fatal)" From c0dd588791388d49a266117f518353557697243f Mon Sep 17 00:00:00 2001 From: Sollan Systems Date: Sat, 4 Jul 2026 16:01:42 -0400 Subject: [PATCH 2/4] feat(c1): pre-commit hook id loop-doctor + consumer fixture test Ships `.pre-commit-hooks.yaml` (hook id loop-doctor -> `loop doctor .`, a language:python hook usable from any consumer .pre-commit-config.yaml on PR1's self-contained wheel) plus `scripts/test_precommit_hook.py`, which asserts the hook definition is sound and the entry matches a declared console script, and an env-guarded consumer fixture that runs the hook through `pre-commit try-repo` end-to-end. The action-dogfood CI job installs pre-commit and runs that fixture for real on the PR checkout. CHANGELOG gains the C1 entry covering both the composite action and the pre-commit gate. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 5 ++++ .pre-commit-hooks.yaml | 7 +++++ CHANGELOG.md | 24 +++++++++++++++++ scripts/test_precommit_hook.py | 49 ++++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+) create mode 100644 .pre-commit-hooks.yaml create mode 100644 scripts/test_precommit_hook.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index afc8720..4599164 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,3 +70,8 @@ jobs: with: path: "." fail-under-score: "90" + + - name: pre-commit consumer fixture + run: | + python -m pip install --quiet pre-commit pytest pyyaml + python -B -m pytest -q -p no:cacheprovider scripts/test_precommit_hook.py diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml new file mode 100644 index 0000000..a992d6b --- /dev/null +++ b/.pre-commit-hooks.yaml @@ -0,0 +1,7 @@ +- id: loop-doctor + name: loop doctor (loop-contract validity gate) + description: "Validate the repo's .loop/ contract objects; fails on a dishonest or malformed contract." + entry: loop doctor . + language: python + pass_filenames: false + always_run: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 6116702..89142c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,30 @@ firewall never locks a session — and a strict no-op for every repo without a (`python3 ${CLAUDE_PLUGIN_ROOT}/hooks/stop_firewall.py`), so a marketplace install gets the firewall with zero configuration. +**C1 — the CI gate.** The proof-of-done gate at the two boundaries where a +foreign repo already runs its checks: a GitHub Action for pull-request CI and a +pre-commit hook for the local commit. Both wrap the same `loop doctor` honesty +gate the runtime enforces, so a consumer adopts the wedge without adopting the +loop-engineer runtime — and the repo dogfoods both on its own contract in CI. + +### Added +- **Composite GitHub Action** (`action.yml`, id `loop-engineer gate`) — runs + `loop doctor` as a hard gate and `loop inspect` as a scorecard (warn-only until + `fail-under-score` is set), installing loop-engineer from PyPI (`version:`) or + from the action's own checkout by default. Writes the scorecard to the job + summary and, given a `github-token`, an optional PR comment. The `action-dogfood` + CI job runs it against this repo's own `.loop/` contract at `fail-under-score: 90`. +- **`.pre-commit-hooks.yaml`** — a `language: python` hook id `loop-doctor` + (`entry: loop doctor .`, `always_run`, `pass_filenames: false`) that a consumer + wires in with three lines of `.pre-commit-config.yaml`; PR1's self-contained + wheel is what makes the `language: python` install work from any consumer repo. +- **Pre-commit acceptance test** (`scripts/test_precommit_hook.py`) — asserts the + hook definition is sound and its entry matches a declared console script, plus a + consumer-fixture path that scaffolds a fresh contract and runs the hook through + `pre-commit try-repo` end-to-end. Env-guarded on the `pre-commit` tool (skips + when absent); the `action-dogfood` CI job installs it and runs the fixture for + real on the PR checkout. + ## 0.6.1 — 2026-07-04 **PyPI substrate.** `loop-engineer` becomes a self-contained wheel that runs from diff --git a/scripts/test_precommit_hook.py b/scripts/test_precommit_hook.py new file mode 100644 index 0000000..03533e3 --- /dev/null +++ b/scripts/test_precommit_hook.py @@ -0,0 +1,49 @@ +"""C1 acceptance: the pre-commit hook definition is sound (always), and it runs +from a consumer-side .pre-commit-config.yaml fixture (env-guarded on the +pre-commit tool; the CI dogfood job installs it and runs this for real).""" + +from __future__ import annotations + +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parent.parent + +sys.path.insert(0, str(REPO_ROOT)) +from loop.scaffold import scaffold # noqa: E402 + + +def _hooks() -> list[dict]: + yaml = pytest.importorskip("yaml") + return yaml.safe_load((REPO_ROOT / ".pre-commit-hooks.yaml").read_text(encoding="utf-8")) + + +def test_hook_definition_is_sound(): + (hook,) = _hooks() + assert hook["id"] == "loop-doctor" + assert hook["entry"] == "loop doctor ." + assert hook["language"] == "python" + assert hook["pass_filenames"] is False + assert hook["always_run"] is True + + +def test_entry_command_matches_a_declared_console_script(): + text = (REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8") + assert 'loop = "loop.__main__:main"' in text + + +@pytest.mark.skipif(shutil.which("pre-commit") is None, reason="pre-commit tool not installed") +def test_hook_runs_from_a_consumer_fixture(tmp_path): + consumer = tmp_path / "consumer" + scaffold(consumer) + subprocess.run(["git", "init", "-q"], cwd=consumer, check=True) + subprocess.run(["git", "add", "-A"], cwd=consumer, check=True) + proc = subprocess.run( + ["pre-commit", "try-repo", str(REPO_ROOT), "loop-doctor", "--all-files"], + cwd=consumer, capture_output=True, text=True, timeout=600, + ) + assert proc.returncode == 0, proc.stdout + proc.stderr From 2aa7bd9025bc630f57de7f7c0993e18df3891915 Mon Sep 17 00:00:00 2001 From: Sollan Systems Date: Sat, 4 Jul 2026 16:13:51 -0400 Subject: [PATCH 3/4] fix(c1): env-indirect action inputs; document dogfood python coupling Env-indirect inputs.version/path/fail-under-score in action.yml's run: steps (install, doctor, inspect+scorecard heredoc argv) to close the GitHub Actions script-injection footgun for a published reusable action. Behavior unchanged for legitimate inputs (verified via a real inspect.json run through both pass/fail branches). Also documents in ci.yml why bare `python` is on PATH in the dogfood job's pre-commit fixture step (the preceding `uses: ./` composite step ran setup-python). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01As92NaD7jMDbnQTqXDzBVa --- .github/workflows/ci.yml | 2 ++ action.yml | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4599164..cab004b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,6 +72,8 @@ jobs: fail-under-score: "90" - name: pre-commit consumer fixture + # bare `python` is on PATH here because the preceding `uses: ./` composite step ran setup-python + # (persisted via GITHUB_PATH) — keep the action step before this one. run: | python -m pip install --quiet pre-commit pytest pyyaml python -B -m pytest -q -p no:cacheprovider scripts/test_precommit_hook.py diff --git a/action.yml b/action.yml index 390bc5b..11de31e 100644 --- a/action.yml +++ b/action.yml @@ -35,24 +35,31 @@ runs: - name: Install loop-engineer shell: bash + env: + LOOP_VERSION: "${{ inputs.version }}" run: | - if [ -n "${{ inputs.version }}" ]; then - python -m pip install --quiet "loop-engineer==${{ inputs.version }}" + if [ -n "$LOOP_VERSION" ]; then + python -m pip install --quiet "loop-engineer==$LOOP_VERSION" else python -m pip install --quiet "${{ github.action_path }}" fi - name: loop doctor (hard gate) shell: bash - run: loop doctor "${{ inputs.path }}" + env: + LOOP_PATH: "${{ inputs.path }}" + run: loop doctor "$LOOP_PATH" - name: loop inspect (scorecard) shell: bash + env: + LOOP_PATH: "${{ inputs.path }}" + LOOP_FAIL_UNDER: "${{ inputs.fail-under-score }}" run: | set +e - loop inspect "${{ inputs.path }}" > "${RUNNER_TEMP}/inspect.json" + loop inspect "$LOOP_PATH" > "${RUNNER_TEMP}/inspect.json" set -e - python - "${RUNNER_TEMP}/inspect.json" "${{ inputs.fail-under-score }}" <<'PY' + python - "${RUNNER_TEMP}/inspect.json" "$LOOP_FAIL_UNDER" <<'PY' import json, os, sys report = json.load(open(sys.argv[1])) From 00a90109f5018fad29056f3d904f4191f5b18998 Mon Sep 17 00:00:00 2001 From: Sollan Systems Date: Sat, 4 Jul 2026 16:20:29 -0400 Subject: [PATCH 4/4] =?UTF-8?q?fix(c1):=20dogfood=20gates=20the=20tracked?= =?UTF-8?q?=20flagship=20example=20=E2=80=94=20root=20.loop/=20is=20gitign?= =?UTF-8?q?ored,=20absent=20in=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01As92NaD7jMDbnQTqXDzBVa --- .github/workflows/ci.yml | 7 +++++-- CHANGELOG.md | 4 +++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cab004b..b0966ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,13 +62,16 @@ jobs: run: python -B -m pytest -q -p no:cacheprovider scripts/test_langgraph_recipe.py action-dogfood: - name: action (dogfood on own contract) + name: action (dogfood on flagship example) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + # The repo root's live .loop/ is gitignored, so it does not exist in a fresh + # CI checkout — the tracked flagship example is the contract the action gates + # (doctor-clean, inspect 90/strong in the action's dependency-free install). - uses: ./ with: - path: "." + path: "examples/coverage-repair" fail-under-score: "90" - name: pre-commit consumer fixture diff --git a/CHANGELOG.md b/CHANGELOG.md index 89142c1..dc2b415 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,7 +73,9 @@ loop-engineer runtime — and the repo dogfoods both on its own contract in CI. `fail-under-score` is set), installing loop-engineer from PyPI (`version:`) or from the action's own checkout by default. Writes the scorecard to the job summary and, given a `github-token`, an optional PR comment. The `action-dogfood` - CI job runs it against this repo's own `.loop/` contract at `fail-under-score: 90`. + CI job runs it against the tracked flagship example contract + (`examples/coverage-repair`) at `fail-under-score: 90` — the repo root's live + `.loop/` is gitignored and absent in a fresh CI checkout. - **`.pre-commit-hooks.yaml`** — a `language: python` hook id `loop-doctor` (`entry: loop doctor .`, `always_run`, `pass_filenames: false`) that a consumer wires in with three lines of `.pre-commit-config.yaml`; PR1's self-contained