Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

66 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

adlc

Reusable GitHub Actions workflow for autonomous AI-driven issue resolution.

Label any GitHub issue ready-for-agent → an agent clones the repo in a worktree, reads the issue, writes a fix, passes CI, opens a PR, and labels it agent-review. Zero human intervention required.

How it works

Issue labeled "ready-for-agent"
  → caller repo's agent-dispatch.yml fires
    → calls neupsh/adlc/.github/workflows/agent-issue.yml
      → self-hosted runner picks up the job
        → claude agent works in an isolated worktree
          → commits (GPG-signed), pushes, opens PR
            → issue labeled "agent-review"

Quick start (new project)

Prerequisites

  • A Linux machine to host the self-hosted runner (kept on; --service enables lingering so it survives logout).
  • A GitHub repo you can add Actions secrets and a self-hosted runner to.
  • A GPG key for signed commits — the agent signs every commit. Create one with gpg --full-generate-key; get its ID via gpg --list-secret-keys --keyid-format=long.
  • A Claude subscription (the agent runs the claude CLI, not the metered API).
  • git and Node.js on the runner box. install.sh installs Claude Code, gh, and jq where it can.

1. Register a runner on your machine

# Get a runner registration token from:
# https://github.com/<owner>/<repo>/settings/actions/runners/new

git clone https://github.com/neupsh/adlc
cd adlc

# Register + install as a persistent user-level systemd service.
# Defaults to 1 runner (one job at a time). Add e.g. --runners 2 to allow more
# concurrency — but each runner is a concurrent agent job, so leave headroom if
# other repos share this machine.
./scripts/install.sh \
  --repo your-org/your-repo \
  --label linux \
  --token <RUNNER_REG_TOKEN> \
  --service

For IBKR-dependent tests, use --label ibkr and run on the machine with IB Gateway.

Concurrency & isolation

--runners N is the concurrency cap for this repo on the machine: GitHub dispatches one job per runner instance and queues the rest, so no more than N agent jobs ever run at once and no labeled issue is dropped — extras wait for a free runner. It defaults to 1 (one job at a time). Bump it only if the box has spare capacity; each extra runner is another concurrent agent process competing for CPU/RAM, and if other repos register their own runners here, total machine load is the sum across all of them.

Re-running install.sh reconciles to the requested count: lowering --runners (or upgrading from an older single-runner install) disables the now-stale services so concurrency never exceeds the cap. Those runners stay registered in GitHub as offline — run uninstall.sh --repo … --token <remove-token> to fully deregister them.

Concurrent jobs never collide on disk — this holds across repos too, since two repos' runners on one machine can fire jobs at the same time:

  • Each runner instance has its own directory and _work checkout.
  • Each job runs the agent in a per-issue git worktree under RUNNER_TEMP (agent/issue-<n> branch), created at job start and removed at the end.
  • The generated prompt and run log also live under RUNNER_TEMP, which is unique per runner and wiped each job — so two jobs on the same box can't clobber them.
  • A per-issue concurrency group ensures the same issue never runs twice at once.

2. Drop the dispatcher workflow into your repo

./scripts/install-dispatcher.sh \
  --repo-path /path/to/your-repo \
  --label linux    # must match the runner label above

Commit and push .github/workflows/agent-dispatch.yml.

3. Add GitHub secrets to your repo

Settings → Secrets and variables → Actions:

Secret Value
GPG_PRIVATE_KEY gpg --armor --export-secret-keys <KEY_ID>
GPG_KEY_ID Your signing key ID
GPG_PASSPHRASE GPG passphrase

Optional — add these two for a branded bot identity (agent actions show as your app instead of github-actions[bot]):

Secret Value
ADLC_APP_ID Your GitHub App's numeric App ID
ADLC_APP_PRIVATE_KEY The app's full .pem private key (BEGIN/END lines included)

4. Create issue labels

Two groups: lifecycle labels (required — the workflow moves issues through them) and taxonomy labels (optional — they pick the prompt and model). Create all of them (run inside your repo clone, or add --repo your-org/your-repo to each):

