Repository navigation
fix(dev-lead): multi-label issue creation can cancel the dev-lead dispatch (race) #443
Description
Summary
When an issue is created (or updated) with the dev-lead label plus one or more other labels in the same API call, the dev-lead agent can silently fail to be dispatched. GitHub fires a separate issues:labeled event per label; the dispatch job only acts when the triggering label is dev-lead, while a per-issue cancel-in-progress concurrency group lets a later (non-dev-lead) labeled event cancel the very run that would have acted. Net effect: the workflow reports success, but the agent never runs and no PR is opened.
Observed behaviour (concrete evidence)
While working issue #438 (created via gh issue create --label "dev-lead,bug,ci,automation"):
- Four
issues:labeledevents fired at ~21:10, all sharing concurrency groupdev-lead-issue-438. - Runs
27306552438and27306552128completedsuccessbut the dispatch job classified intent as:i.e. they were triggered by theINTENT_TYPE: skip INTENT_REASON: label-not-dev-leadbug/ci/automationlabel events and correctly skipped. - The other two runs were
cancelledbycancel-in-progress— and the run triggered by thedev-leadlabel was among them, so it never dispatched the agent. - Result after 25 min: issue OPEN, 0 comments, no
in-progresslabel, no branch, no PR. A "successful" workflow that did nothing. - Recovery: cycling the label alone (remove + re-add
dev-lead, no competing events) fired run27308171856→agent ran (Pre-flight → Install engine CLIs → Run issue, all success) → PR feat: implement issue #438 — fix(compliance): remove dev-lead workflow manipulation from re-trigger sweep (Step 1 is out of scope) #442 opened.INTENT_TYPE: issue INTENT_REASON: issue-labeled-dev-lead
This also means such issues are not self-healing: the daily compliance-retrigger sweep only re-triggers issues carrying the compliance-audit label, so a plain dev-lead issue stuck this way stays stuck indefinitely.
Root Cause
Per standards/ci-standards.md:421-428, the issue-intent job is gated and serialised as:
if: >-
github.event_name == 'issues' && github.event.action == 'labeled' &&
github.event.label.name == 'dev-lead'
concurrency:
group: dev-lead-issue-${{ github.event.issue.number }}
cancel-in-progress: trueTwo facts combine into the race:
- Gate keys on the triggering label (
github.event.label.name), not on whether the issue currently has thedev-leadlabel. So only the single event for thedev-leadlabel can act. cancel-in-progresscollapses all same-issue events, and the cancellation happens at the workflow/concurrency layer before theif:decides a run is a no-op. So a non-acting run (wrong label) can cancel the one acting run (dev-leadlabel) purely based on arrival order.
When dev-lead is not the last label applied, the acting run loses the race.
Reproduction
gh issue create --repo <org>/<repo> --label "dev-lead,bug" --title t --body b- Observe the dev-lead runs: the
dev-lead-triggered run is cancelled; survivors skip withlabel-not-dev-lead. - Issue receives no agent dispatch (no plan comment, no PR).
- Confirm recovery by cycling
dev-leadalone.
Recommended Fix
Gate on the issue's current label set rather than the triggering label, and let per-issue concurrency collapse the duplicate events onto a single run that still acts:
if: >-
github.event_name == 'issues' && github.event.action == 'labeled' &&
contains(github.event.issue.labels.*.name, 'dev-lead')
concurrency:
group: dev-lead-issue-${{ github.event.issue.number }}
cancel-in-progress: trueWith this, every one of the N labeled events passes the gate; cancel-in-progress collapses them to the last-arriving run, which now acts (because the gate no longer depends on which label triggered it). Pair with an idempotency guard so re-entrant events can't double-dispatch (skip if a dev-lead/issue-<n>-* PR or fresh in-progress label already exists — the same check dl_dev_lead_active already encodes).
Apply the same fix to the org template (standards/workflows/dev-lead.yml / the reusable) and roll it to the fleet.
Acceptance Criteria
- Creating an issue in a single API call with
dev-leadplus ≥1 other label dispatches the agent exactly once (not zero). - Dispatch does not depend on
dev-leadbeing the last label applied. - Adding
dev-leadto an issue that already carries other labels (single add) still dispatches. - At most one concurrent agent run per issue; simultaneous events never produce duplicate PRs.
- Label cycling / re-trigger recovery path (relied on by
compliance-retrigger.sh) still works. - Regression test simulates multiple near-simultaneous
issues:labeledevents wheredev-leadis not last, and asserts exactly one agent dispatch. - Standard (
standards/ci-standards.md) and template updated to match.
Context
Discovered while monitoring #438 (whose fix, PR #442, is unrelated to this race). Part of the Compliance program / dev-lead reliability work. Sibling to #431/#432 (lost-labeled-event recovery).
Note: when assigning this to dev-lead, add the
dev-leadlabel on its own (or cycle it last) to avoid triggering the very race described here.
Activity
don-petry commentedon Jun 11, 2026
Dev-Lead Implementation Plan
Issue: #443 — fix(dev-lead): multi-label issue creation can cancel the dev-lead dispatch (race)
Scope
Fix the event classifier (
dev-lead-intent.sh) to gate on whether the issue's current label set containsdev-leadrather than solely on the triggering label, so any one of the Nissues:labeledevents passes the gate and dispatches the agent exactly once.Root Cause (confirmed)
In
.dev-lead/scripts/dev-lead-intent.sh(lines 339-361), theissuesrouting checks.label.name(the single triggering label) instead of.issue.labels[](the issue's current label set). With concurrent events andcancel-in-progress: true(the old claude.yml pattern), the actingdev-lead-triggered run could be cancelled. The concurrency race was already fixed (#402, cancel-in-progress: false in the reusable), but the intent classifier still reads the wrong field — so whendev-leadis not the last label applied, the surviving run emitsskip:label-not-dev-leadeven though the issue carries the label.Implementation Checklist
.dev-lead/scripts/dev-lead-intent.sh— changeissueshandler to usehas_label "dev-lead"(reads.issue.labels[]) instead of comparing.label.nameto"dev-lead"test/scripts/dev-lead-intent/helpers/setup.bash— test harness helperstest/scripts/dev-lead-intent/dev-lead-intent.bats— bats regression tests.github/workflows/dev-lead-intent-tests.yml— CI workflow that checks out.github-privateand runs the intent test suitestandards/ci-standards.md— document that the intent classifier gates on current label set, not triggering label; add note about multi-label race fixTests to Write
dev-leadis the triggering label — baseline:INTENT_TYPE=issuedev-leadis NOT the triggering label but IS in the label set — the core race scenario; must emitINTENT_TYPE=issue, notskipdev-leadis not in the current label set — no regression: events for unrelated labels on issues withoutdev-leadstill skiplabeled— unchanged baselinecheck_existing_prguard still active — handled bydev-lead-fix-issue.sh; intent script emitsissueintent regardless, duplicate dispatch prevented downstreamTest Command
sudo apt-get install -y bats && bats --print-output-on-failure test/scripts/dev-lead-intent/Notes
The idempotency guard (
check_existing_prindev-lead-fix-issue.sh,dl_dev_lead_activeinscripts/lib/dev-lead-retrigger.sh) already prevents double-dispatch; no change needed there. The fix is a one-line change in the intent classifier'sissuescase.don-petry commentedon Jun 11, 2026
Dev-Lead Implementation Plan
Issue: #443 — fix(dev-lead): multi-label issue creation can cancel the dev-lead dispatch (race)
Scope
Fix the event classifier (
dev-lead-intent.sh) to gate on the issue's current label set rather than the triggering label, so anyissues:labeledevent dispatches the agent whendev-leadis among the issue's labels — regardless of which specific label triggered the event.Root Cause (confirmed)
In
.dev-lead/scripts/dev-lead-intent.shlines 350–360, theissuesrouting checks.label.name(the triggering label):The existing
has_labelhelper already reads.issue.labels[]?.name(the full label set). Using it instead eliminates the race.Implementation Checklist
test/dev-lead/helpers/setup.bash— test helper (TDD first)test/dev-lead/dev-lead-intent.bats— bats test suite covering race scenario and regression cases.dev-lead/scripts/dev-lead-intent.sh— changeissuesrouting from triggering-label check tohas_label "dev-lead".github/workflows/dev-lead-intent-tests.yml— CI gate: shellcheck + bats on every PR touching the intent classifierstandards/ci-standards.md— document the label-set-based gate in the Dev-Lead Agent sectionTests to Write
dev-leadis the only label applied (single-label happy path — regression)bugis the triggering label butdev-leadis already in the issue's label set → dispatches (the race scenario)dev-leadis the triggering label but other labels are also present → dispatchesdev-leadtriggering label,dev-leadnot in issue labels → skips (correct skip preserved)issues_action != labeled→ skips (existing behavior preserved)check_existing_prindev-lead-fix-issue.shalready guards against double-dispatch when multiple events reach the agentTest Command
don-petry commentedon Jul 14, 2026
Closing as already resolved by #450 (per-issue/per-PR concurrency lanes, merged 2026-06-06 — 5 days before this was filed on 2026-06-11 from the pre-fix #438 incident evidence).
The reusable now uses
cancel-in-progress: falsewith separatedev-lead-issue-<n>/dev-lead-pr-<n>lanes: same-laneissues:labeledevents queue behind the active run instead of cancelling it, so the dev-lead-triggered pickup always finishes regardless of label order. All runtime acceptance criteria hold:label-not-dev-lead);Regression guard added in combsmith/.github-private#1221.
Doc-hygiene follow-up (not blocking):
standards/ci-standards.mdstill carries a legacyclaude-issueexample (~L417) showinggithub.event.label.name == 'claude'+cancel-in-progress: true— the retiredclaudelabel (#456) and the exact anti-pattern this issue warned about. That example block should be updated to the current dev-lead pattern or removed as part of the claude-deprecation doc cleanup.