Packages
Secure BEAM sandbox runtime for LLM code mode and MCP aggregation. Run concurrent LLM/tool clients safely while agents orchestrate approved tools, call upstream MCP/OpenAPI servers, and transform data.
Current section
Files
Jump to
Current section
Files
docs/guidelines/github-workflows.md
# GitHub Workflows Overview
This document describes the Claude-powered GitHub workflows for autonomous issue implementation.
## Design Principles
1. **Trust Claude to be smart** - Keep workflow YAML simple, let Claude figure out context
2. **Epic as source of truth** - All project state lives in the epic issue
3. **Serialized implementation, durable queue** - Main-mutating work runs one
at a time via the `claude-impl` lock; requests are queued in labels so none
are ever silently dropped. Read-only workflows run in their own per-event
groups and never block implementation.
4. **Dependencies checked at runtime** - Claude reads "Blocked by:" and refuses if blockers open
5. **Never lose work silently** - Always leave a trail (status comments, branches, PRs)
6. **Fresh context breaks bias** - Second opinion reviews catch issues original implementer missed
7. **Protect specs from cheating** - Implementation matches spec, not vice versa
## Workflow Summary
| Workflow | Trigger | Purpose |
|----------|---------|---------|
| `claude-code-review.yml` | PR events, `claude-review` label | Review PR code, detect protected file changes |
| `claude-auto-triage.yml` | After code-review completes | Triage review findings |
| `claude-issue-review.yml` | `needs-review` label | Review issue, add `ready-for-implementation` |
| `claude-issue-enqueue.yml` | `@claude` + `ready-for-implementation` | Validate request, add `impl:queued` (durable queue entry) |
| `claude-issue.yml` | `impl:queued` label / schedule / dispatch | **Serialized runner**: drains the queue one issue at a time, implements with mandatory status |
| `claude-pr-fix.yml` | `@claude` on PR | Fix PR issues (max 3 attempts) |
| `claude-second-opinion.yml` | `needs-second-opinion` label | Fresh context review after failed fixes |
| `claude-epic-start.yml` | `status:active` on epic | Start epic by triggering first unblocked issue |
| `claude-epic-update.yml` | Issue closed with `epic:*` label | Update epic checkboxes |
| `claude-blocker-resolved.yml` | Issue closed | Add `needs-review` to unblocked issues |
| `claude-stale-check.yml` | Every 2 hours (scheduled) | Detect stuck implementations |
| `claude-batch-fix.yml` | Manual or 5+ `quick-fix` issues | Batch fix trivial issues |
## Workflow Interactions
```
┌─────────────────────────────────────────────────────────────────────┐
│ PR WORKFLOW │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ PR Created ──► code-review.yml ──► auto-triage.yml │
│ │ │ │
│ │ ┌──────────┴──────────┐ │
│ │ ▼ ▼ │
│ │ FIX_NOW DEFER_ISSUE │
│ │ (posts @claude) (creates issue) │
│ │ │ │
│ │ ▼ │
│ │ claude-pr-fix.yml │
│ │ │ │
│ │ ┌────┴────┐ │
│ │ ▼ ▼ │
│ │ Success 3 failures │
│ │ │ │ │
│ │ │ ▼ │
│ │ │ second-opinion.yml │
│ │ │ │ │
│ │ │ ┌────┴────┐ │
│ │ │ ▼ ▼ │
│ │ │ Fixed Escalate │
│ │ │ │ (needs-human-review) │
│ │ │ │ │
│ │ ▼ ▼ │
│ │ Auto-merge │
│ │ │
│ Protected files? ─┴──► spec-change-detected label │
│ (anti-cheating review) │
│ │
└──────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ ISSUE WORKFLOW │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Epic activated ──► epic-start.yml ──► Adds `needs-review` │
│ (status:active) │ to first unblocked issue │
│ │ │ │
│ │ ▼ │
│ │ issue-review.yml │
│ │ │ │
│ │ ▼ │
│ │ Adds `ready-for-implementation` │
│ │ + posts @claude trigger │
│ │ │ │
│ │ ▼ │
│ │ claude-issue.yml │
│ │ │ │
│ │ ┌──────────────┼──────────────┐ │
│ │ ▼ ▼ ▼ │
│ │ SUCCESS INCOMPLETE BLOCKED │
│ │ (creates PR) (posts status, (removes │
│ │ │ needs-attention) label) │
│ │ │ │ │
│ │ │ ▼ │
│ │ │ stale-check.yml │
│ │ │ (retries stale) │
│ │ │ │
│ │ ▼ │
│ │ PR merged │
│ │ │ │
│ │ ┌────┴────┐ │
│ │ ▼ ▼ │
│ │ epic- blocker-resolved.yml │
│ │ update (adds needs-review to │
│ │ .yml next unblocked issue) │
│ │ │ │
│ └──────────────┘ (chains to next issue) │
│ │
└──────────────────────────────────────────────────────────────────────┘
```
## Robustness Features
### Timeout Handling
All Claude workflows have explicit timeouts to prevent runaway jobs:
| Workflow | Timeout |
|----------|---------|
| `claude-issue.yml` | 75 minutes |
| `claude-pr-fix.yml` | 30 minutes |
| `claude-second-opinion.yml` | 45 minutes |
If a workflow times out, the fallback status handler posts an INCOMPLETE status.
### Structured Automation State
State is stored in issue/PR body (not comments) using JSON metadata:
```markdown
## Automation State
<!-- automation-state: {"status":"INCOMPLETE","pr":123,"branch":"claude/45-feature","attempts":2} -->
| Field | Value |
|-------|-------|
| Status | `INCOMPLETE` |
| PR | #123 |
| Branch | `claude/45-feature` |
| Attempts | 2 |
```
**Why body instead of comments:**
- Comments can be deleted, breaking state tracking
- Body sections survive edits and are API-updatable
- Single source of truth per issue/PR
**PR fix tracking:**
```markdown
## Fix Automation State
<!-- fix-state: {"attempts":2} -->
Fix attempts: 2/3
```
If Claude doesn't update status, the workflow posts a fallback and adds `needs-attention`.
### Protected Files (Anti-Cheating)
Implementation PRs should rarely modify these files:
| Pattern | Purpose |
|---------|---------|
| `docs/specs/*.md` | Specifications (source of truth) |
| `docs/guidelines/*.md` | Process documentation |
| `.github/workflows/*.yml` | Automation workflows |
| `.credo.exs`, `.formatter.exs` | Linter/formatter configs |
When protected files change:
1. `spec-change-detected` label is added
2. Code review checks if changes are **LEGITIMATE** (real spec gap) or **AVOIDANCE** (cheating)
3. Signs of avoidance:
- Spec weakens requirements to match buggy implementation
- Spec adds exceptions for unhandled edge cases
- Linter rules disabled instead of fixing code
### Scope Guards
Implementation stops for scope creep (principle-based, not hard thresholds):
**Signs of scope creep:**
- Changes spanning multiple unrelated modules or subsystems
- Discovering substantial prerequisite work not in the issue
- Implementation feels like 2-3 separate issues bundled together
- Non-mechanical changes touching areas unrelated to the issue's focus
**NOT scope creep (proceed normally):**
- Mechanical changes across many files (renames, import updates, type fixes)
- Related changes that naturally flow from the core implementation
- Test files matching the implementation scope
When stopping:
1. Posts INCOMPLETE status
2. Adds `needs-breakdown` and explains how to split
3. If discovered blocker: creates issue, updates "Blocked by:" section
### Fix Attempt Tracking
PR fixes track attempts via structured state in PR body (not comment counting):
- Attempt counter stored in `<!-- fix-state: {"attempts":N} -->`
- After 3 attempts → escalate to `needs-second-opinion`
- Second opinion uses fresh Claude context (no sunk cost bias)
- If second opinion also fails → escalate to `needs-human-review` (terminal state)
**Escalation chain:**
```
fix attempt 1 → fix attempt 2 → fix attempt 3 → second opinion → human review
```
### Duplicate PR Detection
Before creating a new PR, implementation checks for existing PRs:
```bash
gh pr list --head "claude/${issue_number}-" --state open
```
If a PR exists:
- Updates existing PR instead of creating duplicate
- Pushes to existing branch
- Posts comment noting continued work
### TODO/Skip Tag Protocol
All TODOs, FIXMEs, and skipped tests MUST reference a GitHub issue.
**Required format:**
```elixir
# TODO(#123): Explanation of what needs to be done
# FIXME(#123): Explanation of the bug
@tag :skip # Skipped: #123 - reason why test is skipped
# credo:disable-for-next-line Credo.Check.Name - #123
```
**Before adding a TODO:**
1. Search for existing issues: `gh issue list --search "keyword" --state open`
2. If none exists, create one with `tech-debt` and `from-pr-review` labels
3. Reference the issue number in the code
**Code review enforcement:**
- PRs with unreferenced TODOs are flagged as "MUST FIX"
- Pattern checked: `TODO` without `(#\d+)`, `@tag :skip` without `#\d+`
**Why this matters:**
- Prevents untracked technical debt
- Enables prioritization via issue labels
- Avoids duplicate issues for same problem
- Creates audit trail of when/why debt was introduced
### Discovered Blocker Protocol
When implementation discovers prerequisite work:
```
1. STOP - don't try to fix it in the same PR
2. Create issue with `discovered-blocker` AND `needs-review` labels
3. Update current issue's "Blocked by:" section
4. Remove `ready-for-implementation` from current issue
5. Post BLOCKED status
6. Blocker gets reviewed and implemented (has needs-review)
7. blocker-resolved.yml adds needs-review to current issue when blocker closes
```
### Stale Detection & Self-Healing
`claude-stale-check.yml` runs every 2 hours with a hybrid bash + Claude approach:
**Detection (bash, always runs, no concurrency group):**
- Epic issues with no labels but all blockers resolved
- Issues with `needs-review` but no review activity after 2 hours
- Issues with `ready-for-implementation` but no PR/status after 2 hours
- Closed issues still carrying `impl:queued` or `impl:running`
- Stale open `impl:running` issues with a recorded workflow run claim
- Adds `needs-attention` label for visibility
- Cancels only a recorded stale run ID validated against GitHub run metadata;
it never guesses from labels or trusts issue-body metadata alone
**Claude fix (only if issues detected):**
- Gets the list of stuck issues from detection
- Reads `docs/guidelines/github-workflows.md` to understand the system
- Figures out what's wrong and fixes it (add labels, fix issue bodies, re-trigger)
- Uses the `claude-impl` concurrency group (serialized with the issue runner)
**Orphan recovery (parallel, no Claude):**
- Detects `claude/*` branches without PRs → creates draft PRs
- Logs stuck PRs with `needs-human-review` for 24+ hours
This design ensures visibility (`needs-attention` label) even if Claude can't run.
The non-Claude detector may remove stale queue labels and cancel recorded
workflow runs, but it never pushes branches or creates implementation PRs.
## Epic-Driven Development
### Epic Issue Structure
The epic issue is the single source of truth for project progress:
```markdown
# [Epic Name] Implementation
## Specification Documents
- [Primary Spec](docs/specs/spec.md)
## Progress
### Phase 1: [Phase Name]
- [ ] #123 - Task description
- [ ] #124 - Another task
- [x] #125 - Completed task
### Phase 2: [Phase Name]
- [ ] #126 - Blocked by #123
...
## Discovered Issues
- #130 - Found during implementation of #124
```
**Required labels on epic:** `type:epic`, `status:active`
**Labels on issues in epic:** `epic:epic-name` (e.g., `epic:message-history`)
### Dependency Tracking
Issues track dependencies in their body:
```markdown
## Dependencies
- **Blocked by:** #123, #124
- **Blocks:** #125, #126
```
**Parsing rules:**
- Only `#number` references in the "Blocked by:" line/section are considered blockers
- The parser stops at "Blocks:" or other sections to avoid false positives
- Self-references are ignored (issue can't block itself)
**Common pitfalls to avoid:**
- Don't put issue numbers in "Blocks:" that match the current issue (typos cause self-blocking)
- Don't reference PRs or closed issues in prose near "Blocked by:" (may be parsed as blockers)
- Keep "Blocked by:" and "Blocks:" sections clearly separated
The implementation workflow:
1. Reads "Blocked by:" section (stops at "Blocks:" or next heading)
2. Skips self-references
3. Checks if each blocker is closed
4. If any blocker is open: posts BLOCKED status, removes label, stops
5. When blocker closes: `blocker-resolved.yml` re-enables dependent issues
## Labels Reference
### Label Priority (Conflict Resolution)
When multiple labels are present, this priority order applies:
| Priority | Label | Effect |
|----------|-------|--------|
| 1 (highest) | `needs-human-review` | **Stops all automation immediately** |
| 2 | `do-not-auto-merge` | Prevents merge but allows fixes |
| 3 | `needs-second-opinion` | Escalates to fresh context review |
| 4 | `needs-attention` | Flags for retry/investigation |
| 5 (lowest) | `ready-for-implementation` | Enables automation |
**Example:** If an issue has both `ready-for-implementation` and `needs-human-review`,
automation will NOT proceed because `needs-human-review` has higher priority.
### Trigger Labels
| Label | Purpose |
|-------|---------|
| `needs-review` | Triggers issue review workflow |
| `ready-for-implementation` | Security gate for implementation |
| `claude-review` | Triggers PR review |
| `claude-approved` | Security gate for PR fixes (non-maintainer) |
### Implementation Queue Labels
| Label | Purpose |
|-------|---------|
| `impl:queued` | Durable queue entry — issue is waiting for the serialized runner. Added by `claude-issue-enqueue.yml`. |
| `impl:running` | The runner has claimed this issue and is implementing it. Prevents double-processing; removed when the run finishes (even on failure). |
### Epic Labels
| Label | Purpose |
|-------|---------|
| `type:epic` | Issue is an epic |
| `status:active` | Currently active epic (only one at a time) |
| `epic:*` | Links issue to specific epic (e.g., `epic:message-history`) |
### Status Labels
| Label | Purpose |
|-------|---------|
| `needs-human-review` | Automation stopped, human must intervene |
| `needs-attention` | Stale or interrupted, needs retry |
| `needs-second-opinion` | Triggers fresh context review |
| `needs-clarification` | Issue needs more details |
| `needs-breakdown` | Issue too large, needs splitting |
| `do-not-auto-merge` | Prevents auto-merge |
| `ready-to-merge` | Triage approved, ready for merge |
| `merge-conflict` | PR has merge conflicts |
| `spec-change-detected` | PR modifies spec files (review carefully) |
### Issue Type Labels
| Label | Purpose |
|-------|---------|
| `from-pr-review` | Issue created from PR review |
| `discovered-blocker` | Found during implementation, blocks another issue |
| `quick-fix` | Trivial fix, batched by batch-fix workflow |
| `tech-debt` | Technical debt tracked via TODO/FIXME in code |
## Concurrency Control
> **Why not one shared group?** A single global `claude-automation` group was
> the original design, but it has a fatal flaw: by default, a GitHub concurrency
> group holds at most **one running + one pending** run. When several workflows
> fire at once, the newest waiting request **evicts and cancels** the older
> pending one. GitHub now supports larger concurrency queues with `queue: max`,
> but labels remain the durable source of implementation state. Read-only
> reviews competing in the same group could still starve implementation work,
> and a burst of `@claude` requests could silently drop work on older default
> queues. (This stranded issue #1039.) The current design replaces the single
> group with a durable queue plus purpose-scoped groups.
### Durable implementation queue
Issue implementation is split into an **enqueue** front door and a
**serialized runner**:
- `claude-issue-enqueue.yml` (per-issue group `claude-enqueue-<n>`, never
blocked) validates the `@claude` request and adds the **`impl:queued`**
label. The label *is* the queue entry — durable state that survives even if
GitHub drops a pending run.
- `claude-issue.yml` is the runner. It holds the global `claude-impl` lock,
picks the oldest `impl:queued` issue, flips it to `impl:running`, implements
it, then releases the label and re-dispatches itself if the queue is
non-empty. Triggers: the `labeled` event (fast path), a `*/10` schedule
(backstop so the queue can never stall), and `workflow_dispatch`. When an
issue is claimed, the runner records the workflow `run_id`, attempt, run URL,
and claim time in the issue body so watchdog cleanup can target only that run.
This guarantees **both** invariants the single group could not:
- **Never parallel** — `claude-impl` (size 1) lets only one main-mutating run
execute at a time.
- **Never dropped** — requests live in labels, not in GitHub's single pending
slot, so a burst is drained in order rather than cancelled.
### Concurrency groups by purpose
| Group | Workflows | `cancel-in-progress` | Rationale |
|-------|-----------|----------------------|-----------|
| `claude-impl` | `claude-issue.yml` (runner), `claude-batch-fix.yml`, `claude-stale-check.yml` | `false` | All branch off `main` / open PRs → strictly serialized while jobs run. Batch fix skips when an open `batch-fix/*` PR already exists, including after queued runs acquire the lock. |
| `claude-enqueue-<issue>` | `claude-issue-enqueue.yml` | `false` | Cheap, idempotent; per-issue so it's never evicted and only collapses duplicate `@claude` comments on the same issue. |
| `claude-pr-<pr>` | `claude-pr-fix.yml`, `claude-second-opinion.yml` | `false` | Push to a PR's *own* branch, not `main`; serialized per-PR but parallel across different PRs. |
| `claude-review-pr-<pr>` | `claude-code-review.yml` | `true` | Read-only; a newer push supersedes an in-flight review. |
| `claude-review-triage-<runid>` | `claude-auto-triage.yml` | `false` | Read-only; keyed to the triggering review run. |
| `claude-review-issue-<issue>` | `claude-issue-review.yml` | `false` | Read-only; per-issue. |
| `claude-epic-<issue>` | `claude-epic-update.yml` | `false` | Edits the epic issue, not `main`; per-trigger. |
Read-only workflows are deliberately **outside** `claude-impl` so they can
never block or evict implementation work.
This ensures:
- Only one main-mutating Claude operation runs at a time (`claude-impl`)
- Batch-fix automation opens at most one batch PR at a time; later runs skip
while an existing `batch-fix/*` PR is open
- No concurrent race conditions on `main`; reduced risk of conflicting
branches/PRs
- Implementation requests queue durably in labels instead of being cancelled
## Security Gates
### For Public Repository Safety
1. **PRs**: Maintainer must add `claude-review` label
2. **PR fixes**: Requires `claude-approved` label (or be maintainer)
3. **Issue implementation**: Requires `ready-for-implementation` label
### Loop Prevention
- PR fixes max 3 attempts before escalating to second opinion
- Second opinion max 1 attempt before escalating to human (terminal state)
- `needs-human-review` label stops all automation
- Concurrency group prevents parallel execution
- Stale check runs on schedule, not on every event
- Structured state in body prevents counter manipulation via comment deletion
**Terminal states (require human intervention):**
- `needs-human-review` label present
- Second opinion already attempted (marked in PR body)
## Recovery Procedures
### Issue Stuck Without Status
```bash
# Check recent workflow runs
gh run list --workflow=claude-issue.yml --limit 5
# Manually trigger retry
gh issue comment ISSUE_NUMBER --body "@claude Please implement this issue."
```
### PR Stuck in Fix Loop
```bash
# Check attempt count
gh pr view PR_NUMBER --json comments \
--jq '[.comments[] | select(.body | test("@claude.*fix"; "i"))] | length'
# Trigger second opinion manually
gh pr edit PR_NUMBER --add-label "needs-second-opinion"
```
### Orphan Branch Recovery
```bash
# List claude branches without PRs
for branch in $(git branch -r | grep 'origin/claude/'); do
gh pr list --head "${branch#origin/}" --state open --json number | jq -e '.[0]' || echo "Orphan: $branch"
done
# Stale check will auto-create draft PRs, or manually:
gh pr create --head "claude/123-feature" --draft --title "[Draft] Recovered #123"
```
### Inspect / Drain the Implementation Queue
The implementation queue lives in labels, not in GitHub's pending-run slot:
```bash
# What's waiting and what's in flight
gh issue list --label "impl:queued" --state open
gh issue list --label "impl:running" --state open
# Manually kick the runner (e.g. if the schedule backstop hasn't fired yet)
gh workflow run claude-issue.yml
```
If a run crashed and left an issue stuck as `impl:running` (the runner clears
this even on failure, so this is rare), free it manually:
```bash
gh issue edit ISSUE_NUMBER --remove-label "impl:running" --add-label "impl:queued"
gh workflow run claude-issue.yml
```
If jobs are stuck running:
```bash
# Prefer the recorded run ID in the issue's Automation Claim.
gh issue view ISSUE_NUMBER --json body --jq '.body'
gh run cancel RUN_ID
```
For closed issues, do not requeue. Remove stale queue labels after cancelling
any recorded active run:
```bash
gh issue edit ISSUE_NUMBER --remove-label "impl:running" --remove-label "impl:queued"
```
## Fork PR Handling
Fork PRs have limited automation:
1. **code-review.yml** - Works normally (read-only)
2. **auto-triage.yml** - Creates issues instead of `@claude` fix comments
3. **claude-pr-fix.yml** - Posts explanation and skips (cannot push to forks)
## Secrets Required
| Secret | Purpose |
|--------|---------|
| `CLAUDE_CODE_OAUTH_TOKEN` | Claude API authentication |
| `PAT_WORKFLOW_TRIGGER` | Enable bot comments to trigger workflows |
Without `PAT_WORKFLOW_TRIGGER`, bot-created comments won't trigger other workflows.
## Quick Reference: Starting Work on an Epic
1. **Create epic issue** with `type:epic` label
2. **Create all issues** from roadmap with:
- `epic:your-epic-name` label
- "Blocked by: #X, #Y" section in body
- Clear acceptance criteria
3. **Add `status:active`** label to the epic
4. **Automation takes over**:
- `epic-start.yml` adds `needs-review` to first unblocked issue
- Review → implement → PR → merge
- `blocker-resolved.yml` adds `needs-review` to next unblocked issue
- Chain continues until all issues complete
## Edge Case Handling
### Circular Dependencies
If A blocks B and B blocks A:
- Issue review should detect and add `needs-clarification`
- Requires human to break the cycle
### Reopened Blockers
If a blocker is reopened after dependent issue started:
- Next implementation check will detect and post BLOCKED status
- Dependent issue waits for blocker to close again
### Multiple Active Epics
Only one epic should have `status:active` at a time.
If multiple exist, epic-update may update the wrong one.
### Spec Changes During Implementation
If spec is edited after `ready-for-implementation` added:
- Implementation uses the new spec version
- For major changes, remove label and re-review