Diagnosis & lessons

What to do with stalls, reasoner verdicts and post-mortems, and how to keep a library of lessons that future runs read.

zurdo v1.36.0

A retry loop that replays the same failure is burning tokens, not converging. The reason subsystem does three things about it:

Detect Spots a task that keeps failing the same way. Free and always on.

Diagnose One LLM call per stall to guide, halt, or re-route the task. Opt-in with [reason] enabled.

Remember Keeps lessons as reviewable files in lessons/, so future runs hear about a quirk before they hit it.

How a failing iteration retries or stalls into a reasoner diagnosis, and how passing retries become lessons reused in later runsHow a failing iteration retries or stalls into a reasoner diagnosis, and how passing retries become lessons reused in later runs

Everything the reasoner produces is advisory or subtractive. It can guide the agent, stop spending, or point you at zurdo heal. It can never mark a criterion passed, relax a hint, or edit the PRD. Verification stays the only grader.

When a task stalls

A task is stalled when stall_attempts consecutive attempts (default 2) fail the same way. Zurdo says so before the budget runs out:

zurdo run prds/greeter.md
─── task-test: Add a smoke test ─── effort=low, deps=[task-greet]
  → iteration 2 of 3
  ✓ agent completed: exit=0, 56ms
    ✗ file-exists: tests/smoke.rs
    ✗ shell: cargo test --test integration
  ⚠ task-test: stalled — attempt 2 repeats fingerprint sha256:b5fc4cd1… (2 consecutive attempts)
  → iteration 3 of 3
  …
  ✗ task-test: failed in 3 iterations (348ms)
Stalls also appear as task_stalled in progress.log and under ## Fingerprint Stalls in zurdo report.

Without the reasoner, a stall is your cue to stop the run and read the failing criterion: the When a run fails page walks it. How Zurdo decides two failures are "the same" is on Internals.

Verdicts

With [reason] enabled = true, a stall with attempts left triggers one call to the reasoner role. It returns a hypothesis, guidance, a confidence, and one of three verdicts:

VerdictWhat Zurdo doesWhat you do
retry_with_guidanceThe next prompt opens with a # Diagnosis section. Same budget.Nothing, unless it stalls again.
halt_taskStops the task now, marked failed; its dependents become blocked-by-dependency.Read the hypothesis in the close-out line, fix the cause or the PRD, then run zurdo run <prd> --reset. For a misaimed grep hint, use the heal recovery path.
suggest_healRetries as above, then prints a --heal <task> criterion <n> line at run end. The hint looks misaimed, not the code.Run zurdo heal on that criterion if you agree. Zurdo never heals on its own.
  • Bounded cost: at most max_diagnoses_per_task (2) per task and max_reasoner_calls_per_run (20) per run. Calls appear as a separate reasoner tally in the report.
  • Fail-open: a timeout, an unparseable reply or a spent budget never fails the task.

Post-mortems

A diagnosis speaks to the agent. A post-mortem speaks to you. It fires when a task stalls again on the same failure after following a diagnosis's guidance, which means the hypothesis was likely wrong. It can only say halt_task or suggest_heal, and it appears in zurdo report's ## Diagnoses table without changing the run's outcome. Treat a suggest_heal post-mortem as your next step: check the criterion, then heal or rewrite it.

Out-of-tree edits

An agent that edits outside the repo (~/.claude/skills/, $HOME) produces work the diff can't see. Zurdo flags absolute paths outside the repo root in zurdo report (## Out-of-Tree References) and in zurdo review. The flag is advisory and fails nothing.

Warning

The authoring rule that prevents it Agents resolve bare dotfile paths against $HOME. When a task edits dotfiles, write the full path (<repo-root>/.claude/skills/) and add a criterion that checks the edit landed in the repo.

Lessons

A lesson is a short, reviewable rule about your repository, such as "tests in tests/ need make dev-up first". Zurdo shows it to future runs that look like they'll need it.

SourceWritten byCosts a token?
StallRecoveryThe runner, when a stalled task later passes (needs [reason] enabled)Yes
HealAcceptancezurdo heal, when you accept a healNo
AuthoringTrailThe zurdo-prd-author skillYour agent session
IntentReviewThe zurdo-prd-review skillYour agent session

