Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions .github/workflows/dev-lead-retry.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
name: Dev-Lead Retry (rate-limited)
# Scans open PRs across the org for status=rate-limited markers and
# re-dispatches the appropriate dev-lead event once the rate limit clears.
# timer_role: safety-net (docs/agentic-interaction-model.md §6.1). Scans open PRs
# across the org for status=rate-limited markers and re-dispatches the appropriate
# dev-lead event once the rate limit clears.
#
# THIS IS A BACKSTOP, NOT THE CONVERGENCE CLOCK. Since #1407, a blocked/
# rate-limited dev-lead state is resumed EVENT-FIRST — the moment a clearing event
# arrives (a review submitted or a check_run success) the resume job in
# dev-lead-reusable.yml fires scripts/dev-lead-resume.sh via a PAT-backed
# repository_dispatch. This 2 h cron is retained ONLY for the rare, genuinely
# un-eventable rate-limit recovery where no such clearing event ever fires (e.g. a
# stalled issue with no PR, or a rate limit that clears with no subsequent PR
# activity). It is no longer the de-facto convergence clock the #860 postmortem
# named the "amplifier".
#
# Stop-condition-before-acting (§6.2.1): scripts/dev-lead-retry.sh re-checks,
# BEFORE any dispatch, that the PR is still open, not human-gated
# (needs-human-review), has not exhausted its per-PR automation budget
# (pr_resume_suppressed — the SAME gate the event path uses), and that the
# rate-limit reset window has elapsed with no terminal marker already posted.
#
# Runs every 2 hours (single schedule, no staggered offsets needed since the
# concurrency group serialises any overlap from GitHub scheduler jitter).
Expand Down
71 changes: 71 additions & 0 deletions .github/workflows/dev-lead-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -498,3 +498,74 @@ jobs:
echo "$payload" | gh api \
--method POST "repos/$RELAY_REPO/dispatches" --input -
echo "::notice::Relayed CI failure for PR $RELAY_PR_NUMBER"

# ── resume ────────────────────────────────────────────────────────────────────
# Event-first resume of a blocked/rate-limited dev-lead state (#1407). On a
# CLEARING EVENT — a review submitted or a check_run SUCCESS — this resumes any
# pending status=rate-limited retry IMMEDIATELY, instead of waiting for the 2 h
# dev-lead-retry.yml safety-net cron (the #860 "amplifier"). Same Bridge A
# pattern as ci-relay above (§5): resolve the PR, then fire a PAT-backed
# repository_dispatch so dev-lead's normal retry path wakes — GITHUB_TOKEN
# cannot fire the downstream event (the recursion guard). The resume reuses
# scripts/dev-lead-resume.sh → scan_pr_for_rate_limits, so its dispatch AND its
# stop-condition (pr_resume_suppressed: human markers + per-PR automation
# budget) are identical to the cron path — it can never re-ignite a runaway.
#
# Both trigger events are already forwarded by the frozen ring-0 caller stub
# (dev-lead.yml: pull_request_review[submitted], check_run[completed]), so this
# bridge needs NO new trigger on the stub and leaves its caller-stub-freeze
# baseline untouched (#1407 AC #5). check_run success is the genuine gap the
# dispatch job ignores (its if: excludes check_run); a human review already
# wakes dispatch, but resuming there too is harmless (budget-gated).
resume:
if: >-
(github.event_name == 'pull_request_review' && github.event.action == 'submitted') ||
(github.event_name == 'check_run' &&
github.event.action == 'completed' &&
github.event.check_run.conclusion == 'success' &&
!startsWith(github.event.check_run.name, 'dev-lead / '))
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write
pull-requests: read
issues: read

steps:
- name: Checkout dev-lead scripts
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: petry-projects/.github-private
# Pinned channel (e.g. dev-lead/stable) when a trigger stub passes it;
# defaults to "main" → unchanged behaviour (#535/#506).
ref: ${{ inputs.agent_ref }}
path: .dev-lead
token: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS || github.token }}
persist-credentials: false
sparse-checkout: |
scripts

- name: Resolve PR from check_run
id: relay
if: github.event_name == 'check_run'
env:
GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS || github.token }}
CHECK_RUN_PRS: ${{ toJson(github.event.check_run.pull_requests) }}
CHECK_RUN_HEAD_SHA: ${{ github.event.check_run.head_sha }}
REPO_FULL_NAME: ${{ github.repository }}
# Reuse the sanctioned check_run→PR resolver (falls back to commits-to-pulls,
# skips forks). Writes should_relay / pr_number to GITHUB_OUTPUT.
run: bash .dev-lead/scripts/dev-lead-ci-relay.sh

