



Real reject-cycle, 3 minutes wall-clock. Goalkeeper caught a MAX_RUNTIME_MS = 9999 // TODO sentinel placeholder in a benchmark test where the validator passed both rounds. Round 1: validator green, judge reject on DoD #3 + #7 with a 3-step fix-list. Round 2: real threshold (500ms = 10x measured baseline) with justification, validator green, judge approve. Full transcript with quoted verdicts: docs/demo.md. Long-form writeup on dev.to.
How it differs
| OpenAI Codex /goal | Ralph loop | goalkeeper |
|---|
| Durable goal across many turns | ✓ | ✓ | ✓ |
| Validator back-pressure | optional | core | core |
| Independent judge gate against an explicit Definition of Done | — | — | core |
Anti-placeholder rule (stubs / .todo / it.only auto-reject) | — | informal | enforced |
| Linear chains of role-specific goals with judge-gated handoff | — | — | /goal-chain |
| Auto-pauses after N consecutive rejections | — | — | 5 (configurable) |
| Append-only checkpoint log per goal | — | informal | enforced |
| Spec lives in | CLI prompt | PROMPT.md | contract.md (validated against JSON Schema) |
| Pre-existing validator failures distinguished from goal-caused | — | — | validator_baseline_* |
Inspired by OpenAI Codex /goal and Geoffrey Huntley's Ralph loop, with one key addition: the judge.
Why
Codex /goal runs autonomously until a stop-condition is met. The Ralph loop runs while :; do cat PROMPT.md | claude-code; done and leans on validators (compile, test, lint) to back-pressure the model. Both work, but both have the same failure mode: a passing validator is not the same as a finished feature. Tests can pass on stubs. Linters can pass on .todos. Codex can declare victory the moment its stop-condition string matches.
goalkeeper adds a second gate. After your validator passes, a fresh subagent — independent context, no rationalizations from the executing agent — reviews the diff and the progress log against your written Definition of Done, and either approves or returns a structured fix-list. After 5 rejections it pauses and asks for human help.
Install
Add the goalkeeper marketplace, then install the plugin:
# inside Claude Code
/plugin marketplace add bonfire-systems/goalkeeper
/plugin install goalkeeper@goalkeeper
Skills become available under the goalkeeper: namespace. Invoke as /goalkeeper:goal "<objective>", /goalkeeper:goal-prep, /goalkeeper:goal-judge, etc. (Most users alias the namespace away — see "Aliasing" below.)
Aliasing the namespace
If you primarily use goalkeeper and want shorter commands, add aliases to ~/.claude/settings.json:
{
"aliases": {
"/goal": "/goalkeeper:goal",
"/goal-prep": "/goalkeeper:goal-prep",
"/goal-pause": "/goalkeeper:goal-pause",
"/goal-resume": "/goalkeeper:goal-resume",
"/goal-clear": "/goalkeeper:goal-clear",
"/goal-judge": "/goalkeeper:goal-judge",
"/goal-chain": "/goalkeeper:goal-chain"
}
}
Quick start
/goal-prep "Migrate the test suite from Jest to Vitest"
goalkeeper reads your repo, asks a few targeted questions (objective, definition-of-done, validator command, non-goals), writes .claude/goals/<slug>/contract.md, and offers to start. Once running, it:
- Works in checkpoints (logged to
log.md).
- Runs your validator after each checkpoint.
- When the validator passes, spawns a subagent judge to check Definition of Done.
- Judge approves → done. Judge rejects → fix-list logged, work continues. After 5 rejections → paused for you.
Check status anytime:
/goal
Commands
| Command | What it does |
|---|
/goal "<objective>" | Start (or resume) a goal. Auto-routes to /goal-prep if no contract exists yet. |
/goal | Show status of the active goal: last checkpoint, validator state, rejection count. |
/goal-prep "<rough idea>" | Interactively draft a contract — the highest-leverage step. Surveys the repo and uses targeted questions to lock in a precise spec. |
/goal-pause | Pause without losing state ("I'll be right back" — keeps the active slot). Pending wakeups become no-ops. |
gk park [slug] --needs "<what>" | Park a goal on a human-gated blocker (credential, consent, decision). Frees the active slot so other goals run; gk status lists the parked queue with each goal's unblock instruction. The goal is parked, never the agent. |
/goal-resume | Resume a paused or parked goal (gk resume [slug]). If resuming with rejections on the clock, asks whether to reset the counter. Re-arms a waiting chain. |
/goal-clear | Stop and archive the goal to .claude/goals/_archive/. Files are moved, never deleted. |
/goal-judge | Run the judge advisorily on the active goal (no state change). Useful as a manual sanity check. |
/goal-chain "<file>" | Run a linear sequence of goals; the judge gates progression between them. |
Contract format
A contract is a markdown file at .claude/goals/<slug>/contract.md with frontmatter:
---
slug: jest-to-vitest-migration
objective: Migrate the test suite from Jest to Vitest with no behavioral regressions and a measurable speed improvement.
non_goals:
- Do not rewrite test logic
- Do not change source code under src/
definition_of_done:
- All test files import from "vitest" instead of "@jest/globals"
- jest.config.* is removed; vitest.config.ts exists with equivalent coverage thresholds
- "pnpm test" runs the full suite under Vitest with 100% of previously-passing tests still passing
- Wall-clock test runtime improves by at least 20% vs the Jest baseline
validator:
command: pnpm test --run && pnpm exec node scripts/check-no-jest-refs.mjs
success: exit_zero
timeout_seconds: 1200
checkpoint_cadence: every 5 file edits OR every 20 minutes
max_rejections: 5
judge_mode: subagent
wakeup_seconds: 270
---
## Context
<freeform body — file pointers, constraints, hints, anti-placeholder reminders>
The body of the contract is your "PROMPT.md" — file pointers, constraints, hints. See examples/ for full contracts.
Schema: schemas/contract.schema.json.
Field reference
- slug — kebab-case directory name. Must be unique within
.claude/goals/.
- objective — one sentence, what success looks like.
- non_goals — explicit out-of-scope items. Judge rejects on violation even if DoD is met.
- definition_of_done — what the judge grades against. Be specific; "works correctly" is not a DoD.
- validator.command — runs after each checkpoint. Necessary but not sufficient.
- validator.success —
exit_zero (default) or regex:<pattern> matched against stdout.
- checkpoint_cadence — agent's guidance for how often to log + validate.
- max_rejections — default 5. After this many judge rejections, pauses for human.
- judge_mode —
subagent (default, gate-quality) or inline (cheap, advisory only).
- wakeup_seconds — between-iteration delay. Tune to validator runtime; if unset, the model picks per the harness's ScheduleWakeup guidance.
- diff_excludes — additional pathspec globs the judge should ignore. Appended to defaults (lockfiles,
dist/, build/, coverage/, IDE files). Add per-repo noise like generated migrations or vendor trees.
Chains
A chain is an ordered list of slugs. The judge gates progression — only after approval does the next goal start.
---
name: bonfire-bass-rust-port
---
1. port-dsp-core-to-rust
2. wire-up-ffi-shim
3. swap-cpp-for-rust-in-host
4. delete-cpp-tree
Run with:
/goal-chain ".claude/goals/chains/bonfire-bass.md"
Each slug must already have a contract at .claude/goals/<slug>/contract.md. Run /goal-prep for each before starting the chain.
Multi-role goals
Most non-trivial goals span multiple roles — backend changes, UI wiring, migrations, tests, docs. Goalkeeper deliberately keeps just two agent slots (executor + judge) and stays framework-agnostic about how you spawn specialists. There are two patterns for getting role-shaped work through goalkeeper, and the right choice depends on how decoupled the roles are.
Pattern A — chain of role-specific contracts (default)
Frame each role as its own contract with its own Definition of Done, validator, and judge. Compose them with /goal-chain. The judge gates progression: backend's judge has to approve before UI starts; UI's judge has to approve before migrations.
---
name: stripe-integration
---
1. stripe-api-routes
2. stripe-db-migration
3. stripe-checkout-ui
4. stripe-e2e-tests
Each slug has its own .claude/goals/<slug>/contract.md with a focused DoD ("Stripe webhook signature verification works", "checkout component handles 3DS challenge", etc.) and a focused validator (pnpm test packages/api, pnpm test packages/web, etc.). See examples/chain.md for a worked example.
Choose Pattern A when: roles are sequenceable. One role's work produces the artifacts the next role consumes. The dependency graph is linear or close to it. You want the judge to gate-keep at each role boundary so a sloppy backend can't silently corrupt the UI step.
Tradeoff: chains force upfront decomposition. You have to know the boundaries before you start. If the boundaries shift mid-flight, you have to clear the chain and re-plan.
Pattern B — in-goal specialists: orchestration (proposed for v0.2)
For tightly-coupled cross-role work — same files, can't be sequenced — Pattern A creates artificial mid-flight pauses. Pattern B is the escape hatch: a single contract with a specialists: field that lists the roles available, plus role-specific system prompts. The executing agent is the orchestrator and dispatches subtasks to specialist subagents (Claude Code general-purpose agents with custom system prompts, your own subagent_type definitions, or MCP-based agents — goalkeeper doesn't ship specialist definitions).
specialists:
- role: backend
system_prompt: "You are a senior Node.js engineer. ..."
- role: ui
system_prompt: "You are a senior React engineer. ..."
The judge still gates final approval against the unified DoD; the executing agent's log records which specialist did what.