Skip to content

porch counts a review lane that never ran as an approval: VERDICT SKIPPED parses to COMMENT, and COMMENT counts as approve #20

Description

@pseudoseed

porch prints All reviewers approved! for a project where one reviewer never ran. The summary a human reads when deciding to merge cannot distinguish a lane that approved from a lane that read nothing.

Mechanism

packages/codev/src/commands/porch/verdict.ts

// No valid VERDICT: line found but the consult ran — treat as COMMENT (non-blocking skip)
return "COMMENT";

parseVerdict recognises only APPROVE, REQUEST_CHANGES, COMMENT. Anything else falls through to COMMENT.

// COMMENT counts as approve (non-blocking feedback).
return reviews.every(r => r.verdict === "APPROVE" || r.verdict === "COMMENT");

So VERDICT: SKIPPED — the value our NOT-RUN convention deliberately writes — becomes COMMENT, and COMMENT becomes approval.

Note the default comment says "but the consult ran". That assumption is the bug: it was written for a reviewer that ran and produced unparseable output. A reviewer that never ran cannot be expressed in this vocabulary at all.

Second path to the same place

if (reviews.length === 0) return true; // No verification = auto-approve

Zero reviewers also approves. Two independent routes from "no review happened" to "approved".

Blast radius

Every project today that recorded a codex skip. Codex has been quota-exhausted since before 08:00 UTC and reset is 2026-08-27, so #2, #4, #11 and #12 all wrote a VERDICT: SKIPPED NOT-RUN file, and porch counted each as an approval. All four merged.

The merges themselves were sound: I approved each gate knowing the coverage was two of three, and each PR body states it explicitly. But that was the human channel working despite the tool, not because of it. Anyone reading porch summary got All reviewers approved!.

The NOT-RUN convention did its job. The parser undid it.

Fix

  1. Recognise SKIPPED as a first-class verdict, distinct from COMMENT.
  2. allApprove must NOT count SKIPPED. A skip is absence of evidence.
  3. Change the fall-through. An unrecognised verdict on a lane that produced output is COMMENT; a lane that produced no output, errored, or was never invoked is SKIPPED. Those are different facts.
  4. Reconsider reviews.length === 0 -> true. Auto-approving zero reviews is defensible for protocols with no consultation step, but it should be an explicit "consultation not configured", not a silent unanimous pass.
  5. The phase summary should state coverage: 2 of 3 lanes ran rather than All reviewers approved!.

Pattern

Sixth arrival today at the rule now in lessons-critical.md: a truncated or absent result must not be emitable as a complete one. Others: the render gate returning CLEAN on an unproven screen (#4), log extraction returning arbitrary lines as a diagnosis (#13), recently-merged timing out into an empty panel (#17), a timed-out check reporting as a test failure (#8), and this.

This one has the largest blast radius because it is the line a human reads before merging.

Found by the #12 builder during its own review phase, and it flagged the finding against its own gate rather than staying quiet.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/porchProtocol orchestratorbugSomething isn't workingfork-onlyDeliberately not upstreamed to cluesmith/codev

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions