Invariant
Every decision — and every non-decision — must record a reason. A pass that declines to review, declines to act, downgrades, defers, or drops work must emit an audit event (and, for error paths, PostHog capture) naming the specific gate that made the call. "Nothing happened and nothing says why" must be structurally impossible.
This came out of the 2026-07-26 restart investigation, where the concrete instances cost hours of reverse-engineering by elimination:
What to build
- Typed reasons, not bare booleans, on gate functions. Any function that can suppress a review/action (
shouldStartAiReviewForAdvisory, shouldRequirePublicAiReviewForAdvisory, eligibility/reputation/skip resolvers, freeze checks, etc.) returns { decision, reason } instead of boolean, and the caller audits the reason on the suppress path. A bare-boolean gate on a dispatch path should fail a lint/unit convention test, the same way no-direct-octokit enforces the write chokepoint.
- One audit event per suppressed lane per pass, bounded-cardinality reason codes (fits the existing
loopover_agent_disposition_total labeling approach), so dashboards can show "reviews suppressed by reason" over time.
- Error-path instrumentation completeness pass: every
.catch(() => undefined) / .catch(() => null) on a decision-relevant path either records an audit event or a PostHog capture before swallowing. A swallowed error that changes behavior (e.g. cache read failure → treated as miss) is itself a decision and needs a reason event at least at debug/counter level.
- Audit-coverage test pattern: for the main publish path, a test that runs a pass with each gate forced to suppress and asserts a reason event was recorded — so a future silent branch can't be added without failing a test.
Acceptance
- For any PR pass in the audit trail, one can answer "why did/didn't the AI review run, and why did/didn't an action execute" from
audit_events alone, without reading code.
- CI enforces the typed-reason convention on the dispatch paths.
Related: #9000 (concrete silent-skip instance), #8997/#8998/#8999 (the investigation that surfaced this).
Invariant
Every decision — and every non-decision — must record a reason. A pass that declines to review, declines to act, downgrades, defers, or drops work must emit an audit event (and, for error paths, PostHog capture) naming the specific gate that made the call. "Nothing happened and nothing says why" must be structurally impossible.
This came out of the 2026-07-26 restart investigation, where the concrete instances cost hours of reverse-engineering by elimination:
forceAiReview: truepass (whose entire purpose is "spend a fresh review") completed with no fresh review and zero audit events explaining why — none of the existing skip/bypass/missing events fired (orb(review): panel re-run checkbox is unreliable end-to-end — clicks silently lost, forced passes can silently skip the review #9000 has the full event-absence matrix).agentHoldAuditDetailproduces a specific named reason for every hold, and that made the fix(engine): key chokepoint ledger paused off kill_switch eventType #8965 side of the investigation take minutes instead of hours. The AI-dispatch lane has partial coverage (cache_miss/hit, force_bypass, auto_review_skipped, frozen, public_summary_missing) with silent gaps between them. Panel-retrigger ingestion has none at all when a delivery is lost (orb(review): panel re-run checkbox is unreliable end-to-end — clicks silently lost, forced passes can silently skip the review #9000).What to build
shouldStartAiReviewForAdvisory,shouldRequirePublicAiReviewForAdvisory, eligibility/reputation/skip resolvers, freeze checks, etc.) returns{ decision, reason }instead ofboolean, and the caller audits the reason on the suppress path. A bare-boolean gate on a dispatch path should fail a lint/unit convention test, the same wayno-direct-octokitenforces the write chokepoint.loopover_agent_disposition_totallabeling approach), so dashboards can show "reviews suppressed by reason" over time..catch(() => undefined)/.catch(() => null)on a decision-relevant path either records an audit event or a PostHog capture before swallowing. A swallowed error that changes behavior (e.g. cache read failure → treated as miss) is itself a decision and needs a reason event at least at debug/counter level.Acceptance
audit_eventsalone, without reading code.Related: #9000 (concrete silent-skip instance), #8997/#8998/#8999 (the investigation that surfaced this).