Skip to content

The quality chain

An explanation. Why Procoder makes "done" a verdict something else gives, rather than a judgment the agent makes about its own work. For the commands, see the command reference; for the daily sequence, see how to ship a change.

The problem it solves

An AI coding agent is a confident narrator of its own progress. It reports work as finished because the last thing it did looked like finishing. Nothing in that loop is adversarial to the agent's own optimism, so "done" degrades into "I stopped".

Procoder's answer is to move the verdict out of the agent. The agent writes; a controller in the binary judges. The chain runs spec → plan → backlog → gate → lessons, and every link refuses to advance until the work is actually complete.

Each box is a stage; each diamond is a controller that can say no. The loops back are the point — nothing advances by being asserted done.

flowchart TD
    IDEA([idea]) --> SPEC[spec]
    SPEC --> SC{spec check}
    SC -->|"gaps · OPEN: questions<br/>untestable criteria"| SPEC
    SC -->|complete| PLAN[plan]
    PLAN --> PC{plan check}
    PC -->|"placeholders<br/>tasks with no files"| PLAN
    PC -->|ready| WORK[story or task]
    WORK --> CC{close controller}
    CC -->|"no evidence<br/>criteria unchecked"| WORK
    CC -->|closed| GATE{procoder check}
    GATE -->|"any blocking finding<br/>unchecked counts as failing"| WORK
    GATE -->|clean| MERGED([merged])
    MERGED --> LESSONS[/"what escaped becomes<br/>an adaptation"/]
    LESSONS --> GATE

The todo list runs beside the chain rather than inside it: the standalone tracker for work not born from a spec, with the same closing rigor.

Why the thinking comes first

The spec exists because design gaps are cheapest to close before code exists, and because an agent asked to build from a vague brief will invent the missing decisions silently. The interview makes those decisions explicit and hands them back to the person who should be making them: anything the user has not decided goes in as an OPEN: line, and the controller refuses while one remains.

procoder spec check blocks on missing sections, unresolved questions, and criteria a reviewer could not verify. It is blunt on purpose:

$ procoder spec check payments
spec payments: NOT ready — the quality controller found:
  - section missing: In scope
  - section missing: Out of scope
  - section missing: Constraints
  - section missing: Interfaces
  - section missing: Data
  - section missing: Edge cases
  - section missing: Failure modes
  - 1 unresolved OPEN: question(s) — resolve each with the user and rewrite it as a decision
  - untestable criterion: the UI is user-friendly — say what a reviewer would observe, not how it should feel

A spec that survives that is a real design document: a different engineer could build the feature from it without asking its author anything.

The plan is the same idea one level down. It is written for an engineer with zero context, because that is what a fresh agent context is. Each task carries its files, the exact interfaces its neighbours expose, and test-first steps — and plan check blocks the phrases that mean nothing was decided: "TBD", "handle edge cases", "similar to Task N".

Why the project layer is separate

Larger work needs a shape that a task list cannot hold, so the backlog adds milestones, epics, and user stories. Seeding an epic from a spec records the spec's fingerprint, which is what lets the board flag drift when the spec changes afterwards — a story quietly built against an older design is a defect nobody sees until review.

The story is the execution unit and carries the same rigor as a standalone task. Closes cascade upward: an epic cannot close while a story is open, a milestone cannot close while an epic is.

Sprints are scope-boxed rather than estimated. One is active at a time, and closing one refuses while a committed story is neither done nor explicitly carried back with a reason. Unfinished work stays visible. There are no story points and no burndown: stories are counted and the goal is the commitment, because a velocity number is something to farm and a goal is something to meet.

The retro is the price of the next sprint, not a ritual — sprint open refuses while the last sprint's retro is empty.

Why evidence, not assertion

A task closes on evidence: what was run, and what the run proved. Fresh runs only. For a regression test, the red-green proof. A subagent's "done" is a claim until it is verified in the diff.

procoder todo close refuses until every criterion is checked, the evidence records what it proved, and the commit gate itself is clean — the binary runs the real gate as the final criterion, so a task cannot close on a broken tree. Under [test] policy = "block" the suite joins that verdict, and a suite that cannot be verified blocks exactly like a failing one.

That last equivalence is the important one. Unverifiable and failing are the same answer, because treating "I could not check" as "it is fine" is how every silent regression ships.

Why one gate

procoder check is the same collection CI runs and procoder git prints. One code path, so the three can never disagree and no argument about "it passes locally" is possible. Unchecked counts as failing. There is no side door.

Why escapes have to close their class

Before a PR exists, the pre-PR self-review reads the diff with fresh context against .procoder/github/REVIEW.md. Anything that still escapes to a downstream reviewer triggers the reflection step: name the layer that should have caught it, adapt that layer in the same PR, and record it.

procoder lessons flags any entry with no adaptation as UNLEARNED and exits 1, because recorded is not learned. A lesson without an adaptation is a diary entry. The rubric grows from real escapes, so the same class cannot escape twice.

Why refusal, not advice

Every controller answers with a refusal that names the gap, never a warning that can be waved through.

Advice gets rationalised under pressure — and an agent under pressure to finish is the normal case, not the edge case. A named, blocking gap gets fixed because there is no other way forward. That is the whole difference between a checklist and a gate, and it is why the chain's verdicts mean something when you read them in a log: "COMPLETE" was earned against something that was trying not to say it.

The cost

Refusal is expensive when the controller is wrong. A criterion that is badly worded, a section the template demands but the work does not need, a gate finding that is a false positive — each costs real time, and the tempting fix is to weaken the criterion until it passes.

That is the failure mode to watch for. The rule is to fix what the refusal names or change the rule deliberately in .procoder/, never to soften the wording until the controller stops objecting. The repository's rules are yours to edit precisely so that disagreeing with a default is a decision you record rather than one you smuggle.