The turn verdict

Which of these did I just do?

A finding you introduced thirty seconds ago and a finding you inherited from a repository's first year are not the same object. owlwarden turn tells them apart. Carried findings never fail a turn, at any threshold.

Three states, one gate

  1. NEW IntroducedPresent now, absent at the base. Can fail the turn.
  2. OLD CarriedAlready at HEAD. Counted on one line, never fails.
  3. FIX FixedGone since the base. Reported, never fails.
The gate sees the introduced set only. There is no flag that changes it.
npx owlwarden turn
◉ᴥ◉ turn / 7 files / since HEAD a1b2c3d / 0.31s
✔ clean - nothing introduced
  2 fixed / 5 carried (already at a1b2c3d, not this turn's)

Three states. One of them can fail the turn.

StateMeaningCan fail the turn
introducedpresent now, absent at the baseyes
carriedpresent in bothno
fixedpresent at the base, absent nowno

Carried findings never fail a turn, at any threshold. There is no flag that changes it and no code path that could grow one: the gate is applied to the introduced set before it is consulted. A control that blocks on debt the turn did not create is a control that gets removed on the second day, and everything it would have caught goes with it.

When something is introduced

◉ᴥ◉ turn / 1 file / since HEAD a8a6b93 / 0.16s
✘ blocked - 1 introduced at or above high
  1 carried (already at HEAD a8a6b93, not this turn's)

Everything introduced is reported whether or not it blocks. A turn that adds a medium under a high gate reads clean at high - 1 introduced below the bar, shown anyway. The verdict never prints nothing introduced over something the turn introduced.

In an agent's loop

npx owlwarden init --claude-code

That writes a Stop hook running turn --hook claude-code --record. Per-edit and pre-command hooks stay on gate: those must be cheap and have no meaningful base to compare against.

Under --hook, stdout is the host's own JSON. The reason the model receives names what is new and says the rest were already there: an agent told "there are eighteen findings" starts fixing files nobody asked it to touch, in a session the developer is paying for.

Claude Code / Cursor / MCP

In CI, on a pull request

The base is a flag, so the "turn" can be a whole branch:

- run: npx owlwarden turn --base origin/${{ github.base_ref }} --fail-on medium

This fails the job for what the branch introduced and stays silent about what main already carried. Use owlwarden scan in a scheduled job for the whole repository - that is a different question, asked at a different time. Needs the full history: set fetch-depth: 0 on actions/checkout. The rest of the CI contract.

Keeping a record

npx owlwarden turn --record

One JSON line per turn in .owlwarden/turns.jsonl, bounded at the last 200. Every field but the timestamp and the stopwatch is derived from the two reports and the base, so two runs over an unchanged tree produce identical records. Commit the file if you want the audit trail in review; gitignore it if you do not.

What a clean turn does not mean

clean is a claim about a comparison. It means nothing was introduced since that commit, never this repository is clean. The base is printed on every line of the output for exactly that reason, and owlwarden scan is the command that answers the other question.

The base is a commit, so the verdict is as trustworthy as the commit is. An agent that can commit can commit a finding and have the next turn report it as carried. This is a detection and review control, not a containment one - the same limit seal states about itself. If that matters for your threat model, run turn --base origin/main in CI, where the base is one somebody else set.

Over 400 changed files, turn refuses. That is a merge or a reformat, not a turn, and the verdict a developer wanted is not in there. Your index and working tree are never touched: git status is byte-identical across a run, staged changes included.

Keep reading