.agents/skills/symfony-bug-triage/SKILL.md
Symfony fixes bugs on the lowest maintained branch where the bug exists, then merges up. This skill produces a retarget recommendation per PR. The final call belongs to a maintainer; never act without explicit confirmation.
Maintained branches:
curl -s https://symfony.com/releases.json
# maintained_versions, e.g. ["6.4","7.4","8.0","8.1","8.2"]; LOWEST = first entry.
# DEV (new-feature target) = first entry AFTER latest_stable_version, not the last
# entry: when X.4 and Y.0 are developed in parallel both are dev, and features go to
# the lower one (they merge up).
Resolve the upstream remote (not always origin):
REMOTE=$(git remote -v | awk '/[:\/]symfony\/symfony(\.git)?[[:space:]]+\(fetch\)/{print $1; exit}')
# Stop and ask the user if empty
Fetch all maintained branches, then collect open Bug PRs:
git fetch "$REMOTE" <each maintained branch>
gh pr list --repo symfony/symfony --label Bug --state open --limit 300 \
--json number,title,baseRefName,milestone,isDraft
# Skip drafts unless asked. A milestone/base mismatch is itself a signal.
Each subagent is read-only and returns a structured verdict.
1a. Fetch PR data:
gh pr view <N> --repo symfony/symfony \
--json number,title,baseRefName,milestone,body,files,labels,closingIssuesReferences,url
gh pr diff <N> --repo symfony/symfony
gh pr view <N> --repo symfony/symfony --comments
From the body Q&A table, read Branch?, Bug fix?, New feature?, Deprecations?.
1b. Bug fix or feature? (objective check)
The Bug fix? field is self-declared and can be wrong. The real check: does the
production diff add public/protected API surface?
# Scan only non-Tests/ files for added API
gh pr diff <N> --repo symfony/symfony \
| awk '/^diff --git /{skip=($0 ~ /\/Tests\//)} !skip' \
| grep -nE '^\+\s*(public|protected)\s+(function|const|readonly|static|\??[A-Za-z\\]+\s+\$)|^\+\s*(final\s+)?(class|interface|trait|enum)\s'
If the diff grows the surface → treat as feature (target DEV, never retarget down).
Two false positives to discount: code merely moved also shows as + lines, and
adding to an @internal/@experimental class is not a BC extension.
Exception: implementing a marker interface (e.g. ResetInterface) to fix a state
leak is still a bug fix; weigh intent in those cases.
1c. Read linked issues:
# Also parse the body for "Fix #", "Closes #", "Resolves #"
gh issue view <ISSUE> --repo symfony/symfony --json title,body,labels,comments
# The reported Symfony version is the strongest signal for how far back the bug reaches.
1d. Probe lower branches (for each branch below the PR's base, oldest first):
# 1. Does the file exist?
git cat-file -e "$REMOTE/<branch>:<path>" && echo present || echo absent
# 2. Does the buggy code exist there?
git show "$REMOTE/<branch>:<path>"
# 3. When did the code arrive? (determines the regression floor)
git log -S'<symbol>' --oneline "$REMOTE/<branch>" -- <path>
1e. (Optional) Reproduce on the candidate branch:
git worktree add --detach ../symfony-sf-triage-<N> "$REMOTE/<branch>"
cd ../symfony-sf-triage-<N> && ./phpunit src/Symfony/Component/<Name>
cd - && git worktree remove --force ../symfony-sf-triage-<N>
Subagent verdict: PR number, component, current base, recommended base, confidence (high/medium/low), deciding evidence, whether the fix needs adaptation.
Keep on current base if any hold:
Retarget DOWN to branch T if all hold:
T is the oldest maintained branch where the bug exists, bounded by the
regression floor and never below LOWEST.T (directly or with a documented adaptation).Raise the base (rare): the PR targets a branch where the code doesn't exist yet, or an unmaintained branch → recommend the lowest maintained branch that has the code.
Collect verdicts into a table ordered by action then component:
PR Component Base → Recommend Conf Why
#64613 Validator 7.4 6.4 high issue reports 6.4; method present & broken on 6.4
#64589 ObjectMapper 8.1 8.1 (keep) high component added in 8.1; absent below
#64576 Serializer 8.1 7.4 med regression introduced in 7.4 (commit abc123)
Routing (each is outward, so wait for confirmation before doing anything):
When to comment-only vs. rebase+push:
maintainerCanModify is false; the fix needs real code
adaptation; or the PR is already approved (a force-push dismisses reviews).Rebase mechanics:
# Cherry-pick PR commits onto a fresh branch off the target (do NOT rebase the whole branch)
git fetch <fork-url> <headRef>; OLD=$(git rev-parse FETCH_HEAD)
git checkout -B retarget-<N> $REMOTE/<target>
git cherry-pick <oid>...
# Run tests before pushing
./phpunit src/Symfony/Component/<Name>
# Push with lease, then change base
git push <fork-url> HEAD:<headRef> --force-with-lease=<headRef>:$OLD
gh pr edit <N> --repo <repo> --base <target> --milestone <target>
# Clean up
git checkout <dev-branch> && git branch -D retarget-<N>
After retargeting, also update the body's | Branch? | value and remove any
labels that contradict the new disposition (e.g. Feature on a confirmed bug fix).
| PR shape | Recommendation | Deciding factor |
|---|---|---|
| Bug on 8.1; issue reproduced on 6.4; method present & broken on 6.4 | Retarget to 6.4 | bug reaches the floor |
| Bug on 8.2; offending line introduced in a commit first shipped in 8.0 | Retarget to 8.0 | regression floor is 8.0 |
| Bug on 8.1 in a component that didn't exist before 8.1 | Keep on 8.1 | code absent on lower branches |
New feature? yes opened on 8.1 | Raise to DEV | features target the dev branch |
| Maintainer commented "please rebase on 7.4" | Retarget to 7.4 (high) | explicit instruction overrides inference |
| Early "rebase on 6.4", later "this is a feature" | Follow the latest: keep on DEV | most recent instruction wins |
| PR already approved; bug reaches lower branches | Comment-only, ask author | force-push dismisses reviews |
| Fix uses 8.x-only API but bug exists on 6.4 | Retarget to 6.4, adapt the fix | don't leave 6.4 broken |