Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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).
46 changes: 46 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -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).
38 changes: 35 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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\<you>\.claude\skills\looper` on Windows.
- The slash command must exist at `C:\Users\<you>\.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
Expand Down
40 changes: 30 additions & 10 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <target>/loop.yaml --out <target>/loop.resolved.json --render <target>/LOOP.md --session-prompt <target>/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 <target>/loop.yaml --out <target>/loop.resolved.json --render <target>/LOOP.md --session-prompt <target>/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
Expand All @@ -81,16 +81,36 @@ 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=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
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 <id> --invoke <cmd> [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 <id> --invoke "<cmd> [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 <target>/loop.yaml --out <target>/loop.resolved.json --render <target>/LOOP.md --session-prompt <target>/RUN_IN_SESSION.md`
`"$LOOPER_PYTHON" ${CLAUDE_SKILL_DIR}/scripts/looper.py compile <target>/loop.yaml --out <target>/loop.resolved.json --render <target>/LOOP.md --session-prompt <target>/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 <target>/loop.resolved.json --out <target>/RUN_IN_SESSION.md`
`"$LOOPER_PYTHON" ${CLAUDE_SKILL_DIR}/scripts/looper.py session-prompt <target>/loop.resolved.json --out <target>/RUN_IN_SESSION.md`

## Confirmation Flow Preview

Expand Down Expand Up @@ -134,7 +154,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
Expand Down
3 changes: 3 additions & 0 deletions agents/openai.yaml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 1 addition & 1 deletion commands/looper.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
17 changes: 13 additions & 4 deletions install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
Loading