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.
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:
─── 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)
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:
| Verdict | What Zurdo does | What you do |
|---|---|---|
retry_with_guidance | The next prompt opens with a # Diagnosis section. Same budget. | Nothing, unless it stalls again. |
halt_task | Stops 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_heal | Retries 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 andmax_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.
| Source | Written by | Costs a token? |
|---|---|---|
StallRecovery | The runner, when a stalled task later passes (needs [reason] enabled) | Yes |
HealAcceptance | zurdo heal, when you accept a heal | No |
AuthoringTrail | The zurdo-prd-author skill | Your agent session |
IntentReview | The zurdo-prd-review skill | Your 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. Pastmax_lessons(200), the least-used are evicted first. - Write your own with the
zurdo-lessonsskill. 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.
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. …
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
| Command | What it does |
|---|---|
zurdo reason match <prd> | Previews which lessons match each task. Read-only. |
zurdo reason status | Lesson counts, any unreadable lesson file, and each run's diagnosis-block count |
zurdo reason clear | Deletes 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. |
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
| Key | Default | Meaning |
|---|---|---|
enabled | false | Reasoner calls: diagnoses, post-mortems, stall-recovery lessons |
stall_attempts | 2 | Same-failure attempts that make a stall (minimum 2) |
max_diagnoses_per_task | 2 | Reasoner calls per task |
max_reasoner_calls_per_run | 20 | Reasoner calls per run |
guidance_max_bytes | 4096 | Cap on guidance carried into the next prompt |
extract_lessons | true | Write lessons on stall recovery and accepted heals |
max_lessons_injected | 2 | Lessons per prompt; 0 turns injection off |
max_lessons | 200 | Library 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.