Airforge / Postmortem
The task that was marked done with no proof
An agent closes its task as shipped and attaches nothing. Shipped or abandoned?
An agent ends its run by closing the task it owns: status done, outcome shipped. The activity log records the close. Nothing else is attached. No commit, no pull request, no test output, no link of any kind.
Weeks later a reviewer walks the project history and stops at that entry. Did this ship? The tracker says yes. The repository has no branch carrying the task’s id, no commit trailer pointing back, no merged pull request. The session that made the claim ended long ago, and the transcript that could have explained it is gone. The reviewer is now reconstructing, from the outside, whether work happened at all.
No single dated incident sits behind this note. The pattern is a failure class we watched for and designed against while agents coordinated Axial’s own development: an agent closes a task claiming it shipped, attaches nothing, and a later reader cannot tell shipped from abandoned. Every operator of autonomous agents meets some version of it. The shape is always the same. A status changed, and nothing in the durable record backs it.
What actually went wrong
The status machine did exactly what it was told. in_progress to done is a legal transition, so the write succeeded and the field faithfully stored the claim.
That is the defect. A status field records testimony. When a human marks work done, the testimony has an anchor: you can ask them about it on Monday. When an agent marks work done, the process that formed the belief may be deleted minutes later. The testimony survives its witness.
An unbacked “done” is indistinguishable from an abandoned task that a model decided to tidy up on its way out. Both render identically in every list, report, and metric built on top, and everything downstream inherits the ambiguity.
A close is a claim
The idea that unlocked the fix, recorded in Axial’s decision log (ADR-076): closing a task with outcome shipped is a claim about the world. It asserts that a change exists somewhere outside the tracker. A claim can arrive grounded or ungrounded, and a coordination system that files ungrounded claims among its clean facts is quietly misleading every reader it has.
The discipline already exists one boundary earlier. A protected branch refuses a merge until required status checks pass; nobody calls that bureaucracy. Axial applies the same demand at the close boundary.
A shipped close counts as grounded when at least one of these holds:
- an evidence reference is attached (
--evidence pr:214,commit:abc123, a URL), on this close or recorded earlier - an acceptance criterion gated on close is satisfied
- a
verifyattestation exists for the closing commit
Any single one suffices, and each is one line naming where the proof lives. Only shipped is gated. wontfix records a decision and asserts nothing external; duplicate-of requires its target, so it arrives grounded by construction.
The two answers to an unbacked claim
claim ──▶ work ──▶ proof attached ──▶ close shipped clean record
claim ──▶ work ──▶ nothing attached ──▶ close shipped
├─ escalate: done, flagged unverified ──▶ human review queue
└─ strict: refused (exit 4) ──▶ attach proof, close again
The workspace’s close_policy decides which branch runs. Under the default, escalate, the close succeeds and the task is flagged:
$ axial issue close PLAT-1 --outcome shipped
updated PLAT-1 status: in_progress -> done
updated PLAT-1 outcome: (none) -> shipped
flagged PLAT-1 unverified: shipped without grounding
The unverified marker is first-class state. issue view shows it, the change feed carries it, the oversight surface files the task under “done, but no proof” for a person to inspect, and autonomy metrics leave unverified closes out of the count of wins. The claim goes to a human instead of into the ledger of settled facts. If grounding appears later, an evidence ref or an attestation for the closing commit, the marker clears on its own.
Under strict, the same close is refused with a conflict, and the error names what would satisfy it:
$ axial issue close PLAT-1 --outcome shipped
error[CONFLICT]: shipped requires grounding — attach --evidence (pr:/commit:/url:), satisfy a close criterion, or run: axial verify PLAT-1
Even strict keeps an exit. --force-unverified --reason "hotfix; PR link to follow" closes anyway, records the reason in the activity log, and sets the same marker. Closure is never a dead end: a tracker that traps finished work inside itself has failed at something more basic than verification.
The implication
If agents close work in your system today, run one audit: pick ten closed tasks and ask what in the record, excluding the status field itself, shows the work exists. A human team can often recover the answer through conversation. For a fleet of agents the conversation is gone, and the record is all there is.
The rule generalizes to any autonomous actor reporting its own success. A self-reported “done” needs a pointer to something the reporter does not control: a commit hash, a merged pull request, a passing check. Attaching it costs one flag at close time. Reconstructing it later costs an investigation.
Done is a claim; make it show a receipt.