@michaelrowejones/temper
v0.0.1
Published
Thin, engine-agnostic, entropy-gated loop runner for AI coding. Drives Claude Code or Codex in a loop on one approved Plan and commits only work that passed a deterministic gate.
Maintainers
Readme
Temper
Let your coding agent loop, and commit only what survives the gate. The work that reaches your git history passed a deterministic check: in scope, no dead code, no duplication, your tests green, and a hidden check the agent never sees so it cannot quietly game the gate. Queue a night of it and you wake up to review-ready commits, not a tree you have to unpick.
Temper is a thin, engine-agnostic, zero-dependency loop runner for AI coding. You approve one Plan; it drives a coding agent (Claude Code or Codex, on your own subscription) in a loop and commits only work that introduced no new entropy. It is not a coding agent, a node-graph, or an API client; it buys the engine and the gate, and adds the loop.
The loop
Plan (you approve)
└─ repeat up to maxIterations:
engine implements (claude -p / codex exec) ← your subscription
scope check (git diff vs the Plan allowlist) ← deterministic
protect check (no edits inside temper:protect) ← deterministic
gate (fallow audit --gate new-only) ← deterministic, regression-scoped
suppression (no new fallow-ignore / @ts-ignore…) ← deterministic, anti-gaming
acceptance (your test/command, optional) ← deterministic
├─ any violation → re-prompt (fix the root cause; show evidence)
│ └─ same domain stuck N× → escalate to you (don't burn iterations)
└─ all green → reuse-critic (semantic, repo-searching) ← LLM judgment
→ held-out check (hidden command, if any) ← deterministic, anti-gaming
→ commitEverything deterministic is code. The engine and the reuse-critic are
the only LLM steps. The critic's reliability is measured, not assumed (npm run critic-check).
The optional held-out check is hidden from the engine's prompt, but it lives in the plan file, so a
repo-exploring engine can read it. It catches an honest-but-insufficient gate, not a determined cheater.
How it's different
Most runners gate on a passing test suite, and they let the agent run the tests. But agents game gates: across published evals (METR, ImpossibleBench, Patronus TRACE), the same CLIs Temper drives edit tests, weaken assertions, hardcode outputs, and special-case inputs to turn a gate green, and they do it more as the models get more capable. A green suite the agent controlled is not evidence.
So Temper gates differently:
- A deterministic cascade, not just tests. Scope-lock, no new dead code, no duplication, completeness vs the Plan: rules Temper runs, not judgment the agent can argue past. Others stop at tests and lint (aider, Ralph), hand you primitives to script yourself (Claude Code hooks), or ask another model to eyeball the diff (Devin, Cursor).
- A held-out check the agent never sees. Temper runs one command that never enters the engine's prompt. If the work passes every visible gate but fails that one, Temper rejects it as gamed and commits nothing. No other shipping AI-coding tool has this. It is one layer, not a cure-all: it catches an insufficient gate, not every determined cheat, but it is the layer everyone else is missing.
- Commit discipline. Only gated work commits, each run on its own branch, and Temper never merges; you do. (aider, by contrast, auto-commits every edit, ungated, to your working branch.)
Temper shares the shape of Geoffrey Huntley's Ralph loop: engine-agnostic, branch-isolated, never-merge, unattended. The difference is the gates. Where Ralph runs tests the agent can edit, Temper adds a deterministic cascade and a check the agent cannot see.
Run it
⚠️ Temper runs in your own terminal, where
claude/codexhold real subscription credentials. It cannot run inside Claude Code's web, desktop "Remote", or Cowork sandboxes (they withhold your subscription auth, so the nestedclaude -p401s). Run it from a plain terminal. The deterministic core is covered bytemper eval; the engine integration is verified end-to-end with both Claude and Codex.
Prerequisites: Node 18+, git, and an engine (claude or codex) installed and logged in.
fallow is optional (npm i -g fallow). It adds the deterministic dead-code/duplication/complexity
gate; without it, Temper runs the loop on the other gates and skips that one. temper doctor checks
everything.
# one-time: clone, then put `temper` on your PATH
git clone https://github.com/michaelrowejones/Temper && cd Temper && npm link
# then, from inside the repo you want to work on:
temper doctor # check prerequisites
temper init # scaffold config, do this FIRST (entry-point-aware fallow config)
temper plan "add a foo widget" # draft a Plan from the codebase
$EDITOR ./PLAN.md # review + approve it (scope allowlist + spec + acceptance)
temper run ./PLAN.md # add --engine codex to switch enginesThe working tree must be clean before a run (Temper needs a clean base to gate against). On success it makes one commit; on failure it leaves the tree for you to inspect and commits nothing.
Commands
| command | what it does |
|---|---|
| temper init | scaffold temper.config.json + an entry-point-aware .fallowrc.json so the dead-code gate doesn't false-positive on new exports/tests |
| temper plan "<task>" [--out <path>] | the engine explores the codebase and drafts a Plan for you to approve (Research → Plan) |
| temper run <plan.md> | run one approved Plan to a green gate (Mode A) |
| temper overnight <dir> | run an ordered queue of Plans (01-*.md, 02-*.md) unattended on its own branch (never main, never merged) + a morning report; resumable via .temper/progress.json, stops on a failing phase (Mode B). temper run-phases <dir> [--overnight] is the older alias |
| temper tasks <file> | draft a scoped Plan per task line into the queue (temper tasks add "<task>" appends one) |
| temper plan-check <dir> | flag plans whose scopes claim the same file (--reconcile adds an LLM suggestion) |
| temper audit [dir] | scan the codebase with fallow and draft scoped cleanup Plans into .temper/audit/ (dead code today: unused exports + files). Review and prune, then temper overnight .temper/audit removes them, gated. It proposes; it never deletes |
| temper status | summarize the current/last queue from the ledger (works mid-run, including after a detached overnight run) |
| temper explain <gate> | what a gate or verdict means and how to clear it (e.g. temper explain fallow-audit) |
| temper eval [--filter <id>] [--update-baseline] | run the deterministic golden-task regression suite (exit 1 on any regression) |
| temper doctor | check prerequisites |
All accept --engine <name>. Exit codes: 0 committed · 1 error · 2 critic-halt · 3 max-iterations · 4 stuck-domain escalation · 5 held-out check failed (gaming) · 6 over global budget.
Protected regions
Lock code an agent must not touch by wrapping it in sentinel comments. Any change whose diff overlaps the region is rejected and re-prompted. The guard is deterministic, language-agnostic, and zero-config: it lives in the source and travels with it.
// temper:protect-start auth
export function verifyToken(t) { /* … */ }
// temper:protect-endTwo engines: switch, or use both
Temper is engine-agnostic. Engines are named presets in config (claude and
codex ship by default):
{
"engine": "claude", // which preset implements
"criticEngine": "codex", // which preset reviews, set to the OTHER engine
// for cross-model review (stronger than self-review)
"fallowCommand": "npx fallow",
"entropyGate": null, // null = `<fallowCommand> audit --gate new-only` (JS/TS). Override with ANY
// command for another language (see "Languages"); `{base}` = the base SHA
"maxIterations": 5,
"maxDomainRetries": 3, // escalate after N consecutive same-domain failures
"maxUnchangedRetries": 2, // escalate sooner when the SAME finding recurs unchanged (~1 retry)
"criticMode": "warn", // warn | halt | off
"checkCompleteness": false,// opt-in: an LLM check that the diff implements the whole Plan
"commitPrefix": "temper:",
"maxQueueSeconds": null, // Mode B: hard wall-clock budget for the whole queue (excl. cap-waits)
"maxQueueIterations": null,// Mode B: hard cap on total engine iterations across all phases
"rateLimit": { // Mode B: survive the subscription cap (deep-merges with defaults)
"fallbackSeconds": 1800, // wait this long if no reset time is parseable, then retry
"marginSeconds": 60 // wait this much past the parsed reset, for clock skew
},
"notifyCommand": null, // Mode B: shell hook on a terminal outcome (done / needs you).
// gets $TEMPER_EVENT/$TEMPER_SUMMARY/$TEMPER_BRANCH/$TEMPER_REPORT.
// e.g. "curl -d \"$TEMPER_SUMMARY\" ntfy.sh/my-topic"
"engines": {
"claude": {
"engine": "cat {promptFile} | claude -p --permission-mode acceptEdits",
"critic": "cat {promptFile} | claude -p"
},
"codex": {
"engine": "cat {promptFile} | codex exec --sandbox workspace-write",
"critic": "cat {promptFile} | codex exec --sandbox read-only"
}
}
}- Switch engines:
temper run plan.md --engine codex, or set"engine". - Use both (recommended): set
criticEngineto the other engine. One model implements, the other reviews the diff, so the critic doesn't share the implementer's blind spots. Cost: it uses both subscriptions, so both rate-limit ceilings apply. - Other CLIs (amp, opencode, …): add a preset under
engines.
The default flags are best-effort. Verify your CLI's exact headless-edit flags.
Languages
Temper is mostly language-agnostic. Scope-lock, protected regions, the suppression guard, your
acceptance tests, the reuse-critic, and held-out checks all work on any language. Only the entropy
gate (dead code / duplication / complexity) is JS/TS-specific, because it's fallow.
- JS/TS: the default.
fallowgives the entropy gate, scoped to what the change introduced. - Any other language: Temper runs today on the gates above (fallow is optional). To add a
deterministic entropy gate too, set
entropyGateto a tool of your choice: any command where a non-zero exit means "new entropy."{base}is substituted with the base commit SHA. Caveat: fallow's--gate new-onlyfails only on what the change introduced; a tool without that scoping will also flag pre-existing issues, so scope it to the diff (e.g. against{base}). - The suppression guard already covers JS/TS, Python, Rust, Go, and Ruby silencing directives;
add a pattern in
src/gates.mjsto cover another.
Prose counts too. The same anti-entropy discipline applies to docs: the engine prompt tells the
agent to update an existing doc rather than spawn a new one and to keep writing terse, and the
reuse-critic now also flags a new doc that restates one that already exists. For a deterministic
guard, point entropyGate at the bundled recipe "entropyGate": "node examples/doc-gate.mjs {base}",
which fails when a change adds a new markdown file, nudging "update, don't create."
Overnight mode (the unattended Plan-queue)
temper overnight <dir> runs an ordered queue of Plans unattended and is built to survive a night.
The design follows the people who've actually shipped with overnight agents (Huntley's Ralph,
HumanLayer): sequential, one task at a time, never parallel fan-out, and never auto-merge. It
layers these on the resumable phase sequencer:
- Rate-limit survival. The subscription cap, not the clock, is the ceiling. When the engine CLI
reports the cap, Temper parses the reset time, sleeps, and resumes. Tune via
rateLimitin config (it deep-merges, so override one field freely). - Branch isolation, no auto-merge. The whole queue runs on a stable
temper/<dir>branch. The base branch is never touched and nothing is merged, and you're restored to your base branch when the run ends (the work stays ontemper/<dir>for you to review and merge). Re-running resumes on the same branch, skipping what already landed. - A run budget.
maxQueueSeconds/maxQueueIterations(in config, or per run via--max-queue-seconds/--max-queue-iterations) cap a single overnight invocation (above the per-phasemaxIterations+ stuck-domain escalation). Over budget ⇒ stop, exit 6. The budget is per invocation: a resume starts a fresh budget, so give any auto-retry loop its own ceiling. - A morning report.
.temper/report.md(what committed, what stopped it and why, what's left) plustemper statusto check progress at any time. - A notify hook. Set
notifyCommandto be pinged on the terminal outcome (done, or escalated/gamed/over-budget and needs you), via$TEMPER_EVENT/$TEMPER_SUMMARY/$TEMPER_BRANCH/$TEMPER_BASE/$TEMPER_REPORT. Wire it to ntfy, Slack, orosascript. - A direction check (opt-in). The per-iteration gates check did we do it right; this checks are we doing the right thing. Before a phase, Temper can ground its APPROACH against a trust-list you supply and flag work built on a deprecated/superseded/contradicted premise before it compounds across the night. Off by default; see below.
Setting up the queue. Phase files are ordered Plans (01-*.md, 02-*.md, …), each the same
format as a temper run Plan. Draft them with temper plan and --out:
mkdir -p .temper/phases
temper plan "phase 1: extract the slug helper" --out .temper/phases/01-slug.md
temper plan "phase 2: use it in the router" --out .temper/phases/02-router.md
# review each, then run the queue:
temper overnight .temper/phasesRun it detached in your own terminal (it needs your real subscription auth, so no cloud/host session):
# tmux survives SSH drops / closing the laptop lid (on a remote host); or use nohup
tmux new -s temper 'temper overnight .temper/phases > temper.log 2>&1'
# …in the morning:
temper status
git log temper/<branch> # review, then merge yourselfThe failure policy is stop the queue: a failing phase halts the run with earlier phases committed; fix it and re-run (the ledger skips what already landed). Decomposition (the ordered phase files) and the final merge stay human jobs, the two places judgment matters most.
Direction check (opt-in). For a long queue against a fast-moving framework, point it at your trusted sources so it flags work built on a superseded approach before it compounds. It stays off until you give it sources:
"directionCheck": {
"enabled": true,
"sources": ["docs/architecture.md", "https://your-framework.dev/docs/migration"], // local paths + URLs
"every": 1, // check every Nth phase (1 = every phase)
"onMiss": "warn" // "warn": flag it in the report · "pause": stop the queue before the phase (exit 7)
}The check is one bounded question to the critic engine, grounded only in your sources (local files are read directly; URLs are fetched only if the engine has web tools), never the open web, so an untrusted page can't steer it. It's fail-open (an unparseable verdict never blocks) and overnight-only.
First-run checklist (the things most likely to bite)
- Run
temper initfirst. Without an entry-point-aware fallow config, the dead-code gate flags new library exports and test files as "unused, not reachable from an entry point," so adding a new exported function escalates instead of committing.temper initscaffolds a.fallowrc.jsonthat treats tests as entry points (and fallow already treatspackage.jsonexports/main/binas the library API). If you skip it,temper runnow catches the gap first: on a project that has tests but no fallow config it scaffolds the config and stops with the one-command fix, rather than letting the gate false-positive mid-run.temper doctorwarns too. This was the #1 dogfood footgun. - Does your
engineCommandactually edit files headlessly? Run it by hand on a throwaway task first. This is the #1 engine failure mode. - Is
fallowresolvable?temper doctorwill tell you; setfallowCommandtonpx fallowornode_modules/.bin/fallowif not. - Subscription rate limits are overnight mode's ceiling: a long run can stall until your cap resets. That's the cost of avoiding the metered API.
- Install your project's deps first (
npm install). fallow warns on a missingnode_modules, and an over-eager agent may chase that warning out of scope. The scope gate will (correctly) reject it, but the loop won't converge.
Use it from inside Claude Code / Codex
Temper is exposed as a tool by wrapping its CLI, not via an MCP server (MCP's always-on context cost isn't worth it for a CLI-shaped tool).
- Claude Code: install the Skill at
skills/temper/SKILL.md(copy or symlink it into~/.claude/skills/temper/or a project.claude/skills/). It tells the agent when and how to invoketempervia Bash, costing ~100 tokens until it's actually used. - Codex: see
integrations/codex.md, and point itsAGENTS.md/ custom prompt at the sametempercommand.
The CLI is the portable, cross-client surface; both integrations just drive it.
Status
Mode A (one Plan → green gate) is done and temper eval-covered, hardened with
stuck-domain + unchanged-finding escalation, within-file protected regions, an optional
diff-vs-Plan completeness check, held-out checks, and a repo-searching reuse-critic whose
reliability is measured (npm run critic-check: catches real + semantic duplication, 0%
flip on no-op refactors). Mode B (the overnight Plan-queue) is built: run-phases --overnight
with rate-limit survival, branch isolation, a global budget, a morning report, a notify hook, and
temper status, covered by test/modeb.test.mjs (npm test runs temper eval + the integration
suites). temper plan drafts Plans from the codebase, including an explicit Context/assumptions
section for you to review.
The tool is feature-complete: the highest-leverage work now is dogfooding and polish, not new capability.
