From 21d9f2bdc1df1e0f43d69beb61d0a8555fac01fd Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 12:56:14 +0000 Subject: [PATCH 1/2] feat: prevent duplicate agent PRs via in-progress labels and umbrella issues - Add `in-progress` label (#fbca04) to standard label set in github-settings.md and apply-repo-settings.sh so all repos have it available for agents to claim issues - Add `in-progress` to compliance-audit.sh REQUIRED_LABELS and ensure_required_labels() so the audit enforces its presence across repos - Remove `--label "claude"` from individual compliance finding issues; individual issues now only get the `compliance-audit` label so multiple agents don't race on them - Add create_umbrella_issue() to compliance-audit.sh: after each audit run, one umbrella issue is created in petry-projects/.github grouping all findings by remediation category. Only the umbrella gets the `claude` label, triggering one coordinated agent run instead of N competing agents each fixing the same script/file - Add "Multi-Agent Issue Coordination" section to AGENTS.md with: - Claim-before-work protocol (check in-progress label, check for open PRs, claim before writing code, release claim on abandonment) - File-conflict check (search open PRs for the target file before creating it) - Compliance umbrella issue guidance (work from umbrella, fix whole category per PR) Closes #75 Co-authored-by: don-petry --- AGENTS.md | 60 +++++++++++++ scripts/apply-repo-settings.sh | 1 + scripts/compliance-audit.sh | 157 +++++++++++++++++++++++++++++++-- standards/github-settings.md | 1 + 4 files changed, 214 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2f4760b..836f0e5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -640,6 +640,66 @@ Before launching parallel agents, verify: --- +## Multi-Agent Issue Coordination + +When multiple autonomous agents work from the same issue queue (e.g., during a compliance remediation run), they MUST +coordinate via GitHub labels and PR checks to prevent duplicate work. This protocol is mandatory for any agent picking +up issues from a shared backlog. + +### Claim-Before-Work Protocol + +Before starting work on **any** GitHub issue, an agent MUST: + +1. **Check the `in-progress` label.** If the issue already has `in-progress`, skip it — another agent owns it. + + ```bash + gh issue view --repo / --json labels \ + --jq '.labels[].name' | grep -q '^in-progress$' && echo "SKIP" + ``` + +2. **Check for an open PR referencing the issue.** If one exists, skip the issue or comment on the PR instead. + + ```bash + gh pr list --repo / --state open --search "closes #" --json number | \ + jq 'length > 0' + ``` + +3. **Claim the issue immediately** by adding the `in-progress` label — before writing any code. + + ```bash + gh issue edit --repo / --add-label "in-progress" + ``` + +4. **Release the claim** if you abandon the issue without opening a PR: + + ```bash + gh issue edit --repo / --remove-label "in-progress" + ``` + +The `in-progress` label is created by `apply-repo-settings.sh` and is part of the standard label set for all repos. + +### File-Conflict Check + +Before creating a new file, check whether any open PR in the repository already creates that file. +If found, comment on the existing PR rather than creating a competing one. + +```bash +# Check if any open PR already creates the target file +gh pr list --repo / --state open --json files \ + --jq '.[].files[].path' | grep -qx "" && echo "FILE ALREADY IN OPEN PR" +``` + +### Compliance Umbrella Issues + +The compliance audit creates one **umbrella issue** per run (in `petry-projects/.github`, labeled `claude`) that groups +all findings by remediation category. When picking up compliance work: + +- Work from the umbrella issue — not from individual finding issues. +- Address an entire remediation category in a single PR (e.g., all label fixes, all ruleset fixes) to avoid N competing PRs for the same script. +- Individual finding issues have the `compliance-audit` label only; they are NOT labeled `claude` and do not need to be claimed individually. + +--- + ## Stacked PRs for Epic/Feature Development When a project has multiple Epics/Features with **sequential dependencies** — where Epic 2 builds on the foundation laid by Epic 1, diff --git a/scripts/apply-repo-settings.sh b/scripts/apply-repo-settings.sh index 114b931..ef26e31 100644 --- a/scripts/apply-repo-settings.sh +++ b/scripts/apply-repo-settings.sh @@ -50,6 +50,7 @@ apply_labels() { "bug|d73a4a|Bug reports" "enhancement|a2eeef|Feature requests" "documentation|0075ca|Documentation changes" + "in-progress|fbca04|An agent is actively working this issue" ) for config in "${label_configs[@]}"; do diff --git a/scripts/compliance-audit.sh b/scripts/compliance-audit.sh index d8d5b7b..eea6731 100755 --- a/scripts/compliance-audit.sh +++ b/scripts/compliance-audit.sh @@ -31,10 +31,11 @@ CREATE_ISSUES="${CREATE_ISSUES:-true}" FINDINGS_FILE="$REPORT_DIR/findings.json" SUMMARY_FILE="$REPORT_DIR/summary.md" +ISSUES_FILE="$REPORT_DIR/issues.json" REQUIRED_WORKFLOWS=(ci.yml codeql.yml sonarcloud.yml claude.yml dependabot-automerge.yml dependency-audit.yml agent-shield.yml) -REQUIRED_LABELS=(security dependencies scorecard bug enhancement documentation) +REQUIRED_LABELS=(security dependencies scorecard bug enhancement documentation in-progress) REQUIRED_SETTINGS_BOOL=( "allow_auto_merge:true:warning:Allow auto-merge must be enabled for Dependabot workflow" @@ -516,6 +517,7 @@ ensure_required_labels() { "bug|d73a4a|Bug reports" "enhancement|a2eeef|Feature requests" "documentation|0075ca|Documentation changes" + "in-progress|fbca04|An agent is actively working this issue" ) for config in "${label_configs[@]}"; do @@ -556,6 +558,15 @@ This finding is still open. **Standard:** [$standard_ref](https://github.com/$ORG/.github/blob/main/$standard_ref)" 2>/dev/null || true info "Updated existing issue #$existing in $repo for: $check" + # Record existing issue for umbrella + jq --null-input \ + --arg repo "$repo" \ + --arg category "$category" \ + --arg check "$check" \ + --arg number "$existing" \ + --arg url "https://github.com/$ORG/$repo/issues/$existing" \ + '{repo:$repo,category:$category,check:$check,number:$number,url:$url}' \ + >> "$ISSUES_FILE" return fi @@ -584,10 +595,11 @@ See the [full standards documentation](https://github.com/${ORG}/.github/tree/ma *This issue was automatically created by the [weekly compliance audit](https://github.com/${ORG}/.github/blob/main/.github/workflows/compliance-audit.yml).*" local issue_url + # Individual finding issues get compliance-audit label only — NOT the claude label. + # The umbrella issue (created separately) gets the claude label to trigger one coordinated agent run. issue_url=$(gh issue create --repo "$ORG/$repo" \ --title "$search_title" \ --label "$AUDIT_LABEL" \ - --label "claude" \ --body "$body" 2>/dev/null || echo "") if [ -n "$issue_url" ]; then @@ -595,15 +607,144 @@ See the [full standards documentation](https://github.com/${ORG}/.github/tree/ma new_issue=$(echo "$issue_url" | grep -oE '[0-9]+$' || echo "") info "Created issue #$new_issue in $repo for: $check ($issue_url)" - # Attempt to assign to claude — the bot user for Claude Code Action + # Record created issue for umbrella if [ -n "$new_issue" ]; then - gh issue edit "$new_issue" --repo "$ORG/$repo" --add-assignee "app/claude" 2>/dev/null || true + jq --null-input \ + --arg repo "$repo" \ + --arg category "$category" \ + --arg check "$check" \ + --arg number "$new_issue" \ + --arg url "$issue_url" \ + '{repo:$repo,category:$category,check:$check,number:$number,url:$url}' \ + >> "$ISSUES_FILE" fi else warn "Failed to create issue in $repo for: $check" fi } +create_umbrella_issue() { + local audit_date + audit_date=$(date -u +%Y-%m-%d) + local title="Compliance audit — $audit_date" + + # Skip if no findings + local total_findings + total_findings=$(jq length "$FINDINGS_FILE") + if [ "$total_findings" -eq 0 ]; then + info "No findings — skipping umbrella issue" + return + fi + + # Check for existing open umbrella issue for today + local existing_umbrella + existing_umbrella=$(gh issue list --repo "$ORG/.github" \ + --label "$AUDIT_LABEL" \ + --state open \ + --search "\"$title\" in:title" \ + --json number,title \ + -q ".[] | select(.title == \"$title\") | .number" \ + 2>/dev/null | head -1 || echo "") + + if [ -n "$existing_umbrella" ]; then + info "Umbrella issue #$existing_umbrella already exists for $audit_date — skipping" + return + fi + + # Map finding categories to remediation groups + # Each group: category_keys|display_name|remediation_script + local groups=( + "settings|Repository Settings|apply-repo-settings.sh" + "labels|Labels|apply_labels() in apply-repo-settings.sh" + "rulesets|Repository Rulesets|apply-rulesets.sh" + "ci-workflows|Workflows|per-repo workflow additions" + "action-pinning|Action SHA Pinning|pin actions to SHA in each workflow file" + "dependabot|Dependabot Configuration|per-repo .github/dependabot.yml" + "standards|CLAUDE.md / AGENTS.md References|per-repo doc updates" + ) + + local body="## Compliance Audit — $audit_date + +This umbrella issue tracks all findings from the automated compliance audit run on **$audit_date**. +Findings are grouped by remediation category. Address each category together to avoid duplicate agent PRs. + +**Total findings:** $total_findings across $(jq -r '[.[].repo] | unique | length' "$FINDINGS_FILE") repositories + +--- + +## Remediation Work Breakdown +" + + for group_entry in "${groups[@]}"; do + IFS='|' read -r cat_key display_name remediation_script <<< "$group_entry" + + local cat_findings + cat_findings=$(jq -c --arg cat "$cat_key" '[.[] | select(.category == $cat)]' "$FINDINGS_FILE") + local cat_count + cat_count=$(echo "$cat_findings" | jq 'length') + + [ "$cat_count" -eq 0 ] && continue + + local affected_repos + affected_repos=$(echo "$cat_findings" | jq -r '[.[].repo] | unique | join(", ")') + + body+=" +### $display_name ($cat_count finding(s)) + +**Remediation:** \`$remediation_script\` +**Affected repos:** $affected_repos + +| Repo | Check | Severity | +|------|-------|----------| +" + # Add per-finding rows with issue links where available + while IFS= read -r finding; do + local f_repo f_check f_severity f_number f_url + f_repo=$(echo "$finding" | jq -r '.repo') + f_check=$(echo "$finding" | jq -r '.check') + f_severity=$(echo "$finding" | jq -r '.severity') + + # Look up issue link if we tracked it + local issue_link="" + if [ -s "$ISSUES_FILE" ]; then + local issue_entry + issue_entry=$(grep -F "\"repo\":\"$f_repo\"" "$ISSUES_FILE" 2>/dev/null | \ + jq -c --arg repo "$f_repo" --arg check "$f_check" \ + 'select(.repo == $repo and .check == $check)' 2>/dev/null | head -1 || echo "") + if [ -n "$issue_entry" ]; then + f_number=$(echo "$issue_entry" | jq -r '.number') + f_url=$(echo "$issue_entry" | jq -r '.url') + issue_link=" ([#$f_number]($f_url))" + fi + fi + + body+="| \`$f_repo\` | \`$f_check\`$issue_link | \`$f_severity\` | +" + done < <(echo "$cat_findings" | jq -c '.[]') + + done + + body+=" +--- +*Generated by the [weekly compliance audit](https://github.com/${ORG}/.github/blob/main/.github/workflows/compliance-audit.yml) on $(date -u "+%Y-%m-%d %H:%M UTC").* +*Address each remediation category as a single coordinated PR to avoid duplicate agent work.*" + + ensure_audit_label ".github" + + local umbrella_url + umbrella_url=$(gh issue create --repo "$ORG/.github" \ + --title "$title" \ + --label "$AUDIT_LABEL" \ + --label "claude" \ + --body "$body" 2>/dev/null || echo "") + + if [ -n "$umbrella_url" ]; then + info "Created umbrella issue: $umbrella_url" + else + warn "Failed to create umbrella issue in $ORG/.github" + fi +} + close_resolved_issues() { local repo="$1" @@ -736,8 +877,9 @@ main() { info "Report directory: $REPORT_DIR" info "Dry run: $DRY_RUN" - # Initialize findings file + # Initialize findings and issues tracking files echo "[]" > "$FINDINGS_FILE" + : > "$ISSUES_FILE" # Get all non-archived repos in the org local repos @@ -807,6 +949,11 @@ main() { # Close issues for resolved findings close_resolved_issues "$repo" done + + # Create one umbrella issue per audit run grouping all findings by remediation category. + # Only the umbrella gets the `claude` label — individual issues do not — so one coordinated + # agent handles related findings together instead of multiple agents producing duplicate PRs. + create_umbrella_issue else info "Skipping issue creation (DRY_RUN=$DRY_RUN, CREATE_ISSUES=$CREATE_ISSUES)" fi diff --git a/standards/github-settings.md b/standards/github-settings.md index ef8507f..01b941e 100644 --- a/standards/github-settings.md +++ b/standards/github-settings.md @@ -217,6 +217,7 @@ All repositories MUST have these labels configured: | `bug` | `#d73a4a` (red) | Bug reports | | `enhancement` | `#a2eeef` (teal) | Feature requests | | `documentation` | `#0075ca` (blue) | Documentation changes | +| `in-progress` | `#fbca04` (yellow) | An agent is actively working this issue | --- From d42132ceee766668f47225f6a1d2adcf92b63b35 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 12:57:57 +0000 Subject: [PATCH 2/2] fix: declare body separately in create_umbrella_issue to satisfy ShellCheck SC2155 Co-authored-by: don-petry --- scripts/compliance-audit.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/compliance-audit.sh b/scripts/compliance-audit.sh index eea6731..7302e1d 100755 --- a/scripts/compliance-audit.sh +++ b/scripts/compliance-audit.sh @@ -663,7 +663,8 @@ create_umbrella_issue() { "standards|CLAUDE.md / AGENTS.md References|per-repo doc updates" ) - local body="## Compliance Audit — $audit_date + local body + body="## Compliance Audit — $audit_date This umbrella issue tracks all findings from the automated compliance audit run on **$audit_date**. Findings are grouped by remediation category. Address each category together to avoid duplicate agent PRs.