Each lesson is one lessons/lesson-<hash8>.md file at the repo root: YAML front matter saying when it applies, and a markdown body saying what to do.

---
schema: 1
match:
  hint_types:
    - shell
  command_head: cargo
  path_prefixes:
    - tests/
source:
  kind: StallRecovery
  prd_slug: auth-a1b2
  task_id: task-auth
  recovered_attempt: 3
created_at: 2026-09-01T12:00:00Z
---

Integration tests under `tests/` need the dev daemon running. Start it with
`make dev-up` before `cargo test --test integration`, or the suite times out.

What you do with them:

  • Commit them. Review a new lesson in its PR like any change, and don't gitignore lessons/.
  • Retire one with git rm. Past max_lessons (200), the least-used are evicted first.
  • Write your own with the zurdo-lessons skill. A lesson must declare at least two match components (hint types, failure reasons, command head, path prefixes), or it never surfaces. How matching scores is on Internals.
  • Preview before a run with zurdo reason match <prd>. It's read-only and works with [reason] off.
zurdo reason match prds/greeter.md
reason: match prds/greeter.md
reason: task task-greet 0 lessons
reason: task task-docs 0 lessons
reason: task task-test 1 lessons
reason: task task-test lesson source=greeter-a4ed/task-test@3 Integration tests under `tests/` need the dev daemon running. …
A read-only preview that works even with [reason] disabled.

In a run, the top max_lessons_injected (2) matching lessons open the agent's prompt under # Lessons From Previous Runs, framed as advice to verify. zurdo analyze, zurdo heal, zurdo-prd-author and zurdo-hint-debugger show them too.

Obligations

A lesson can also bind future PRDs with a requires block:

requires:
  scope: prd                                     # prd (default) or task
  criterion_matching: 'go test (-count=1 )?\./\.\.\.'

Any PRD the lesson applies to must then carry a criterion whose prose or hints match the regex: anywhere in the PRD with scope: prd, in each matching task with scope: task. A miss is an unaddressed-lesson warning from zurdo analyze; --strict never promotes it. A lesson with requires isn't injected into prompts, so write two lessons if you want both.

The zurdo reason CLI

CommandWhat it does
zurdo reason match <prd>Previews which lessons match each task. Read-only.
zurdo reason statusLesson counts, any unreadable lesson file, and each run's diagnosis-block count
zurdo reason clearDeletes the git-tracked lessons/ directory at the repo root. Leaves .zurdo/reason/usage.json and every .zurdo/<slug>/reason/ untouched. Asks for confirmation on a terminal; --yes skips the prompt.
zurdo reason status
reason: library 1 lessons
reason: library 0 lessons carry an obligation
reason: library match-key cargo 1 lessons
reason: greeter-a4ed 0 blocks

Configuration

zurdo init doesn't add these. Add them by hand to opt in:

[roles.reasoner]                  # optional; falls back to [roles.analyzer]
provider = "anthropic"
model    = "claude-opus-4-7"

[reason]
enabled = true                    # master switch; default false
KeyDefaultMeaning
enabledfalseReasoner calls: diagnoses, post-mortems, stall-recovery lessons
stall_attempts2Same-failure attempts that make a stall (minimum 2)
max_diagnoses_per_task2Reasoner calls per task
max_reasoner_calls_per_run20Reasoner calls per run
guidance_max_bytes4096Cap on guidance carried into the next prompt
extract_lessonstrueWrite lessons on stall recovery and accepted heals
max_lessons_injected2Lessons per prompt; 0 turns injection off
max_lessons200Library cap

enabled = true with neither role configured is a config-load error. Stall detection and reading committed lessons work with enabled = false, because neither costs a token.

Reading the results

zurdo report adds these sections when they have rows: ## Diagnoses, ## Fingerprint Stalls, ## Halt Attributions, ## Heal Routings, ## Out-of-Tree References, ## Lessons Extracted and ## Lessons Injected. Start with ## Halt Attributions and ## Heal Routings: they name the tasks waiting on you.