@nickysagan/issue-orchestrator
v0.1.2
Published
A small, supervised GitHub issue queue that keeps autonomous coding workers alive.
Downloads
469
Readme
issue-orchestrator
A small, supervised issue queue. A single Node supervisor picks up open issues
labeled agent-ready (ascending) and keeps at most two autonomous
subscription-authenticated ccode implementation workers alive — each in its own
tmux window running:
ccode --print --permission-mode auto --model claude-opus-4-8 "/github-issue <number>"Usage limits are enforced by
Usage Sentinel, which pauses this
orchestrator's entire Docker container — supervisor and workers together — rather
than this repository stopping workers itself. The full model name pins issue
workers to Opus 4.8 instead of the Claude Code default or the moving opus
alias.
A finished PR is not merged automatically. It first passes through the managed review gate below, which uses a third, separately reserved worker slot.
Orchestrator
Zero-dependency Node ≥20.8.0 on Linux (the supervisor guard uses a Linux abstract Unix socket). No build step.
Installed/current-repository usage
The package is published publicly on npmjs, so no registry configuration or token is needed:
npm install -g @nickysagan/issue-orchestrator@latestThe scope names the package; the installed command is issue-orchestrator.
Run it from the target project repository, not from the package checkout:
cd /path/to/target-project
issue-orchestratorThe shared agent-devcontainer
also exposes that command through the literal alias:
start workThe supervisor resolves the origin of that current working directory,
checks GitHub App authentication, registers its own container with Sentinel, and
starts autonomous workers itself:
ccode --print --permission-mode auto --model claude-opus-4-8 "/github-issue <number>"Non-interactive print mode skips the workspace-trust prompt, and each process exits after its command finishes. A supervisor that cannot resolve its container ID or register its lease exits nonzero rather than run unenforced. Do not start Claude separately.
Only one supervisor per repository per Linux network namespace may run at a
time. A second invocation in the same namespace for the same resolved
owner/repo exits nonzero with an already running error before
authentication, the container lease, tmux, or worker startup. Ownership is held by a
kernel-owned Linux abstract Unix socket, so it is released on normal exit and
automatically when the supervisor process dies. Devcontainers normally have
distinct network namespaces and therefore independent guards; containers that
share a network namespace also share this guard.
Neither the image build nor devcontainer startup launches the supervisor or
an LLM. Work begins only when a user explicitly runs issue-orchestrator or
its start work alias.
Repository-checkout usage
npm test # node --test over test/
node bin/supervisor.mjs # start the supervisor loop
tmux attach -t orchestrator # watch the workers directlyHow it works
Claim — the lowest-numbered
agent-readyissue has its label swappedagent-ready→agent-running, then a tmux windowissue-<n>opens running this autonomous command:ccode --print --permission-mode auto --model claude-opus-4-8 "/github-issue <n>"Non-interactive print mode skips the workspace-trust prompt; the process exits and its tmux window closes after the command finishes.
Complete — a worker finishes by leaving its PR in draft and putting
agent-reviewon the issue. Reaching any phase label is what marks implementation done: the supervisor closes the tmux window and frees the slot, andagent-runningstays. An already-closed window is treated as closed; a failure at any later step is logged and retried by the next poll.Vanished — if a worker window disappears before the issue reaches a phase label, the workflow died. There is no repair worker, so the supervisor fails closed: it comments on the issue — including the vanished worker's log path and the tail of its output — and sets
agent-blocked. The issue is never restarted automatically, because its draft PR or worktree may still exist.Worker logs — every launch
tees the window's combined output to<git-common-dir>/issue-orchestrator/logs/<issue|review>-<n>.<attempt>.log, keeping the latest three attempts. Surfaced tails are scrubbed of secrets. A run that reaches its end stamps a final[orchestrator <ISO>] ... finished ...line into its log, so a tail can be placed in time without the file's mtime.Active-container lease — at startup the supervisor resolves its own exact 64-character Docker container ID (from
/proc/self/mountinfo; the shorthostnameform is not accepted) and registers it withPUT /managed-containers/<id>on Sentinel. Registration is a lease, not an admission decision: Sentinel never allows or denies a start. Every later poll re-PUTs the same ID as its heartbeat, well inside the five-minute lease; a clean exitDELETEs it as best-effort cleanup, and an abrupt exit is handled by Sentinel's lease expiry. A failed heartbeat is logged and retried on the next poll. The supervisor keeps a local guard of two implementation workers plus one reviewer.Usage enforcement lives in Sentinel — when Claude Code's five-hour or weekly window reaches 95%, Sentinel
docker pauses every registered container, freezing this supervisor and every worker process in place, and unpauses only after a fresh valid reading puts both windows below 95%. This repository reads no usage telemetry, applies no thresholds, calls no Docker API, and never kills a worker because of usage.Stop — the supervisor exits when nothing is queued, no worker or reviewer window is live, and every managed issue rests in
agent-blockedoruser-merge-review. Merge and post-merge cleanup monitoring are not yet implemented.
Live container pause/unpause check: docs/smoke-checks/README.md.
Managed review gate
A managed PR receives an independent automated review and then either stops with documented blockers or becomes ready for the owner. The supervisor never approves and never merges.
Labels
At startup — after the repository is resolved and the GitHub App auth check passes, and before the first poll — the supervisor creates any of these labels that the repository is missing. Labels that already exist are never renamed, recoloured, or re-described, and no GitHub mutation ever happens at image-build time (the build has neither a target repository nor runtime credentials).
| Label | Colour | Meaning |
|---|---|---|
| agent-ready | 0e8a16 | Ready for issue-orchestrator claim |
| agent-running | 1d76db | Managed by issue-orchestrator |
| agent-review | 5319e7 | Waiting for or undergoing automated review |
| agent-blocked | d73a4a | Automated workflow blocked; findings posted |
| user-merge-review | fbca04 | Automated review passed; waiting for owner review and merge |
agent-running is the durable ownership marker: it is applied at claim and
stays until post-merge cleanup (not yet implemented). Exactly one phase label —
agent-review, agent-blocked, or user-merge-review — is active alongside
it. Issue labels are canonical and the PR mirrors them; every poll repairs a
mismatch, issuing an edit only when the sets actually differ.
A PR with no linked agent-running issue is ignored entirely — manual
/github-issue PRs and unrelated agent/* branches are never touched. More
than one PR closing an issue, a branch that disagrees with its closing
reference, or an agent-review issue with no PR all fail closed: an actionable
comment plus agent-blocked.
Capacity
Two reserved implementation slots plus one reviewer slot that is never borrowed
for implementation. The pools are separate tmux window namespaces — issue-<n>
and review-<n> — each counting only its own live windows. The oldest eligible
agent-review PR gets a fresh reviewer window running:
ccode --print --permission-mode auto --model claude-opus-4-8 "/review-pr <pr>"The reviewer slot frees as soon as that window exits. Waiting on CI holds no slot, because it is pure label state with no window.
Review marker
The reviewer (Sadotu/agent-skills, review-pr) records each pass in one
immutable PR comment carrying a single-line marker:
<!-- review-pr:v1 {"fingerprint":"<64-hex>","head":"<40-hex>","base":"<40-hex>","issueUpdatedAt":"<ISO>","prUpdatedAt":"<ISO>","issue":41,"pr":72,"pass":1,"verdict":"PASS"} -->The reviewer is the producer and owns this format; this repository only consumes
it. Verdicts are exactly PASS and BLOCKING.
A marker is applied only when all three hold:
- Authorship — the comment has
viewerDidAuthor: true, so the same GitHub App identity the supervisor authenticates as wrote it. Every fingerprint input is public, so without this check any commenter could forge aPASS. - Identity — its
issueandprare the managed pair being gated. - Freshness — its
fingerprintequals the live fingerprint,sha256(head 0x1e base 0x1e issue-body 0x1e pr-body). Bodies decide freshness, notupdatedAt: a new comment or a label edit moves the timestamps while reviewing the same content, whereas any real change to the head, the base, the issue text, or the PR text invalidates the pass.issueUpdatedAtandprUpdatedAtare recorded for audit only.
Comparing against live state rather than a snapshot captured at launch needs no persistence, so the same content is handled once even across a supervisor restart or a crash between the comment and the label transition. A marker that fails any check is discarded silently: nothing is posted, nothing transitions, and the PR simply queues a fresh pass. Parsing fails closed on an unsupported tag, malformed JSON, or a missing or ill-typed field. A trusted, applicable marker carrying an unsupported verdict is the one case that is not discarded — it blocks, so a contract break surfaces instead of looping.
Transitions
BLOCKING — the PR is kept or returned to draft and agent-review becomes
agent-blocked on issue and PR. The reviewer's findings stand alone in its own
comment; the supervisor adds nothing and stops. There is no automatic repair.
PASS — the PR is marked ready for review first, so repositories whose CI
triggers on ready_for_review start their checks. Checks are read on the
following poll, never in the same one that opened the PR:
| Checks | Outcome |
|---|---|
| None configured | Green → user-merge-review |
| All complete and successful, neutral, or skipped | Green → user-merge-review |
| Any still running | Keep agent-review; reviewer slot stays free |
| Any failed | Return to draft, agent-blocked, post the failing check names and URLs |
| Data unreadable | Transition nothing; retry next poll |
Every check GitHub reports is treated as required. "No checks configured" and "could not read the checks" are deliberately distinct: only the former counts as green.
GitHub authentication
gh does not auto-consume the GitHub App credential, so the supervisor mints a
short-lived App installation token (via gh-app-token.sh, overridable with
GH_APP_TOKEN_SCRIPT) and injects it as GH_TOKEN for every gh call,
re-minting each poll. At startup it runs an authenticated gh repo view smoke
check and exits with a clear message if the App is not authenticated — rather
than churning on unauthenticated calls every poll.
Configuration
The local concurrency guard is fixed at two implementation slots plus one reviewer slot. Two knobs are environment-tunable:
| Var | Default | Meaning |
|-----|---------|---------|
| SENTINEL_URL | http://usage-sentinel:4317 | Usage Sentinel base URL for the managed-container lease, on the shared container network; set this explicitly (for example, http://host.docker.internal:4317) only when Sentinel is exposed on the host |
| POLL_MS | 60000 | Poll interval |
Labels are created at startup — see the managed review gate below.
Repo contents
| Path | Purpose |
|------|---------|
| agents.toml / agents.lock | dotagents manifest — declares which skills are installed and pins their source commits |
| .agents/skills/ | Installed skills (github-issue, setup) — managed artifacts, restored from the manifest, not committed |
| .claude/skills | Symlink to .agents/skills so Claude Code picks the skills up |
| CLAUDE.md | Agent instructions and gotchas for working in this repo |
Skills
github-issue— runs a GitHub issue end to end: select issue, open a draft PR immediately, self-resolve design decisions (logged to the PR), implement in an isolated worktree, verify against the issue, mark ready.setup— connects the repo to thecontainer-coding-agentGitHub App and verifiesgit/ghauthenticate as the App.
Both are sourced from Sadotu/agent-skills.
Releasing
package.json holds the version, and merging the bump is the whole release.
Bump it in a normal pull request; when that lands on main,
.github/workflows/publish.yml sees a version the registry does not carry,
runs the tests, publishes, and pushes the matching v<version> tag. A merge
that does not bump the version finds its version already published and exits
without releasing anything.
Publishing goes to npmjs via trusted publishing, so no npm token is stored anywhere: npm exchanges the workflow's OIDC identity for a short-lived credential. Tagging is a second job, so the job holding an npm credential cannot write to this repository and the job that can push a tag holds no npm credential. A published version cannot be republished; bump and merge again.
Pushing a v* tag by hand still works and takes the same path, with one extra
guard: the tag must equal v<version> or the run fails.
Trusted publishing can only be configured on a package that already exists, so
0.1.0 was published by hand (npm login && npm publish) to bootstrap it.
Every later release goes through the workflow.
Package visibility is set on the package, not inherited from this private repository, and the setting appears only once a version exists. So after the first release, set it once under Sadotu → Packages → issue-orchestrator → Package settings.
Development environment
Agents work on this repo from a shared, sandboxed devcontainer. That
environment is not part of this app — it lives in its own repo:
Sadotu/agent-devcontainer
(image, setup scripts, security model, auth docs). A local .devcontainer/
folder pointing at that image may exist in a checkout but is gitignored.
GitHub access is via the scoped container-coding-agent GitHub App — never a
personal token, never gh auth login. See CLAUDE.md for the auth wiring.
