# L3 Simulator: agent orchestrator over GH issues

> Source: <https://github.com/czarandy/l3-simulator>
> Published: 2026-07-16 18:14:21+00:00

Have you ever wanted a tireless junior engineer who never sleeps or doom-scrolls?
L3 Simulator orchestrates AI coding agents over your GitHub issues: label an issue
`ready`

and your synthetic L3 claims it, optionally writes up a plan,
dispatches a coding agent (Claude Code or Codex) in an isolated git worktree,
opens a PR, and babysits it through CI failures, review comments, and merge
conflicts. In the meantime you go do something befitting your seniority.

**L3 Simulator never merges anything. You are still responsible for code review and merging.** The `gh`

wrapper hard-rejects merge commands, agents are
denied `gh`

and `git push`

, and CI greps for regressions. Your L3s can open all
the PRs they like, but shipping is above their pay grade.

- Node >= 22
authenticated (`gh`

`gh auth login`

)`claude`

(Claude Code) and/or`codex`

CLI installed- A local clone of the repo you want to orchestrate

Install the CLI globally from npm:

```
npm install -g l3-simulator
```

This puts an `l3-simulator`

command on your PATH. Prefer not to install
globally? `npx l3-simulator <command>`

works too. To build from source, see
[Development](#development).

Then, inside each repo you want to orchestrate:

```
l3-simulator init
```

`init`

writes `.l3-simulator.json`

into the repo (gitignore it; it contains a
local path) and creates the five workflow labels on GitHub. Any command run
from inside that repo picks up its config, so
multiple repos each get their own daemon, worktrees, state, and logs.

GitHub labels are the orchestrator's source of truth: the issues UI is the dashboard, and a crash or restart re-derives everything from labels + PRs. Your L3s have no memory of their own. You can interact either via the TUI or via the GitHub issues UI.

| Label | Meaning | Who sets it |
|---|---|---|
`ready` |
Start working on this | you |
`needs-plan` |
Plan first; reply `#approve` to start coding, anything else revises the plan |
you |
`needs-plan-noreview` |
Plan first, then code immediately without waiting for approval | you |
`in-progress` |
Claimed; an agent is (or will be) working | orchestrator |
`blocked` |
Needs your input (plan approval, questions, failures) | orchestrator |
`in-review` |
An agent PR is open. Review and merge it yourself | orchestrator |
`failure` |
Added with `blocked` when an agent actually failed. Urgent, reply to retry |
orchestrator |

Lifecycle:

- Create issue with
`ready`

tag. Optionally add`needs-plan`

or`needs-plan-noreview`

- L3 claims an issue (adds
`in-progress`

tag) - (Optional) L3 plans work and posts a comment with the plan, waits for approval
- Coding L3 starts in a worktree on
`l3/issue-<n>-<slug>`

- Submits a PR (tags
`in-review`

) - (Optional) comment on the PR to request updates
- You merge, and the labels are removed

**Cross-repo prerequisites file themselves.** When a coding agent concludes
the work is blocked on changes in another repository, it describes the
issue(s) to file there, the orchestrator creates them and
the original issue automatically resumes once every prerequisite issue is
closed. You can run another instance of L3 Simulator on that repo as well.

**Plans are approved explicitly.** When a plan is posted (`needs-plan`

), the
issue blocks until you reply. A reply containing `#approve`

starts coding —
anything else in that reply is passed to the coding agent as guidance. Any
other reply is treated as plan feedback: the plan agent produces a revised
plan and re-posts it for review, as many rounds as you like.

**Everything else blocked resumes on a reply.** Planning-agent questions,
agent failures, and the fix-attempt cap all add `blocked`

plus an explanatory
comment. Replying with any comment unblocks the issue and work resumes with
your reply as context. Removing the `blocked`

label by hand also works. Human comments are passed
to the coding agent and take precedence over the plan.

**Multi-stage issues.** When a coding agent reports its PR is a first slice
(`done: false`

), the issue keeps `in-progress`

alongside `in-review`

, and the
next stage starts only after you merge the current PR.

While a PR is open the orchestrator polls its health: failing checks, new
review comments, or merge conflicts dispatch a fix agent into the same
worktree (capped at `maxFixIterationsPerPr`

per PR; pushing your own commit to
the branch resets the counter). Branches that fall behind the base are
updated automatically, and merge conflicts are resolved even while an issue
is blocked on something else.

Closed issues are tidied automatically: workflow labels are stripped (so the
closed list looks clean) and worktrees whose PRs are merged or closed are
removed once the issue is no longer active — worktrees with uncommitted
changes are left for `l3-simulator cleanup --force`

.

```
l3-simulator start                 # TUI dashboard + daemon (q to quit)
l3-simulator start --headless      # plain log lines (cron, background)
l3-simulator start --dry-run       # log every mutation instead of doing it
l3-simulator run-once              # single tick (demos, cron)
l3-simulator status                # one-shot table derived from GitHub
l3-simulator cleanup               # prune worktrees for merged/closed PRs
```

`start`

opens a live TUI: every workflow issue with its phase (queued, planning, coding,
fixing, plan review, blocked, in review, next stage), the running agent's
role + elapsed time + live progress line (on the selected row), the open PR
with a CI glyph (✓ ✗ ●), the worktree branch, and a "recently completed"
section of closed issues the orchestrator shipped. A log pane shows recent
events (`--verbose`

adds per-tool agent progress).

Keys: `↑↓`

/`j k`

select · `n`

file a new issue (title, body, label choice —
appears on the next tick) · `r`

force a poll now · `o`

open a new **tmux pane**
cd'd into the selected issue's worktree · `⏎`

open the PR (or issue) in your
browser · `q`

/ctrl-C graceful shutdown (kills agents, releases the lock).

Agent transcripts land in
`~/.local/share/l3-simulator/logs/<owner--repo>/issue-<n>/`

.

While a fix agent addresses PR feedback, the issue's `in-review`

label swaps
to `in-progress`

(and back after the push) so GitHub also shows work is
active.

`~/.config/l3-simulator/config.json`

(override with `--config`

or
`L3_SIMULATOR_CONFIG`

):

```
{
  "repo": {"path": "/home/you/myrepo", "slug": "you/myrepo"},
  "maxConcurrentAgents": 2,
  "pollIntervalSeconds": 60,
  "maxFixIterationsPerPr": 3,
  "agents": {
    "plan": {"backend": "claude", "timeoutMinutes": 15},
    "code": {"backend": "claude", "timeoutMinutes": 60, "model": "opus"},
    "fix": {"backend": "codex", "timeoutMinutes": 30},
  },
}
```

Each role (`plan`

/`code`

/`fix`

) independently chooses `claude`

or `codex`

,
plus optional `model`

, `permissionMode`

(Claude), and `extraArgs`

. Use
`init --global`

for a single per-user config instead
(`~/.config/l3-simulator/config.json`

). Resolution order:

`--config`

flag`L3_SIMULATOR_CONFIG`

- nearest
`.l3-simulator.json`

up the directory tree - user config dir.

**No merging.** The only`gh pr`

subcommands in the codebase are create/list/view/checks/comment/update-branch; the single`gh`

choke point (`src/github/gh.ts`

) rejects`merge`

argv outright.**Agents don't touch GitHub.** Claude runs with`gh`

and`git push`

disallowed; Codex runs sandboxed. The orchestrator pushes branches and opens PRs itself, and only after verifying the agent actually committed.**Claude code/fix agents run with** inside their isolated worktree. Headless`bypassPermissions`

`acceptEdits`

aborts on any shell command, so autonomous work is impossible without it. The`gh`

/`git push`

deny rules are enforced even in bypass mode; set`agents.<role>.permissionMode`

to tighten this if you prefer explicit allowlists.Anything the orchestrator can't handle is commented on the issue and handed to you. Your L3s know when to escalate to an adult.`blocked`

is inert.- Recommended: branch protection on your default branch (require review, no force pushes). One daemon per repo (enforced by a pidfile).

```
npm install              # install dependencies
npm run dev -- status    # run from source (tsx)
npm run demo             # render the sample dashboard (handy for screenshots)
npm test                 # vitest
npm run typecheck && npm run lint && npm run format:check
```

To use a source build as the global `l3-simulator`

command: `npm run build && npm link`

.
