Skip to content

fix(review-gate): derive the lens verdict from its own Blockers section - #229

Open
kjgbot wants to merge 4 commits into
mainfrom
fix/lens-verdict-218
Open

fix(review-gate): derive the lens verdict from its own Blockers section#229
kjgbot wants to merge 4 commits into
mainfrom
fix/lens-verdict-218

Conversation

@kjgbot

@kjgbot kjgbot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Closes the half of #218 that is blocking work right now.

What broke

A verdict that contradicted its own review. On #227 the maintainability lens printed:

### Blockers
None. The invariants that could break silently do fail closed ...
REVIEW_FAILED

No blockers, and REVIEW_FAILED. There is no legitimate appeal — lens-runner.sh makes the exit code authoritative on purpose, since a substring gate would be fail-open. So a broken review blocks finished work and the branch takes the blame.

Prompt drift between the two consumers. lens-runner.sh carried detailed prompts; review-swarm.yaml carried one-line summaries with every specific instruction stripped — and auto-merge acts on the swarm, the weaker of the two. That is how #215 merged carrying defects the local run had already named.

What this changes

The token is now derived, not chosen. Each prompt requires a section headed exactly ### Blockers, None under it when there are none, and states that the final token follows from that section. Concerns and notes are explicitly not blockers and must not change it.

The runner labels a contradiction instead of passing it through:

PRESWARM_<lens>: CONTRADICTION — review says 'Blockers: None' but emitted
REVIEW_FAILED; treating as NO_VERDICT (gate defect, not a finding)

This never upgrades a verdict — exit stays 1. Turning a failure into a pass on a substring is exactly the fail-open the classifier refuses; relabelling one so a branch is not blamed for a gate defect is not the same thing.

The three swarm roles now carry the runner's clauses, including the derivation rule, so both consumers ask the same question.

Verification

Detector tested against six shapes, including the two adversarial ones:

