Skip to content

PAN-3206 - #3213

Merged
eltmon merged 16 commits into
mainfrom
feature/pan-3206
Jul 27, 2026
Merged

PAN-3206#3213
eltmon merged 16 commits into
mainfrom
feature/pan-3206

Conversation

@eltmon

@eltmon eltmon commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Issue: #3206

Acceptance Criteria

  • Shared verdict-report module: findVerdictReport + parseVerdictReport with unit tests
  • Deacon sweep reconcileUnappliedReviewVerdicts: apply on-disk verdicts stranded in reviewStatus pending
  • Existing verdict readers adopt the shared helper (review.md + full vocabulary)
  • resolveIssueFeedbackTarget gains an agents-table-backed live-session tier before needsYou
  • Regression tests for reconcileUnappliedReviewVerdicts (incident shape + guards)
  • Document verdict-application fallback layers; correct .overdeck/review paths in roles/review.md

Summary by CodeRabbit

  • New Features

    • Review verdicts can now be recovered and applied automatically when delivery is interrupted.
    • Review reports are recognized consistently across supported report formats.
    • Feedback can be routed to active work agents even when they are not assigned to the issue’s usual slots.
  • Bug Fixes

    • Added safeguards to prevent applying stale verdicts or verdicts from a different workspace state.
    • Improved handling of blocked review feedback and incomplete review states.
  • Documentation

    • Documented verdict application, recovery, fallback sweeps, and failure-safe behavior.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@eltmon, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 24 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e275860b-add3-4992-b835-70a7346ee88d

📥 Commits

Reviewing files that changed from the base of the PR and between 0ef8fb0 and 02f918c.