gh label create ready-for-agent     -c 0075ca -d "Ready for agentic pickup"
gh label create agent-coding    -c e4e669 -d "Agent is actively coding"
gh label create agent-review    -c d93f0b -d "Agent PR is in review"
gh label create agent-failed    -c b60205 -d "Agent run failed"
gh label create type:initiative -c 0075ca -d "Decompose into epics (architect, no code)"
gh label create type:epic       -c 7057ff -d "Spec + break into stories (architect, no code)"
gh label create type:story      -c d4c5f9 -d "Implement the change + open a PR (default)"
gh label create type:bug        -c d73a4a -d "Diagnose + minimal fix + regression test"
gh label create model:opus      -c e4e669 -d "Force Opus for this issue"
gh label create model:sonnet    -c cfd3d7 -d "Force Sonnet for this issue"
gh label create auto-merge      -c 0e8a16 -d "Merge the PR automatically once a review approves it"

agent-deferred-ratelimit (spend/rate-limit breaker tripped — not a failure, auto-requeues) is created on first use by scripts/spend-guard.sh; you don't need to pre-create it.

How the taxonomy routes a run (resolved at job start from the issue's labels):

Label What the agent does Model
(no type: label)type:story Implement the change, open a PR model input (default claude-sonnet-4-6)
type:bug Diagnose → minimal fix + regression test → PR same default
type:epic Architect mode: write a spec, open type:story issues. No code claude-opus-4-8
type:initiative Architect mode: propose an ordered list of epics. No code claude-opus-4-8
model:opus / model:sonnet (any type) override the model for this issue per label

5. Authenticate Claude (once per machine)

claude auth login   # uses your subscription, not API pricing

6. Trigger an agent run

Label any issue ready-for-agent. The agent fires within seconds. Add a type: label to route it (default is type:story); add model:opus/model:sonnet to override the model. The issue moves ready-for-agentagent-codingagent-review (PR opened) or agent-failed.


Review loop

Two reviewers feed the same coder agent: an automatic AI pass, then you.

AI review (automatic). Right after the coder opens the PR — in the same run — an AI reviewer (Opus) judges the diff. If it requests changes, the coder fixes them and the reviewer looks again, up to review_rounds rounds (default 3). This runs inline because a GITHUB_TOKEN-authored review can't trigger another workflow. Outcome:

  • Approved → if the issue has auto-merge, the PR is merged; otherwise it waits for you.
  • Didn't converge in N rounds → it stops and hands off to you at agent-review.
  • Set review_rounds: "0" in your dispatcher to disable the AI reviewer.

An AI reviewing the same pipeline's AI-written code is correlated — treat it as a strong first-pass lint, not a substitute for your review.

Your review. Submit a "Request changes" review or comment /fix on the PR, and the coder agent picks up all feedback (review body, inline comments, conversation comments) and revises the PR in place (same branch, no new PR). Re-review; repeat until you Approve.

  • A plain comment without /fix and without a formal "Request changes" review does not fire a revise. /fix only works from a repo collaborator.
  • Each revise round commits (signed) and pushes to the PR branch.
  • With the auto-merge label, your Approve then merges the PR (see below).

On-demand AI review. Comment /review on any PR (collaborators only) and the AI reviewer runs the same review→fix loop on it now — useful for PRs that predate the issue run, or to re-trigger a review after changes. It's the subscription-priced, loop-driving equivalent of @claude review (the official Code Review is advisory-only and API-billed).


Auto-merge (opt-in)

By default every PR stops at agent-review and waits for a human — that gate is the safe default and stays the default. To let a specific PR finish the loop on its own, opt it in with the auto-merge label:

  • Add auto-merge to the issue (alongside ready-for-agent) and it's copied onto the PR when the agent opens it. To opt in after the PR exists, add auto-merge directly to the PR.
  • It merges (squash, branch deleted) when either the inline AI reviewer approves (during the coder run) or a human approves the PR. Unlabeled PRs are never touched.

So a labeled issue can go all the way hands-off: agent writes → AI reviews → approves → merges, with no human in the loop. Leave the label off and it always stops for your review.

Mechanics: the AI-approval merge happens inline in the coder's run; the human-approval merge runs on a GitHub-hosted runner via the pull_request_review event.

Note: a human /fix comment or review fires the agent because those events are human-authored. GitHub does not re-trigger workflows for GITHUB_TOKEN-authored events, which is exactly why the AI review→fix→merge loop runs inline (one job) rather than as separate event-triggered workflows. (With the optional GitHub App, agent-authored events do trigger workflows — the dispatcher's user.type != 'Bot' guards keep that from looping.)


Spend/rate-limit circuit breaker

If the Claude subscription's monthly spend limit (or a rate limit) is hit mid-run, the pipeline used to keep dispatching every other ready-for-agent issue behind it — each one failed its first model call in a few seconds and got stamped agent-failed, a label reserved for the agent genuinely failing to solve the issue. One real limit hit could poison an entire backlog that then needed manual re-labeling.

How it works:

  • Every agent-issue.yml run classifies its output log for a known limit signal (scripts/spend-guard.sh classify). A hit defers the issue instead of failing it: ready-for-agent/agent-coding are removed, agent-deferred-ratelimit is added, and a comment explains why + when it'll auto-requeue.
  • The same run also trips a circuit breaker — a small JSON file at ~/.adlc/breaker.json on the runner box. Every subsequent agent-issue.yml run checks it before checkout/GPG/worktree setup and defers immediately if it's tripped, so a poisoned run costs the rest of the queue a few seconds each instead of a real attempt.
  • The breaker is machine-local by design: every repo's self-hosted runner on one box shares one Anthropic account, so the limit is machine-wide already — a local file needs no token and is automatically the right scope. If you run adlc across multiple boxes on separate accounts, each has its own independent breaker.
  • Auto-requeue: a per-repo hourly cron (agent-requeue-cron.yml, generated by install-dispatcher.sh or added manually) calls the reusable agent-requeue.yml. Once the breaker's cooldown has passed, it clears the breaker and releases the single oldest agent-deferred-ratelimit issue back to ready-for-agent — never more than one per tick, and never while another issue is already agent-coding. This must run on the same self-hosted box as the dispatcher (the breaker file lives there).

There's no reliable "remaining budget" API for a subscription spend limit, so the cooldown is a conservative guess (next UTC midnight for a spend limit; a short fixed window for a 429), not a verified reset — worst case after a guess expires is one more wasted dispatch, which re-trips the breaker and re-defers. That's the deliberate trade-off: never a cascade, at most an occasional single retry.

Manually checking / clearing the breaker (on the runner box):

bash scripts/spend-guard.sh status   # healthy / tripped-until / expired
bash scripts/spend-guard.sh clear    # force-clear (e.g. you know the limit already reset)

Everything here fails open: a missing jq, an unreadable breaker file, or an unexpected error is treated as healthy / not-a-limit rather than blocking a good issue — these scripts load from adlc's main unpinned into every consumer repo, so a bug here must never become a new way to fail issues that would otherwise have succeeded.


Branded bot identity (optional GitHub App)

By default every agent action — commits' push, the PR, comments, merges, issue closes — is performed with the built-in GITHUB_TOKEN and shows up as github-actions[bot]. Commits themselves still show your verified Agent Coder GPG identity; only the actor on pushes/PRs/comments/merges is the bot.

To rebrand that actor as your own app (custom name + avatar), register a GitHub App and add two secrets. It's free, opt-in, and falls back cleanly: with no app secrets present, everything behaves exactly as before. As a bonus, an App token isn't subject to GITHUB_TOKEN's anti-recursion rule, so GitHub's native linked-issue auto-close on merge starts working too (the manual close stays as a guarded backstop).

Cost & privacy: GitHub Apps are free with higher API rate limits than tokens. The app's private key can mint tokens for every repo it's installed on, so scope the install to specific repos and keep permissions minimal. No webhook is needed — the app is only used to mint a short-lived, repo-scoped token at job start.

Set it up (once)

  1. Register the app — GitHub → Settings → Developer settings → GitHub AppsNew GitHub App. (Use your org's developer settings if you want org-level secrets later — see multi-repo below.)
    • Name: whatever you want the bot to be called (e.g. myproj-agent). This is what shows on comments as myproj-agent[bot].
    • Homepage URL: anything (your repo URL is fine).
    • Webhook: uncheck “Active” — not needed.
    • Repository permissions:
      • Contents: Read and write (push, merge, delete branch)
      • Pull requests: Read and write (create, comment, merge)
      • Issues: Read and write (comment, close, label)
      • Metadata: Read-only (mandatory, auto-selected)
    • Leave everything else untouched → Create GitHub App.
  2. Grab the App ID — on the app's page, copy the numeric App ID.
  3. Generate a private key — same page → Private keysGenerate a private key. A .pem downloads. This is ADLC_APP_PRIVATE_KEY (the whole file, including the -----BEGIN…/-----END… lines).
  4. Install the app — app page → Install App → install it on the account/org that owns your repos → choose Only select repositories and pick the ones using adlc. (Registering ≠ installing — the app does nothing until installed.)
  5. Add the secrets — in each repo (or once at org level), add:
    • ADLC_APP_ID = the numeric App ID
    • ADLC_APP_PRIVATE_KEY = the full .pem contents
  6. Refresh the dispatcher — regenerate it so the bot-recursion guards land (re-run install-dispatcher.sh, or pull the two user.type != 'Bot' conditions and secrets: inherit on the auto-merge job into your existing agent-dispatch.yml). This matters only with the app on: app-authored events can trigger workflows, and the guards stop the inline reviewer's own comments from looping back. Without the app, the old dispatcher is unaffected.

That's it — the next agent run uses the app. Remove the two secrets to instantly revert to github-actions[bot].

Multiple repos on a free account

One app covers many repos — you do not register one per repo:

  • Register one app, install it on all the repos you want (step 4, select them).
  • Personal account: there's no shared secret store, so add the same two secrets (ADLC_APP_ID, ADLC_APP_PRIVATE_KEY) to each repo. Same values everywhere.
  • Many repos? Use a free org. Create a (free) GitHub Organization, register the app under the org's developer settings, then set the two values once as organization secrets (Org → Settings → Secrets and variables → Actions) scoped to the repos that need them. Every repo inherits them — set once, not N times.

The minted token is always scoped to just the repo the run is in, so one shared app across many repos stays least-privilege per run.


Project-specific configuration

Option A — .adlc/conventions.md

Create this file in your repo root. The agent reads it automatically:

## Build
- `cargo check` must show 0 warnings, 0 errors
- `cargo test -p <affected-crate>` must pass

## Commit scopes
core, api, cli, web, data

## Never touch
.env, secrets/, credentials/

Option B — workflow inputs

Override in your agent-dispatch.yml:

with:
  build_check_cmd: "npm run lint && npm run build"
  build_test_cmd:  "npm test"
  project_conventions: |
    - TypeScript strict mode, no `any`
    - Run `npm run lint` before committing

Option C — .adlc/build.sh

#!/usr/bin/env bash
# .adlc/build.sh check|test
case "$1" in
  check) cargo check ;;
  test)  cargo test -p affected-crate ;;
esac

Runner management

Services are named agentic-runner-<org>-<repo>-<N>, one per instance:

# Check status of all instances for a repo
systemctl --user list-units 'agentic-runner-your-org-your-repo-*'

# Restart instance #2
systemctl --user restart agentic-runner-your-org-your-repo-2

# Uninstall — auto-discovers and removes every instance (and any legacy install)
./scripts/uninstall.sh --repo your-org/your-repo --token <REMOVE_TOKEN>
# Remove token: https://github.com/<owner>/<repo>/settings/actions/runners

Runner routing

Use different runner labels to route jobs to the right machine:

Label Use case
linux General coding tasks (default)
ibkr Tests that need IB Gateway running locally
prod Deploy jobs on the production server

Register multiple runners on the same machine with different labels:

./scripts/install.sh --repo your-org/your-repo --label ibkr --token <TOKEN> --service

Scaling to multiple projects

Register a runner per repo on the same machine. Each gets its own service:

./scripts/install.sh --repo your-org/project-a --label linux --token <TOKEN_A> --service
./scripts/install.sh --repo your-org/project-b --label linux --token <TOKEN_B> --service

For org-level runners (share one runner across all repos), create a GitHub Organization and register at the org level — the install script's --repo can accept <org> directly once you have an org runner token.

About

Reusable GitHub Actions workflow for autonomous AI-driven issue resolution

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages