fix(pr-followup): route ai-pr-reviewer CHANGES_REQUESTED reviews by structured findings - #630

Merged
joryirving merged 1 commit into
mainfrom
fix/pr-review-findings-lane-routing
Jul 17, 2026
Merged

fix(pr-followup): route ai-pr-reviewer CHANGES_REQUESTED reviews by structured findings#630
joryirving merged 1 commit into
mainfrom
fix/pr-review-findings-lane-routing

Conversation

@joryirving

Copy link
Copy Markdown
Contributor

Summary

  • CHANGES_REQUESTED reviews were routed to a lane by classifyFeedback() running keyword regexes over the review prose, defaulting to needs_human on no match. The ai-pr-reviewer writes long narrative markdown that matches none of those patterns, so every AI review fell through to needs_human → NEEDS_HUMAN lane → immediate human escalation, and the PR-fix auto-fix loop never engaged.
  • The reviewer already embeds a machine-readable verdict in the body as <!-- ai-pr-reviewer:{...} --> with open_findings. New parseAiReviewerFindings() reads it; the review path now routes to NORMAL (auto-fix) when findings carry messages, and only falls back to classifyFeedback for human prose reviews with no payload.
  • The PR_FIX_MAX_ATTEMPTS → ESCALATED → NEEDS_HUMAN ladder still handles "coder can't fix it" — same as the check_run path, which already hardcodes NORMAL with a comment warning against prose-classifying.

Verification

  • vitest run on pr-followup-ingestion, pr-followup/{sync,webhook}, pr-fix-queue: 73 passed. New tests: parseAiReviewerFindings (extract / ignore sibling markers / null on malformed); an ai-reviewer review with findings routes to NORMAL even though its prose classifies as needs_human; a payload-less vague review still routes to NEEDS_HUMAN.
  • tsc --noEmit and eslint clean.

Notes

  • Real-world trigger: misospace/miso-chat#691 — saffron correctly requested changes (blocker: no package upgrade performed), but it was bounced to a human instead of auto-fixed.
  • No change to the reviewer's output — this is consumer-side only. Human reviews are unaffected (prose classifier fallback preserved).

…tructured findings
A CHANGES_REQUESTED review's lane was chosen by classifyFeedback() running
keyword regexes over the review prose, defaulting to needs_human when nothing
matched. The ai-pr-reviewer writes long narrative markdown that matches none
of those patterns, so every AI review fell through to needs_human, was routed
to the NEEDS_HUMAN lane, and dead-ended at human escalation — the PR-fix
auto-fix loop never engaged (mirrors the check_run path's own warning against
prose-classifying).
The reviewer already embeds a machine-readable verdict in the body as
<!-- ai-pr-reviewer:{...} --> with open_findings. Parse it and, when it carries
findings with messages, route to NORMAL so the coder attempts a fix; the
PR_FIX_MAX_ATTEMPTS -> ESCALATED -> NEEDS_HUMAN ladder handles 'coder can't fix
it', exactly as for CI failures. Human prose reviews (no payload) still fall
back to classifyFeedback.
AI assistance: authored with Claude Code.

@its-saffronits-saffronBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AI Automated Review

Full PR review.

Analysis engine: MiniMax-M2.7@https://litellm.jory.dev/v1 (anthropic) — escalated (fast_low_confidence)

Review: Approve

This PR correctly fixes the routing bug where ai-pr-reviewer structured reviews were falling through to NEEDS_HUMAN instead of engaging the auto-fix loop.

Change-by-Change Findings

src/lib/pr-followup-ingestion.ts (+60/-1):

  1. parseAiReviewerFindings() — New function that extracts the machine-readable <!-- ai-pr-reviewer:{...json...} --> payload from review bodies. Handles the bare marker, sibling sibling sha/fingerprint comments, null/undefined/empty input, and malformed JSON by returning null. The regex /<!--\s*ai-pr-reviewer:\s*([\s\S]*?)-->/ is non-greedy (*?) on the JSON content, which is correct since JSON.parse won't consume --> as part of the value. JSDoc is thorough and explains the design rationale.

  2. reviewLane() — New lane-routing function for CHANGES_REQUESTED reviews. If the parsed payload contains any finding with a non-empty message, it returns NORMAL (actionable by the coder). Otherwise it falls back to laneFor() which uses classifyFeedback() on the prose. This is the correct priority: structured findings override prose classification.

  3. INGEST_DESCRIPTORS['REVIEW'] — Single-line change replacing lane: laneFor(event.body ?? "") with lane: reviewLane(event.body ?? ""). All other event types and lane paths are unchanged.