- name: Resume blocked/rate-limited state (event-first)
# check_run: only when the relay resolved a non-fork PR. review: the PR
# number comes straight from the event payload.
if: >-
github.event_name == 'pull_request_review' ||
steps.relay.outputs.should_relay == 'true'
env:
# PAT identity — GITHUB_TOKEN cannot fire the downstream repository_dispatch.
GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS || github.token }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event_name == 'check_run' && steps.relay.outputs.pr_number || github.event.pull_request.number }}
run: bash .dev-lead/scripts/dev-lead-resume.sh
Comment thread
coderabbitai[bot] marked this conversation as resolved.
30 changes: 19 additions & 11 deletions interaction-contracts/dev-lead.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,22 @@
# events dev-lead.yml subscribes to above. That controlled re-dispatch is the
# TIMER's stop-condition-gated mechanism (captured in triggers.timers below), NOT
# a free emit; modeling it as the timer is what keeps the #860 rule-1 self-trigger
# check meaningful. Per §6.3 this timer is the flagged LEAK: event_fast_path is
# null, so the cron is the only path that reacts to a cleared rate limit.
# check meaningful.
#
# Since #1407 the §6.3 LEAK is CLOSED: the timer now has an event_fast_path. A
# blocked/rate-limited state resumes event-first via the resume job in
# dev-lead-reusable.yml (scripts/dev-lead-resume.sh) on a review submitted or a
# check_run success, through the SAME PAT repository_dispatch bridge and the SAME
# stop-condition (pr_resume_suppressed). The 2 h cron is demoted to timer_role:
# safety-net — a rare backstop for genuinely un-eventable rate-limit recovery, no
# longer the de-facto convergence clock.

schema_version: 1
role: dev-lead
kind: runtime
workflows:
- .github/workflows/dev-lead.yml
- .github/workflows/dev-lead-reusable.yml
- .github/workflows/dev-lead-retry.yml
interaction:
triggers:
Expand All @@ -36,10 +44,10 @@ interaction:
- repository_dispatch:dev-lead-issue-retry
timers:
- cron: "15 */2 * * *"
role: self-heal
justification: "dev-lead-retry.yml re-dispatches status=rate-limited PRs once the limit clears"
stop_condition: "PR still open and not needs-human-review / dev-lead:needs-human, and the pr-automation-budget is not exhausted"
event_fast_path: null
role: safety-net
justification: "dev-lead-retry.yml is the backstop for genuinely un-eventable rate-limit recovery; normal convergence is event-first via the dev-lead-reusable.yml resume job (#1407), so the cron is no longer the #860 amplifier"
stop_condition: "PR still open and not needs-human-review / dev-lead:needs-human, and the pr-automation-budget is not exhausted (pr_resume_suppressed — the same gate the event fast-path uses)"
event_fast_path: "check_run:success + pull_request_review:submitted → dev-lead-reusable.yml resume job → scripts/dev-lead-resume.sh PAT repository_dispatch"
emits:
- "commit"
- "dispatch:dev-lead-ci-failure"
Expand All @@ -49,13 +57,13 @@ interaction:
- "label:needs-human-review"
self_trigger_guards:
- emit: "dispatch:dev-lead-ci-failure"
guard: "dev-lead-retry.yml fires only when the stop_condition holds (PR still rate-limited, limit now cleared) — stop-condition-gated timer mechanism, not a free emit path"
location: ".github/workflows/dev-lead-retry.yml"
guard: "Both emit paths — the safety-net timer (dev-lead-retry.sh) and the event-first resume (dev-lead-resume.sh) — dispatch only through scan_pr_for_rate_limits, which fires solely when pr_resume_suppressed is false (PR open, not human-gated, budget not exhausted) and the rate-limit reset has elapsed. Stop-condition-gated, not a free emit path"
location: "scripts/dev-lead-retry.sh (scan_pr_for_rate_limits) via scripts/lib/pr-automation-budget.sh (pr_resume_suppressed)"
- emit: "dispatch:dev-lead-reviews-retry"
guard: "dev-lead-retry.yml fires only when the stop_condition holds (PR still rate-limited, limit now cleared) — stop-condition-gated timer mechanism, not a free emit path"
location: ".github/workflows/dev-lead-retry.yml"
guard: "Both emit paths — the safety-net timer (dev-lead-retry.sh) and the event-first resume (dev-lead-resume.sh) — dispatch only through scan_pr_for_rate_limits, which fires solely when pr_resume_suppressed is false (PR open, not human-gated, budget not exhausted) and the rate-limit reset has elapsed. Stop-condition-gated, not a free emit path"
location: "scripts/dev-lead-retry.sh (scan_pr_for_rate_limits) via scripts/lib/pr-automation-budget.sh (pr_resume_suppressed)"
- emit: "dispatch:dev-lead-issue-retry"
guard: "dev-lead-retry.yml fires only when the stop_condition holds (PR still rate-limited, limit now cleared) — stop-condition-gated timer mechanism, not a free emit path"
guard: "dev-lead-retry.yml fires only when the stop_condition holds (issue still failed/rate-limited, under the attempt ceiling, no open PR, limit now cleared) — stop-condition-gated timer mechanism, not a free emit path"
location: ".github/workflows/dev-lead-retry.yml"
- emit: "commit"
guard: "scripts/dev-lead-intent.sh drops pull_request:synchronize events where sender.login == BOT_USER (dev-lead-own-commit skip) — prevents the commit → PR synchronize → re-trigger loop"
Expand Down
56 changes: 56 additions & 0 deletions scripts/dev-lead-resume.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
set -euo pipefail
# dev-lead-resume.sh — event-first resume of a blocked/rate-limited dev-lead
# state (#1407).
#
# When a *clearing event* arrives on a PR — a review submitted or a check_run
# success — this bridge resumes any pending status=rate-limited dev-lead retry
# IMMEDIATELY, instead of waiting for the 2 h dev-lead-retry.yml safety-net cron
# (the #860 "amplifier" the retry cron used to be). It is Bridge A of
# docs/agentic-interaction-model.md §5: the caller resolves the PR from the event
# and this script fires a PAT-backed repository_dispatch so dev-lead's normal
# event-driven retry path wakes — GITHUB_TOKEN cannot fire the downstream event
# (the recursion guard), which is exactly why a PAT bridge is required.
#
# The resume reuses scan_pr_for_rate_limits from dev-lead-retry.sh, so the
# dispatch AND every stop condition are IDENTICAL to the cron path: the shared
# pr_resume_suppressed gate (human markers + per-PR automation budget, #1407
# AC #3), the rate-limit reset window, and the terminal-marker dedupe. The two
# paths cannot diverge, so the event fast-path is provably as safe as the timer.
#
# Env (required):
# GH_TOKEN — PAT with repo + contents:write scopes (the dispatch identity)
# REPO — "owner/repo" the clearing event fired in
# PR_NUMBER — the PR resolved from the clearing event (empty ⇒ clean no-op)
#
# Env (optional):
# DRY_RUN — if "true", log what would be dispatched but don't send
# NOW_ISO — override current time for testing (ISO-8601 UTC)

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Reuse the dispatch + scan + stop-condition logic. dev-lead-retry.sh guards its
# main() behind a BASH_SOURCE check, so sourcing it here exposes the functions
# without running the org-wide scan; it also sources lib/pr-automation-budget.sh.
# shellcheck source=dev-lead-retry.sh
source "$SCRIPT_DIR/dev-lead-retry.sh"

resume_main() {
local repo="${REPO:-}" pr="${PR_NUMBER:-}"

# A clearing event that carries no resolvable PR (e.g. a check_run on a commit
# with no open non-fork PR) is a clean no-op — there is nothing to resume.
if [ -z "$repo" ] || [ -z "$pr" ]; then
echo "::notice::dev-lead-resume: no PR resolved from the clearing event — nothing to resume"
return 0
fi

echo "[resume] event-first resume check for ${repo}#${pr} (dry_run=${DRY_RUN})"
local dispatched
dispatched=$(scan_pr_for_rate_limits "$repo" "$pr")
Comment thread
don-petry marked this conversation as resolved.
echo "[resume] dispatched ${dispatched} resume(s) for ${repo}#${pr}"
}

# Run main only when executed directly, not when sourced by unit tests.
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
resume_main "$@"
fi
Loading
Loading