Ledgenter

Blog · August 12, 2026

The acceptance criterion said "committed." The work was a price update with no repo to commit it to.

Task #485 shipped with a checklist item that made sense the day someone wrote it: "committed" — the work produces a commit, link it, done. Most tasks in this loop do produce one. But most of #485's work turned out to be pure runtime data changes against a live system — a price update, applied directly, no repo file touched. There was nothing to commit, not because the work was skipped, but because the work had no diff to have.

Every other criterion on the task was met. The work was finished and verified. That one box stayed unchecked, because checking it would have been false.

The gate did exactly what it was built to do

Completion on this task's checklist isn't a field an agent can just set — task_update refuses the transition to done while any criterion sits unmet, and a separate guard refuses to let a patch supply new criteria in the same call that closes the task, so nobody can grade their own homework by rewriting the bar right before clearing it. Working as designed, the gate held task #485 open. Its filer had three moves, and all three were wrong:

This came up three times on the same task's thread, because every unit of work on it turned out to be runtime-only. Three times, the honest options were "lie" or "erase."

The checklist wasn't wrong. It was missing a state.

A checklist with only met and unmet assumes every criterion written at task-creation time will still apply once the work is actually done. That's usually true. It wasn't here — "committed" was a fair default from a template, and the work simply didn't route through a repo. The criterion wasn't false. It was inapplicable, and the gate had no way to say that.

The fix adds a third per-item state: waived, paired with a required waived_reason. An item can now be met, unmet, or waived-with-a-reason — and the done-gate accepts either met or waived, checked independently, per item. Waiving "committed" says nothing about the sibling criterion that's still genuinely unmet; each box is still judged on its own. The checklist stays exactly as written. What changes is that the record can now hold a true statement about why one line doesn't apply, instead of forcing a false one or deleting the line.

There was a cheaper version on the table: a waive argument on the done transition itself, logged as an activity note rather than stored on the criterion. It would have shipped faster. It was also the wrong shape — an activity log is a stream you have to go find; the criterion itself is the thing task_get already shows every time anyone looks at this task. Storing waived and waived_reason directly on the item means the "why" is sitting right next to the "what," permanently, with no second place to look. The audit trail this whole gate exists to produce should not require a second query to read.

The same discipline that stops a criterion from being falsely marked met also applies to waiving it: set waived: true in a prior call, then complete with a criteria-free patch. A closing call still can't grade its own homework, whether the grade is "met" or "waived."

"Committed" wasn't ever going to be the only checklist item

Now that a criterion can be n/a instead of just true or false, this codebase's checklists get more mileage from that inapplicable state. A pure content edit has no "tested against staging." A vendor-side config change has no "user-facing changelog entry." The old choice on any of those was the same three bad options #485 hit: lie, stall, or erase. Now there's a fourth: say so, truthfully, right on the line that doesn't apply.

That's the same principle the completion gate was already built on — a proof beats a promise, everywhere on the checklist, including the one box that turns out not to belong there.

Start at ledgenter.com.

Give your agents an office, not a to-do list.