Give it an issue. It sorts it out.
issue-worm turns GitHub issues into pull requests using LLMs. This repo is
the free shell: issue filing, a standalone single-pass build, and run
history. The full automated review → implement → verify pipeline with a
verifier/retry loop and a scheduler (triage, poll) is
issue-worm-pro, a private
package — see Access below.
Installing issue-worm-pro upgrades this shell in place rather than
replacing it: the issue-worm command stays the same, and build starts
running pro's full pipeline instead of the single pass described here.
This is the free shell — no subscription, no cloud API key, honest about what it does and doesn't do: AI writes the code, you do the rest.
For comparison, issue-worm-pro runs AI through the whole pipeline — triage, coding, judging its own failures, and drafting the PR description:
Watch both: this free tier is genuinely useful on its own for a well-scoped task, but pro is what "AI does the whole loop" looks like.
For a deeper look at pro's retry loop and scheduler in action — real transcripts, a bounded 3-attempt retry with genuine Analyser feedback, and a real PR opened end to end — see docs/demo-2026-08-30-issue-388.md (pro-only features; not representative of the free tier).
issue-worm create— file a new issue, guided interactively (via cicaid).issue-worm build <issue> --repo owner/name— a deterministic (non-LLM) check that the issue is scoped enough to dispatch (an## Implementation notessection withFILES:/DONE:), then a single pass through a coder that writes the proposed changes to the working tree — a local Ollama instance by default, or a cloud/remote LLM if configured (see Coder configuration below). No verifier/retry loop, no scheduler. With issue-worm-pro installed this command runs pro's pipeline instead, so the behaviour described here is what you get on the free tier alone. The GitHub Action wraps this command with a singlecicaid run-ci-checks --allverifier before publishing, but thebuildcommand itself does not run those checks.issue-worm history— list or inspect past runs recorded by the pipeline.issue-worm status— show runs currently in progress (from the run registry state dir) plus the last N completed runs (fromissue-worm history's own store).-n/--limitcaps how many completed runs are shown (default 10);--jsonemits the whole payload as one document. PrintsNo active runs.when nothing is running — on a free-tier build, the completed-runs list is often empty too, since only issue-worm-pro's scheduler writes to run history; that's normal, not a bug.issue-worm triage/poll— parse their flags (so--helpstays accurate) but report themselves unavailable, since the scheduler and LLM-driven triage that implement them live in issue-worm-pro.
issue-worm isn't published on PyPI. Install the latest release wheel directly from GitHub Releases:
pip install https://github.com/leonarduk/issue-worm/releases/download/v0.7.0/issue_worm-0.7.0-py3-none-any.whlscripts/bump_readme_version.py, run by
the release workflow, keeps this URL in
sync with the latest tag on every release.
build's coder is picked at run time by CODER_MODEL_SOURCE, read via
config.py and dispatched by coder.build_coder (coder.py):
CODER_MODEL_SOURCE |
Talks to | Required env vars |
|---|---|---|
local (default) |
A local/self-hosted Ollama instance's /api/generate. |
CODER_TARGETS (see below); optionally CODER_OLLAMA_ENDPOINT / CODER_OLLAMA_MODEL to override per role. |
lmstudio |
A local LM Studio server's OpenAI-compatible /v1/chat/completions — the same client as remote, with local defaults and no API key. |
None. Optionally LMSTUDIO_ENDPOINT (default http://localhost:1234, no trailing /v1) and LMSTUDIO_MODEL (default: the first model LM Studio's /v1/models reports, preferring one whose name contains coder). |
remote |
Any OpenAI-compatible /v1/chat/completions endpoint — OpenAI itself, a self-hosted vLLM/SGLang box, or an Ollama instance serving the OpenAI API. |
REMOTE_LLM_ENDPOINT (no trailing /v1 — that's appended automatically), REMOTE_LLM_MODEL, REMOTE_LLM_API_KEY. |
cloud |
DeepSeek's API (https://api.deepseek.com), which is itself OpenAI-compatible, so it reuses the same remote client with DeepSeek's endpoint/model as the default. |
DEEPSEEK_API_KEY; optionally DEEPSEEK_MODEL (default deepseek-v4-flash) and CODER_MAX_TOKENS (output-token cap sent as max_tokens; default 32768 for cloud, not sent for remote unless set). |
claude |
Not implemented by this free engine's build coder yet. Setting it fails fast with an explanatory error rather than silently falling back to local. |
— |
An unset CODER_MODEL_SOURCE defaults to local — today's original
behaviour, unchanged. Setting remote or cloud without its required env
var(s) fails the build immediately with a message naming the missing
variable, rather than constructing a coder that talks to an endpoint that
isn't there. lmstudio needs no variables at all, but fails the same way
when no model can be resolved (LM Studio not running, or nothing loaded).
local and lmstudio are the two fully-local options — the code never
leaves the machine and there is no per-token cost. They use the same env
var names as cicaid-pro's reviewer side, so one .env configures both
(see .env-example-local / .env-example-lmstudio in issue-worm-pro).
This is what makes remote/cloud usable on a GitHub-hosted runner,
which has no local Ollama reachable — see the Action's runs-on options
below.
This repo also ships itself as a composite action (action.yml)
that runs the free engine against one issue and opens a PR from the
result. It deliberately does not set runs-on — the calling job
chooses the runner, so the same action works unmodified on GitHub-hosted
and self-hosted runners:
on:
issues:
types: [labeled]
concurrency:
group: issue-worm-${{ github.event.issue.number }}
cancel-in-progress: false
permissions:
contents: read
jobs:
build:
# Load-bearing, not cosmetic — see .github/workflows/issue-worm.yml
# for why: without it, the PAT-driven label writes below would
# re-trigger this same workflow in a loop.
if: github.event.label.name == 'issue-worm'
runs-on: ubuntu-latest # or: [self-hosted, issue-worm]
steps:
- uses: leonarduk/issue-worm@v1
with:
issue: ${{ github.event.issue.number }}
github-token: ${{ secrets.WORM_PAT }}
# license-key: ${{ secrets.ISSUE_WORM_LICENSE }} # optional, see below
env:
CODER_OLLAMA_ENDPOINT: ${{ secrets.CODER_OLLAMA_ENDPOINT }}
CODER_OLLAMA_MODEL: ${{ secrets.CODER_OLLAMA_MODEL }}See .github/workflows/issue-worm.yml
for the working copy this repo runs on itself.
@v1 is a floating tag: the release workflow
moves it to each new vX.Y.Z release, so you pick up fixes without editing
your workflow. It's the action's interface version, separate from the 0.x
package version, and only changes (to v2) for a breaking change to the
inputs below. For a reproducible pin, use a release tag (@v0.2.3) or a
full commit SHA instead.
| Input | Required | Description |
|---|---|---|
issue |
yes | Number of the issue to work. |
github-token |
yes | A PAT or GitHub App token with contents: write, pull-requests: write, and issues: read on the target repo. A classic PAT's repo scope covers all three; a fine-grained PAT needs each granted separately — issues: read is easy to miss, since only the issue-body fetch needs it, and that runs (and fails) before the push/PR steps ever do. Also grant issues: write (included in a classic PAT's repo scope already) for two best-effort features that silently no-op without it instead of failing the build: self-heal persisting its drafted section back onto the issue, and the live progress comment (see below) actually being posted/edited — issues: read is not enough to write a comment. The built-in secrets.GITHUB_TOKEN is not sufficient either way — a PR opened (or pushed to) with it deliberately does not trigger other workflow runs, so anything gated on the PR (CI, review bots, required checks) would never fire. |
license-key |
no | Reserved for the pro engine. Currently accepted and logged only — installing the pro wheel from a license key is a separate, unimplemented piece of work (leonarduk/issue-worm-pro#584). Omit it (the default) to run the free engine, which is everything the action does today. |
runs-on: ubuntu-latest— free GitHub-hosted minutes. A GitHub-hosted runner has no local Ollama, solocal(the defaultCODER_MODEL_SOURCE) only works here ifCODER_OLLAMA_ENDPOINTpoints at one this runner can actually reach over the network (a self-hosted Ollama box you expose, or a hosted Ollama-compatible endpoint). The more common choice on a hosted runner is to setCODER_MODEL_SOURCE: remoteorcloudinstead (see Coder configuration above) and supply the matchingREMOTE_LLM_*/DEEPSEEK_API_KEYsecrets asenv:— that needs no self-hosted Ollama at all.runs-on: [self-hosted, issue-worm]— a self-hosted runner, typically one that also runs Ollama locally (CODER_OLLAMA_ENDPOINT=http://localhost:11434, the defaultcoder.pyalready assumes if unset) withCODER_MODEL_SOURCEleft at itslocaldefault.
Either way the action does not branch on which one you picked — the
runs-on: line in your own job is the only place that decision is made;
CODER_MODEL_SOURCE and its matching secrets in your own env: block are
what actually select the coder.
- Checks out the calling repo (
actions/checkout, credentialed withgithub-token) and sets up Python. - Installs this action's own checkout (
pip install) — not thepip install-from-wheel flow above; the action always runs the code at its pinned ref. - Runs
issue-worm build <issue> --repo <owner/name> --workspace <checkout>, reusing the already-checked-out, already-credentialed working tree instead ofbuild's normal unauthenticated fresh clone. - If that produced changes, commits them to a deterministic
issue-worm/issue-<N>branch, force-pushes it (so re-labelling the issue supersedes a previous attempt rather than piling up branches — see leonarduk/issue-worm-pro#582's retry UX), and opens a PR withgh pr create(or leaves the existing PR for that branch as-is if one is already open).
The action never commits its own per-run bookkeeping: it stages
everything with git add -A . and then unstages .issue-worm/ (this
run's history.jsonl and the in-flight registry) before committing, so
that directory never lands in your history whether or not you already
ignore it. If you run issue-worm in-repo and want it out of git status
too, add it to your .gitignore:
.issue-worm/— issue-worm's per-run bookkeeping directory (the action resets it defensively, so it is safe to ignore).
As its very first step, before checkout or install, the action labels the
issue in-progress and posts one comment linking to the Actions run. It
then edits that same comment as each stage finishes — coder, verifier,
and publish — in the same format issue-worm-pro's own scheduler uses, so
an issue looks the same whichever engine dispatched it. The
in-progress label is removed when the run ends, pass or fail:
🪱 issue-worm · done
[Actions run](https://github.com/owner/repo/actions/runs/123456789)
- [x] coder (5.1s)
- [x] verifier (14.2s)
- [x] publish (2.0s)
**Result:** ✅ https://github.com/owner/repo/pull/42
Without this, the only trace of a run was ~8 lines in the Actions run
log — there was no way to tell from the issue whether a run had started,
was still going, failed, or which PR it opened. Every write here is
best-effort: a GitHub API hiccup while posting or editing the comment is
logged and never fails the build — that also means a github-token
without issues: write (see that input's description above) silently
disables this feature rather than failing the build, so a missing
comment is worth checking that scope for.
claudeisn't implemented as a coder source yet.local,remote, andcloud(DeepSeek) all work — see Coder configuration above — butCODER_MODEL_SOURCE=claudefails the build with an explanatory error rather than running anything.- No pro engine yet.
license-keyis accepted and logged, nothing more — see leonarduk/issue-worm-pro#584. - No in-progress/pr-opened/needs-help label lifecycle. That belongs to issue-worm-pro's scheduler; this action only opens (or updates) the PR and lets the job's own success/failure be the signal.
| Secret | Purpose | Required scopes |
|---|---|---|
PIN_UPDATE_TOKEN |
Fine-grained PAT used by the pin-updater workflow to push updated version pins into .github/workflows/ files, bypassing the GITHUB_TOKEN restriction on that path. |
Contents: Read and write, Workflows: Read and write |
issue-worm build — the free-tier one; issue-worm-pro's resolves its own
workspace and does not accept --workspace — works in a checkout it calls
the workspace, chosen by --workspace, else WORKSPACE_ROOT, else a
default under .issue-worm-workspace/. When that path is missing or empty it is
fresh-cloned over HTTPS with no credentials — ensure_base_clone
builds https://github.com/<owner>/<name>.git and nothing attaches a
token.
Git may still satisfy that from the machine's own configuration — a
credential helper, gh auth setup-git, or an insteadOf rewrite to SSH
— so private HTTPS cloning does work on a host set up that way. Without
one, the clone fails to authenticate.
The setup that does not depend on any of that is to create the checkout yourself and point the workspace at it; an existing checkout is reused as-is, whatever protocol it was cloned with:
git clone git@github.com:owner/private-repo.git /srv/worm/private-repo# .env
WORKSPACE_ROOT=/srv/worm/private-repoThe credentials that clone was made with — an SSH key, a stored HTTPS token, a credential helper — are what the later fetches and pushes use, since they run inside that checkout.
If those credentials lapse, what you see depends on where you are running.
When stdin is not a terminal — the Scheduler, CI, or any invocation with stdin redirected — every prompt is disabled and git fails immediately:
| Remote | Message |
|---|---|
| HTTPS | fatal: could not read Username for 'https://github.com': terminal prompts disabled |
| SSH | Permission denied (publickey), or Host key verification failed |
That takes three settings, not one. GIT_TERMINAL_PROMPT=0 covers only
git's own prompt; GIT_ASKPASS/SSH_ASKPASS are consulted before
it, so a process launched from a desktop session could otherwise block on
a GUI dialog; and for an SSH remote git execs ssh, which reads a
passphrase from /dev/tty directly and never sees any of git's
variables — so BatchMode=yes is appended to GIT_SSH_COMMAND. Since
the pre-clone above is normally an SSH checkout, that last one is the one
that matters here.
Without them a git fetch would wait for typing nobody can see until the
120-second fetch timeout, once per pass, and report a timeout — naming
the wrong cause.
Note that stdin is what decides this, not stdout: issue-worm build | tee run.log still has a terminal on stdin and still prompts.
Run by hand from a terminal, you get git's normal prompts and can
answer them — but the same 120-second bound applies to the fetch, so
dawdling at the prompt turns into git fetch origin timed out after 120.0s. The clone is the exception: it disables prompts unconditionally,
because its bound is ten minutes.
A reused checkout is checked against the repo you asked for: if its
origin names a different project the run stops rather than committing
to the wrong codebase. SSH and HTTPS remotes of the same repo count as
the same repo, and a remote the check cannot read or parse — a bare
local path, a file:// mirror — is allowed through rather than blocked.
| Variable | Effect |
|---|---|
WORM_SKIP_REMOTE_CHECK=1 |
Downgrade a repository mismatch from an error to a warning. For a deliberate fork-origin workspace, where origin is your fork rather than the repo you pass to --repo. |
Note that with a fork origin the workspace is refreshed from the fork's
main, so it is only as current as your last sync — that staleness is
what the check exists to surface.
The automated pipeline (multi-agent retry loop, LLM-based issue triage/review) is the part of issue-worm that's actually hard to reproduce, and is kept in a private package, issue-worm-pro. Contact the maintainer for access, or watch this repo — a payment link is planned. See the open-core split for a complete list of what is available in this MIT-licensed package and what requires the private package.
For a component-by-component breakdown of the public shell and private pipeline, see the open-core split.
MIT — see LICENSE.