keel

Plan-driven operating layer for coding agents

The part of the boat you never see.

A keel keeps the hull on course when the wind pushes sideways. keel does that for Codex, Claude Code, Gemini CLI, Cursor, Copilot, OpenCode, Aider and Pi.

$ git clone https://github.com/matrixfede/keel.git /tmp/keel $ cp -r /tmp/keel/{AGENTS.md,prompts,adapters,scripts,PLAN.template.md,PROGRESS.template.md} . $ mkdir -p docs && cp /tmp/keel/docs/AGENT_SOP.md docs/ $ bash scripts/install_git_hook.sh # 4 deterministic checks, any agent $ bash adapters/install_adapter.sh claude # only if the agent doesn't read AGENTS.md
Ballast
bash 3.2 · python3 · git
Crew
8 agents, one rules file
Hull checks
4, deterministic, at every commit
Sea trials
61 end-to-end · Ubuntu + macOS

How a session goes

Plan first. Get the plan approved. Then, and only then, code.

You describe a problem, not a command. The agent classifies it; if it's complex it writes PLAN.md — objective, dated requirements, a task table with a verification per row — and stops. Nothing is written until you reply go. From there every task closes only on a verification that actually ran.

HUMAN AGENT describe a problem "go" or corrections → back to DRAFT classify: complex? PLAN.mdDRAFT · graph OK · pre-mortem APPROVEDPROGRESS.md created task 🔄 verify FAIL → fix · 3 fails → ⛔ stop, diagnose + PROGRESS.md entry
The gate is the pause between the plan and the work. Thirty seconds of human reading cost less than a wrong refactor discovered at the fourth task. Verification is a command whose last line says PASS or FAIL — "it should work" is not a result.

What keel puts under each gear

Agent, loop, graph are the engine. keel is what sits underneath.

Every framework ships the three gears — an agent that gets the end state, a loop that iterates against a check, a graph that runs work in parallel. keel does not compete on them. It adds the parts none of them ship: the gate, the append-only log, the arguer, the fake edge test, output contracts, the model-free reduction, the hash-sealed verdict. Each one is a brake, not a throttle.

Under the agent · the append-only log

PLAN.md is the state. PROGRESS.md is the history.

The plan stays short and says where we are. The log is append-only, one entry per checkpoint — DID / DECIDED / BLOCKED / NEXT, under 100 words — and says how we got here. On resume the agent reads both and restarts from the last NEXT. To hand the work to a person, point them at the log. The hook refuses commits that delete a line from it.

Under the agent · the arguer

Nobody attacked the plan. Now something does.

Ask an agent "is this plan good?" and it says yes. premortem.sh opens a fresh session that sees only PLAN.md and has it narrate how the plan failed twelve months out — aiming at the ASSUMED lines, the edges, the verifications that would pass even with a wrong result. The top three risks enter the plan as PRE-MORTEM lines with a countermeasure, and you approve them with the table.

Under the loop · the rubric that shows its work for documents, reports, analyses — outputs a test cannot judge DRAFT SCORE 1–10 GAPS LOG row CALL any criterion < 8 → next pass, weakest criterion first · max 5 passes all ≥ 8 → ✅scores + keep rate in Notes ceiling → ⛔last scores + why it won't converge logs/agent/rubric_task4.md | pass | weakest | score | what changed | new score | | 2 | c3 | 5 | added dates to 7 sources | c1=6 c2=7 c3=8 | score goes down → stop, revert · same criterion weakest 3× → the criterion is vague: reword it through the gate
Two stop conditions, always: success or ceiling — never one. And four conditions before declaring a rubric at all: it will recur, it can grade itself, it closes without human input, the finish line is a fact. Miss one and the right verification is "user review".
Under the graph · the fake edge test, output contracts, the model-free reduction "Depends on" is a graph, not an order. plan_graph.py prints the execution waves. WAVE 1 · [P1] parallel task 1 → out/a.md task 2 → out/b.tsv task 3 → out/c.json one session each · sees only its task + contract REDUCE · no model, zero tokens fanout.sh --reduce OUT exists & non-empty check: `head -1 out/b.tsv | grep -q ^id` worker ended with TASK n: DONE PLAN.md & PROGRESS.md untouched FANOUT: PASS | FAIL WAVE 2 · convergence task 4: synthesizedepends on 1, 2, 3 · critical only on PASS
Every task in a parallel group declares a contract in PLAN.md — OUT, shape, a runnable check. A node with a declared output shape is readable by the next node with nobody in between; an output outside its shape is rejected, not adapted to. Sequential remains the default: parallelism is chosen, not suffered.
Under the graph · the hash-sealed verdict for tasks marked critical WORKING SESSION · full context conversation · repo · PLAN.md artifact "is it good?" → yes, always NO SHARED CONTEXT cp artifact → empty tmp dir FRESH SESSION · read-only artifact only prompts/checker.md: find why it fails accurate? current? references hold? logs/agent/checker_task3.txt VERDICT: KEEP ← script decides: model KEEP AND ≥ 2 of 3 PASS SHA256: 72d8264b…d4af artifact edited later → KEEP expires pre-commit hook recomputes the hash
The same reasoning that produced a mistake is the reasoning that would look for it. An earlier design let the worker write "KEEP" in the plan's Notes — a deterministic check on a datum forgeable by the party being checked. Now the verdict is a file the hook verifies, hash included.

