github-issue-automation
Skill3 starsChanged 2 months agoMITView on GitHub
Reference for scripts/automation/claude/handle_issues.py (and the codex/common equivalents) — the cron-driven, label-driven "execute the prompt from a GitHub issue/PR" automation. Load when working on, debugging, or extending that automation, or its .claude/commands/handle-issue.md command.
- Reads credentials
- Deletes or force-pushes
- Commits and pushes
In this repository45 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- .cursor/rules/issue-clarification-questions.mdcCursor rule
- .cursor/rules/reset-font-assets.mdcCursor rule
- .cursor/rules/src-dotnet-build-release.mdcCursor rule
- .cursor/rules/unity-plugins-before-work.mdcCursor rule
- codex-issueSkill
- commitSkill
- complete-prdSkill
- dependency-docsSkill
- dotnet-buildSkill
- dotnet-testSkill
- generate-imageSkill
- implementSkill
- meeting-joinSkill
- meeting-scheduleSkill
- meeting-startSkill
- plan-reviewSkill
- planSkill
- prSkill
- specifySkill
- unity-e2e-runSkill
- unity-pluginsSkill
- add-terminal-commandSkill
- dependency-docsSkill
- dotnet-benchmarkSkill
- dotnet-buildSkill
- dotnet-testSkill
- end-game-score-calibrationSkill
- flag-assetsSkill
- image-generationSkill
- localizationSkill
- meeting-joinSkill
- meeting-scheduleSkill
- meeting-startSkill
- milestone-completeSkill
- province-config-generatorSkill
- unity-e2e-runSkill
- unity-pluginsSkill
- unity-prefabsSkill
- unity-scenesSkill
- update-branchSkill
- cursor-meeting-joinSkill
- cursor-meeting-startSkill
- unity-pluginsSkill
The file
We split this skill into the rules it gives an agent
2 rules. About 0 are common, 0 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 54
checkout_clean: if the local branch exists and is ahead of origin/<branch>, push it first, then force-reset. - Very specific: line 96
Do not also set ANTHROPIC_API_KEY — its presence makes the CLI bill the API instead of the subscription.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Label-Driven GitHub Issue/PR Automation
scripts/automation/claude/handle_issues.{py,sh,ps1} runs on a cron schedule in the user's own environment (a personal machine or server the user controls — not this Claude Code Remote session, not a GitHub Actions runner). The script itself does cheap discovery via plain gh calls (no LLM usage); it only invokes claude -p "/handle-issue ..." (.claude/commands/handle-issue.md) — and only then spends subscription usage — when discovery actually finds a candidate. Discovery/locking/claim/clear logic shared with the Codex and Cursor equivalents (scripts/automation/codex|cursor/handle_issues.py) lives in scripts/automation/common/issue_handler.py.
The labels are the whole state machine
There is no local state file, no timestamp bookkeeping, no comment-heading parsing, and no reaction handling — the label set on each issue/PR is the entire state, visible and manually overridable on GitHub:
claude/codex/cursor— provider opt-in (owner or auto-router). Description plus trusted later comments are the prompt — no required format, no hardcoded spec→plan pipeline; whatever workflow is wanted (e.g. "run /specify for X") goes in the prompt itself.ai-in-progress— a run is actively working the item. Skipped by discovery. Shared across providers.ai-need-attention— the automation stopped and is waiting on the owner (question asked, blocked, partial work, missing environment). Skipped by discovery. Shared across providers.ai-complete— the prompt is fully done. Skipped by discovery. Shared across providers.ai-specify/ai-plan/ai-implement— informational stage progress while an agent runs/specify,/plan, or/implementon the item. Shared across providers. Not discovery status labels (they do not skip candidates by themselves). Agents set the matching label at the start of that command and remove the other two so at most one stage label is present.
A candidate is any open, configured-contributor-authored issue or PR carrying the provider opt-in label and none of the three shared ai-* status labels (ai-in-progress / ai-need-attention / ai-complete). Stage labels do not affect that rule. That single rule is all of discovery — one gh issue list plus one gh pr list, filtered locally. Claim and clear only touch the claimed item's number, so one provider/instance cannot clear another's in-flight work.
Each candidate gets its own CLI invocation from a guaranteed-clean checkout of its valid branch — main for an issue, the PR's head branch for a PR. The wrapper prepares it itself before every invocation (git fetch + if the local branch exists and is ahead of origin/<branch>, git push -u origin <branch> first + git checkout -f -B <branch> origin/<branch> + git clean -fd, which keeps ignored files like Logs/ intact). The ahead-push preserves unpushed salvage commits; if that push fails, the force-reset is skipped so the local tip is not discarded. The CLI run never starts from a stale or dirty tree, and candidates needing different branches can't contaminate each other. The CLI run must never do its own reset/clean at the start.
Resume semantics: the owner resumes a need-attention/complete item by replying in a comment and removing that status label. Removing the label is the explicit "go again" signal — the automation never guesses whether a comment was new instructions. The trade-off (forgetting to remove the label leaves the item parked, visibly) is accepted for the predictability.
Per-item lifecycle (the command side)
For each candidate, the Python wrapper claims (claim_candidate → ai-in-progress) before checkout/CLI, then clears that label after the CLI returns (clear_in_progress). The CLI run (see .claude/commands/handle-issue.md for the exact rules) never adds or removes ai-in-progress:
- Reads the item's live description + owner comments as the prompt (later comments override earlier ones; its own
<!-- claude-automation -->-marked comments are context, never instructions). - Executes it — on the PR's head branch for PR candidates, on
feature/<feature_name>for issue candidates (reused if it already exists on origin). When the prompt runs/specify,/plan, or/implement, the agent sets the matching stage label (ai-specify/ai-plan/ai-implement) at the start of that command and clears the other two. - Always commits and pushes, even partial work — the pushed branch is the next run's starting point.
- Ensures a PR exists for issue work (
Closes #N). Never merges anything — merging is always the owner's click. - Posts one summary comment (marker first line) — the handoff for both the owner and the next memory-less run.
- Applies
ai-completeorai-need-attentiononly — except after finishing/implement, which always ends withai-complete(neverai-need-attentionfor implement completion; skipped Editor-only work is noted in the comment).
Crash recovery: manual stuck-label cleanup
There is no automatic stale-label reclaim. A hard-kill (SIGKILL, power loss, host crash) after claim and before harness post-CLI clear can leave ai-in-progress stuck indefinitely — discovery keeps skipping the item until the owner manually removes ai-in-progress (and any needed follow-up labels) on GitHub. That is intentional: automatic reclaim was the peer-steal root cause across independently scheduled instances (#111/#112).
A crash before claim simply leaves the item a plain candidate — it retries next tick. That window is one claim-protocol call wide; accepted.
Session/usage limits: a planned pause, never a crash
A subscription session/usage-limit hit is handled entirely separately from crashes:
- Detection (Claude): matches legacy
… limit reachedand productionYou've hit your session/weekly limit(and usage) phrasing. Looks at non-JSON CLI lines and the final error result always; also consults assistant stream-json text only when the run already ended non-zero or error-shaped (is_error/ subtype starts witherror) — so an issue whose prompt merely talks about limits on a successful run can't false-positive. Reset time preference: embedded|epoch→ parseableresets 2:10pm (UTC)/resets 12am (UTC)wall-clock (next occurrence, aware UTC) → else now +--limit-backoff-minutes(default 60). - Detection (Codex):
usage limit|rate limit|quota exceededin error/failed output only. Reset time preference: parseabletry again at Aug 5th, 2026 9:31 AM(aware UTC; message has no timezone) → else now +--limit-backoff-minutes(default 60). - Detection (Cursor):
usage limit|rate limit|requests limit|too many requests|resource_exhausted|quota exceeded|spend limitin error/failed output only. Production CLI phrasing includesYou've hit your usage limit/free requests limitplusYour usage limits will reset when your monthly cycle ends on 8/14/2025. Reset time preference: thatM/D/YYYYas aware-UTC midnight (next day if already past) → else now +--limit-backoff-minutes(default 60). - Salvage: before writing the limit file / releasing labels, the wrapper runs deterministic Python git (
salvage_uncommitted_work): if the tree is dirty,git add -A, commit with fixed messagechore: salvage uncommitted work after session limitunderGIT_AUTHOR_*/GIT_COMMITTER_*identityGlobalStrategy Automation <automation@local>, thengit push -u origin HEAD. Clean tree → no commit. No agent/commit.mdpipeline, no version bump, no backup branches. - Pause orchestration (
handle_limit_pause): salvage → always write the shared provider-keyed state file → on clean/committed:clear_in_progressfor the claimed number only (never a provider-wide scan), thenreroute_auto_item_after_limit, then best-effort automation note on the item (salvage outcome + either the reroute or pause-until-retry_at); on salvage failure: applyai-need-attention, thenclear_in_progressfor that number, then best-effort failure comment - no reroute, since the item is now waiting on the owner, not just paused. Note posting is best-effort after save/clear so a failedpost_commentcannot undo a successful clear. Exits 0. - Immediate reroute for auto-routed items (
reroute_auto_item_after_limit): if the limit-hit candidate still carriesauto-ai(i.e. it was routed rather than opted in with a plain provider label), the release above is followed by dropping the current provider label and re-runningselect_auto_providerover the other two providers right there in the same process - no waiting for this provider's own backoff window, and no waiting for the next scheduledhandle_issues_auto.pytick. If every other provider is limited too,park_auto_item_unroutableappliesai-need-attentionand posts an<!-- auto-ai-automation -->note. Items opted in with a plain provider label (not auto-routed) are left on that label and simply wait out the pause, same as before. - Every later run checks the stored timestamp right after acquiring the lock, comparing aware-UTC to aware-UTC so the machine's local timezone never skews it, and skips the whole run (no GitHub calls, no CLI invocation) while the window is still in effect. Once it has passed, the file is deleted and normal runs resume.
checkout_clean: if the local branch exists and is ahead oforigin/<branch>, push it first, then force-reset; a failed ahead-push does not reset over the local tip (so an unpushed salvage commit survives until push succeeds).
Concurrency: local flock + cross-instance claim
handle_issues.py acquires an exclusive flock on Logs/handle_issues_claude.lock before doing anything else; a run that can't get the lock exits immediately. This stays a local OS-level lock rather than a GitHub label because it releases automatically the moment the process exits, crash or not. (On Windows it uses msvcrt locking; also set Task Scheduler's "don't start a new instance" option as a second safeguard.) The flock only serializes one wrapper process on one machine — it is not what prevents cross-instance double work.
That flock does nothing for two distinct automation instances (separate schedules/machines) — the failure mode behind issue #104 / duplicate PRs #105/#106. Cross-instance exclusion is claim_candidate in scripts/automation/common/issue_handler.py: each provider wrapper calls it once per candidate before checkout_clean / CLI spawn. It pairs the (non-CAS) ai-in-progress label with a uniquely-tokened, fully-invisible claim comment, sleeps a short settle delay so a near-simultaneous rival can post, then breaks the tie by monotonic GitHub comment id among comments fresher than a short freshness window. Losers log and continue with no checkout, CLI, branch, commit, PR, or summary comment for that item. After a successful claim, wrappers wrap checkout + CLI in try/finally: clear_in_progress(logger, number) so the label is cleared for that item only when the CLI returns (including Cursor SystemExit and Claude max-turns). Limit paths also clear only the claimed number — never a provider-wide scan that could peer-steal a sibling instance's live claim.
Security
Every action only ever triggers on content authored by KonH — the issue/PR itself (discovery filters --author KonH) and its comments (the command re-verifies per comment). Content from anyone else is ignored entirely, even a collaborator. Applying labels requires triage access, so the opt-in label itself is also gated. The automation authenticates with the owner's own credentials, so its own comments are distinguished from the owner's by the <!-- claude-automation --> marker prefix, not by author.
Environment limits
The automation host has no Unity Editor, no Unity MCP, and no image-generation pipeline. Prompts needing those get everything that is possible (C# verifiable via dotnet build/dotnet test, configs, docs, scripts) and an explicit list of what was skipped. If the run finished /implement, it still ends with ai-complete; otherwise it ends with ai-need-attention. Implementation tooling like scripts/automation/claude/ralph.py remains available as a standalone tool but is no longer wired into this automation.
One-time setup
Labels must exist before they can be applied:
gh label create claude --color 5319E7 --description "Execute this item's prompt via the Claude automation"
gh label create codex --color 5319E7 --description "Execute this item's prompt via the Codex automation"
gh label create cursor --color 5319E7 --description "Execute this item's prompt via the Cursor automation"
gh label create auto-ai --color 1D76DB --description "Auto-route this item to an available AI provider"
gh label create ai-in-progress --color FBCA04 --description "Automation actively working this item"
gh label create ai-need-attention --color D93F0B --description "Automation waiting on the owner"
gh label create ai-complete --color 0E8A16 --description "Automation finished this item's prompt"
gh label create ai-specify --color 2DC37E --description "Automation currently running /specify on this item"
gh label create ai-plan --color 29C8C6 --description "Automation currently running /plan on this item"
gh label create ai-implement --color B1C727 --description "Automation currently running /implement on this item"Only the plain provider / auto-ai labels are ever applied by the owner when opting an item in — the three shared ai-* status labels plus the three stage labels are managed by the automation (removal of ai-need-attention/ai-complete to resume being the one owner-side exception). When auto-ai routing (or a post-limit immediate reroute) finds every provider usage/session-limited, park_auto_item_unroutable parks the item with ai-need-attention and an <!-- auto-ai-automation --> comment rather than leaving it unlabeled for endless empty polls.
Setup checklist
gh auth loginon the machine that will run this, authenticated asKonH.- The label create commands above (once, on the repo).
- Subscription-based
claudeauth on that same machine, via a long-lived token rather than interactive login — the cron job runs unattended, so there's nobody there to complete a browser OAuth redirect or paste a fallback code each time:- On any machine with normal browser access (doesn't have to be the automation host), run
claude setup-token. It opens the browser OAuth flow and prints a token to the terminal after approval — it does not save the token anywhere itself. - On the automation host,
export CLAUDE_CODE_OAUTH_TOKEN=<that token>(in the cron job's environment, e.g. the crontab's own env or a sourced profile — cron doesn't inherit an interactive shell's exports). - Do not also set
ANTHROPIC_API_KEY— its presence makes the CLI bill the API instead of the subscription.
- On any machine with normal browser access (doesn't have to be the automation host), run
- A dedicated clone of this repo for the automation to run against —
handle_issues.pyforce-resets to the candidate's start branch (and removes untracked files) before every CLI invocation, which would blow away uncommitted work in a normal dev checkout. - A cron entry (Linux/macOS/WSL) or Scheduled Task (Windows) calling
scripts/automation/handle_issues_auto.sh/.ps1(or a provider-specific wrapper) from that dedicated clone's root, on whatever interval the user wants (this is real polling, not a webhook — the interval is simply the latency until a new label/reply is noticed; an empty poll costs a fewghcalls and zero LLM usage).
Interactive testing (not the cron path)
Running claude by hand in a remote container (Codespaces, SSH, WSL2) to sanity-check things: the OAuth browser redirect can't reach the CLI's local callback server there, so instead of redirecting, the browser shows a short code — paste it into the terminal at the Paste code here if prompted prompt. This is automatic CLI behavior, not something to configure. It's a one-off login for manual testing; the cron job itself should still use CLAUDE_CODE_OAUTH_TOKEN as above.
Why "the user's own environment" and not Actions or a Routine
Two earlier designs were tried and abandoned:
- Claude Code Remote scheduled Routine — fired the handler command into a fresh session hourly. Abandoned after hitting three compounding blockers: fired sessions get no MCP connector tools, the environment's injected
GITHUB_TOKENis blocked at the proxy layer for every repo-scoped REST endpoint (GET /repos/{owner}/{repo}/...) even after installing the Claude GitHub App, and fired sessions don't even start inside a git checkout of the repo. - GitHub Actions (
claude-code-action) — solves all three of the above (real checkout, realghCLI, real API access), and was actually working. Abandoned anyway because it only supportsanthropic_api_keyauthentication — pay-per-token API billing, entirely separate from a claude.ai Pro/Max subscription. No OAuth/subscription option exists for the action.
Running claude -p directly in an environment where the user is logged into their own subscription (claude login, no API key) avoids that billing split entirely — usage draws from the same subscription pool as any other interactive Claude Code session, at the cost of owning the polling infrastructure (cron, the machine staying on, gh already authenticated there) instead of getting it for free from a hosted trigger.
History: the previous spec→plan→merge state machine
Before 2026-07, this automation was a multi-phase pipeline (spec draft → 👍 → plan draft → 👍 → auto-merge → implementation proposal → 👍 → Ralph loop → 👍 → auto-merge) driven by comment-heading parsing, reaction detection, a timeline-based activity scan, a lookback-window state file, and an edited-in-place checklist comment. It was replaced by the label-driven design above to remove the hardcoded workflow, the approval round-trips, and all hidden state — see the git history of this file and .claude/commands/handle-feature-issue.md (deleted) for the old design.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