"### Blockers\nNone. The invariants..."        -> NONE   (the real #227 text)
"### Blockers\n1. real\n### Concerns\nNone."   -> HAS    (not fooled by a later None)
no Blockers section at all                      -> HAS    (fail-closed)
"### Blockers\n\nNone."                        -> NONE   (blank line tolerated)
"**None** — nothing blocking"                   -> NONE   (bold tolerated)
two numbered blockers                           -> HAS

bash -n clean; review-swarm.yaml still parses.

What this does not do

It does not consolidate the prompts into one file both consumers read — the end state #218 proposes, which needs the swarm spec to load role text from disk. This makes them agree and adds the derivation rule. The single source of truth stays open, and so does the drift risk lens-runner.sh has warned about in its header since it was written.

Note on who wrote this

I am the lead whose PRs this gate judges, so by RFC-0001 settled decision #6 I would not normally touch it. Khaliq directed this explicitly. Worth noting the direction: decision #6 guards against an agent widening its own gate, and every change here tightens or clarifies it — nothing can now pass that could not pass before.

Refs #218, #227, #215

Two failures in one day, both from the same root: the verdict token is a
separate judgement from the findings, and the two consumers ask different
questions.

**1. A verdict that contradicted its own review.** On #227 the maintainability
lens printed:

    ### Blockers
    None. The invariants that could break silently do fail closed ...
    REVIEW_FAILED

No blockers, and REVIEW_FAILED. A caller cannot appeal that: lens-runner.sh
makes the exit code authoritative on purpose, because a substring gate would be
fail-open. So a broken review blocks finished work with no recourse.

The prompt now makes the token DERIVED rather than chosen: head a section
exactly `### Blockers`, write None when there are none, and the token follows
from that section. Concerns and notes are explicitly not blockers and must not
change it.

The runner also detects the contradiction and labels it:

    PRESWARM_<lens>: CONTRADICTION — review says 'Blockers: None' but emitted
    REVIEW_FAILED; treating as NO_VERDICT (gate defect, not a finding)

This NEVER upgrades a verdict. Exit stays 1. Turning a failure into a pass on a
substring is exactly the fail-open the classifier refuses; relabelling one so a
branch is not blamed for a gate defect is not.

**2. Prompt drift between the two consumers.** `lens-runner.sh` carried detailed
prompts while `review-swarm.yaml` carried one-line summaries with every specific
instruction stripped — and auto-merge acts on the swarm, the weaker of the two.
That is how #215 merged with defects the local run had named. The three roles now
carry the same clauses as the runner, including the Blockers-derivation rule.

Verified the detector against six shapes, including the two that matter:

    "### Blockers\nNone. The invariants..."          -> NONE   (the real #227 text)
    "### Blockers\n1. real\n### Concerns\nNone."     -> HAS    (not fooled by a later None)
    no Blockers section at all                        -> HAS    (fail-closed)
    "### Blockers\n\nNone."                          -> NONE   (blank line tolerated)
    "**None** — nothing blocking"                     -> NONE   (bold tolerated)
    two numbered blockers                             -> HAS

`bash -n` clean; review-swarm.yaml still parses.

Does not consolidate the prompts into one file both consumers read — that is the
end state #218 proposes and needs the swarm spec to load role text from disk.
This makes them agree and adds the derivation rule; the single source of truth
is still open.

Refs #218, #227, #215

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR
@coderabbitai

coderabbitai Bot commented Sep 7, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 3bc76560-ef25-4e8e-b080-c557cc3e4f69


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kjgbot

kjgbot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Note on how this PR must be reviewed — the repo already refuses to let it judge itself

I tried to smoke-test the modified runner against its own diff. It declined, correctly:

lens-runner: REFUSING to run — this diff modifies the pre-swarm-check itself
             (ops/preswarm-check/** or workflows/preswarm-check.yaml).
             A branch-owned gate cannot judge its own modifications
             (RFC-0001 settled decision 6).
             Set PRESWARM_ALLOW_SELF_JUDGE=1 to override with the understanding
             that the post-push review-swarm — running from main's copy — is the
             authoritative gate for this PR.

Good guard, and it makes my earlier note about decision #6 mechanical rather than a matter of my judgement. I have not set the override.

The consequence worth naming

The authoritative gate for this PR is the post-push review-swarm running from main's copy — which is the weaker, drifted one this PR exists to fix. So the repair will be judged by the defect.

Concretely, from main's copy, the maintainability role is still:

Reviews for maintainability — will a stranger understand and safely change this in six months?

with no ### Blockers requirement and no derivation rule. It can therefore still emit a verdict uncorrelated with its findings while reviewing the change that stops exactly that. A PASS from it is weak evidence, and a FAIL from it may be the very contradiction documented in #218.

That is not an argument for merging unreviewed. It is an argument for a human reading this diff, which is small: two files, +58/-3, and the whole behavioural change is one prompt rule plus one classifier branch that can only relabel a failure, never upgrade one.

What I did verify, since the gate could not

  • Detector against six shapes including two adversarial ones (a real blocker with the word "None" under Concerns → still HAS; no Blockers section → HAS, fail-closed).
  • bash -n clean on the runner; review-swarm.yaml parses.
  • The change cannot make anything pass that could not pass before — exit stays 1 on every path that was 1.

kjgbot pushed a commit that referenced this pull request Sep 7, 2026
…defect it repairs

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread ops/preswarm-check/lens-runner.sh
Comment thread ops/preswarm-check/lens-runner.sh Outdated
@kjgbot

kjgbot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Review swarm: maintainability

No fresh transcript was produced for run 5d97b90d-1bf4-4a3c-9d1e-b73678548bd0 (MISSING).

@kjgbot

kjgbot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Review swarm: history

No fresh transcript was produced for run 5d97b90d-1bf4-4a3c-9d1e-b73678548bd0 (MISSING).

@kjgbot

kjgbot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Review swarm: structure

No fresh transcript was produced for run 5d97b90d-1bf4-4a3c-9d1e-b73678548bd0 (MISSING).

@kjgbot

kjgbot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

🎯 review-swarm: FAILED (M:fail H:fail S:pass)

Lens transcripts posted as sibling comments above.

kjgbot pushed a commit that referenced this pull request Sep 7, 2026
CLOUD_API_KEY + CLOUD_API_URL landed 20:04:40Z. The step that blocked all six
PRs all day is green. Re-ran one PR, not six, to bound the cost of any
downstream surprise.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

@kjgbot kjgbot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spec review at a7b23cf: BLOCKED / leave open.

Serves RFC-0001 §2 rule 7 and decision #11: quality verdicts belong to the evidence layer. However the requested deterministic correspondence between Blockers and the final token is incomplete.

P1 — ops/preswarm-check/lens-runner.sh:285: the REVIEW_PASSED arm never validates Blockers; a review that lists unauthorized writes as a blocker and ends in REVIEW_PASSED exits 0. That fails closed-gate discipline (covenant 2 / §2 rule 4).

P2 — ops/preswarm-check/lens-runner.sh:264: awk stops at the first matching heading/body, accepts arbitrary heading levels, and does not isolate a section. A later genuine blocker is mislabeled CONTRADICTION when an earlier Blockers section said None. The comment promises the LAST exact heading.

I executed the classifier extracted verbatim from the stated head (no gate edits and no full preswarm approval claimed). Captured command and output:

$ python3 ops/spec-review-0907-evidence/229-classifier-repro.py
blocker_plus_pass: exit=0
PRESWARM_structure: REVIEW_PASSED
first_none_last_blocker: exit=1
PRESWARM_structure: CONTRADICTION — review says 'Blockers: None' but emitted REVIEW_FAILED; treating as NO_VERDICT (gate defect, not a finding)
none_plus_fail: exit=1
PRESWARM_structure: CONTRADICTION — review says 'Blockers: None' but emitted REVIEW_FAILED; treating as NO_VERDICT (gate defect, not a finding)

exit_code=0

Both existing review threads remain unresolved and valid. Required repair: an independent gate owner must validate the final exact Blockers section in BOTH verdict arms and pin malformed/multiple-section behavior with tests, on this PR branch. Decision #6 prohibits me from editing the review gates judging this assignment.

CI is separately blocked on credentials, not a content verdict. Captured command: gh run view 34124184705 --log-failed; relevant literal output:

review	Launch cloud swarm	2026-09-07T20:44:33.0836035Z Workflow prepare failed: 401 Unauthorized: Unauthorized

The credential owner must restore CI authentication and rerun at the final head. No merge attempted.

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown

Review swarm: FAILED

  • maintainability: MISSING
  • history: MISSING
  • structure: MISSING

Cloud run: 5d97b90d-1bf4-4a3c-9d1e-b73678548bd0

@kjgbot kjgbot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Final disposition at a7b23cf: LEFT OPEN.

The P1 false pass at ops/preswarm-check/lens-runner.sh:285 and P2 first-section parser at :264 remain reproduced and unresolved. An independent gate owner must repair both verdict arms and final exact section selection on this branch (RFC decision #6 prevents this assigned reviewer from modifying the gate). The rerun passed launch but failed with all three fresh lens transcripts MISSING. Review infrastructure owner must restore real transcripts and rerun. Leave open.

Captured exact-head check query and output:

$ gh api repos/AgentWorkforce/flows/commits/a7b23cff7f394e16a77d02bc727be958365b8ae9/check-runs --jq '[.check_runs[] | {name,head_sha,status,conclusion,details_url}]'
[{"conclusion":"failure","details_url":"https://github.com/AgentWorkforce/flows/actions/runs/34124184705/job/101873935143","head_sha":"a7b23cff7f394e16a77d02bc727be958365b8ae9","name":"review","status":"completed"},{"conclusion":"success","details_url":"https://www.cubic.dev/pr/AgentWorkforce/flows/pull/229","head_sha":"a7b23cff7f394e16a77d02bc727be958365b8ae9","name":"cubic · AI code reviewer","status":"completed"}]

exit_code=0

Current swarm report: #229 (comment)

Review swarm: FAILED

  • maintainability: MISSING
  • history: MISSING
  • structure: MISSING

Cloud run: aaac776e-b663-49a5-baf5-56cc57688502

An independent spec review found a fail-open in my own fix. The REVIEW_PASSED
arm checked only the CLI exit code and never consulted the Blockers section, so
a review that enumerated blockers -- unauthorized writes among them -- and
ended in REVIEW_PASSED exited 0.

The comment above that arm claims the classifier "NEVER upgrades a verdict",
and it does not. That was the wrong safety property to reason about. One-
directional safety guards fail->pass, which fails CLOSED anyway, and leaves the
fail-OPEN direction unguarded, which is the only direction a gate cannot afford
to get wrong. I wrote that comment as a proof of safety; it was a proof about
the harmless half.

`blockers_are_listed` is deliberately NOT the negation of `blockers_say_none`:
an ABSENT Blockers section returns false, so a review that never emitted the
section keeps its previous behaviour rather than newly failing. That closes the
unambiguous hole without changing the blast radius for non-conforming lenses.

Verified across all five arms:

    blockers listed + PASSED   -> CONTRADICTION (exit 1)   was: exit 0
    Blockers: None  + PASSED   -> REVIEW_PASSED  (exit 0)
    no section      + PASSED   -> REVIEW_PASSED  (exit 0)  unchanged
    Blockers: None  + FAILED   -> CONTRADICTION (exit 1)
    blockers listed + FAILED   -> REVIEW_FAILED  (exit 1)

Direction of the change is strictly tightening: it can only turn a pass into a
non-verdict, never a failure into a pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 1 file (changes from recent commits).

Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.

Re-trigger cubic

Comment thread ops/preswarm-check/lens-runner.sh
kjgbot and others added 2 commits September 8, 2026 00:57
The P2 from the same spec review, and it defeated the P1 fix I shipped an hour
ago. Both helpers used:

    awk '/^#+[[:space:]]*Blockers[[:space:]]*$/{f=1;next} f&&NF{print;exit}'

which flags on the FIRST matching heading and exits at its first body line. A
review with an early "Blockers: None" summary and a later real section is read
as "None":

    first-match awk -> None                       (guard passes the review)
    last-match awk  -> - unauthorized write       (guard blocks it)

So the fail-open I closed was still reachable through a differently-shaped
review, and `blockers_are_listed` inherited the flaw the moment I wrote it on
top of the same pattern.

The comment above these helpers has said "the LAST `### Blockers` heading"
since the original change. The code never did that. A comment describing
intent rather than behaviour is worse than no comment: I read it twice while
fixing P1 and took it as a description of what the code did.

Both helpers now accumulate to the last matching section. Verified across seven
arms, including the two multi-section cases that motivated this:

    early None + LATER real blockers + PASSED -> CONTRADICTION (exit 1)
    early real + LATER None          + FAILED -> CONTRADICTION (exit 1)
    blockers listed + PASSED                  -> CONTRADICTION (exit 1)
    Blockers: None  + PASSED                  -> REVIEW_PASSED  (exit 0)
    no section      + PASSED                  -> REVIEW_PASSED  (exit 0)
    Blockers: None  + FAILED                  -> CONTRADICTION (exit 1)
    blockers listed + FAILED                  -> REVIEW_FAILED  (exit 1)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR
cubic P1 on #229. A lens that emits REVIEW_PASSED with no `### Blockers`
section cleared the gate, because `blockers_are_listed` returns false for an
absent section and the PASSED arm read that as "no blockers".

My own comment defended this: an absent section should "keep its previous
behaviour instead of newly failing", to avoid widening the blast radius. That
was protecting a case the prompt already forbids — it requires the heading and
says the first word under it must be `None` when there are none. A review
without it has not answered the question the gate asks.

Add `blockers_section_present` as a separate guard rather than overloading the
existing boolean, so the log distinguishes "the lens contradicted itself" from
"the lens ignored the output contract"; those need different fixes.

Verified against all four cases: no section + PASSED -> NO_VERDICT; None +
PASSED -> PASSED; blockers listed + PASSED -> CONTRADICTION; and a second
Blockers section listing one still reads the LAST section.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR
kjgbot pushed a commit that referenced this pull request Sep 8, 2026
…s P1

Swept threads rather than check status. Two P1s were invisible. Fixed the
review-gate fail-open my own comment had been defending with a justification
for a case the prompt already forbids.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="ops/preswarm-check/lens-runner.sh">

<violation number="1" location="ops/preswarm-check/lens-runner.sh:283">
P2: A malformed review headed `# Blockers` or `#### Blockers` can still pass because `blockers_section_present` does not enforce the required literal `### Blockers` heading. Match the exact heading so the new missing-section guard cannot accept an invalid section as a pass.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

# here argued absence should "keep its previous behaviour instead of newly
# failing". That was protecting a case the prompt already forbids.
blockers_section_present() {
printf '%s\n' "$OUTPUT" | grep -qE '^#+[[:space:]]*Blockers[[:space:]]*$'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: A malformed review headed # Blockers or #### Blockers can still pass because blockers_section_present does not enforce the required literal ### Blockers heading. Match the exact heading so the new missing-section guard cannot accept an invalid section as a pass.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At ops/preswarm-check/lens-runner.sh, line 283:

<comment>A malformed review headed `# Blockers` or `#### Blockers` can still pass because `blockers_section_present` does not enforce the required literal `### Blockers` heading. Match the exact heading so the new missing-section guard cannot accept an invalid section as a pass.</comment>

<file context>
@@ -273,6 +273,16 @@ blockers_are_listed() {
+# here argued absence should "keep its previous behaviour instead of newly
+# failing". That was protecting a case the prompt already forbids.
+blockers_section_present() {
+  printf '%s\n' "$OUTPUT" | grep -qE '^#+[[:space:]]*Blockers[[:space:]]*$'
+}
+
</file context>
Suggested change
printf '%s\n' "$OUTPUT" | grep -qE '^#+[[:space:]]*Blockers[[:space:]]*$'
printf '%s\n' "$OUTPUT" | grep -qE '^### Blockers[[:space:]]*$'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant