Tickets in. Reviewed pull requests out. Nobody watching.
A Claude Code skill that runs your Linear board overnight and hands you a morning of pull requests to read.
The whole interface is two surfaces. A ticket in Linear, and a comment on a pull request. There is no chat window to steer, no label to remember, no plan to approve inside the tracker. To change the direction of the work you comment on the PR, and the loop reads it, answers it by name, and acts on it. That works on a merged PR too, which is where it earns its keep: merge, note the follow-up work the PR spotted but left out of scope, and the loop turns your note into the next ticket by itself. Teammates can steer it the same way, on purpose.
Draft means something. A PR leaves draft only on evidence β a review, a fix pass over that review, a screenshot when a user can see the change, no merge conflict, and CI green on the head commit. It never leaves draft on an agent's say-so. So what is not a draft in the morning is what is actually ready, and the drafts are the pile you can ignore until you have time.
Idle costs nothing. Nothing runs until there is work. The gate probes capacity, your open PRs and the Linear ready column every 60 seconds in pure shell β no model call, no turn started. A night with an empty board costs nothing at all, not "not much".
It runs on your machine, on the Claude you already pay for. No hosted sandbox,
no per-session credit, no repo access granted to a third party. That also means it
uses whatever the machine has: it measures free memory every probe and runs as many
agents as will really fit β one per LO_RAM_PER_AGENT gigabytes β so parallelism
is bounded by your laptop rather than by a plan.
A person decides every merge. Author and reviewer are the same model, so "no
blockers left" is evidence about care, not proof of correctness. The loop stops at
a reviewed, non-draft PR. It merges only when a person on the merge list
(LO_SLACK_MERGE_EMAILS) names that PR in Slack. The one standing order is the fix
of a small break of the base branch build: a type, lint or test failure with one
cause and no change a user sees. The loop never merges into the default branch.
- The gate blocks until there is work β capacity, your PRs, the Linear ready column.
- An agent takes the ticket, moves it to In Progress, branches, opens the draft PR before it writes any code, writes the code, tests it in a real browser, and stops.
- A second agent reviews that PR in a fresh context β it has the diff, the ticket and your repo's agent guide, and none of the reasoning the author talked itself into. It logs the review as one entry in the PR's single log comment, makes exactly one fix pass over what it found, and corrects the PR body where the diff moved past it.
- The PR leaves draft on the five gates, or stays a draft.
- Your comment re-opens any of it. An unanswered comment on one of its PRs outranks every other kind of work on the next tick.
Every agent gets its own repo clone, Terminal window and real Chrome, which is why front-end tickets work here: the screenshots on the PR come from driving the actual screen, not from a headless guess.
The mechanics β the shell gate, and how the loop tells your comments from its own β
are in HOW-IT-WORKS.md.
People who already run coding agents locally and would rather spend a machine than a per-seat credit. The install is honestly longer than "connect GitHub", and the constraints are real:
- macOS or Linux, and Claude only. It drives Claude (Opus) agents through
multiclaudeand reads Linear throughagent-codemode, which reads Claude Code's OAuth from the macOS Keychain, or from~/.claude/.credentials.jsonon Linux. - A Linux server has no real Chrome. Its agents run in
screensessions without Claude in Chrome, so they test front-end work with headless Playwright. - One machine. Parallelism caps at what one laptop's memory holds.
--dangerously-skip-permissionsis required. The agents run unattended with permission prompts off. That is the point, and it is also the risk β give the loop a repo where the worst an agent can do is open a bad PR.- Your repo needs its own agent guide (
CLAUDE.md). The prompt handed to each agent is short because the guide supplies branching, local checks, the PR template, testing and security.
You also need gh (authenticated), jq, and the
Linear MCP server connected to the session that runs
the loop.
git clone https://github.com/janwilmake/watchloop.git \
~/.claude/skills/watchloop
cd ~/.claude/skills/watchloop
cp .env.example .env && $EDITOR .envInstalled it before 15 Oct 2026, as linear-orchestrator? Move both
folders, point the remote at the new name, and start it with /watchloop:
mv ~/.claude/skills/linear-orchestrator ~/.claude/skills/watchloop
mv ~/.claude/linear-orchestrator ~/.claude/watchloop
git -C ~/.claude/skills/watchloop remote set-url origin https://github.com/janwilmake/watchloop.gitThen start it from your repo:
cd /path/to/your/repo
claude --dangerously-skip-permissions/watchloop
status reports what is running; stop ends the loop and lists the PRs it
produced.
The loop also runs on a Linux server (tested on Ubuntu 24.04), which keeps it running when the laptop sleeps. On the server:
- Install
git,gh,jq,screen, Node and Claude Code, and log in togh, Claude Code and the Linear MCP (claude mcp login linear) as the account the loop acts as.agent-codemodereads the Linear login from~/.claude/.credentials.json. - Mark the agent slot folders
~/.claude/agents/agent-Nas trusted in~/.claude.json, and accept the Bypass Permissions warning once. Otherwise each new agent stops at a question nobody answers. - Optional, for front-end work: Chrome on a virtual display (Xvfb), with the
Claude extension signed in and x11vnc serving that display on localhost:5900.
ccapasses--chromeonly when Chrome is installed; without it, agents test with headless Playwright. - Start the loop in a
screensession, so it outlives your ssh connection:screen -dmS orchestrator bash -lc 'cd /path/to/repo && claude --dangerously-skip-permissions "/watchloop start"'.
To watch it from a Mac or a Linux desktop, set LO_REMOTE_HOST (and, for the
browser view, LO_REMOTE_VNC_PASSWORD_FILE) in .env and run
./watch-remote.sh. It opens one terminal window per agent and the
orchestrator, adds windows for new agents, closes a window when its agent ends,
and opens a VNC view of the server's Chrome: Screen Sharing on a Mac; TigerVNC's
vncviewer, else Remmina, on Linux. On Linux, LO_REMOTE_TERMINAL picks the
terminal (for example kitty or gnome-terminal --); without it the script
takes the first one it finds. Ctrl-C, or ./watch-remote.sh stop, closes every
view; the loop keeps running. Close a single window with the window manager,
never Ctrl-C: keys typed in a window reach that agent.
All the values that change per user live in .env beside SKILL.md. It is
gitignored, so your paths and names never reach the repo β which is what lets the
skill be a symlink to a checkout:
LO_REPO="/path/to/your/repo" # local checkout the agents clone from
LO_BASE="dev" # base branch
LO_TEAM="Your Team" # Linear team name
LO_PREFIX="ENG" # ticket id prefix
LO_TIER1="Preferred Assignee" # display name. ONLY these assignees are taken
LO_TIER2="Fallback Assignee" # display name
LO_ALLOW_UNASSIGNED="0" # 1 = also take unassigned tickets (default off)
LO_ALLOW_BACKLOG="0" # 1 = fall to Backlog when Todo is empty (default off)
LO_OWNER="" # this instance's tag; required if two share a repo
LO_RAM_PER_AGENT="2.5" # GB per agent (claude + vite + chrome)
LO_MAX_AGENTS="0" # ceiling on concurrent agents; 0 = unlimited
LO_FEEDBACK_SINCE="2026-08-18T17:00:00Z" # the day you installed thisLO_RAM_PER_AGENT is the one people get wrong: it is a divisor, not a threshold.
Every tick spawns free memory / LO_RAM_PER_AGENT agents β all of them, not one,
so the loop reaches full capacity on its first tick with work. LO_MAX_AGENTS
defaults to 0, meaning memory is the only ceiling; set a positive number to
hold part of the machine back. The default divisor assumes one Claude session,
one dev server and one Chrome β a stack with Docker or a local database costs far
more, and the probe cannot detect that.
What it takes is narrow on purpose. Only tickets in Todo assigned to
LO_TIER1 or LO_TIER2. Unassigned tickets and the Backlog column are each
one switch away (LO_ALLOW_UNASSIGNED, LO_ALLOW_BACKLOG) and both default to
off β a name on a ticket in a ready column is somebody deciding it is ready, and
that decision is the signal the loop runs on.
Running two orchestrators against one repo β say one per laptop β needs
LO_OWNER set to a different tag in each. Splitting by assignee separates them
on the tracker, but not on GitHub: every instance writes the same π marker,
and that marker is what decides which PRs a loop reviews, promotes and answers
comments on. With LO_OWNER the marker becomes π lo:<owner> and the two stop
overlapping. Both instances must run this version for the split to hold β an
older one matches the bare marker and still adopts everything.
Both gate.sh and the tick read these, so there is nothing to edit inside
SKILL.md.
