A minimal automation system: a Bash worker that picks up Linear issues assigned to you, has OpenCode implement them autonomously, runs the tests, opens a pull request, and moves the issue to In Review — across all of your repositories, from a single installation.
No framework, no database, no containers. Just Bash, Git, GitHub CLI, the Linear API, and the OpenCode CLI.
┌────────────┐ assigned Linear issue ┌──────────────┐
│ loop start │ ───────────────────────► │ Linear API │
│ (worker) │ ◄─────────────────────── │ │
└─────┬──────┘ issue: title, body └──────────────┘
│ resolve repo (team name → repos.yaml)
▼
1. checkout repo base branch, create branch ai/<KEY>-<slug>
2. opencode run <coding prompt> --dir <repo path> --auto
3. commit changes
4. run tests (npm test / cargo test / go test / pytest)
5. git push, gh pr create
6. move Linear issue to "In Review"
│
└──► repeat until stop/pause
cd automation
./install.sh # checks tools, creates config.env + repos.yaml, installs `loop`
vim config.env # set LINEAR_API_KEY, GITHUB_TOKEN
vim repos.yaml # register your repositories
loop start
loop status
loop logs
loop pause
loop resume
loop stop
loop run LIN-123
loop run LIN-123 --repo loopengineeringSee INSTALL.md for full setup instructions.
| Command | What it does |
|---|---|
loop start |
Starts the worker in the background (refuses if already running). |
loop stop |
Stops the worker gracefully — after the current issue (or immediately if idle). |
loop pause |
Pauses after the current issue finishes; no new issues are started. |
loop resume |
Clears the pause request. |
loop run KEY |
Runs the full pipeline for one issue, in the foreground. Refuses while the worker is running. |
loop run KEY --repo NAME |
As above, forcing the repository (overrides auto-detection). |
loop status |
Shows running/stopped, paused, current issue + repo, current step, last completed issue, last PR. |
loop logs [-e|-o] [N] |
Worker log (default, last 50 lines), -e error log, -o opencode log. |
loop repos |
Lists the repositories registered in repos.yaml. |
The individual scripts (start.sh, run-issue.sh, ...) still work directly;
loop is just a thin CLI over them.
| Variable | Purpose |
|---|---|
LINEAR_API_KEY |
Linear personal API key (https://linear.app/settings/api). |
GITHUB_TOKEN |
GitHub token used by gh (exported to the worker). |
OPENCODE_COMMAND |
OpenCode invocation, e.g. opencode run or opencode run -m provider/model. |
CHECK_INTERVAL |
Seconds the worker waits between iterations when idle. |
All your repositories live in one registry file. Adding a project is a new entry — the automation code never changes:
repositories:
hermes:
path: ~/Projects/Hermes
loopengineering:
path: ~/Projects/LoopEngineering
branch: develop # optional, defaults to main
ielts-journey:
path: ~/Projects/IELTS-Journey
aliases: [engineering, ieltsjourney] # optional: Linear team/project names- From the Linear issue: the issue's Linear team name (or, failing that,
its project name) is matched against the registry. Matching is loose —
"Loop Engineering" finds an entry named
loopengineeringorloop-engineering, "IELTS Journey" findsielts-journey. Names listed underaliases:also match, which is what you need when the Linear team is called something generic like "Engineering". - Single-repo fallback: if exactly one repository is registered, it is used when nothing else matches (unambiguous by definition).
--repo <name>override:loop run LIN-123 --repo hermesforces a repository when detection is not possible or you want to override it.
Plain text files, no database:
| File | Meaning |
|---|---|
worker.pid |
PID of the running worker. |
current_issue |
Linear identifier currently being processed (ABC-123). |
current_repo |
Repository currently being worked on. |
current_step |
Current pipeline stage: branch, coding, commit, testing, pushing, pr, linear, idle, paused. |
last_issue |
Identifier and title of the last completed issue. |
last_pr |
URL of the last pull request created. |
pause.flag |
Present while a pause is requested. |
stop.flag |
Present while a stop is requested. |
prompt.md |
Scratch: the prompt handed to OpenCode for the current issue. |
| File | Contents |
|---|---|
worker.log |
Timestamped progress of the worker and the pipeline (each line is prefixed with the repo name). |
error.log |
Errors only. |
opencode.log |
Full output of each OpenCode run. |
- Issue selection: the worker takes the most recently updated issue assigned
to you that is in an
unstartedorstartedworkflow state — across all registered repositories. Completed and canceled issues are skipped. - Branches: named
ai/<KEY>-<slug>, cut from the repo's base branch (fromrepos.yaml, defaultmain). If the branch already exists (e.g. after a crash), it is reused instead of failing. - Tests: detected automatically (npm/cargo/go/pytest). If tests fail, the PR is still created and an error is logged — review it.
- Linear updates: the issue is moved to a workflow state named In Review. Your Linear team must have a state with exactly that name.
- No changes: if OpenCode produces no diff, no PR is created and the issue is moved to In Review.
- Unresolvable issue: if no registry entry matches the issue's team/project
name, the worker logs an error and moves on; use
loop run KEY --repo NAMEto process it manually. - Stopping:
loop stopsends SIGTERM; a running OpenCode session is allowed to finish before the worker exits. - Prompt: edit
prompts/coding.mdto change how OpenCode works on issues.
error.logsays "could not resolve Linear user id" —LINEAR_API_KEYis invalid or unset.error.logsays "could not resolve a repository" — the issue's Linear team name matches no entry inrepos.yaml. Add an entry (or use--repo).status.shshows the wrong state — stale pid file is detected and reported as "Stopped".- PR creation fails — check
GITHUB_TOKENscopes (repo,pull_request) and thatghis authenticated.