From e5319b7a7bea614e0f1cf50f43dcaf62eadc8157 Mon Sep 17 00:00:00 2001 From: Kevin Simback <16635828+ksimback@users.noreply.github.com> Date: Sat, 4 Jul 2026 23:07:24 -0700 Subject: [PATCH 1/2] Fix Windows skill path, align docs, add CHANGELOG and CONTRIBUTING - SKILL.md helper commands now resolve the venv Python on Windows (.venv/Scripts/python.exe) before falling back to python3/python; the previous POSIX-only path plus python3 fallback made the compile step fail on a plain Windows install. - allowed-tools aligned to 'Read, Write, Bash' in both SKILL.md and the slash command (three files previously declared three different scopes; all documented commands are bash-syntax and Git Bash ships with the required Git install). - SKILL.md flow-preview stop line now shows the token budget the real renderer emits; register-model example uses the quoted-invoke form. - README: complete emitted-file list, generic Windows paths instead of a personal username, and a new footprint + uninstall section documenting ~/.looper/models.json (previously written to the home directory without any mention in the docs). - agents/openai.yaml gets a header explaining what it is and that the Claude Code path never reads it. - Add CHANGELOG.md (0.1.0 history + 0.2.0 notes) and CONTRIBUTING.md (dev setup, safety invariants, spec-change rules); bump version to 0.2.0. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 79 ++++++++++++++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 46 +++++++++++++++++++++++++++ README.md | 38 ++++++++++++++++++++-- SKILL.md | 35 ++++++++++++++------ agents/openai.yaml | 3 ++ commands/looper.md | 2 +- pyproject.toml | 2 +- 7 files changed, 190 insertions(+), 15 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d7eead6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,79 @@ +# Changelog + +All notable changes to Looper are documented here. Versions follow +[Semantic Versioning](https://semver.org/); the loop spec format is versioned +separately via `version:` in `loop.yaml` (currently `1`). + +## 0.2.0 — 2026-07-05 + +Remediation release from a full audit of the runner, compiler, installers, +and docs. No loop spec format changes: `version: 1` loop.yaml files compile +unchanged (compiled output gains default `timeout_sec` fields on programmatic +checks and cmd context sources). + +### Fixed — runner (`templates/run-loop.py`) +- `fixed_passes` gates no longer trip the no-progress detector with their own + synthetic marker; reviewer-only gates with `max_revisions >= 2` complete + cleanly (#11). +- Judge verdicts containing nested JSON parse correctly; trailing prose after + the JSON block is tolerated (#11). +- A no-progress stop ends the whole run instead of only the current delivery + attempt (#11). +- Resume works at gate boundaries: iteration restored from `state.json`, + existing artifacts kept, wall-clock budget accrues across resumes (#11). +- Runner failures mark `state.json` as `failed` instead of leaving `running`; + malformed specs produce friendly errors, not tracebacks (#11). +- `loop_control.human_checkpoints` (`after_plan`) is honored (#11). +- Subprocess I/O pinned to UTF-8 — fixes crashes/corruption on Windows with + non-ASCII model output (#11). +- Example wrapper runs against its own `loop.resolved.json` (#11). +- Python 3.9/3.10 compatibility: `datetime.timezone.utc` instead of the + 3.11-only `datetime.UTC` (#9, fixes #8). + +### Security +- Consent fails closed: any non-local council member requires explicit + first-send consent even without a `privacy.egress` entry (#11). +- Context gathering honors configured privacy redactions; redaction globs + match at any path depth (bare `.env` now covers `config/.env`); defaults + always apply and configured globs extend them (#11). +- Workspace and context paths must stay inside the loop directory — enforced + at compile time and again at runtime (#11, #12). +- `register-model` refuses invoke/notes values that look like credentials; + the registry stores invocation metadata only (#12). + +### Fixed — compiler (`scripts/looper.py`) +- Programmatic checks and `cmd` context sources compile with validated + timeouts (defaults 300s / 60s) (#12). +- Empty argv arrays, negative/typed-wrong budgets, duplicate council ids, + gate verdict sources not listed in the gate's members, and context sources + with both/neither of `file`/`cmd` are rejected with clean errors (#12). +- Null `context_sources:` / `criteria:` / `members:` keys no longer crash + with `TypeError` (#12). +- UTF-8 BOM tolerated on JSON/YAML reads; all generated artifacts written + with LF newlines on every platform (#12). +- `register-model --invoke "claude -p"` (quoted, flag-bearing invocations) + now registrable (#12). +- JSON schemas reconciled with the actual compile contract (#12). + +### Added +- GitHub Actions CI: unittest + example compile smoke test across + ubuntu/windows/macos and Python 3.9–3.13 (#10). +- 9 new regression tests (16 total). +- CHANGELOG, CONTRIBUTING, uninstall docs, and a "what Looper writes on your + machine" section documenting `~/.looper/models.json`. + +### Docs +- SKILL.md helper commands resolve the venv Python correctly on Windows + (`.venv/Scripts/python.exe`) with POSIX and Git Bash fallbacks. +- `allowed-tools` aligned across SKILL.md and the slash command. +- Clarified token/USD budget limits as operator-advisory (#5). + +## 0.1.0 — 2026-06-19 + +Initial public scaffold: `/looper` skill and slash command, seven-stage +design interview with rubrics, `loop.yaml` → `loop.resolved.json` compiler, +LOOP.md + RUN_IN_SESSION.md rendering, external Python runner, model +detection/registry, installers, and the `ai-workflow-mapping` example. +Early fixes: `fixed_passes` hard failure handling (#2), prompt redaction +before reviewer sends (#3), PyYAML installed with the skill venv (#4), +CLI error handling (#6). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b709f6b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,46 @@ +# Contributing to Looper + +Thanks for helping — external bug reports and PRs have already made this +project better. + +## Ground rules + +- **Looper is a scaffolder, not an orchestrator.** Its own process never + invokes a model to do loop work; it reads input, coaches, and writes files. + PRs that blur that boundary (scheduling, durable orchestration, hidden model + calls) will be redirected to the spec's "hand off to an orchestrator" story. +- **The loop spec is the contract.** `loop.yaml` (authoring) compiles to + `loop.resolved.json` (execution). Changes to either format need a schema + update in `schemas/`, compiler validation in `scripts/looper.py`, and runner + support in `templates/run-loop.py` — in the same PR. +- **Safety invariants are non-negotiable:** argv arrays (never shell strings), + a timeout on every external invocation, no secrets in generated files or the + model registry, consent before any cross-vendor send, redaction defaults + always applied. + +## Dev setup + +```bash +git clone https://github.com/ksimback/looper +cd looper +python -m pip install "PyYAML>=6.0" +python -m unittest discover -s tests +``` + +Supported floor is **Python 3.9** on Windows, macOS, and Linux. CI runs the +suite plus an example compile smoke test across that matrix; please keep both +green. If you fix a bug, add a regression test that fails without your fix. + +## Pull requests + +- Keep PRs focused; one concern per PR. +- Describe the failure scenario, not just the change. +- Update `CHANGELOG.md` under an "Unreleased" heading if your change is + user-visible. +- Ports to other agent hosts are welcome as long as the spec format stays + schema-identical — open an issue first so we can talk contract. + +## Reporting bugs + +Open a GitHub issue with the repro steps, your OS and Python version, and the +relevant `state.json` / `run-log.md` snippets (redact anything sensitive). diff --git a/README.md b/README.md index 0fa99c2..294fab0 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ flowchart TD **A loop design coach for Claude Code.** Looper is a skill that helps you design a *good* agent loop — a sharp goal, checkable verification, and a second model in the review seat — then lets you run it in the same session or save it as a portable spec. It is a design layer first: it writes files and hands the current session a clear execution prompt. -Invoke it with `/looper`. It interviews you, critiques your design against built-in best-practice rubrics, lets you wire in a cross-model reviewer or judge (including non-Claude models), shows you the loop as a terminal-friendly ASCII flow preview, and writes out `RUN_IN_SESSION.md`, `loop.yaml`, a compiled `loop.resolved.json`, and a thin `run-loop.py` you own and edit. +Invoke it with `/looper`. It interviews you, critiques your design against built-in best-practice rubrics, lets you wire in a cross-model reviewer or judge (including non-Claude models), shows you the loop as a terminal-friendly ASCII flow preview, and writes out `RUN_IN_SESSION.md`, `loop.yaml`, a compiled `loop.resolved.json`, a human-readable `LOOP.md`, a thin `run-loop.py` you own and edit, plus an empty `loop-workspace/` and a README for the loop. Maintainer: Kevin Simback · GitHub [@ksimback](https://github.com/ksimback) · X [@ksimback](https://x.com/ksimback) License: MIT @@ -204,12 +204,44 @@ here, then install or update the global skill by cloning or copying the repo to If Claude Code says `Unknown command: /looper`, check both install locations: - The skill must exist at your real home directory, for example - `C:\Users\kevin\.claude\skills\looper` on Windows. -- The slash command must exist at `C:\Users\kevin\.claude\commands\looper.md` + `C:\Users\\.claude\skills\looper` on Windows. +- The slash command must exist at `C:\Users\\.claude\commands\looper.md` on Windows. - If you see a literal folder named `~` inside your project, your shell did not expand `~`; rerun the installer or manual PowerShell commands above. +### What Looper writes on your machine + +Looper is transparent about its footprint. Outside of the loop folders you ask +it to scaffold, it touches exactly four locations: + +- `~/.claude/skills/looper` — the skill itself (a git checkout). +- `~/.claude/commands/looper.md` — the `/looper` slash command. +- `~/.claude/skills/looper/.venv` — a private venv with PyYAML for the helper + compiler. +- `~/.looper/models.json` — the model registry written by `detect-models` / + `register-model`. It stores invocation metadata only (command names and + argv arrays), never API keys or other credentials; auth stays in each CLI's + own config or keychain. + +### Uninstall + +Windows PowerShell: + +```powershell +Remove-Item -Recurse -Force "$env:USERPROFILE\.claude\skills\looper" +Remove-Item -Force "$env:USERPROFILE\.claude\commands\looper.md" +Remove-Item -Recurse -Force "$env:USERPROFILE\.looper" # optional: model registry +``` + +macOS/Linux: + +```bash +rm -rf "$HOME/.claude/skills/looper" +rm -f "$HOME/.claude/commands/looper.md" +rm -rf "$HOME/.looper" # optional: model registry +``` + --- ## How it works diff --git a/SKILL.md b/SKILL.md index 2da4154..9fd19e5 100644 --- a/SKILL.md +++ b/SKILL.md @@ -11,7 +11,7 @@ description: > plus portable loop.yaml, loop.resolved.json, LOOP.md, and run-loop.py. disable-model-invocation: true argument-hint: "[target-dir]" -allowed-tools: Write Bash +allowed-tools: Read, Write, Bash --- # Looper @@ -59,9 +59,9 @@ advanced external runner. - `run-loop.py` - `loop-workspace/` - `README.md` -10. After writing `loop.yaml`, run: - `LOOPER_PYTHON="${CLAUDE_SKILL_DIR}/.venv/bin/python"; [ -x "$LOOPER_PYTHON" ] || LOOPER_PYTHON=python3; "$LOOPER_PYTHON" ${CLAUDE_SKILL_DIR}/scripts/looper.py compile /loop.yaml --out /loop.resolved.json --render /LOOP.md --session-prompt /RUN_IN_SESSION.md` - If `python3` is not available, try `python`. +10. After writing `loop.yaml`, resolve the helper Python (see Helper Python + below) and run: + `"$LOOPER_PYTHON" ${CLAUDE_SKILL_DIR}/scripts/looper.py compile /loop.yaml --out /loop.resolved.json --render /LOOP.md --session-prompt /RUN_IN_SESSION.md` 11. Ask whether the user wants to run the loop now in this session. If yes, follow `RUN_IN_SESSION.md` directly as the active task. If no, explain that the same file is the easy restart path and `run-loop.py` is available for @@ -81,16 +81,31 @@ advanced external runner. - Copy `templates/run-loop.py` exactly unless the user explicitly asks to edit the external runner contract. +## Helper Python + +The installer creates a private venv inside the skill directory. Its Python +lives at `.venv/bin/python` on macOS/Linux and `.venv/Scripts/python.exe` on +Windows. Shell state does not persist between commands, so prefix every helper +invocation below with this resolution (works in POSIX shells and Git Bash on +Windows): + +```bash +LOOPER_PYTHON="${CLAUDE_SKILL_DIR}/.venv/bin/python"; [ -x "$LOOPER_PYTHON" ] || LOOPER_PYTHON="${CLAUDE_SKILL_DIR}/.venv/Scripts/python.exe"; [ -x "$LOOPER_PYTHON" ] || LOOPER_PYTHON="$(command -v python3 || command -v python)" +``` + ## Helper Scripts +Each command below assumes the Helper Python resolution is prefixed in the +same shell invocation: + - Detect model CLIs: - `LOOPER_PYTHON="${CLAUDE_SKILL_DIR}/.venv/bin/python"; [ -x "$LOOPER_PYTHON" ] || LOOPER_PYTHON=python3; "$LOOPER_PYTHON" ${CLAUDE_SKILL_DIR}/scripts/looper.py detect-models --write` -- Register a custom CLI: - `LOOPER_PYTHON="${CLAUDE_SKILL_DIR}/.venv/bin/python"; [ -x "$LOOPER_PYTHON" ] || LOOPER_PYTHON=python3; "$LOOPER_PYTHON" ${CLAUDE_SKILL_DIR}/scripts/looper.py register-model --invoke [args...]` + `"$LOOPER_PYTHON" ${CLAUDE_SKILL_DIR}/scripts/looper.py detect-models --write` +- Register a custom CLI (quote the whole invocation if it contains flags): + `"$LOOPER_PYTHON" ${CLAUDE_SKILL_DIR}/scripts/looper.py register-model --invoke " [args...]"` - Compile and render: - `LOOPER_PYTHON="${CLAUDE_SKILL_DIR}/.venv/bin/python"; [ -x "$LOOPER_PYTHON" ] || LOOPER_PYTHON=python3; "$LOOPER_PYTHON" ${CLAUDE_SKILL_DIR}/scripts/looper.py compile /loop.yaml --out /loop.resolved.json --render /LOOP.md --session-prompt /RUN_IN_SESSION.md` + `"$LOOPER_PYTHON" ${CLAUDE_SKILL_DIR}/scripts/looper.py compile /loop.yaml --out /loop.resolved.json --render /LOOP.md --session-prompt /RUN_IN_SESSION.md` - Render only the in-session handoff: - `LOOPER_PYTHON="${CLAUDE_SKILL_DIR}/.venv/bin/python"; [ -x "$LOOPER_PYTHON" ] || LOOPER_PYTHON=python3; "$LOOPER_PYTHON" ${CLAUDE_SKILL_DIR}/scripts/looper.py session-prompt /loop.resolved.json --out /RUN_IN_SESSION.md` + `"$LOOPER_PYTHON" ${CLAUDE_SKILL_DIR}/scripts/looper.py session-prompt /loop.resolved.json --out /RUN_IN_SESSION.md` ## Confirmation Flow Preview @@ -134,7 +149,7 @@ Use this shape and customize labels: | all gates clean | +--------------------------------+ -Stops: pass gates | max 12 iterations | no progress x2 | budget 30m, $5.0 +Stops: pass gates | max 12 iterations | no progress x2 | budget 30m, $5.0, 2000000 tokens ``` ## Emit Checklist diff --git a/agents/openai.yaml b/agents/openai.yaml index dbeb861..bc5af7d 100644 --- a/agents/openai.yaml +++ b/agents/openai.yaml @@ -1,3 +1,6 @@ +# OpenAI/Codex agent-interface stub for Looper. The Claude Code install path +# does not read this file; it exists so Codex-style hosts can surface the +# skill with the same invocation policy (explicit invocation only). interface: display_name: "Looper" short_description: "Design and scaffold agent loops" diff --git a/commands/looper.md b/commands/looper.md index e6353b2..bf2dd4e 100644 --- a/commands/looper.md +++ b/commands/looper.md @@ -1,7 +1,7 @@ --- description: Design and scaffold a Looper agent loop. argument-hint: [target-dir] -allowed-tools: Read, Write, Bash, PowerShell +allowed-tools: Read, Write, Bash --- # /looper diff --git a/pyproject.toml b/pyproject.toml index 40e0ddf..186265f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "looper-skill" -version = "0.1.0" +version = "0.2.0" description = "A Claude Code skill that scaffolds well-designed agent loops." readme = "README.md" requires-python = ">=3.9" From eb4a093b261c57f4433497d731ca90c6790681ff Mon Sep 17 00:00:00 2001 From: Kevin Simback <16635828+ksimback@users.noreply.github.com> Date: Sat, 4 Jul 2026 23:09:08 -0700 Subject: [PATCH 2/2] Validate Python by executing it, not locating it Verified on a real Windows install: 'python3' on PATH resolved to the Microsoft Store alias stub, which Get-Command/command -v find but which cannot run scripts (exit 49). The SKILL.md helper resolution and install.ps1 now execute '-c ""' on each candidate before accepting it. Co-Authored-By: Claude Fable 5 --- SKILL.md | 7 ++++++- install.ps1 | 17 +++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/SKILL.md b/SKILL.md index 9fd19e5..a975d07 100644 --- a/SKILL.md +++ b/SKILL.md @@ -90,9 +90,14 @@ invocation below with this resolution (works in POSIX shells and Git Bash on Windows): ```bash -LOOPER_PYTHON="${CLAUDE_SKILL_DIR}/.venv/bin/python"; [ -x "$LOOPER_PYTHON" ] || LOOPER_PYTHON="${CLAUDE_SKILL_DIR}/.venv/Scripts/python.exe"; [ -x "$LOOPER_PYTHON" ] || LOOPER_PYTHON="$(command -v python3 || command -v python)" +LOOPER_PYTHON="${CLAUDE_SKILL_DIR}/.venv/bin/python"; [ -x "$LOOPER_PYTHON" ] || LOOPER_PYTHON="${CLAUDE_SKILL_DIR}/.venv/Scripts/python.exe"; [ -x "$LOOPER_PYTHON" ] || { LOOPER_PYTHON=python3; "$LOOPER_PYTHON" -c "" >/dev/null 2>&1 || LOOPER_PYTHON=python; } ``` +The final fallback executes the candidate rather than just locating it: on +Windows, `python3` on PATH is often the Microsoft Store alias stub, which +exists but cannot run scripts. If no candidate can execute `-c ""`, tell the +user to rerun the Looper installer (it creates the venv). + ## Helper Scripts Each command below assumes the Helper Python resolution is prefixed in the diff --git a/install.ps1 b/install.ps1 index ccdd227..62ec1b8 100644 --- a/install.ps1 +++ b/install.ps1 @@ -14,12 +14,21 @@ if (-not (Get-Command git -ErrorAction SilentlyContinue)) { throw "Git is required to install Looper. Install Git, then rerun this command." } -$Python = Get-Command python -ErrorAction SilentlyContinue -if (-not $Python) { - $Python = Get-Command py -ErrorAction SilentlyContinue +# Validate by executing, not just locating: on Windows, "python" on PATH is +# often the Microsoft Store alias stub, which exists but cannot run scripts. +$Python = $null +foreach ($Candidate in @("python", "py")) { + $Command = Get-Command $Candidate -ErrorAction SilentlyContinue + if ($Command) { + & $Command.Source -c "" 2>$null + if ($LASTEXITCODE -eq 0) { + $Python = $Command + break + } + } } if (-not $Python) { - throw "Python 3 is required to install Looper. Install Python 3, then rerun this command." + throw "A working Python 3 is required to install Looper. Install Python 3 (not the Microsoft Store alias), then rerun this command." } New-Item -ItemType Directory -Force -Path $SkillsDir, $CommandsDir | Out-Null