diff --git a/tasks/in-progress/11-failed-agent-runs-must-be-visible.md b/tasks/review/11-failed-agent-runs-must-be-visible.md similarity index 57% rename from tasks/in-progress/11-failed-agent-runs-must-be-visible.md rename to tasks/review/11-failed-agent-runs-must-be-visible.md index f5ca61a..6fbffdd 100644 --- a/tasks/in-progress/11-failed-agent-runs-must-be-visible.md +++ b/tasks/review/11-failed-agent-runs-must-be-visible.md @@ -1,6 +1,6 @@ # 11 — A failed agent run must leave a visible trace on the card -**Status:** In Progress +**Status:** Review **Assignee:** istos **Priority:** High — three agents died today and the board showed nothing a human would notice **Type:** Feature @@ -86,3 +86,21 @@ Born from the 2026-07-29 API outage: agents on 06, 07 and 08 died with (card 10), and every one of them looked identical to "nothing happening". The empty-branch guard from card 05 fixed the silent *success* path; this is its sibling for the failure path. + + +--- + +## Work report — 2026-07-30 13:10 (Milo) + +nded` on every run and, when a run exits non-zero and was not held, calls the new `_record_failure`. That puts a `failure` dict on the run's record (exit code, ended-at, the cleaned tail of the log as the excerpt, the stage the card was in, the log path) and broadcasts a toast. Because it hangs off `_finish`, all four headless kinds get it — work, act-pr, PR review, relevance check. `_agent_public` exposes `failure` and `ended`, so `/api/state` carries them. The failure arm of `_reap_agent` now also calls `_discard_untouched_worktree`: a dead run with zero commits leaves no worktree behind, so **▸ start work** functions without a hand `git worktree remove`; a dead run *with* commits keeps its worktree. All four ticker lines now name what the log ended on instead of saying "see its log". +- `manager/core/watch.py` — the move branch of `narrate` calls `agents.forget_failure(file)`, so a card that changes stage arrives without the alarm. (`agents` sits left of `watch` in the module map, so the import direction holds.) +- `manager/core/board.html` — `lastRunOn` / `failedRun` / `whyFailed`; a `.card.run-failed` alarm border, a `run failed` pill carrying the excerpt as its tooltip, and an alarm well on the card showing `rc=N · `. The card sheet shows the whole excerpt in a bounded `
` plus the log path. The failure outranks a PR verdict, and is only worn on the stage the run died in.
+- `AGENTS.md` — a new "A run that died" subsection documents the state, its scoping and the worktree cleanup; the stale `.agent/logs/` path in the sentence above it is corrected to `local/state/agent/logs/`.
+- `tests/test_failed_run_visible.py` — real launches through a real stub adapter (scripts that print a line and exit 1, that commit then exit 1, and that exit 1 silently), covering the record, the toast, the surviving ticker line, the public payload, the worktree cleanup and the successful relaunch after it, a dead relevance check, excerpt/headline edge cases, `forget_failure` scoping, and source-level invariants for board.html.
+
+**What a reviewer should look at first**
+
+- `manager/core/agents.py:452` (`_finish`) — recording the failure for every kind in one place is the design decision the rest follows from.
+- `manager/core/board.html:720` (`failedRun`) — the two-part scoping (most recent run, and only in the stage it failed in) is what keeps the alarm off other cards and out of review/.
+
+**One thing to know:** I verified the frontend logic by extracting `lastRunOn` / `failedRun` / `whyFailed` and running them under `node` against fabricated state (failure shows on its own card in its stage, disappears when the card is in review/, does not appear on another card, is superseded by a newer run, and the headline is the log's last line), plus the source-level invariants in the test file. I did not open the board in a browser to look at the rendered card, so the visual — border, pill and well together — is the one thing worth eyeballing before merge.