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/orcodex
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.
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 addneeds-plan
orneeds-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
flagL3_SIMULATOR_CONFIG
- nearest
.l3-simulator.json
up the directory tree - user config dir.
No merging. The onlygh pr
subcommands in the codebase are create/list/view/checks/comment/update-branch; the singlegh
choke point (src/github/gh.ts
) rejectsmerge
argv outright.Agents don't touch GitHub. Claude runs withgh
andgit 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. HeadlessbypassPermissions
acceptEdits
aborts on any shell command, so autonomous work is impossible without it. Thegh
/git push
deny rules are enforced even in bypass mode; setagents.<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
.