Repository navigation
Turn Gate
This page describes the turngate slice of the core plugin claudetto.
It is for a user who wants a turn to end only when the work is done or the reply says that it is not done, and for a contributor who edits the slice.
A turn that ends with text only is a report, and not proof that the work is done. A reply can say "the tests pass" when no test ran after the last edit. A reply can say "I will run the tests now" and then stop. A reply can stop while a background command still runs, so its output never comes back to the work. A reply can give the user a step that the agent can do itself, or give up a correct position when the user disagrees. A prompt is the weakest control, so the slice adds a gate at the end of each turn.
- The
prompt.submithook keeps the last prompt that the user wrote, from the composer, a bridge, or the SDK. A prompt that a hook or a command adds is not the request. - The
tool.checkhook denies aBashcommand that starts a process that Claude Code does not track (domain/jobs.ts): a lone&with nowait, ornohup,setsid, ordisown. It also denies a stop of processes by a pattern:pkill,killall, orkillwithpgrep. The reason tells Claude to userun_in_background: trueandTaskStop, or a PID. The check reads the parse of the shell reader, and makes no model call. - The
tool.callhook keeps one summary line for each tool call of the turn: the tool, the target, and the outcome. The outcome isok,ok, read-only, or the first line of the error. A call that a permission check or the user denies is an error. The main loop and each subagent have their own record. - The
classic.Stopandclassic.SubagentStophooks run the gate.- When a background task still runs (
background_taskswith the statusrunning), the gate blocks the stop. The reason names the id of each task, and tells Claude to wait for the output or to stop the task withTaskStop. This part makes no model call. - Else, when the
judgeoption is on and the reply is not empty, one judge call reads the request, the earlier reply, the reply, and the summary of the calls. A turn with no tool calls is judged too. When the judge finds a fault, the gate blocks the stop.
- When a background task still runs (
- The reason of a block holds the quote of the reply, the rule, and the fix, and no line of the rubric.
- The gate keeps its own count of blocks for each turn, and gives no pass for
stop_hook_active(D58). AfterMAX_BLOCKSblocks, the next stop passes, and a notice tells the user what the gate found. - A stop that the gate allows ends the turn, so it clears the record.
An interrupted turn (
turn.completewithisAborted) clears it too. - A block of a hook beneath the gate stands, and the gate does not judge it or count it.
The judge is one $.model.complete call in the hooks module, with no tools.
It reads only the request, the end of the earlier reply, the reply, and the summary of the calls, and never the transcript.
The request and the earlier reply go in with their last 2,000 characters.
A subagent has no request and no earlier reply, so the judge gets (none) for both.
The reply goes in with its start and its end, and a mark of the cut when it is long.
The rubric has five faults:
-
claim: the reply says that work is done or that a check passes, and no call after the last edit of that work shows it. -
promise: the last paragraph says that the agent does more work now or next, and the turn ends before that work. -
opener: the first sentence is an apology, or an agreement with the user that gives no fact. -
defer: the reply gives the user a step or a decision that the agent can do itself, and names no block that stops the agent. A block is a denied call, a login or a secret that only the user has, a cost that needs the approval of the user, or a choice that only the user can make. -
cave: the request disagrees with the earlier reply, and the reply changes a position of the earlier reply with no new evidence. New evidence is a call of the turn, or a fact that the request gives.
These replies have no fault: a reply that only asks a question that the request does not settle, or waits on a proposal, an honest report of a failure, and an answer that claims no work.
The judge reads the reply in its own language and judges its meaning, so the gate works in each language of the reply.
The judge gives JSON: {"fault":"none"}, or a fault with quote and fix.
A judge call that fails, that times out, or that gives no valid verdict allows the stop, and a notice names the cause.
The judge is on by default.
To turn it off, set the plugin option judge (title "Turn Judge") to false.
With the judge off, the background-task part and the job check still run.
The background-task part and the job check make no model call.
The judge runs once for each turn with a reply that is not empty, also in a turn of chat only.
Each judge call writes its usage to the debug log (--debug-file).
The cost figure comes from that usage, because the total_cost_usd of the session leaves out a model call of a hook (D63).
A block costs one more model response of the main model.
One measured session on 2026-10-16 gave these figures for the judge on Sonnet 5.5:
| Call | Input tokens | Output tokens | Time | Verdict | Cost at list price |
|---|---|---|---|---|---|
| 1 | 798 | 68 | 1,571 ms | claim |
$0.0023 |
| 2 | 826 | 11 | 1,152 ms | none |
$0.0018 |
The method and the source are on Failure Gaps 0.1.0.
- The job check sees only the text of the
Bashcommand. A script that starts a detached process inside it passes the check. - A
Stophook runs after the user sees the reply (D58), so the user can see a reply that the gate then blocks.
| Bound | Value | File | Use |
|---|---|---|---|
MAX_BLOCKS |
2 | slices/turngate/limits.ts |
Most blocks of the gate in one turn |
JUDGE_MODEL |
sonnet |
slices/turngate/limits.ts |
The model of the judge |
JUDGE_EFFORT |
medium |
slices/turngate/limits.ts |
The effort of the judge |
JUDGE_TIMEOUT_MS |
30,000 | slices/turngate/limits.ts |
Most time of one judge call |
JUDGE_MAX_TOKENS |
1,024 | slices/turngate/limits.ts |
Most output tokens of one judge call |
MAX_CALLS |
60 | slices/turngate/limits.ts |
Most calls in the summary, from the end of the turn |
The other bounds of the slice are on Bounds.
-
User Checks: the other
Stophook of the core plugin. - Read Gates: the gates on a read and on a rejected call.
- Architecture: the slices of the core plugin.
- Start
- How claudetto Works
- Add-On Plugins
- How Claude Code Works
- Reference
- Rationale
- Evidence
- Development
- Archive