Positioning

keel is not an orchestrator.

Orchestrators make agents go: they execute the graph, retry, fan out, scale. keel makes them accountable: the graph lives in a plan a person approves, and the rules are enforced by git, not by the model's good will. Use both — keel sits above whatever runs the work.

Where the graph livesWho approves before code is writtenWhat is enforced — and by whatVendor
Orchestration frameworks
LangGraph · CrewAI · AutoGen
Python code, written by a developerOptional human-in-the-loop node, if you build oneNothing beyond your own codeAny model, your stack
Agent-native subagents
Claude Code · Codex · Copilot
Implicit in the conversationPer-tool permission prompts; PR review afterwardsVendor hooks or sandbox, one agent onlyOne vendor
A bare AGENTS.mdNowhereWhatever the text saysNothing — instructions the model may skipAny
keelPLAN.md, a table a person reads, validated by plan_graph.pyExplicit DRAFT → APPROVED state before any writeGit pre-commit hook, 4 checks · hash-sealed checker verdict · write-time hook where the agent allows itAny agent, any model

What keel does not do: run your graph at scale, manage distributed state, retries or queues, replace CI, or remove the human. It is the keel, not the engine.

Compatibility

Any agent. Any model. One rules file.

Everything binding is markdown, standard python3 and git. What varies per agent is isolated in two places: how it reads AGENTS.md (adapters/) and how a fresh session is opened (scripts/lib_agent.sh, one backend per CLI plus custom). What an agent can't provide is documented, not faked.

CodexCursorGitHub CopilotOpenCodePi Claude CodeGemini CLIAider

reads AGENTS.md natively · the others get a one-line bridge file from adapters/install_adapter.sh

Desktop apps too, not only the terminal

Files in a folder work wherever an agent works in that folder.

The Codex app, Cursor, Windsurf, VS Code with Claude Code or Copilot, Claude Desktop (Cowork) with the project folder connected, a chat with the folder attached. Where the app reads AGENTS.md or CLAUDE.md the rules load on their own; where it doesn't, one first message does it: "Read AGENTS.md and docs/AGENT_SOP.md, then: <your problem>". The plan, the log and the approval are the same files; the pre-commit hook fires whenever anyone — app or human — commits.

Schematic of the flow in a desktop agent app: problem, PLAN.md as DRAFT, go, APPROVED, PROGRESS.md, task verified and closed
Schematic, not a screenshot of any one product: the plan is a file in the folder, the approval is a word in the chat, the verification is a command whose last line says PASS.

Safety net

Rules the model can skip. A hook it can't.

Everything in AGENTS.md is an instruction, and an instruction can be ignored on a bad day. The pre-commit hook is git: identical for every agent, and it fires exactly where unapproved work would become permanent.

CHECK 1

No code while the plan is DRAFT

Any staged file other than PLAN.md / PROGRESS.md with Plan status: DRAFT → commit refused.

CHECK 2

No critical ✅ without a valid verdict

Needs checker_task<n>.txt with VERDICT: KEEP and a SHA-256 that still matches the artifact.

CHECK 3

No broken graph

plan_graph.py must say GRAPH: OK: no cycles, no fake independence, no task without verification or contract.

CHECK 4

No line removed from PROGRESS.md

The log is append-only. A diff that deletes an entry is refused; you add an entry saying what changed and why.

Claude Code additionally gets the gate at write time through a PreToolUse hook. Deliberate bypass everywhere: git commit --no-verify.