Failure recovery
Nudge the agent toward a debug tool when the recent transcript looks like a retry loop or the turn has stacked up failures.
After two or three back-to-back failures the agent often spirals. It retries the same approach, restates the same error, and never reaches for a debug tool. You want a Stop-event nudge that fires only when the recent narration reads like a retry loop, plus a separate per-failure nudge that points at the project’s debugging playbook. Both silence themselves once the agent has already done the thing you’d suggest.
"""Break the agent out of a retry loop after repeated failures."""
from __future__ import annotations
import re
from captain_hook import Allow, Event, Input, ReadFile, Signal, Signals, T, UsedSkill, Warn, nudge
RETRY_SIGNALS = Signals(
patterns=[
Signal(pattern=r"let me try again", weight=2, flags=re.IGNORECASE),
Signal(pattern=r"one more attempt", weight=2, flags=re.IGNORECASE),
Signal(pattern=r"same (error|failure|issue)", weight=2, flags=re.IGNORECASE),
Signal(pattern=r"\bretrying\b", weight=1, flags=re.IGNORECASE),
],
threshold=4,
window=10,
scope="window",
)
nudge(
"Repeated failures detected. Stop retrying and pick a debug tool:\n"
" - run `/codex` for a second opinion on the failing approach\n"
" - open the trace in your observability tool to inspect the failing span\n"
" - isolate the minimum failing case before changing more code",
signals=RETRY_SIGNALS,
events=Event.Stop,
skip_if=[UsedSkill("codex", scope="session"), ReadFile("DEBUGGING.md")],
max_fires=1,
tests={
Input(transcript=[T.assistant("Same error again. Let me try again.")]): Warn(pattern="debug tool"),
Input(transcript=[T.assistant("All checks pass; wrapping up.")]): Allow(),
},
)
nudge(
"Three tool failures this turn without a debug skill. Read DEBUGGING.md before the next attempt.",
events=Event.PostToolUseFailure,
when=lambda evt: evt.ctx.turn.count_failures() >= 3,
skip_if=[ReadFile("DEBUGGING.md")],
max_fires=2,
tests={
Input(
transcript=[
line
for _ in range(3)
for line in T.tool_turn("Bash", result="ModuleNotFoundError", is_error=True, command="uv run pytest")
]
): Warn(pattern="DEBUGGING.md"),
Input(
transcript=T.tool_turn("Bash", result="ModuleNotFoundError", is_error=True, command="uv run pytest")
): Allow(),
},
)This page composes two nudge primitives: a signals-driven Stop nudge that scores the agent’s narration, and a PostToolUseFailure nudge gated on the turn’s failure count. The retry bundle sets scope="window" because its tells span attempts — one retry phrase per failed try pools toward the threshold, where the default per-text scope needs a single message to reach it alone.
What it catches
Same error again. Let me try again. # narration hits two weight-2 retry phrases -> reaches threshold=4
3 failed Bash tool calls in one turn # count_failures() >= 3 with no debug skill read -> points at DEBUGGING.md
What it allows
All checks pass; wrapping up. # no retry-loop narration -> Stop nudge stays quiet
2 failed tool calls in one turn # below the 3-failure gate -> no DEBUGGING.md nudge
The block / allow split mirrors the hook inline tests, so it stays true as the hook evolves.
Run it yourself
uvx capt-hook --hooks docs/examples test