# 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.

``` python
"""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](../../reference/primitives.md#captain_hook.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

``` text
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

``` text
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

``` bash
uvx capt-hook --hooks docs/examples test
```


# See also

- [Signals guide](../../docs/guide/llm-hooks.md#score-patterns-with-signals)
- [Primitives guide](../../docs/guide/primitives.md)
- [Conditions reference](../../docs/reference/conditions.md)
