Re-homed from the stale pre-decomposition epic #1058 (its one live child was never re-filed elsewhere; the epic's other four children shipped via #1059, #1060/#1097, #1022, and #975/#976/#977/#1029).
The existing pull_request.closed webhook handler that backs pr_outcome is recordPrOutcome (src/review/outcomes-wire.ts:285-344), called from the main webhook-processing path in src/queue/processors.ts:5796. On every pull_request closed event it writes a pr_outcome row to review_audit (eventType: "pr_outcome", decision merged/closed) and to audit_events, and — already, right there — extracts the PR author's login (authorLogin, outcomes-wire.ts:297), currently used only for the self-close anti-poisoning guard (outcomes-wire.ts:299-310). That's the exact login whose decision pack should be refreshed; the function just doesn't currently do anything decision-pack-related.
The "~6h debounce" this item refers to is DECISION_PACK_MAX_AGE_MS = 6 * 60 * 60 * 1000 (src/services/decision-pack.ts:72). It's checked passively, only at serving time, inside loadContributorDecisionPackForServing (decision-pack.ts:352-388): stale = cached.stale || snapshotAgeMs(cached.generatedAt) > maxAgeMs (decision-pack.ts:374). A pack only gets rebuilt when it's next requested (e.g. via the gittensory_get_decision_pack MCP tool) and found older than 6h — a PR closing does not itself trigger anything. Separately, DECISION_PACK_REBUILD_DEBOUNCE_MS = 15 * 1000 (decision-pack.ts:81, 15 seconds — much shorter, easy to confuse with the 6h figure) already exists purely to stop tryEnqueueDecisionPackRebuild (decision-pack.ts:390-404) from double-enqueueing the same login's rebuild if it's already in flight; that function is exactly what this issue should call, just from a new trigger point instead of only from the passive serving-time check.
Deliverables
References
src/review/outcomes-wire.ts:285-344 (recordPrOutcome — the existing pull_request.closed handler that backs pr_outcome; authorLogin already extracted at line 297)
src/queue/processors.ts:5788-5804 (the webhook-processing call site, showing the existing best-effort .catch() pattern to match)
src/services/decision-pack.ts:72,81,352-404 (DECISION_PACK_MAX_AGE_MS, DECISION_PACK_REBUILD_DEBOUNCE_MS, loadContributorDecisionPackForServing, tryEnqueueDecisionPackRebuild — the passive check and the exact function this issue wires up proactively)
- Tracker
#2353, re-homed from epic #1058
Re-homed from the stale pre-decomposition epic #1058 (its one live child was never re-filed elsewhere; the epic's other four children shipped via #1059, #1060/#1097, #1022, and #975/#976/#977/#1029).
The existing
pull_request.closedwebhook handler that backspr_outcomeisrecordPrOutcome(src/review/outcomes-wire.ts:285-344), called from the main webhook-processing path insrc/queue/processors.ts:5796. On everypull_requestclosedevent it writes apr_outcomerow toreview_audit(eventType: "pr_outcome", decisionmerged/closed) and toaudit_events, and — already, right there — extracts the PR author's login (authorLogin, outcomes-wire.ts:297), currently used only for the self-close anti-poisoning guard (outcomes-wire.ts:299-310). That's the exact login whose decision pack should be refreshed; the function just doesn't currently do anything decision-pack-related.The "~6h debounce" this item refers to is
DECISION_PACK_MAX_AGE_MS = 6 * 60 * 60 * 1000(src/services/decision-pack.ts:72). It's checked passively, only at serving time, insideloadContributorDecisionPackForServing(decision-pack.ts:352-388):stale = cached.stale || snapshotAgeMs(cached.generatedAt) > maxAgeMs(decision-pack.ts:374). A pack only gets rebuilt when it's next requested (e.g. via thegittensory_get_decision_packMCP tool) and found older than 6h — a PR closing does not itself trigger anything. Separately,DECISION_PACK_REBUILD_DEBOUNCE_MS = 15 * 1000(decision-pack.ts:81, 15 seconds — much shorter, easy to confuse with the 6h figure) already exists purely to stoptryEnqueueDecisionPackRebuild(decision-pack.ts:390-404) from double-enqueueing the same login's rebuild if it's already in flight; that function is exactly what this issue should call, just from a new trigger point instead of only from the passive serving-time check.Deliverables
tryEnqueueDecisionPackRebuild(env, authorLogin)(src/services/decision-pack.ts:390) from within or right afterrecordPrOutcome(src/review/outcomes-wire.ts:285) once apr_outcomerow is written, using theauthorLoginalready extracted at outcomes-wire.ts:297 — so a decision pack rebuilds within seconds of a relevant PR closing instead of waiting up to 6h for the next passive readrecordPrOutcomeis already called (processors.ts:5796wraps it in.catch(...)so outcome recording never blocks the webhook) — a decision-pack enqueue failure must not affectpr_outcomerecording itselfDECISION_PACK_MAX_AGE_MS(the 6h passive staleness window) stays as a fallback/ceiling for contributors whose PRs don't trigger this new path (e.g. pack requested before any PR has closed) — this issue adds a proactive trigger, it does not remove the passive checkauthorLoginis non-empty per the existing check at outcomes-wire.ts:296-297 before calling)tryEnqueueDecisionPackRebuildfor the PR author; a self-close (already suppressed by the existing anti-poisoning guard, outcomes-wire.ts:303-310) does NOT trigger a rebuild either, for consistency with what already doesn't get recorded as ground truth; and a rebuild-enqueue failure doesn't throw out ofrecordPrOutcomeReferences
src/review/outcomes-wire.ts:285-344(recordPrOutcome— the existingpull_request.closedhandler that backspr_outcome;authorLoginalready extracted at line 297)src/queue/processors.ts:5788-5804(the webhook-processing call site, showing the existing best-effort.catch()pattern to match)src/services/decision-pack.ts:72,81,352-404(DECISION_PACK_MAX_AGE_MS,DECISION_PACK_REBUILD_DEBOUNCE_MS,loadContributorDecisionPackForServing,tryEnqueueDecisionPackRebuild— the passive check and the exact function this issue wires up proactively)#2353, re-homed from epic#1058