fix(triage): drift-as-banner — demote drift from gate to modifier (#476)

Post-Phase 4 verification showed two issues (#311, #448) where the
pipeline successfully produced valuable findings against current
code, but the top-of-gate drift veto routed them to 8b drift-only
and the findings were discarded. The reporter cited an older version
(1.1.7464 on #311), the investigation ran cleanly on current
(1.3.5610), and the reviewer approved the findings — yet the comment
still read "couldn't reach a confident read."

This change keeps drift detection and keeps the drift-bridge sweep.
What changes is Stage 7: drift is no longer at the top of the gate.

When drift is detected and 8a or 8c would render cleanly, the
renderer prepends a drift banner (⚠ You reported this on X; bot
investigated on Y. Citations may still apply.) and appends the
drift-bridge-candidates block at the bottom. The finding citations
stand — they describe current code in hypothesis voice, which is
what the reader can verify against their own checkout.

When drift is detected and the pipeline would otherwise route to 8b
for any other reason (fetch-failure, invest-failure, review-failure,
no-findings, low-confidence), the reason is overridden to
`version-drift`. Drift-bridge candidates give the maintainer a more
actionable signal than "no findings" on its own.

Reviewer prompt gains one rubric addition: downgrade-confidence when
the cited surface clearly post-dates the reporter's version. Catches
the case where a finding is valid on current but wouldn't reproduce
on what the reporter saw. Doesn't degrade findings indiscriminately
— only when the reviewer can see version-specific evidence.

Confirmed-duplicate routing wins over the drift-reason override
(explicit exclusion in the override clause) because `triage:
duplicate` is still the more specific read.

Co-authored-by: Claude <claude@anthropic.com>
This commit is contained in:
Aaddrick
2026-04-21 08:33:49 -04:00
committed by GitHub
parent 3344832b4e
commit f1eed0e16f
2 changed files with 104 additions and 15 deletions

View File

@@ -982,15 +982,20 @@ jobs:
} >> "$GITHUB_OUTPUT"
# Stage 7 — decision gate. Selects the final comment variant and
# reason. Priority per spec §7 with Phase 3's duplicate gate and
# Phase 4's enhancement gate slotted in:
# drift → fetch-failure → duplicate → invest-failure →
# review-failure → enhancement (8c) → no-findings →
# low-confidence → 8a
# The enhancement gate fires for `classification=enhancement`
# after review succeeded (or for 0-findings enhancements where
# review was skipped by design). For bug/duplicate paths the
# no-findings + low-confidence checks still gate 8a.
# reason. Priority per spec §7 with Phase 3's duplicate gate,
# Phase 4's enhancement gate, and drift demoted from top-of-gate
# veto to a banner modifier:
# fetch-failure → duplicate → invest-failure → review-failure
# → enhancement (8c) → no-findings → low-confidence → 8a
# Drift no longer vetoes the comment. When drift is detected
# AND 8a or 8c would render, the renderer prepends a drift
# banner and appends the drift-bridge candidates block; the
# finding citations stand but the reader is warned they're on
# current code, not the reporter's version. When drift is
# detected AND any other gate routes to 8b, the reason is
# overridden to `version-drift` (drift is the more actionable
# signal for the maintainer than the underlying no-findings /
# low-confidence cause).
- name: Decide comment variant
id: decide
run: |
@@ -1014,10 +1019,7 @@ jobs:
dup_rating="${{ steps.filter.outputs.duplicate_of_rating }}"
# Shared gates that apply to every investigate route.
if [[ "${drift}" == "true" ]]; then
variant=8b
reason_id=version-drift
elif [[ "${fetch_ok}" != "true" ]]; then
if [[ "${fetch_ok}" != "true" ]]; then
variant=8b
reason_id=reference-source-unavailable
elif [[ "${classification}" == "duplicate" \
@@ -1062,6 +1064,19 @@ jobs:
reason_id=
fi
# Drift override — when drift is detected AND the pipeline
# would fall back to 8b for any other reason, report drift
# as the deferral reason. Drift-bridge candidates give the
# maintainer a more actionable signal than "no findings" /
# "low confidence" on their own. When the pipeline reaches
# 8a or 8c cleanly, drift stays as a banner flag — handled
# by the renderer, not this gate.
if [[ "${drift}" == "true" \
&& "${variant}" == "8b" \
&& "${reason_id}" != "duplicate" ]]; then
reason_id=version-drift
fi
{
echo "variant=${variant}"
echo "reason_id=${reason_id}"
@@ -1220,12 +1235,29 @@ jobs:
if: steps.decide.outputs.variant == '8a'
env:
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
DRIFT_DETECTED: ${{ steps.drift.outputs.drift_detected }}
CURRENT_VERSION: ${{ vars.CLAUDE_DESKTOP_VERSION }}
run: |
c=/tmp/triage/comment-findings.json
hypothesis=$(jq -r '.hypothesis_line' "${c}")
# Drift banner — prepended when the classifier picked up a
# claimed_version that differs from CLAUDE_DESKTOP_VERSION.
# The finding citations still stand (they describe current
# code), but the reader needs to know the gap.
drift_banner=""
if [[ "${DRIFT_DETECTED}" == "true" ]]; then
claimed=$(jq -r '.claimed_version // "unknown"' \
/tmp/triage/classification.json)
drift_banner="⚠ You reported this on \`${claimed}\`; the bot investigated against the current release \`${CURRENT_VERSION}\`. Findings below are from current code — if the drift-bridge candidates at the bottom already address your case, you can probably close. Otherwise the file:line citations may still apply."
fi
{
echo "**Automated draft — AI analysis, not maintainer judgment.** This bot won't close issues, apply labels beyond triage routing, or claim fixes are shipped. Findings below are starting points; the code citations are what to verify first."
if [[ -n "${drift_banner}" ]]; then
echo ""
echo "${drift_banner}"
fi
echo ""
echo "${hypothesis}"
echo ""
@@ -1259,6 +1291,23 @@ jobs:
echo "Related: ${related_line}"
fi
# Drift-bridge candidates block — same shape as in 8b,
# appended here when drift detected + sweep returned ≥1.
if [[ "${DRIFT_DETECTED}" == "true" \
&& -f /tmp/triage/drift-bridge-candidates.json ]]; then
candidate_count=$(jq \
'(.commits | length) + (.prs | length)' \
/tmp/triage/drift-bridge-candidates.json)
if [[ "${candidate_count}" -gt 0 ]]; then
echo ""
echo "Drift-bridge candidates — commits or PRs in the drift window that touched the relevant surface and may already address this:"
jq -r '
(.commits[]? | "- \(.sha[0:8]) — \(.subject) (\(.date))"),
(.prs[]? | "- #\(.number) — \(.title) (\(.mergedAt))")
' /tmp/triage/drift-bridge-candidates.json
fi
fi
echo ""
echo "Full investigation artifacts (\`investigation.json\`, \`validation.json\`) are attached to the [triage workflow run](${RUN_URL})."
} > /tmp/triage/comment.md
@@ -1393,14 +1442,30 @@ jobs:
if: steps.decide.outputs.variant == '8c'
env:
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
DRIFT_DETECTED: ${{ steps.drift.outputs.drift_detected }}
CURRENT_VERSION: ${{ vars.CLAUDE_DESKTOP_VERSION }}
run: |
c=/tmp/triage/comment-enhancement.json
tax=.claude/scripts/taxonomies/enhancement-design-questions.json
ack=$(jq -r '.acknowledgment_line' "${c}")
surface_count=$(jq '.existing_surfaces | length' "${c}")
# Drift banner — same shape as 8a; reframed for enhancements
# (surfaces point at current code, which may have moved
# since the reporter's version).
drift_banner=""
if [[ "${DRIFT_DETECTED}" == "true" ]]; then
claimed=$(jq -r '.claimed_version // "unknown"' \
/tmp/triage/classification.json)
drift_banner="⚠ You reported this on \`${claimed}\`; the bot surfaced existing code on the current release \`${CURRENT_VERSION}\`. If the drift-bridge candidates at the bottom already address the enhancement, those may be a better starting point than the surfaces below."
fi
{
echo "**Automated draft — AI analysis, not maintainer judgment.** This bot won't approve enhancements, prioritize roadmap, or commit timelines. The notes below flag existing surfaces and design questions that may be worth considering before implementation."
if [[ -n "${drift_banner}" ]]; then
echo ""
echo "${drift_banner}"
fi
echo ""
echo "${ack}"
@@ -1426,6 +1491,24 @@ jobs:
else "- \($text)" end
' "${c}"
# Drift-bridge candidates block — appended when drift
# detected + sweep returned ≥1 (spec §7). Helps the
# maintainer spot PRs that may already address the ask.
if [[ "${DRIFT_DETECTED}" == "true" \
&& -f /tmp/triage/drift-bridge-candidates.json ]]; then
candidate_count=$(jq \
'(.commits | length) + (.prs | length)' \
/tmp/triage/drift-bridge-candidates.json)
if [[ "${candidate_count}" -gt 0 ]]; then
echo ""
echo "Drift-bridge candidates — commits or PRs in the drift window that touched the relevant surface and may already address this:"
jq -r '
(.commits[]? | "- \(.sha[0:8]) — \(.subject) (\(.date))"),
(.prs[]? | "- #\(.number) — \(.title) (\(.mergedAt))")
' /tmp/triage/drift-bridge-candidates.json
fi
fi
echo ""
echo "Full investigation artifacts attached to the [triage workflow run](${RUN_URL})."
} > /tmp/triage/comment.md