detent

Let nothing finish until your own checks pass.

detent dispatches Claude Code at a queue of tasks, one at a time. It holds each one behind the repository's own gauge on the way out, has a second model read every green diff, merges what survives into a branch of its own, and writes down what each run cost in attempts, time and money.

npm i -g @kazimvakhobov/detent
Version 0.9.0 · Node 22 · macOS or Linux · Claude Code installed and signed in · zero dependencies
Two repositories at once. Red costs a round.
A detent is the catch that holds a mechanism in a defined position until it is deliberately released. Here it is released by a green gauge, and by nothing else.

The three claims

The constraints are in the code, not in the prompt.

When is the agent allowed to stop?

When the project's checks pass, not when it says it is done. Red means it does not get to leave, and it is handed the failure text to work from.

Was it worth it?

Every run is a line in a journal: which model, how many rounds, which stage it died on, how long, how much. "Which model should run this repository" becomes a number.

Who says it is done?

A second model reads the diff against the acceptance criteria. It may demote, never approve. Green is granted by machine checks only.

What a run says when it is over

Two tasks off the queue. The lines are the ones detent run actually prints — one as a task starts, one when it has stopped being detent's problem.

acme — detent run
$ detent run
0042  acme-api  wallet-endpoint  …
      tail -f runs/logs/0042-attempt-1.jsonl  (one per attempt, up to -attempt-3)
0042  passed — merged into agent/dev (9f2c1ab), 2 attempts, $0.63
0043  acme-web  tracking-map  …
      tail -f runs/logs/0043-attempt-1.jsonl  (one per attempt, up to -attempt-3)
0043  paused (escalated): three identical failures on test — 3 attempts, $1.21

passed is the gauge's word and not the agent's: the commit named is a merge that has already happened, and 2 attempts is the round a red gauge cost. 0043 spent its three and is now in tasks/hold/ with its branch kept and a person told — the run did not quietly settle for it.

How a run goes

A task is a markdown file, and only its body reaches the agent. Up to attempts rounds (three by default), each one spent the same way whether the gauge was red or the reviewer had a doubt.

lets it throughcosts a roundstops and asks a person

Every run leaves one line in runs/journal.jsonl: outcome, rounds, the stage it died on, the model that actually ran, and what it cost. The dashboard and detent summary are computed from that file and nothing else.

The line agents do not cross

detent reads and writes refs under refs/heads/agent/ and nothing else. Both gates between the two territories are human operations, in both directions.

It refuses to start on a dirty tree, and never moves a ref outside its own namespace. Nothing in a repository of your product is ever pushed by it, not agent/dev and not a tag.

Watch a run in the dashboard

detent dashboard serves one page on 127.0.0.1 that loads nothing from anywhere else. The demo is that same page, built from this version's code against a made-up product called acme: two repositories, a queue in all four folders, a journal, and a task that is running right now.

A task is the prompt

The frontmatter is bookkeeping. The spec written before the work is what the agent gets.

---
id: 0001
repo: acme-api
touches_contract: false
---

# Goal

One paragraph: what must work when this is done.

# Acceptance criteria

- [ ] a checkable statement

# Out of scope

What not to do while you are in there.

The commands

detent doctorwhether this product is in the shape detent expects
detent initscan the workspace and write product.json
detent plandiscuss a task with an agent; it writes the task files
detent runwork the queue
detent promotemerge agent/dev into agent/staging behind the batch gauge
detent summarypass rate, rounds, cost per pass
detent dashboarda page that watches this product
detent gaugerun a repository's gauge by hand
detent recoverreturn repositories an interrupted run left behind
detent prunedelete agent/task-* branches already merged