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
60 changes: 60 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <issue-number> --repo <owner>/<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 <owner>/<repo> --state open --search "closes #<issue-number>" --json number | \
jq 'length > 0'
```

3. **Claim the issue immediately** by adding the `in-progress` label — before writing any code.

```bash
gh issue edit <issue-number> --repo <owner>/<repo> --add-label "in-progress"
```

4. **Release the claim** if you abandon the issue without opening a PR:

```bash
gh issue edit <issue-number> --repo <owner>/<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 <owner>/<repo> --state open --json files \
--jq '.[].files[].path' | grep -qx "<path/to/file>" && 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,
Expand Down
1 change: 1 addition & 0 deletions scripts/apply-repo-settings.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
158 changes: 153 additions & 5 deletions scripts/compliance-audit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -584,26 +595,157 @@ 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
local new_issue
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
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"

Expand Down Expand Up @@ -736,8 +878,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
Expand Down Expand Up @@ -807,6 +950,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
Expand Down
1 change: 1 addition & 0 deletions standards/github-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

---

Expand Down
Loading