src/lib/pr-followup-ingestion.test.ts (+92/-0):

  1. parseAiReviewerFindings unit tests — Three cases: full JSON payload extraction, bare/sibling-comment markers returning null, and malformed/absent payload returning null.

  2. routes ai-pr-reviewer review with findings to NORMAL — Includes an inline guard assertion expect(classifyFeedback(body)).toBe("needs_human") proving the prose alone would dead-end, then verifies ingestReviewEvent returns lane: "NORMAL". This is the regression test for the real-world trigger (misospace/miso-chat#691).

  3. routes vague CHANGES_REQUESTED with no payload to NEEDS_HUMAN — Confirms the prose-classification fallback path is preserved when no structured payload exists.

Standards Compliance

ConventionStatus
Error handling (error instanceof Error pattern)✅ N/A — JSON.parse wrapped in try/catch with silent return null
API routes return appropriate HTTP status codes✅ N/A — library function only
Validation before DB operations✅ N/A — no DB writes
TypeScript interfaces for structured dataAiReviewerFinding, AiReviewerPayload defined
Exported for testingparseAiReviewerFindings explicitly exported
JSDoc comments for public functions✅ Full JSDoc on parseAiReviewerFindings, reviewLane

No agent-specific names in generic docs, no secrets committed, no schema changes, no auth changes. Fully compliant.

CI Verification

  • Typecheck: ✅ success
  • Lint: ✅ success
  • Tests: ✅ success (73 passed including new tests)
  • Build: ✅ success
  • Docker Build: ✅ success

All CI checks passed. No blockers.

Tool Harness Findings

No tool calls issued; reviewed corpus directly. No findings.

@joryirving
joryirving merged commit 853d3f4 into mainJul 17, 2026
6 checks passed
@joryirving
joryirving deleted the fix/pr-review-findings-lane-routing branch July 17, 2026 02:45
Sign up for freeto 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

@joryirving
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(pr-followup): route ai-pr-reviewer CHANGES_REQUESTED reviews by structured findings - #630

Merged
joryirving merged 1 commit into
mainfrom
fix/pr-review-findings-lane-routing
Jul 17, 2026
Merged

fix(pr-followup): route ai-pr-reviewer CHANGES_REQUESTED reviews by structured findings#630
joryirving merged 1 commit into
mainfrom
fix/pr-review-findings-lane-routing

Conversation

@joryirving

Copy link
Copy Markdown
Contributor

Summary

  • CHANGES_REQUESTED reviews were routed to a lane by classifyFeedback() running keyword regexes over the review prose, defaulting to needs_human on no match. The ai-pr-reviewer writes long narrative markdown that matches none of those patterns, so every AI review fell through to needs_human → NEEDS_HUMAN lane → immediate human escalation, and the PR-fix auto-fix loop never engaged.
  • The reviewer already embeds a machine-readable verdict in the body as <!-- ai-pr-reviewer:{...} --> with open_findings. New parseAiReviewerFindings() reads it; the review path now routes to NORMAL (auto-fix) when findings carry messages, and only falls back to classifyFeedback for human prose reviews with no payload.
  • The PR_FIX_MAX_ATTEMPTS → ESCALATED → NEEDS_HUMAN ladder still handles "coder can't fix it" — same as the check_run path, which already hardcodes NORMAL with a comment warning against prose-classifying.

Verification

  • vitest run on pr-followup-ingestion, pr-followup/{sync,webhook}, pr-fix-queue: 73 passed. New tests: parseAiReviewerFindings (extract / ignore sibling markers / null on malformed); an ai-reviewer review with findings routes to NORMAL even though its prose classifies as needs_human; a payload-less vague review still routes to NEEDS_HUMAN.
  • tsc --noEmit and eslint clean.

Notes

  • Real-world trigger: misospace/miso-chat#691 — saffron correctly requested changes (blocker: no package upgrade performed), but it was bounced to a human instead of auto-fixed.
  • No change to the reviewer's output — this is consumer-side only. Human reviews are unaffected (prose classifier fallback preserved).

…tructured findings
A CHANGES_REQUESTED review's lane was chosen by classifyFeedback() running
keyword regexes over the review prose, defaulting to needs_human when nothing
matched. The ai-pr-reviewer writes long narrative markdown that matches none
of those patterns, so every AI review fell through to needs_human, was routed
to the NEEDS_HUMAN lane, and dead-ended at human escalation — the PR-fix
auto-fix loop never engaged (mirrors the check_run path's own warning against
prose-classifying).
The reviewer already embeds a machine-readable verdict in the body as
<!-- ai-pr-reviewer:{...} --> with open_findings. Parse it and, when it carries
findings with messages, route to NORMAL so the coder attempts a fix; the
PR_FIX_MAX_ATTEMPTS -> ESCALATED -> NEEDS_HUMAN ladder handles 'coder can't fix
it', exactly as for CI failures. Human prose reviews (no payload) still fall
back to classifyFeedback.
AI assistance: authored with Claude Code.

@its-saffronits-saffronBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AI Automated Review

Full PR review.

Analysis engine: MiniMax-M2.7@https://litellm.jory.dev/v1 (anthropic) — escalated (fast_low_confidence)

Review: Approve

This PR correctly fixes the routing bug where ai-pr-reviewer structured reviews were falling through to NEEDS_HUMAN instead of engaging the auto-fix loop.

Change-by-Change Findings

src/lib/pr-followup-ingestion.ts (+60/-1):

  1. parseAiReviewerFindings() — New function that extracts the machine-readable <!-- ai-pr-reviewer:{...json...} --> payload from review bodies. Handles the bare marker, sibling sibling sha/fingerprint comments, null/undefined/empty input, and malformed JSON by returning null. The regex /<!--\s*ai-pr-reviewer:\s*([\s\S]*?)-->/ is non-greedy (*?) on the JSON content, which is correct since JSON.parse won't consume --> as part of the value. JSDoc is thorough and explains the design rationale.

  2. reviewLane() — New lane-routing function for CHANGES_REQUESTED reviews. If the parsed payload contains any finding with a non-empty message, it returns NORMAL (actionable by the coder). Otherwise it falls back to laneFor() which uses classifyFeedback() on the prose. This is the correct priority: structured findings override prose classification.

  3. INGEST_DESCRIPTORS['REVIEW'] — Single-line change replacing lane: laneFor(event.body ?? "") with lane: reviewLane(event.body ?? ""). All other event types and lane paths are unchanged.

src/lib/pr-followup-ingestion.test.ts (+92/-0):

  1. parseAiReviewerFindings unit tests — Three cases: full JSON payload extraction, bare/sibling-comment markers returning null, and malformed/absent payload returning null.

  2. routes ai-pr-reviewer review with findings to NORMAL — Includes an inline guard assertion expect(classifyFeedback(body)).toBe("needs_human") proving the prose alone would dead-end, then verifies ingestReviewEvent returns lane: "NORMAL". This is the regression test for the real-world trigger (misospace/miso-chat#691).

  3. routes vague CHANGES_REQUESTED with no payload to NEEDS_HUMAN — Confirms the prose-classification fallback path is preserved when no structured payload exists.

Standards Compliance

ConventionStatus
Error handling (error instanceof Error pattern)✅ N/A — JSON.parse wrapped in try/catch with silent return null
API routes return appropriate HTTP status codes✅ N/A — library function only
Validation before DB operations✅ N/A — no DB writes
TypeScript interfaces for structured dataAiReviewerFinding, AiReviewerPayload defined
Exported for testingparseAiReviewerFindings explicitly exported
JSDoc comments for public functions✅ Full JSDoc on parseAiReviewerFindings, reviewLane

No agent-specific names in generic docs, no secrets committed, no schema changes, no auth changes. Fully compliant.

CI Verification

  • Typecheck: ✅ success
  • Lint: ✅ success
  • Tests: ✅ success (73 passed including new tests)
  • Build: ✅ success
  • Docker Build: ✅ success

All CI checks passed. No blockers.

Tool Harness Findings

No tool calls issued; reviewed corpus directly. No findings.

@joryirving
joryirving merged commit 853d3f4 into mainJul 17, 2026
6 checks passed
@joryirving
joryirving deleted the fix/pr-review-findings-lane-routing branch July 17, 2026 02:45
Sign up for freeto 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

@joryirving
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(pr-followup): route ai-pr-reviewer CHANGES_REQUESTED reviews by structured findings - #630

Merged
joryirving merged 1 commit into
mainfrom
fix/pr-review-findings-lane-routing
Jul 17, 2026
Merged

fix(pr-followup): route ai-pr-reviewer CHANGES_REQUESTED reviews by structured findings#630
joryirving merged 1 commit into
mainfrom
fix/pr-review-findings-lane-routing

Conversation

@joryirving

Copy link
Copy Markdown
Contributor

Summary

  • CHANGES_REQUESTED reviews were routed to a lane by classifyFeedback() running keyword regexes over the review prose, defaulting to needs_human on no match. The ai-pr-reviewer writes long narrative markdown that matches none of those patterns, so every AI review fell through to needs_human → NEEDS_HUMAN lane → immediate human escalation, and the PR-fix auto-fix loop never engaged.
  • The reviewer already embeds a machine-readable verdict in the body as <!-- ai-pr-reviewer:{...} --> with open_findings. New parseAiReviewerFindings() reads it; the review path now routes to NORMAL (auto-fix) when findings carry messages, and only falls back to classifyFeedback for human prose reviews with no payload.
  • The PR_FIX_MAX_ATTEMPTS → ESCALATED → NEEDS_HUMAN ladder still handles "coder can't fix it" — same as the check_run path, which already hardcodes NORMAL with a comment warning against prose-classifying.

Verification

  • vitest run on pr-followup-ingestion, pr-followup/{sync,webhook}, pr-fix-queue: 73 passed. New tests: parseAiReviewerFindings (extract / ignore sibling markers / null on malformed); an ai-reviewer review with findings routes to NORMAL even though its prose classifies as needs_human; a payload-less vague review still routes to NEEDS_HUMAN.
  • tsc --noEmit and eslint clean.

Notes

  • Real-world trigger: misospace/miso-chat#691 — saffron correctly requested changes (blocker: no package upgrade performed), but it was bounced to a human instead of auto-fixed.
  • No change to the reviewer's output — this is consumer-side only. Human reviews are unaffected (prose classifier fallback preserved).

…tructured findings
A CHANGES_REQUESTED review's lane was chosen by classifyFeedback() running
keyword regexes over the review prose, defaulting to needs_human when nothing
matched. The ai-pr-reviewer writes long narrative markdown that matches none
of those patterns, so every AI review fell through to needs_human, was routed
to the NEEDS_HUMAN lane, and dead-ended at human escalation — the PR-fix
auto-fix loop never engaged (mirrors the check_run path's own warning against
prose-classifying).
The reviewer already embeds a machine-readable verdict in the body as
<!-- ai-pr-reviewer:{...} --> with open_findings. Parse it and, when it carries
findings with messages, route to NORMAL so the coder attempts a fix; the
PR_FIX_MAX_ATTEMPTS -> ESCALATED -> NEEDS_HUMAN ladder handles 'coder can't fix
it', exactly as for CI failures. Human prose reviews (no payload) still fall
back to classifyFeedback.
AI assistance: authored with Claude Code.

@its-saffronits-saffronBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AI Automated Review

Full PR review.

Analysis engine: MiniMax-M2.7@https://litellm.jory.dev/v1 (anthropic) — escalated (fast_low_confidence)

Review: Approve

This PR correctly fixes the routing bug where ai-pr-reviewer structured reviews were falling through to NEEDS_HUMAN instead of engaging the auto-fix loop.

Change-by-Change Findings

src/lib/pr-followup-ingestion.ts (+60/-1):

  1. parseAiReviewerFindings() — New function that extracts the machine-readable <!-- ai-pr-reviewer:{...json...} --> payload from review bodies. Handles the bare marker, sibling sibling sha/fingerprint comments, null/undefined/empty input, and malformed JSON by returning null. The regex /<!--\s*ai-pr-reviewer:\s*([\s\S]*?)-->/ is non-greedy (*?) on the JSON content, which is correct since JSON.parse won't consume --> as part of the value. JSDoc is thorough and explains the design rationale.

  2. reviewLane() — New lane-routing function for CHANGES_REQUESTED reviews. If the parsed payload contains any finding with a non-empty message, it returns NORMAL (actionable by the coder). Otherwise it falls back to laneFor() which uses classifyFeedback() on the prose. This is the correct priority: structured findings override prose classification.

  3. INGEST_DESCRIPTORS['REVIEW'] — Single-line change replacing lane: laneFor(event.body ?? "") with lane: reviewLane(event.body ?? ""). All other event types and lane paths are unchanged.

src/lib/pr-followup-ingestion.test.ts (+92/-0):

  1. parseAiReviewerFindings unit tests — Three cases: full JSON payload extraction, bare/sibling-comment markers returning null, and malformed/absent payload returning null.

  2. routes ai-pr-reviewer review with findings to NORMAL — Includes an inline guard assertion expect(classifyFeedback(body)).toBe("needs_human") proving the prose alone would dead-end, then verifies ingestReviewEvent returns lane: "NORMAL". This is the regression test for the real-world trigger (misospace/miso-chat#691).

  3. routes vague CHANGES_REQUESTED with no payload to NEEDS_HUMAN — Confirms the prose-classification fallback path is preserved when no structured payload exists.

Standards Compliance

ConventionStatus
Error handling (error instanceof Error pattern)✅ N/A — JSON.parse wrapped in try/catch with silent return null
API routes return appropriate HTTP status codes✅ N/A — library function only
Validation before DB operations✅ N/A — no DB writes
TypeScript interfaces for structured dataAiReviewerFinding, AiReviewerPayload defined
Exported for testingparseAiReviewerFindings explicitly exported
JSDoc comments for public functions✅ Full JSDoc on parseAiReviewerFindings, reviewLane

No agent-specific names in generic docs, no secrets committed, no schema changes, no auth changes. Fully compliant.

CI Verification

  • Typecheck: ✅ success
  • Lint: ✅ success
  • Tests: ✅ success (73 passed including new tests)
  • Build: ✅ success
  • Docker Build: ✅ success

All CI checks passed. No blockers.

Tool Harness Findings

No tool calls issued; reviewed corpus directly. No findings.

@joryirving
joryirving merged commit 853d3f4 into mainJul 17, 2026
6 checks passed
@joryirving
joryirving deleted the fix/pr-review-findings-lane-routing branch July 17, 2026 02:45
Sign up for freeto 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

@joryirving
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(pr-followup): route ai-pr-reviewer CHANGES_REQUESTED reviews by structured findings - #630

Merged
joryirving merged 1 commit into
mainfrom
fix/pr-review-findings-lane-routing
Jul 17, 2026
Merged

fix(pr-followup): route ai-pr-reviewer CHANGES_REQUESTED reviews by structured findings#630
joryirving merged 1 commit into
mainfrom
fix/pr-review-findings-lane-routing

Conversation

@joryirving

Copy link
Copy Markdown
Contributor

Summary

  • CHANGES_REQUESTED reviews were routed to a lane by classifyFeedback() running keyword regexes over the review prose, defaulting to needs_human on no match. The ai-pr-reviewer writes long narrative markdown that matches none of those patterns, so every AI review fell through to needs_human → NEEDS_HUMAN lane → immediate human escalation, and the PR-fix auto-fix loop never engaged.
  • The reviewer already embeds a machine-readable verdict in the body as <!-- ai-pr-reviewer:{...} --> with open_findings. New parseAiReviewerFindings() reads it; the review path now routes to NORMAL (auto-fix) when findings carry messages, and only falls back to classifyFeedback for human prose reviews with no payload.
  • The PR_FIX_MAX_ATTEMPTS → ESCALATED → NEEDS_HUMAN ladder still handles "coder can't fix it" — same as the check_run path, which already hardcodes NORMAL with a comment warning against prose-classifying.

Verification

  • vitest run on pr-followup-ingestion, pr-followup/{sync,webhook}, pr-fix-queue: 73 passed. New tests: parseAiReviewerFindings (extract / ignore sibling markers / null on malformed); an ai-reviewer review with findings routes to NORMAL even though its prose classifies as needs_human; a payload-less vague review still routes to NEEDS_HUMAN.
  • tsc --noEmit and eslint clean.

Notes

  • Real-world trigger: misospace/miso-chat#691 — saffron correctly requested changes (blocker: no package upgrade performed), but it was bounced to a human instead of auto-fixed.
  • No change to the reviewer's output — this is consumer-side only. Human reviews are unaffected (prose classifier fallback preserved).

…tructured findings
A CHANGES_REQUESTED review's lane was chosen by classifyFeedback() running
keyword regexes over the review prose, defaulting to needs_human when nothing
matched. The ai-pr-reviewer writes long narrative markdown that matches none
of those patterns, so every AI review fell through to needs_human, was routed
to the NEEDS_HUMAN lane, and dead-ended at human escalation — the PR-fix
auto-fix loop never engaged (mirrors the check_run path's own warning against
prose-classifying).
The reviewer already embeds a machine-readable verdict in the body as
<!-- ai-pr-reviewer:{...} --> with open_findings. Parse it and, when it carries
findings with messages, route to NORMAL so the coder attempts a fix; the
PR_FIX_MAX_ATTEMPTS -> ESCALATED -> NEEDS_HUMAN ladder handles 'coder can't fix
it', exactly as for CI failures. Human prose reviews (no payload) still fall
back to classifyFeedback.
AI assistance: authored with Claude Code.

@its-saffronits-saffronBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AI Automated Review

Full PR review.

Analysis engine: MiniMax-M2.7@https://litellm.jory.dev/v1 (anthropic) — escalated (fast_low_confidence)

Review: Approve

This PR correctly fixes the routing bug where ai-pr-reviewer structured reviews were falling through to NEEDS_HUMAN instead of engaging the auto-fix loop.

Change-by-Change Findings

src/lib/pr-followup-ingestion.ts (+60/-1):

  1. parseAiReviewerFindings() — New function that extracts the machine-readable <!-- ai-pr-reviewer:{...json...} --> payload from review bodies. Handles the bare marker, sibling sibling sha/fingerprint comments, null/undefined/empty input, and malformed JSON by returning null. The regex /<!--\s*ai-pr-reviewer:\s*([\s\S]*?)-->/ is non-greedy (*?) on the JSON content, which is correct since JSON.parse won't consume --> as part of the value. JSDoc is thorough and explains the design rationale.

  2. reviewLane() — New lane-routing function for CHANGES_REQUESTED reviews. If the parsed payload contains any finding with a non-empty message, it returns NORMAL (actionable by the coder). Otherwise it falls back to laneFor() which uses classifyFeedback() on the prose. This is the correct priority: structured findings override prose classification.

  3. INGEST_DESCRIPTORS['REVIEW'] — Single-line change replacing lane: laneFor(event.body ?? "") with lane: reviewLane(event.body ?? ""). All other event types and lane paths are unchanged.

src/lib/pr-followup-ingestion.test.ts (+92/-0):

  1. parseAiReviewerFindings unit tests — Three cases: full JSON payload extraction, bare/sibling-comment markers returning null, and malformed/absent payload returning null.

  2. routes ai-pr-reviewer review with findings to NORMAL — Includes an inline guard assertion expect(classifyFeedback(body)).toBe("needs_human") proving the prose alone would dead-end, then verifies ingestReviewEvent returns lane: "NORMAL". This is the regression test for the real-world trigger (misospace/miso-chat#691).

  3. routes vague CHANGES_REQUESTED with no payload to NEEDS_HUMAN — Confirms the prose-classification fallback path is preserved when no structured payload exists.

Standards Compliance

ConventionStatus
Error handling (error instanceof Error pattern)✅ N/A — JSON.parse wrapped in try/catch with silent return null
API routes return appropriate HTTP status codes✅ N/A — library function only
Validation before DB operations✅ N/A — no DB writes
TypeScript interfaces for structured dataAiReviewerFinding, AiReviewerPayload defined
Exported for testingparseAiReviewerFindings explicitly exported
JSDoc comments for public functions✅ Full JSDoc on parseAiReviewerFindings, reviewLane

No agent-specific names in generic docs, no secrets committed, no schema changes, no auth changes. Fully compliant.

CI Verification

  • Typecheck: ✅ success
  • Lint: ✅ success
  • Tests: ✅ success (73 passed including new tests)
  • Build: ✅ success
  • Docker Build: ✅ success

All CI checks passed. No blockers.

Tool Harness Findings

No tool calls issued; reviewed corpus directly. No findings.

@joryirving
joryirving merged commit 853d3f4 into mainJul 17, 2026
6 checks passed
@joryirving
joryirving deleted the fix/pr-review-findings-lane-routing branch July 17, 2026 02:45
Sign up for freeto 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

@joryirving
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(pr-followup): route ai-pr-reviewer CHANGES_REQUESTED reviews by structured findings - #630

Merged
joryirving merged 1 commit into
mainfrom
fix/pr-review-findings-lane-routing
Jul 17, 2026
Merged

fix(pr-followup): route ai-pr-reviewer CHANGES_REQUESTED reviews by structured findings#630
joryirving merged 1 commit into
mainfrom
fix/pr-review-findings-lane-routing

Conversation

@joryirving

Copy link
Copy Markdown
Contributor

Summary

  • CHANGES_REQUESTED reviews were routed to a lane by classifyFeedback() running keyword regexes over the review prose, defaulting to needs_human on no match. The ai-pr-reviewer writes long narrative markdown that matches none of those patterns, so every AI review fell through to needs_human → NEEDS_HUMAN lane → immediate human escalation, and the PR-fix auto-fix loop never engaged.
  • The reviewer already embeds a machine-readable verdict in the body as <!-- ai-pr-reviewer:{...} --> with open_findings. New parseAiReviewerFindings() reads it; the review path now routes to NORMAL (auto-fix) when findings carry messages, and only falls back to classifyFeedback for human prose reviews with no payload.
  • The PR_FIX_MAX_ATTEMPTS → ESCALATED → NEEDS_HUMAN ladder still handles "coder can't fix it" — same as the check_run path, which already hardcodes NORMAL with a comment warning against prose-classifying.

Verification

  • vitest run on pr-followup-ingestion, pr-followup/{sync,webhook}, pr-fix-queue: 73 passed. New tests: parseAiReviewerFindings (extract / ignore sibling markers / null on malformed); an ai-reviewer review with findings routes to NORMAL even though its prose classifies as needs_human; a payload-less vague review still routes to NEEDS_HUMAN.
  • tsc --noEmit and eslint clean.

Notes

  • Real-world trigger: misospace/miso-chat#691 — saffron correctly requested changes (blocker: no package upgrade performed), but it was bounced to a human instead of auto-fixed.
  • No change to the reviewer's output — this is consumer-side only. Human reviews are unaffected (prose classifier fallback preserved).

…tructured findings
A CHANGES_REQUESTED review's lane was chosen by classifyFeedback() running
keyword regexes over the review prose, defaulting to needs_human when nothing
matched. The ai-pr-reviewer writes long narrative markdown that matches none
of those patterns, so every AI review fell through to needs_human, was routed
to the NEEDS_HUMAN lane, and dead-ended at human escalation — the PR-fix
auto-fix loop never engaged (mirrors the check_run path's own warning against
prose-classifying).
The reviewer already embeds a machine-readable verdict in the body as
<!-- ai-pr-reviewer:{...} --> with open_findings. Parse it and, when it carries
findings with messages, route to NORMAL so the coder attempts a fix; the
PR_FIX_MAX_ATTEMPTS -> ESCALATED -> NEEDS_HUMAN ladder handles 'coder can't fix
it', exactly as for CI failures. Human prose reviews (no payload) still fall
back to classifyFeedback.
AI assistance: authored with Claude Code.

@its-saffronits-saffronBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AI Automated Review

Full PR review.

Analysis engine: MiniMax-M2.7@https://litellm.jory.dev/v1 (anthropic) — escalated (fast_low_confidence)

Review: Approve

This PR correctly fixes the routing bug where ai-pr-reviewer structured reviews were falling through to NEEDS_HUMAN instead of engaging the auto-fix loop.

Change-by-Change Findings

src/lib/pr-followup-ingestion.ts (+60/-1):

  1. parseAiReviewerFindings() — New function that extracts the machine-readable <!-- ai-pr-reviewer:{...json...} --> payload from review bodies. Handles the bare marker, sibling sibling sha/fingerprint comments, null/undefined/empty input, and malformed JSON by returning null. The regex /<!--\s*ai-pr-reviewer:\s*([\s\S]*?)-->/ is non-greedy (*?) on the JSON content, which is correct since JSON.parse won't consume --> as part of the value. JSDoc is thorough and explains the design rationale.

  2. reviewLane() — New lane-routing function for CHANGES_REQUESTED reviews. If the parsed payload contains any finding with a non-empty message, it returns NORMAL (actionable by the coder). Otherwise it falls back to laneFor() which uses classifyFeedback() on the prose. This is the correct priority: structured findings override prose classification.

  3. INGEST_DESCRIPTORS['REVIEW'] — Single-line change replacing lane: laneFor(event.body ?? "") with lane: reviewLane(event.body ?? ""). All other event types and lane paths are unchanged.

src/lib/pr-followup-ingestion.test.ts (+92/-0):

  1. parseAiReviewerFindings unit tests — Three cases: full JSON payload extraction, bare/sibling-comment markers returning null, and malformed/absent payload returning null.

  2. routes ai-pr-reviewer review with findings to NORMAL — Includes an inline guard assertion expect(classifyFeedback(body)).toBe("needs_human") proving the prose alone would dead-end, then verifies ingestReviewEvent returns lane: "NORMAL". This is the regression test for the real-world trigger (misospace/miso-chat#691).

  3. routes vague CHANGES_REQUESTED with no payload to NEEDS_HUMAN — Confirms the prose-classification fallback path is preserved when no structured payload exists.

Standards Compliance

ConventionStatus
Error handling (error instanceof Error pattern)✅ N/A — JSON.parse wrapped in try/catch with silent return null
API routes return appropriate HTTP status codes✅ N/A — library function only
Validation before DB operations✅ N/A — no DB writes
TypeScript interfaces for structured dataAiReviewerFinding, AiReviewerPayload defined
Exported for testingparseAiReviewerFindings explicitly exported
JSDoc comments for public functions✅ Full JSDoc on parseAiReviewerFindings, reviewLane

No agent-specific names in generic docs, no secrets committed, no schema changes, no auth changes. Fully compliant.

CI Verification

  • Typecheck: ✅ success
  • Lint: ✅ success
  • Tests: ✅ success (73 passed including new tests)
  • Build: ✅ success
  • Docker Build: ✅ success

All CI checks passed. No blockers.

Tool Harness Findings

No tool calls issued; reviewed corpus directly. No findings.

@joryirving
joryirving merged commit 853d3f4 into mainJul 17, 2026
6 checks passed
@joryirving
joryirving deleted the fix/pr-review-findings-lane-routing branch July 17, 2026 02:45
Sign up for freeto 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

@joryirving
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(pr-followup): route ai-pr-reviewer CHANGES_REQUESTED reviews by structured findings - #630

Merged
joryirving merged 1 commit into
mainfrom
fix/pr-review-findings-lane-routing
Jul 17, 2026
Merged

fix(pr-followup): route ai-pr-reviewer CHANGES_REQUESTED reviews by structured findings#630
joryirving merged 1 commit into
mainfrom
fix/pr-review-findings-lane-routing

Conversation

@joryirving

Copy link
Copy Markdown
Contributor

Summary

  • CHANGES_REQUESTED reviews were routed to a lane by classifyFeedback() running keyword regexes over the review prose, defaulting to needs_human on no match. The ai-pr-reviewer writes long narrative markdown that matches none of those patterns, so every AI review fell through to needs_human → NEEDS_HUMAN lane → immediate human escalation, and the PR-fix auto-fix loop never engaged.
  • The reviewer already embeds a machine-readable verdict in the body as <!-- ai-pr-reviewer:{...} --> with open_findings. New parseAiReviewerFindings() reads it; the review path now routes to NORMAL (auto-fix) when findings carry messages, and only falls back to classifyFeedback for human prose reviews with no payload.
  • The PR_FIX_MAX_ATTEMPTS → ESCALATED → NEEDS_HUMAN ladder still handles "coder can't fix it" — same as the check_run path, which already hardcodes NORMAL with a comment warning against prose-classifying.

Verification

  • vitest run on pr-followup-ingestion, pr-followup/{sync,webhook}, pr-fix-queue: 73 passed. New tests: parseAiReviewerFindings (extract / ignore sibling markers / null on malformed); an ai-reviewer review with findings routes to NORMAL even though its prose classifies as needs_human; a payload-less vague review still routes to NEEDS_HUMAN.
  • tsc --noEmit and eslint clean.

Notes

  • Real-world trigger: misospace/miso-chat#691 — saffron correctly requested changes (blocker: no package upgrade performed), but it was bounced to a human instead of auto-fixed.
  • No change to the reviewer's output — this is consumer-side only. Human reviews are unaffected (prose classifier fallback preserved).

…tructured findings
A CHANGES_REQUESTED review's lane was chosen by classifyFeedback() running
keyword regexes over the review prose, defaulting to needs_human when nothing
matched. The ai-pr-reviewer writes long narrative markdown that matches none
of those patterns, so every AI review fell through to needs_human, was routed
to the NEEDS_HUMAN lane, and dead-ended at human escalation — the PR-fix
auto-fix loop never engaged (mirrors the check_run path's own warning against
prose-classifying).
The reviewer already embeds a machine-readable verdict in the body as
<!-- ai-pr-reviewer:{...} --> with open_findings. Parse it and, when it carries
findings with messages, route to NORMAL so the coder attempts a fix; the
PR_FIX_MAX_ATTEMPTS -> ESCALATED -> NEEDS_HUMAN ladder handles 'coder can't fix
it', exactly as for CI failures. Human prose reviews (no payload) still fall
back to classifyFeedback.
AI assistance: authored with Claude Code.

@its-saffronits-saffronBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AI Automated Review

Full PR review.

Analysis engine: MiniMax-M2.7@https://litellm.jory.dev/v1 (anthropic) — escalated (fast_low_confidence)

Review: Approve

This PR correctly fixes the routing bug where ai-pr-reviewer structured reviews were falling through to NEEDS_HUMAN instead of engaging the auto-fix loop.

Change-by-Change Findings

src/lib/pr-followup-ingestion.ts (+60/-1):

  1. parseAiReviewerFindings() — New function that extracts the machine-readable <!-- ai-pr-reviewer:{...json...} --> payload from review bodies. Handles the bare marker, sibling sibling sha/fingerprint comments, null/undefined/empty input, and malformed JSON by returning null. The regex /<!--\s*ai-pr-reviewer:\s*([\s\S]*?)-->/ is non-greedy (*?) on the JSON content, which is correct since JSON.parse won't consume --> as part of the value. JSDoc is thorough and explains the design rationale.

  2. reviewLane() — New lane-routing function for CHANGES_REQUESTED reviews. If the parsed payload contains any finding with a non-empty message, it returns NORMAL (actionable by the coder). Otherwise it falls back to laneFor() which uses classifyFeedback() on the prose. This is the correct priority: structured findings override prose classification.

  3. INGEST_DESCRIPTORS['REVIEW'] — Single-line change replacing lane: laneFor(event.body ?? "") with lane: reviewLane(event.body ?? ""). All other event types and lane paths are unchanged.

src/lib/pr-followup-ingestion.test.ts (+92/-0):

  1. parseAiReviewerFindings unit tests — Three cases: full JSON payload extraction, bare/sibling-comment markers returning null, and malformed/absent payload returning null.

  2. routes ai-pr-reviewer review with findings to NORMAL — Includes an inline guard assertion expect(classifyFeedback(body)).toBe("needs_human") proving the prose alone would dead-end, then verifies ingestReviewEvent returns lane: "NORMAL". This is the regression test for the real-world trigger (misospace/miso-chat#691).

  3. routes vague CHANGES_REQUESTED with no payload to NEEDS_HUMAN — Confirms the prose-classification fallback path is preserved when no structured payload exists.

Standards Compliance

ConventionStatus
Error handling (error instanceof Error pattern)✅ N/A — JSON.parse wrapped in try/catch with silent return null
API routes return appropriate HTTP status codes✅ N/A — library function only
Validation before DB operations✅ N/A — no DB writes
TypeScript interfaces for structured dataAiReviewerFinding, AiReviewerPayload defined
Exported for testingparseAiReviewerFindings explicitly exported
JSDoc comments for public functions✅ Full JSDoc on parseAiReviewerFindings, reviewLane

No agent-specific names in generic docs, no secrets committed, no schema changes, no auth changes. Fully compliant.

CI Verification

  • Typecheck: ✅ success
  • Lint: ✅ success
  • Tests: ✅ success (73 passed including new tests)
  • Build: ✅ success
  • Docker Build: ✅ success

All CI checks passed. No blockers.

Tool Harness Findings

No tool calls issued; reviewed corpus directly. No findings.

@joryirving
joryirving merged commit 853d3f4 into mainJul 17, 2026
6 checks passed
@joryirving
joryirving deleted the fix/pr-review-findings-lane-routing branch July 17, 2026 02:45
Sign up for freeto 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

@joryirving
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(pr-followup): route ai-pr-reviewer CHANGES_REQUESTED reviews by structured findings - #630

Merged
joryirving merged 1 commit into
mainfrom
fix/pr-review-findings-lane-routing
Jul 17, 2026
Merged

fix(pr-followup): route ai-pr-reviewer CHANGES_REQUESTED reviews by structured findings#630
joryirving merged 1 commit into
mainfrom
fix/pr-review-findings-lane-routing

Conversation

@joryirving

Copy link
Copy Markdown
Contributor

Summary

  • CHANGES_REQUESTED reviews were routed to a lane by classifyFeedback() running keyword regexes over the review prose, defaulting to needs_human on no match. The ai-pr-reviewer writes long narrative markdown that matches none of those patterns, so every AI review fell through to needs_human → NEEDS_HUMAN lane → immediate human escalation, and the PR-fix auto-fix loop never engaged.
  • The reviewer already embeds a machine-readable verdict in the body as <!-- ai-pr-reviewer:{...} --> with open_findings. New parseAiReviewerFindings() reads it; the review path now routes to NORMAL (auto-fix) when findings carry messages, and only falls back to classifyFeedback for human prose reviews with no payload.
  • The PR_FIX_MAX_ATTEMPTS → ESCALATED → NEEDS_HUMAN ladder still handles "coder can't fix it" — same as the check_run path, which already hardcodes NORMAL with a comment warning against prose-classifying.

Verification

  • vitest run on pr-followup-ingestion, pr-followup/{sync,webhook}, pr-fix-queue: 73 passed. New tests: parseAiReviewerFindings (extract / ignore sibling markers / null on malformed); an ai-reviewer review with findings routes to NORMAL even though its prose classifies as needs_human; a payload-less vague review still routes to NEEDS_HUMAN.
  • tsc --noEmit and eslint clean.

Notes

  • Real-world trigger: misospace/miso-chat#691 — saffron correctly requested changes (blocker: no package upgrade performed), but it was bounced to a human instead of auto-fixed.
  • No change to the reviewer's output — this is consumer-side only. Human reviews are unaffected (prose classifier fallback preserved).

…tructured findings
A CHANGES_REQUESTED review's lane was chosen by classifyFeedback() running
keyword regexes over the review prose, defaulting to needs_human when nothing
matched. The ai-pr-reviewer writes long narrative markdown that matches none
of those patterns, so every AI review fell through to needs_human, was routed
to the NEEDS_HUMAN lane, and dead-ended at human escalation — the PR-fix
auto-fix loop never engaged (mirrors the check_run path's own warning against
prose-classifying).
The reviewer already embeds a machine-readable verdict in the body as
<!-- ai-pr-reviewer:{...} --> with open_findings. Parse it and, when it carries
findings with messages, route to NORMAL so the coder attempts a fix; the
PR_FIX_MAX_ATTEMPTS -> ESCALATED -> NEEDS_HUMAN ladder handles 'coder can't fix
it', exactly as for CI failures. Human prose reviews (no payload) still fall
back to classifyFeedback.
AI assistance: authored with Claude Code.

@its-saffronits-saffronBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AI Automated Review

Full PR review.

Analysis engine: MiniMax-M2.7@https://litellm.jory.dev/v1 (anthropic) — escalated (fast_low_confidence)

Review: Approve

This PR correctly fixes the routing bug where ai-pr-reviewer structured reviews were falling through to NEEDS_HUMAN instead of engaging the auto-fix loop.

Change-by-Change Findings

src/lib/pr-followup-ingestion.ts (+60/-1):

  1. parseAiReviewerFindings() — New function that extracts the machine-readable <!-- ai-pr-reviewer:{...json...} --> payload from review bodies. Handles the bare marker, sibling sibling sha/fingerprint comments, null/undefined/empty input, and malformed JSON by returning null. The regex /<!--\s*ai-pr-reviewer:\s*([\s\S]*?)-->/ is non-greedy (*?) on the JSON content, which is correct since JSON.parse won't consume --> as part of the value. JSDoc is thorough and explains the design rationale.

  2. reviewLane() — New lane-routing function for CHANGES_REQUESTED reviews. If the parsed payload contains any finding with a non-empty message, it returns NORMAL (actionable by the coder). Otherwise it falls back to laneFor() which uses classifyFeedback() on the prose. This is the correct priority: structured findings override prose classification.

  3. INGEST_DESCRIPTORS['REVIEW'] — Single-line change replacing lane: laneFor(event.body ?? "") with lane: reviewLane(event.body ?? ""). All other event types and lane paths are unchanged.

src/lib/pr-followup-ingestion.test.ts (+92/-0):

  1. parseAiReviewerFindings unit tests — Three cases: full JSON payload extraction, bare/sibling-comment markers returning null, and malformed/absent payload returning null.

  2. routes ai-pr-reviewer review with findings to NORMAL — Includes an inline guard assertion expect(classifyFeedback(body)).toBe("needs_human") proving the prose alone would dead-end, then verifies ingestReviewEvent returns lane: "NORMAL". This is the regression test for the real-world trigger (misospace/miso-chat#691).

  3. routes vague CHANGES_REQUESTED with no payload to NEEDS_HUMAN — Confirms the prose-classification fallback path is preserved when no structured payload exists.

Standards Compliance

ConventionStatus
Error handling (error instanceof Error pattern)✅ N/A — JSON.parse wrapped in try/catch with silent return null
API routes return appropriate HTTP status codes✅ N/A — library function only
Validation before DB operations✅ N/A — no DB writes
TypeScript interfaces for structured dataAiReviewerFinding, AiReviewerPayload defined
Exported for testingparseAiReviewerFindings explicitly exported
JSDoc comments for public functions✅ Full JSDoc on parseAiReviewerFindings, reviewLane

No agent-specific names in generic docs, no secrets committed, no schema changes, no auth changes. Fully compliant.

CI Verification

  • Typecheck: ✅ success
  • Lint: ✅ success
  • Tests: ✅ success (73 passed including new tests)
  • Build: ✅ success
  • Docker Build: ✅ success

All CI checks passed. No blockers.

Tool Harness Findings

No tool calls issued; reviewed corpus directly. No findings.

@joryirving
joryirving merged commit 853d3f4 into mainJul 17, 2026
6 checks passed
@joryirving
joryirving deleted the fix/pr-review-findings-lane-routing branch July 17, 2026 02:45
Sign up for freeto 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

@joryirving
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(pr-followup): route ai-pr-reviewer CHANGES_REQUESTED reviews by structured findings - #630

Merged
joryirving merged 1 commit into
mainfrom
fix/pr-review-findings-lane-routing
Jul 17, 2026
Merged

fix(pr-followup): route ai-pr-reviewer CHANGES_REQUESTED reviews by structured findings#630
joryirving merged 1 commit into
mainfrom
fix/pr-review-findings-lane-routing

Conversation

@joryirving

Copy link
Copy Markdown
Contributor

Summary

  • CHANGES_REQUESTED reviews were routed to a lane by classifyFeedback() running keyword regexes over the review prose, defaulting to needs_human on no match. The ai-pr-reviewer writes long narrative markdown that matches none of those patterns, so every AI review fell through to needs_human → NEEDS_HUMAN lane → immediate human escalation, and the PR-fix auto-fix loop never engaged.
  • The reviewer already embeds a machine-readable verdict in the body as <!-- ai-pr-reviewer:{...} --> with open_findings. New parseAiReviewerFindings() reads it; the review path now routes to NORMAL (auto-fix) when findings carry messages, and only falls back to classifyFeedback for human prose reviews with no payload.
  • The PR_FIX_MAX_ATTEMPTS → ESCALATED → NEEDS_HUMAN ladder still handles "coder can't fix it" — same as the check_run path, which already hardcodes NORMAL with a comment warning against prose-classifying.

Verification

  • vitest run on pr-followup-ingestion, pr-followup/{sync,webhook}, pr-fix-queue: 73 passed. New tests: parseAiReviewerFindings (extract / ignore sibling markers / null on malformed); an ai-reviewer review with findings routes to NORMAL even though its prose classifies as needs_human; a payload-less vague review still routes to NEEDS_HUMAN.
  • tsc --noEmit and eslint clean.

Notes

  • Real-world trigger: misospace/miso-chat#691 — saffron correctly requested changes (blocker: no package upgrade performed), but it was bounced to a human instead of auto-fixed.
  • No change to the reviewer's output — this is consumer-side only. Human reviews are unaffected (prose classifier fallback preserved).

…tructured findings
A CHANGES_REQUESTED review's lane was chosen by classifyFeedback() running
keyword regexes over the review prose, defaulting to needs_human when nothing
matched. The ai-pr-reviewer writes long narrative markdown that matches none
of those patterns, so every AI review fell through to needs_human, was routed
to the NEEDS_HUMAN lane, and dead-ended at human escalation — the PR-fix
auto-fix loop never engaged (mirrors the check_run path's own warning against
prose-classifying).
The reviewer already embeds a machine-readable verdict in the body as
<!-- ai-pr-reviewer:{...} --> with open_findings. Parse it and, when it carries
findings with messages, route to NORMAL so the coder attempts a fix; the
PR_FIX_MAX_ATTEMPTS -> ESCALATED -> NEEDS_HUMAN ladder handles 'coder can't fix
it', exactly as for CI failures. Human prose reviews (no payload) still fall
back to classifyFeedback.
AI assistance: authored with Claude Code.

@its-saffronits-saffronBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AI Automated Review

Full PR review.

Analysis engine: MiniMax-M2.7@https://litellm.jory.dev/v1 (anthropic) — escalated (fast_low_confidence)

Review: Approve

This PR correctly fixes the routing bug where ai-pr-reviewer structured reviews were falling through to NEEDS_HUMAN instead of engaging the auto-fix loop.

Change-by-Change Findings

src/lib/pr-followup-ingestion.ts (+60/-1):

  1. parseAiReviewerFindings() — New function that extracts the machine-readable <!-- ai-pr-reviewer:{...json...} --> payload from review bodies. Handles the bare marker, sibling sibling sha/fingerprint comments, null/undefined/empty input, and malformed JSON by returning null. The regex /<!--\s*ai-pr-reviewer:\s*([\s\S]*?)-->/ is non-greedy (*?) on the JSON content, which is correct since JSON.parse won't consume --> as part of the value. JSDoc is thorough and explains the design rationale.

  2. reviewLane() — New lane-routing function for CHANGES_REQUESTED reviews. If the parsed payload contains any finding with a non-empty message, it returns NORMAL (actionable by the coder). Otherwise it falls back to laneFor() which uses classifyFeedback() on the prose. This is the correct priority: structured findings override prose classification.

  3. INGEST_DESCRIPTORS['REVIEW'] — Single-line change replacing lane: laneFor(event.body ?? "") with lane: reviewLane(event.body ?? ""). All other event types and lane paths are unchanged.

src/lib/pr-followup-ingestion.test.ts (+92/-0):

  1. parseAiReviewerFindings unit tests — Three cases: full JSON payload extraction, bare/sibling-comment markers returning null, and malformed/absent payload returning null.

  2. routes ai-pr-reviewer review with findings to NORMAL — Includes an inline guard assertion expect(classifyFeedback(body)).toBe("needs_human") proving the prose alone would dead-end, then verifies ingestReviewEvent returns lane: "NORMAL". This is the regression test for the real-world trigger (misospace/miso-chat#691).

  3. routes vague CHANGES_REQUESTED with no payload to NEEDS_HUMAN — Confirms the prose-classification fallback path is preserved when no structured payload exists.

Standards Compliance

ConventionStatus
Error handling (error instanceof Error pattern)✅ N/A — JSON.parse wrapped in try/catch with silent return null
API routes return appropriate HTTP status codes✅ N/A — library function only
Validation before DB operations✅ N/A — no DB writes
TypeScript interfaces for structured dataAiReviewerFinding, AiReviewerPayload defined
Exported for testingparseAiReviewerFindings explicitly exported
JSDoc comments for public functions✅ Full JSDoc on parseAiReviewerFindings, reviewLane

No agent-specific names in generic docs, no secrets committed, no schema changes, no auth changes. Fully compliant.

CI Verification

  • Typecheck: ✅ success
  • Lint: ✅ success
  • Tests: ✅ success (73 passed including new tests)
  • Build: ✅ success
  • Docker Build: ✅ success

All CI checks passed. No blockers.

Tool Harness Findings

No tool calls issued; reviewed corpus directly. No findings.

@joryirving
joryirving merged commit 853d3f4 into mainJul 17, 2026
6 checks passed
@joryirving
joryirving deleted the fix/pr-review-findings-lane-routing branch July 17, 2026 02:45
Sign up for freeto 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

@joryirving