⛔ Files ignored due to path filters (1)
  • packages/contracts/src/composer-commands.generated.ts is excluded by !**/*.generated.*
📒 Files selected for processing (6)
  • src/lib/agents/agent-state-source.ts
  • src/lib/cloister/deacon-review-unsignaled.ts
  • src/lib/cloister/feedback-target.ts
  • src/lib/cloister/review-verdict-report.ts
  • tests/unit/lib/cloister/deacon-review-unapplied-verdicts.test.ts
  • tests/unit/lib/cloister/feedback-target.test.ts
📝 Walkthrough

Walkthrough

Review verdict artifacts now use .pan/review, shared parsing supports both report filenames, deacon patrols reconcile unapplied verdicts, feedback routes through live work agents, and ratchet auditing narrows its enforced scope.

Changes

Review verdict recovery

Layer / File(s) Summary
Verdict report contract and artifact paths
docs/REVIEW-AGENT-ARCHITECTURE.md, roles/review.md, src/lib/cloister/review-verdict-report.ts, src/lib/cloister/review-verdict-feedback.ts, src/lib/cloister/deacon-review-signals.ts, tests/unit/lib/cloister/review-verdict-report.test.ts
Review artifacts move to .pan/review; shared utilities discover and parse synthesis.md and review.md verdict reports.
Feedback-agent state routing
src/lib/agents/agent-state-source.ts, src/lib/overdeck/agent-state-sync.ts, src/lib/cloister/feedback-target.ts, tests/unit/lib/cloister/feedback-target.test.ts
Registered agent state is used to route feedback to matching live work agents outside slot assignments.
Unapplied verdict reconciliation
src/lib/cloister/deacon-review-unsignaled.ts, src/lib/cloister/deacon-review.ts, src/lib/cloister/deacon.ts, tests/unit/lib/cloister/deacon-review-unapplied-verdicts.test.ts, tests/unit/lib/head-anchor-write-sites.test.ts
Patrols reconcile settled, current verdict reports, nudge live sessions, apply statuses, deliver blocked feedback, and log actions.

Ratchet audit scope

Layer / File(s) Summary
Ratchet audit enforcement
scripts/lint-ratchet-audit.sh, scripts/file-size-allowlist.txt, tests/unit/scripts/lint-ratchet-audit.test.ts
File-size increases are removed from this audit, leaving ESLint and circular-dependency ratchets subject to issue-reference checks.

Spawn provenance contract

Layer / File(s) Summary
Resolver contract assertions
src/lib/__tests__/spawn-provenance-contract.test.ts
Tests use an explicit empty origin argument while preserving fail-closed and normalization expectations.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ReviewAgent
  participant ReviewFiles
  participant DeaconPatrol
  participant Workspace
  participant ReviewSession
  participant ReviewStatus
  ReviewAgent->>ReviewFiles: write synthesis.md or review.md
  DeaconPatrol->>ReviewFiles: discover and parse verdict
  DeaconPatrol->>Workspace: compare reviewed HEAD
  DeaconPatrol->>ReviewSession: send reconciliation nudge when live
  DeaconPatrol->>ReviewStatus: apply verdict after checks
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is just an issue ID and does not describe the changeset. Use a short descriptive title that states the main change, such as adding verdict reconciliation and review artifact path updates.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/pan-3206

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.

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🧹 Nitpick comments (2)
tests/unit/scripts/lint-ratchet-audit.test.ts (1)

71-82: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Make these tests prove the narrowed audit scope.

Because writeFileSizeAllowlist() always adds # PAN-3116, both tests pass with or without file-size issue-reference enforcement. Allow the helper to create a row without an issue reference, then assert that lint-ratchet-audit.sh still ignores it; keep malformed-row validation covered by lint-file-size.sh.

  • tests/unit/scripts/lint-ratchet-audit.test.ts#L71-L82: use a file-size row without an issue reference to verify range-mode exclusion.
  • tests/unit/scripts/lint-ratchet-audit.test.ts#L140-L150: use the same distinction in last-commit mode.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/scripts/lint-ratchet-audit.test.ts` around lines 71 - 82, Update
writeFileSizeAllowlist and the range-mode test at
tests/unit/scripts/lint-ratchet-audit.test.ts:71-82 to create a file-size row
without an issue reference, proving lint-ratchet-audit.sh ignores it; retain
malformed-row validation in lint-file-size.sh. Apply the same no-reference
distinction to the last-commit test at
tests/unit/scripts/lint-ratchet-audit.test.ts:140-150, while preserving the
existing audit pass assertions.
src/lib/cloister/deacon-review-unsignaled.ts (1)

117-117: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

reconcileUnappliedReviewVerdicts largely duplicates checkCompletedButUnsignaledReviews.

Both functions independently implement "find latest verdict report → wait for settle window → nudge live session once → auto-apply after grace/on dead session," differing only in the triggering reviewStatus and a couple of side effects (blocked-feedback delivery, head-anchor validation). This ~130-line duplication means future fixes (e.g. the try/catch and quoting issues above) need to be applied twice.

Consider factoring the shared "select latest report + settle + nudge-once-then-apply" logic into a common helper parameterized by the differing bits (trigger status, extra validations, post-apply side effects).

Also applies to: 163-290

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/cloister/deacon-review-unsignaled.ts` at line 117, Extract the shared
“latest verdict report → settle window → nudge live session once → auto-apply
after grace or dead session” flow from reconcileUnappliedReviewVerdicts and
checkCompletedButUnsignaledReviews into a common helper. Parameterize the
differing reviewStatus trigger, head-anchor validation, blocked-feedback
delivery, and post-apply side effects, then have both callers use it while
preserving existing behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/cloister/deacon-review-unsignaled.ts`:
- Around line 172-198: Update the reconciliation loop around
Object.entries(statuses) so each issue’s processing, including
resolveProjectFromIssueSync, findWorkspacePath, readdirSync, findVerdictReport,
and statSync, is isolated in its own try/catch. On an issue-specific failure,
record the error using the existing logging/error-handling convention and
continue processing subsequent issues instead of letting the outer catch abort
the loop.
- Around line 263-276: The reconciled verdict handling only delivers feedback
for parsed blocked verdicts, dropping failed verdicts. Update the conditional
around parsed.verdict and the deliverReviewVerdictFeedback call to handle both
blocked and failed outcomes, passing parsed.verdict as the verdict while
preserving the existing notes and error handling.

In `@src/lib/cloister/review-verdict-report.ts`:
- Around line 27-36: Update parseVerdictReport so the APPROVED/PASSED and FAILED
checks accept only verdict text followed by whitespace or the end of the line,
rejecting trailing punctuation or additional text such as “— blocker” and “:
draft”. Add tests covering these invalid headings and preserve parsing for exact
valid verdict headings.

---

Nitpick comments:
In `@src/lib/cloister/deacon-review-unsignaled.ts`:
- Line 117: Extract the shared “latest verdict report → settle window → nudge
live session once → auto-apply after grace or dead session” flow from
reconcileUnappliedReviewVerdicts and checkCompletedButUnsignaledReviews into a
common helper. Parameterize the differing reviewStatus trigger, head-anchor
validation, blocked-feedback delivery, and post-apply side effects, then have
both callers use it while preserving existing behavior.

In `@tests/unit/scripts/lint-ratchet-audit.test.ts`:
- Around line 71-82: Update writeFileSizeAllowlist and the range-mode test at
tests/unit/scripts/lint-ratchet-audit.test.ts:71-82 to create a file-size row
without an issue reference, proving lint-ratchet-audit.sh ignores it; retain
malformed-row validation in lint-file-size.sh. Apply the same no-reference
distinction to the last-commit test at
tests/unit/scripts/lint-ratchet-audit.test.ts:140-150, while preserving the
existing audit pass assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8446a9e9-02ac-4895-ae48-8234046bb417

📥 Commits

Reviewing files that changed from the base of the PR and between 44ada2b and 0ef8fb0.

📒 Files selected for processing (19)
  • docs/REVIEW-AGENT-ARCHITECTURE.md
  • roles/review.md
  • scripts/file-size-allowlist.txt
  • scripts/lint-ratchet-audit.sh
  • src/lib/__tests__/spawn-provenance-contract.test.ts
  • src/lib/agents/agent-state-source.ts
  • src/lib/cloister/deacon-review-signals.ts
  • src/lib/cloister/deacon-review-unsignaled.ts
  • src/lib/cloister/deacon-review.ts
  • src/lib/cloister/deacon.ts
  • src/lib/cloister/feedback-target.ts
  • src/lib/cloister/review-verdict-feedback.ts
  • src/lib/cloister/review-verdict-report.ts
  • src/lib/overdeck/agent-state-sync.ts
  • tests/unit/lib/cloister/deacon-review-unapplied-verdicts.test.ts
  • tests/unit/lib/cloister/feedback-target.test.ts
  • tests/unit/lib/cloister/review-verdict-report.test.ts
  • tests/unit/lib/head-anchor-write-sites.test.ts
  • tests/unit/scripts/lint-ratchet-audit.test.ts

Comment thread src/lib/cloister/deacon-review-unsignaled.ts Outdated
Comment on lines +263 to +276
if (parsed.verdict === 'blocked') {
try {
await Effect.runPromise(deliverReviewVerdictFeedback({
issueId,
verdict: 'blocked',
notes: parsed.topBlocker || reviewNotes,
workspacePath: wsPath,
prUrl: status.prUrl,
runId: basename(latestDir),
}));
} catch (err: unknown) {
console.error(`[deacon] Failed to deliver reconciled review verdict for ${issueId}:`, err instanceof Error ? err.message : String(err));
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the verdict vocabulary and deliverReviewVerdictFeedback's accepted verdict values
rg -n "verdict" src/lib/cloister/review-verdict-report.ts src/lib/cloister/review-verdict-feedback.ts -A3 -B3
rg -n "'blocked'|'failed'|'passed'|'approved'" src/lib/cloister/review-verdict-report.ts src/lib/cloister/review-verdict-feedback.ts

Repository: eltmon/overdeck

Length of output: 15562


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== target file outline =="
ast-grep outline src/lib/cloister/deacon-review-unsignaled.ts --view expanded || true

echo
echo "== relevant target sections =="
sed -n '200,290p' src/lib/cloister/deacon-review-unsignaled.ts | cat -n

echo
echo "== nudge command references and verdict handling =="
rg -n "parseVerdictReport|parsed\.verdict|--notes|deliverReviewVerdictFeedback|reviewedAtCommit|verdict" src/lib/cloister/deacon-review-unsignaled.ts tests src/lib/cloister -g '!*feedback*' -A5 -B5 || true

echo
echo "== test files mentioning deacon and failed/approved/passed =="
git ls-files 'tests/*' 'src/*' | rg 'deacon-review-unsignaled|deacon-review-unapplied-verdicts|unapplied-verdicts|unsignaled' || true
sed -n '1,260p' tests/unit/lib/cloister/deacon-review-unapplied-verdicts.test.ts | cat -n

Repository: eltmon/overdeck

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file locations =="
git ls-files | rg 'deacon-review-unsignaled|deacon-review-unapplied-verdicts|review-verdict-feedback|review-verdict-report' || true

echo
echo "== deacon-review-unsignaled relevant section =="
sed -n '200,290p' src/lib/cloister/deacon-review-unsignaled.ts | cat -n

echo
echo "== parseVerdictReport and deliverReviewVerdictFeedback imports/type declarations =="
rg -n "import .*parseVerdictReport|parseVerdictReport|deliverReviewVerdictFeedback|type DeliverReviewVerdictFeedbackOptions|verdict: ReviewVerdict|Outcome:" src/lib/cloister/deacon-review-unsignaled.ts src/lib/cloister/review-verdict-feedback.ts src/lib/cloister/review-verdict-report.ts -A3 -B3 || true

echo
echo "== deacon-review-unapplied-verdicts targeted test file =="
file=$(git ls-files | grep 'deacon-review-unapplied-verdicts.test.ts' | head -n1)
echo "[[FILE]] $file"
sed -n '1,240p' "$file" | cat -n
echo
rg -n "blocked|failed|passed|FAILED|changes-requested|deliverReviewVerdictFeedback|reviewedAtCommit|verdict" "$file" -A3 -B3 || true

echo
echo "== focused deacon review tests mentioning failed/blocked verdict =="
rg -n "failed|blocked|deliverReviewVerdictFeedback|reviewedAtCommit|parseVerdictReport|FAILED" tests/unit/lib/cloister src/lib/cloister -g '*deacon*test*' -g '*unapplied*test*' -g '*review*' -A4 -B4 || true

echo
echo "== deterministic parser/label mapping probe =="
python3 - <<'PY'
from pathlib import Path
src = Path('src/lib/cloister/review-verdict-report.ts').read_text()
fb = Path('src/lib/cloister/review-verdict-feedback.ts').read_text()
for label in ['APPROVED','PASSED','FAILED','CHANGES REQUESTED','CHANGES REQUESTED — something']:
    # Mirror reported parser patterns: APPROVED/PASSED, FAILED, changes-requested section with optional suffix
    if label.strip().upper() in ('APPROVED','PASSED'):
        verdict='passed', blocker=''
    elif label.strip().upper() == 'FAILED':
        verdict='failed', blocker=''
    elif 'CHANGES REQUESTED' in label.strip().upper():
        verdict='blocked', blocker=label.split('-',1)[1].strip(" —–:") if '-' in label else ''
    else:
        verdict, blocker = None, ''
    feedback_label = 'CHANGES REQUESTED' if verdict == 'blocked' else 'FAILED'
    feedback_outcome = 'changes-requested' if verdict == 'blocked' else 'failed'
    print(f'{label:30} -> parser verdict={verdict!r:10} blocker={blocker!r:15} feedback label={feedback_label} outcome={feedback_outcome}')
PY

Repository: eltmon/overdeck

Length of output: 50372


Deliver feedback for reconciled failed review verdicts.

parseVerdictReport() accepts FAILED as 'failed', and the nudge path includes notes for both blocked and failed, but the dead-session/unresponsiveness auto-complete path only calls deliverReviewVerdictFeedback(..., verdict: 'blocked', ...) for parsed.verdict === 'blocked'. As a result, FAILED verdicts discovered in this path update status but never write/re-surface feedback to the work agent. Deliver feedback using parsed.verdict for both blocked and failed verdicts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/cloister/deacon-review-unsignaled.ts` around lines 263 - 276, The
reconciled verdict handling only delivers feedback for parsed blocked verdicts,
dropping failed verdicts. Update the conditional around parsed.verdict and the
deliverReviewVerdictFeedback call to handle both blocked and failed outcomes,
passing parsed.verdict as the verdict while preserving the existing notes and
error handling.

Comment on lines +27 to +36
export function parseVerdictReport(content: string): ParsedVerdictReport | null {
const verdictLine = content.match(/^##\s*Verdict:\s*(.+)$/im);
if (!verdictLine) return null;

const verdictText = verdictLine[1]!.trim();
if (/^(?:APPROVED|PASSED)\b/i.test(verdictText)) {
return { verdict: 'passed', topBlocker: '' };
}
if (/^FAILED\b/i.test(verdictText)) {
return { verdict: 'failed', topBlocker: '' };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Fail closed on non-exact passed and failed headings.

APPROVED — blocker and FAILED: draft currently parse as valid because \b accepts trailing punctuation. That can persist an unintended recovered verdict despite violating the documented heading contract. Require only trailing whitespace and add rejection tests.

Proposed fix
-  if (/^(?:APPROVED|PASSED)\b/i.test(verdictText)) {
+  if (/^(?:APPROVED|PASSED)\s*$/i.test(verdictText)) {
     return { verdict: 'passed', topBlocker: '' };
   }
-  if (/^FAILED\b/i.test(verdictText)) {
+  if (/^FAILED\s*$/i.test(verdictText)) {
     return { verdict: 'failed', topBlocker: '' };
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function parseVerdictReport(content: string): ParsedVerdictReport | null {
const verdictLine = content.match(/^##\s*Verdict:\s*(.+)$/im);
if (!verdictLine) return null;
const verdictText = verdictLine[1]!.trim();
if (/^(?:APPROVED|PASSED)\b/i.test(verdictText)) {
return { verdict: 'passed', topBlocker: '' };
}
if (/^FAILED\b/i.test(verdictText)) {
return { verdict: 'failed', topBlocker: '' };
export function parseVerdictReport(content: string): ParsedVerdictReport | null {
const verdictLine = content.match(/^##\s*Verdict:\s*(.+)$/im);
if (!verdictLine) return null;
const verdictText = verdictLine[1]!.trim();
if (/^(?:APPROVED|PASSED)\s*$/i.test(verdictText)) {
return { verdict: 'passed', topBlocker: '' };
}
if (/^FAILED\s*$/i.test(verdictText)) {
return { verdict: 'failed', topBlocker: '' };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/cloister/review-verdict-report.ts` around lines 27 - 36, Update
parseVerdictReport so the APPROVED/PASSED and FAILED checks accept only verdict
text followed by whitespace or the end of the line, rejecting trailing
punctuation or additional text such as “— blocker” and “: draft”. Add tests
covering these invalid headings and preserve parsing for exact valid verdict
headings.

@eltmon

eltmon commented Jul 27, 2026

Copy link
Copy Markdown
Owner Author

Review CHANGES REQUESTED for PAN-3206

Recovered verdicts omit passed anchors, fail polyrepo HEAD matching, and can misroute or inject feedback commands

Required action

Fix every blocking review finding, commit the fixes, then re-request review with:

pan review request PAN-3206 -m "Fixed review issues"

Co-Authored-By: Claude <noreply@anthropic.com>
@eltmon
eltmon merged commit d4d143f into main Jul 27, 2026
9 checks passed
This was referenced Jul 28, 2026
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