name: bugfix description: End-to-end bug fix workflow — report, analyze, fix, verify, ship (PR + merge + deploy + knowledge capture) argument-hint: Bug description or BUG-xxx ID
/bugfix — Bug Fix Workflow
You are fixing a bug using a streamlined workflow that skips the full spec ceremony but follows the same deployment strategy as a feature: changes land via PR, ride the project's CI/CD pipeline (staging-first if the project has one), and aren't marked resolved until every declared deploy target is confirmed.
Ethos
!sh .adlc/partials/ethos-include.sh 2>/dev/null || sh ~/.claude/skills/partials/ethos-include.sh
Context
- Project config: !
cat .adlc/config.yml 2>/dev/null || echo "No config — single-repo legacy mode" - Bug template: !
cat .adlc/templates/bug-template.md 2>/dev/null || cat ~/.claude/skills/templates/bug-template.md 2>/dev/null || echo "No bug template found" - Conventions: !
cat .adlc/context/conventions.md 2>/dev/null || echo "No conventions found" - Existing bugs: !
ls .adlc/bugs/ 2>/dev/null || echo "No bugs directory found"
Input
Bug report: $ARGUMENTS
Prerequisites
Before proceeding, verify that .adlc/bugs/ exists. If it doesn't, stop and tell the user: "The .adlc/ structure hasn't been initialized. Run /init first."
Instructions
Phase 1: Report
- If given a bug description (not a BUG ID), create a bug report:
- Determine the next BUG ID using the global atomic counter file
~/.claude/.global-next-bug(shared across all repos for unique IDs, mirroring the REQ counter — see LESSON-004). The counter is now a cache, not the authority — the remote is the source of truth (REQ-518): allocation derives the remote high-water, takesmax(remote, local) + 1, and fast-forwards the local counter, all inside the existingmkdirlock. Allocate via the sharedpartials/id-alloc.shhelper (BR-5 — the lock block + its REQ-416/LESSON-014 rationale live in the partial). Source it and calladlc_alloc_idin the same fenced block (the cross-fence-fn rule — see conventions.md "Bash in skills"):. .adlc/partials/id-alloc.sh 2>/dev/null || . ~/.claude/skills/partials/id-alloc.sh BUG_NUM=$(adlc_alloc_id bug) # `exit 1` inside adlc_alloc_id's subshell terminates only the subshell — BUG_NUM # would be silently empty. Guard the parent context (REQ-416 verify D-pass). [ -n "$BUG_NUM" ] || { echo "ERROR: failed to allocate BUG number — aborting before writing malformed bug report" >&2; exit 1; } # If ADLC_ALLOC_DEGRADED=1 (remote unreachable), the helper warned on stderr — note # "id allocated without remote verification — verify before PR" (BR-3). Never block.adlc_alloc_id bughandles the absent-counter bootstrap scan internally (highestBUG-xxxunder$ADLC_REPOS_ROOT; bug reports are.mdfiles so the scan uses-type f), themkdirlock that serializes concurrent sessions, and the remote high-water max. Single-machine behavior is unchanged when the remote has no higher allocation (BR-7). Note: the legacy per-repo.adlc/.next-bugcounter is deprecated and no longer consulted — existing files can be left in place but should not be read or written. - Pre-push recheck (BR-4, BR-8). Before the bug file is committed on a branch for push, re-verify
BUG-<id>against the remote — a colleague on another machine may have pushed the same id since allocation. Sourcepartials/id-recheck.shand calladlc_recheck_idin the same fenced block; a collision halts with the renumber instruction rather than pushing a duplicate:. .adlc/partials/id-recheck.sh 2>/dev/null || . ~/.claude/skills/partials/id-recheck.sh BUG_ID=$(printf 'BUG-%03d' "$BUG_NUM") if ! adlc_recheck_id bug "$BUG_ID"; then echo "Halting: $BUG_ID collides on the remote — renumber before pushing (see message above)." >&2 exit 1 fi - Create
.adlc/bugs/BUG-xxx-slug.md(always in the current repo — this becomes the "primary" for the bug) using the template from.adlc/templates/bug-template.md - Fill in: description, reproduction steps (if known), expected vs actual behavior, environment
- Set status to
open, severity based on impact - Cross-repo: if
.adlc/config.ymldeclares siblings AND the bug's fix likely lives in a sibling (e.g., a frontend symptom whose root cause is in a backend repo), add arepo: <sibling-id>field to the bug frontmatter. If the fix spans multiple repos, add atouched_repos: [<id>, <id>]field. Therepo:field determines where Phase 3's commit and Phase 4's PR land.
- Determine the next BUG ID using the global atomic counter file
- If given a BUG ID, read the existing bug report — note any
repo:ortouched_repos:field for routing.
Phase 2: Analyze
- Launch Explore agents to trace the bug:
- Search for relevant code paths based on the bug description
- Trace the execution flow that triggers the bug
- Identify the root cause (not just symptoms)
- Read the identified files to understand the context
- Document the root cause in the bug report's "Root Cause" section
- Validate the analysis:
- Re-read the affected code paths to confirm the root cause is correct
- Check for secondary issues or edge cases related to the bug
- Adjust the root cause and fix approach if validation reveals inaccuracies
- Update the bug report with the validated findings
Phase 3: Fix
- Determine target repo: if the bug's frontmatter has
repo:and it names a sibling (not this repo), cd into that sibling's path from.adlc/config.ymland do all fix work there. Fortouched_repos: [...], cd into each in turn — one commit per repo, on a shared branch name. Otherwise fix in the current repo. - Proceed directly with the validated fix approach — do not pause for user confirmation
- Implement the fix following project conventions
- Ensure the fix addresses the root cause, not just symptoms
- Update related test files if the fix changes behavior
- Track progress with TodoWrite
Phase 4: Verify
- Run the test suite:
npm test(or appropriate test command) - If tests fail, fix and re-run
- Update the bug report (do NOT mark
resolvedyet — that happens in Phase 6 after the fix is merged and deployed):- Leave status as
open(or set toin-reviewif your project uses that value) - Fill in "Resolution" section with what was changed and why
- Fill in "Files Changed" section with specific file paths
- Update the
updateddate
- Leave status as
- Present an interim summary:
- Root cause
- What was fixed
- Files changed
- Test results
- Then continue to Phase 5
Phase 5: Ship — Create Pull Request(s)
For each touched repo (just the current repo in single-repo mode; each entry in touched_repos: in cross-repo mode):
- Push the fix branch:
git -C <worktree> push -u origin fix/bug-xxx-slug - Create the PR with
adlc_forge_pr_create(sourcepartials/forge.shin the same fence; run from inside the worktree, or pass-R <owner/repo>). All PR ops route through the forge adapter, never directgh(REQ-520 BR-1). In cross-repo mode, create the primary repo's PR last so its body can link every sibling.- Title:
fix(BUG-xxx): short description— when cross-repo, scope to the repo (e.g.,fix(api): null deref in user serializer [BUG-042]). - Body:
## Summary [1-2 lines describing what broke and what was fixed in THIS repo] ## Bug BUG-xxx: [bug title] Severity: [critical | high | medium | low] Primary repo: <primary-repo-id> ## Root Cause [Pulled from the bug report's Root Cause section] ## Files Changed (this repo) - `path/to/file.ts` — what changed and why ## Related PRs (cross-repo) [Omit in single-repo mode. Otherwise list each sibling PR URL — back-fill sibling bodies via `adlc_forge_pr_edit` once every URL is known.] ## Test Plan - [ ] Unit/integration tests pass locally - [ ] CI green on this PR - [ ] Staging deploy succeeded (verified in Phase 6) - [ ] Production deploy succeeded (verified in Phase 6)
- Title:
- After all sibling PRs exist, edit each one (
adlc_forge_pr_edit <prUrl> --body ...) to fill in the Related PRs section. - Wait for CI to pass on every PR:
gh pr checks <prUrl>. If CI fails, diagnose and re-push — never bypass with--no-verifyor admin-merge. - Report all PR URLs to the user, grouped by repo.
Phase 6: Wrapup — Merge, Deploy, Knowledge Capture
This is the equivalent of /proceed's Phase 8 / /wrapup steps, condensed for bugs.
Step 1 — Merge each PR.
- Verify the PR is mergeable:
adlc_forge_pr_view <prUrl> --json mergeable,mergeStateStatusshould reportMERGEABLE(on GitHub; ADO normalizes viapr_view). If main has advanced, rebase the fix branch ontoorigin/main, force-push with lease, and wait for CI to re-pass. - Merge with squash + branch delete:
adlc_forge_pr_merge <prUrl> --squash --delete-branch. In cross-repo mode, walktouched_repos:order (ormerge_order:from.adlc/config.ymlif not specified on the bug).
Step 2 — Confirm deploys (this is the staging-first gate when the project has one — same model as features).
Skip this step entirely if the project doesn't deploy via Cloud Run (i.e., stack.backends in .adlc/config.yml doesn't include cloud-run and there's no gcp: block).
Otherwise, for each touched service that has a services: entry in .adlc/config.yml, look up gcp.staging_project and gcp.production_project from the config and confirm both:
# Staging
gcloud run services describe <service> \
--project=<gcp.staging_project from config> \
--region=<services[<id>].region or gcp.default_region> \
--format="value(status.latestReadyRevisionName,status.traffic[0].revisionName)"
# Production
gcloud run services describe <service> \
--project=<gcp.production_project from config> \
--region=<services[<id>].region or gcp.default_region> \
--format="value(status.latestReadyRevisionName,status.traffic[0].revisionName)"
Confirm the merge SHA's revision is serving 100% traffic in each. If gcp.production_project is omitted (no separate prod project), only confirm staging.
If staging deployed but production has NOT yet been promoted, wait — the pipeline runs them sequentially. If either fails, surface to the user with the failed deploy log link before claiming the bug resolved.
iOS deploy (only when stack.frontends in .adlc/config.yml includes ios AND the fix touched the iOS repo):
- Read
ios.deploy_targets,ios.derived_data_clean, andios.deploy_commandfrom.adlc/config.yml. - If
ios.derived_data_cleanis true:rm -rf ~/Library/Developer/Xcode/DerivedData/* - From the iOS repo's worktree, run
<ios.deploy_command>and deploy to every device inios.deploy_targets— never skip one. Don't leave this as a follow-up for the user.
If stack.frontends doesn't include ios, skip this section entirely.
Step 3 — Update the bug report.
- Set status to
resolved - Update the
updateddate - Confirm Resolution and Files Changed sections are filled in (from Phase 4)
- Add a Deployment section noting the staging + production revisions
Step 4 — Capture knowledge (NEVER skip — per memory feedback_wrapup_knowledge_capture.md).
Evaluate honestly: did this bug reveal something a future implementer should know?
- A surprising failure mode (race condition, schema mismatch, mocked-vs-real divergence, etc.)?
- A pattern or anti-pattern worth recording?
- A check that would have caught this earlier?
- An assumption from a prior REQ that turned out false?
If yes, write a lesson to .adlc/knowledge/lessons/LESSON-xxx-slug.md using the global atomic counter ~/.claude/.global-next-lesson (shared across all repos for unique IDs, mirroring the REQ/BUG counters — see LESSON-004). The counter is now a cache, not the authority — the remote is the source of truth (REQ-518): allocation derives the remote high-water, takes max(remote, local) + 1, and fast-forwards the local counter, all inside the shared mkdir-lock (~/.claude/.global-next-lesson.lock.d, shared with /wrapup so concurrent /bugfix and /wrapup runs mutually exclude). Allocate via the shared partials/id-alloc.sh helper (BR-5 — the lock block + its LESSON-014 symlink pre-check live in the partial). Source it and call adlc_alloc_id in the same fenced block (the cross-fence-fn rule — see conventions.md "Bash in skills"):
. .adlc/partials/id-alloc.sh 2>/dev/null || . ~/.claude/skills/partials/id-alloc.sh
LESSON_NUM=$(adlc_alloc_id lesson)
# `exit 1` inside adlc_alloc_id's subshell terminates only the subshell — LESSON_NUM
# would be silently empty. Guard the parent context (REQ-416 verify D-pass).
[ -n "$LESSON_NUM" ] || { echo "ERROR: failed to allocate LESSON number — aborting before writing malformed lesson" >&2; exit 1; }
adlc_alloc_id lesson handles the absent-counter bootstrap scan internally (highest LESSON-xxx under $ADLC_REPOS_ROOT; lessons are .md files so the scan uses -type f), the shared mkdir lock, and the remote high-water max. Single-machine behavior is unchanged when the remote has no higher allocation (BR-7). Note: the legacy per-repo .adlc/.next-lesson counter is deprecated and no longer consulted — existing files can be left in place but should not be read or written.
Pre-push recheck (BR-4, BR-8). Before the lesson file is committed on a branch for push, re-verify LESSON-<id> against the remote — a colleague on another machine may have pushed the same id since allocation. Source partials/id-recheck.sh and call adlc_recheck_id in the same fenced block; a collision halts with the renumber instruction rather than pushing a duplicate:
. .adlc/partials/id-recheck.sh 2>/dev/null || . ~/.claude/skills/partials/id-recheck.sh
LESSON_ID=$(printf 'LESSON-%03d' "$LESSON_NUM")
if ! adlc_recheck_id lesson "$LESSON_ID"; then
echo "Halting: $LESSON_ID collides on the remote — renumber before pushing (see message above)." >&2
exit 1
fi
Use the lesson template (.adlc/templates/lesson-template.md, fall back to ~/.claude/skills/templates/lesson-template.md). Filename format is LESSON-xxx-slug.md only — no date prefixes, no bare-numeric prefixes. Include domain, component, and tags so future runs of /spec, /architect, /reflect, and /review can filter by relevance.
If the bug genuinely produced no useful lesson (one-line typo, etc.), say so explicitly in the final summary — don't silently skip.
Step 5 — Clean up.
- Switch the local checkout to main and pull:
git -C <main-worktree> checkout main && git -C <main-worktree> pull - If the fix was done in a separate worktree, remove it:
git -C <main-worktree> worktree remove <fix-worktree-path> - If the fix branch still exists locally after squash-merge, delete it:
git branch -D fix/bug-xxx-slug - Prune remote-tracking refs:
git fetch --prune
Step 6 — Final ship summary.
## BUG-xxx: Bug Title — Resolved
**Severity**: <severity>
**PR(s)**: #nn (and siblings if cross-repo)
**Merged**: YYYY-MM-DD
### Root cause
- 1-2 lines
### Fix
- 1-2 lines
### Deployment
- Staging: <service> revision <hash> @ 100% traffic
- Production: <service> revision <hash> @ 100% traffic
- iOS: deployed to <list of ios.deploy_targets from config> (or "n/a — backend-only fix")
### Lessons captured
- `.adlc/knowledge/lessons/LESSON-xxx-slug.md` — one-line hook
(or "None — fix was straightforward and revealed no new pattern")
Branch Naming
Use fix/bug-xxx-slug for the branch name. In cross-repo bugs, use the same branch name in every touched repo so PRs can be linked visually.
Commit Message Format
fix(BUG-xxx): short description of the fix
Cross-Repo Bugs (brief)
When a bug's fix spans repos (via touched_repos: in the bug frontmatter):
- The bug report itself always lives in the repo
/bugfixwas invoked from (the "primary" for this bug). - Phase 3 makes one commit per touched repo, each on a branch with the same name (
fix/bug-xxx-slug). - Phase 5 opens one PR per touched repo and cross-links them (primary PR's body is created last so it can reference every sibling URL).
- Phase 6 merges in the order the repos are listed in
touched_repos:. If the bug report doesn't specify an order, use themerge_orderfrom.adlc/config.yml. - If this gets complicated (more than 2 touched repos, or ordering matters), consider promoting the bug into a full REQ and using
/proceedinstead.
Next.js App Router Expert
Development
A skill that turns Claude into a Next.js App Router expert.
README Generator
Development
Creates professional and comprehensive README.md files for your projects.
API Documentation Writer
Development
Generates comprehensive API documentation in OpenAPI/Swagger format.