@nickysagan/issue-orchestrator 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +314 -0
- package/bin/supervisor.mjs +614 -0
- package/package.json +19 -0
- package/src/labels.mjs +68 -0
- package/src/managedContainer.mjs +120 -0
- package/src/reviewGate.mjs +308 -0
- package/src/reviewMarker.mjs +80 -0
- package/src/workerLogs.mjs +175 -0
package/README.md
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# issue-orchestrator
|
|
2
|
+
|
|
3
|
+
A small, supervised issue queue. A single Node supervisor picks up open issues
|
|
4
|
+
labeled `agent-ready` (ascending) and keeps at most **two** autonomous
|
|
5
|
+
subscription-authenticated `ccode` implementation workers alive — each in its own
|
|
6
|
+
tmux window running:
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
ccode --print --permission-mode auto --model claude-opus-4-8 "/github-issue <number>"
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Usage limits are enforced by
|
|
13
|
+
[Usage Sentinel](https://github.com/Sadotu/usage-sentinel), which pauses this
|
|
14
|
+
orchestrator's entire Docker container — supervisor and workers together — rather
|
|
15
|
+
than this repository stopping workers itself. The full model name pins issue
|
|
16
|
+
workers to Opus 4.8 instead of the Claude Code default or the moving `opus`
|
|
17
|
+
alias.
|
|
18
|
+
|
|
19
|
+
A finished PR is not merged automatically. It first passes through the managed
|
|
20
|
+
review gate below, which uses a third, separately reserved worker slot.
|
|
21
|
+
|
|
22
|
+
## Orchestrator
|
|
23
|
+
|
|
24
|
+
Zero-dependency Node ≥20.8.0 on Linux (the supervisor guard uses a Linux abstract
|
|
25
|
+
Unix socket). No build step.
|
|
26
|
+
|
|
27
|
+
### Installed/current-repository usage
|
|
28
|
+
|
|
29
|
+
The package is published publicly on
|
|
30
|
+
[npmjs](https://www.npmjs.com/package/@nickysagan/issue-orchestrator), so no
|
|
31
|
+
registry configuration or token is needed:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install -g @nickysagan/issue-orchestrator@latest
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The scope names the package; the installed command is `issue-orchestrator`.
|
|
38
|
+
|
|
39
|
+
Run it from the target project repository, not from the package checkout:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
cd /path/to/target-project
|
|
43
|
+
issue-orchestrator
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The shared [`agent-devcontainer`](https://github.com/Sadotu/agent-devcontainer)
|
|
47
|
+
also exposes that command through the literal alias:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
start work
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The supervisor resolves the `origin` of that current working directory,
|
|
54
|
+
checks GitHub App authentication, registers its own container with Sentinel, and
|
|
55
|
+
starts autonomous workers itself:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
ccode --print --permission-mode auto --model claude-opus-4-8 "/github-issue <number>"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Non-interactive print mode skips the workspace-trust prompt, and each process
|
|
62
|
+
exits after its command finishes. A supervisor that cannot resolve its container
|
|
63
|
+
ID or register its lease exits nonzero rather than run unenforced. Do not start
|
|
64
|
+
Claude separately.
|
|
65
|
+
|
|
66
|
+
Only one supervisor per repository per Linux network namespace may run at a
|
|
67
|
+
time. A second invocation in the same namespace for the same resolved
|
|
68
|
+
`owner/repo` exits nonzero with an `already running` error before
|
|
69
|
+
authentication, the container lease, tmux, or worker startup. Ownership is held by a
|
|
70
|
+
kernel-owned Linux abstract Unix socket, so it is released on normal exit and
|
|
71
|
+
automatically when the supervisor process dies. Devcontainers normally have
|
|
72
|
+
distinct network namespaces and therefore independent guards; containers that
|
|
73
|
+
share a network namespace also share this guard.
|
|
74
|
+
|
|
75
|
+
Neither the image build nor devcontainer startup launches the supervisor or
|
|
76
|
+
an LLM. Work begins only when a user explicitly runs `issue-orchestrator` or
|
|
77
|
+
its `start work` alias.
|
|
78
|
+
|
|
79
|
+
### Repository-checkout usage
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npm test # node --test over test/
|
|
83
|
+
node bin/supervisor.mjs # start the supervisor loop
|
|
84
|
+
tmux attach -t orchestrator # watch the workers directly
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### How it works
|
|
88
|
+
|
|
89
|
+
- **Claim** — the lowest-numbered `agent-ready` issue has its label swapped
|
|
90
|
+
`agent-ready` → `agent-running`, then a tmux window `issue-<n>` opens running
|
|
91
|
+
this autonomous command:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
ccode --print --permission-mode auto --model claude-opus-4-8 "/github-issue <n>"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Non-interactive print mode skips the workspace-trust prompt; the process exits
|
|
98
|
+
and its tmux window closes after the command finishes.
|
|
99
|
+
- **Complete** — a worker finishes by leaving its PR in draft and putting
|
|
100
|
+
`agent-review` on the issue. Reaching any phase label is what marks
|
|
101
|
+
implementation done: the supervisor closes the tmux window and frees the slot,
|
|
102
|
+
and `agent-running` stays. An already-closed window is treated as closed; a
|
|
103
|
+
failure at any later step is logged and retried by the next poll.
|
|
104
|
+
- **Vanished** — if a worker window disappears before the issue reaches a phase
|
|
105
|
+
label, the workflow died. There is no repair worker, so the supervisor fails
|
|
106
|
+
closed: it comments on the issue — including the vanished worker's log path
|
|
107
|
+
and the tail of its output — and sets `agent-blocked`. The issue is never
|
|
108
|
+
restarted automatically, because its draft PR or worktree may still exist.
|
|
109
|
+
- **Worker logs** — every launch `tee`s the window's combined output to
|
|
110
|
+
`<git-common-dir>/issue-orchestrator/logs/<issue|review>-<n>.<attempt>.log`,
|
|
111
|
+
keeping the latest three attempts. Surfaced tails are scrubbed of secrets.
|
|
112
|
+
- **Active-container lease** — at startup the supervisor resolves its own exact
|
|
113
|
+
64-character Docker container ID (from `/proc/self/mountinfo`; the short
|
|
114
|
+
`hostname` form is not accepted) and registers it with
|
|
115
|
+
`PUT /managed-containers/<id>` on Sentinel. Registration is a lease, not an
|
|
116
|
+
admission decision: Sentinel never allows or denies a start. Every later poll
|
|
117
|
+
re-PUTs the same ID as its heartbeat, well inside the five-minute lease; a
|
|
118
|
+
clean exit `DELETE`s it as best-effort cleanup, and an abrupt exit is handled
|
|
119
|
+
by Sentinel's lease expiry. A failed heartbeat is logged and retried on the
|
|
120
|
+
next poll. The supervisor keeps a local guard of two implementation workers
|
|
121
|
+
plus one reviewer.
|
|
122
|
+
- **Usage enforcement lives in Sentinel** — when Claude Code's five-hour or
|
|
123
|
+
weekly window reaches 95%, Sentinel `docker pause`s every registered container,
|
|
124
|
+
freezing this supervisor and every worker process in place, and unpauses only
|
|
125
|
+
after a fresh valid reading puts both windows below 95%. This repository reads
|
|
126
|
+
no usage telemetry, applies no thresholds, calls no Docker API, and never kills
|
|
127
|
+
a worker because of usage.
|
|
128
|
+
- **Stop** — the supervisor exits when nothing is queued, no worker or reviewer
|
|
129
|
+
window is live, and every managed issue rests in `agent-blocked` or
|
|
130
|
+
`user-merge-review`. Merge and post-merge cleanup monitoring are not yet
|
|
131
|
+
implemented.
|
|
132
|
+
|
|
133
|
+
Live container pause/unpause check:
|
|
134
|
+
[docs/smoke-checks/README.md](docs/smoke-checks/README.md).
|
|
135
|
+
|
|
136
|
+
## Managed review gate
|
|
137
|
+
|
|
138
|
+
A managed PR receives an independent automated review and then either stops with
|
|
139
|
+
documented blockers or becomes ready for the owner. The supervisor never
|
|
140
|
+
approves and never merges.
|
|
141
|
+
|
|
142
|
+
### Labels
|
|
143
|
+
|
|
144
|
+
At startup — after the repository is resolved and the GitHub App auth check
|
|
145
|
+
passes, and before the first poll — the supervisor creates any of these labels
|
|
146
|
+
that the repository is missing. Labels that already exist are never renamed,
|
|
147
|
+
recoloured, or re-described, and no GitHub mutation ever happens at image-build
|
|
148
|
+
time (the build has neither a target repository nor runtime credentials).
|
|
149
|
+
|
|
150
|
+
| Label | Colour | Meaning |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| `agent-ready` | `0e8a16` | Ready for issue-orchestrator claim |
|
|
153
|
+
| `agent-running` | `1d76db` | Managed by issue-orchestrator |
|
|
154
|
+
| `agent-review` | `5319e7` | Waiting for or undergoing automated review |
|
|
155
|
+
| `agent-blocked` | `d73a4a` | Automated workflow blocked; findings posted |
|
|
156
|
+
| `user-merge-review` | `fbca04` | Automated review passed; waiting for owner review and merge |
|
|
157
|
+
|
|
158
|
+
`agent-running` is the durable ownership marker: it is applied at claim and
|
|
159
|
+
stays until post-merge cleanup (not yet implemented). Exactly one phase label —
|
|
160
|
+
`agent-review`, `agent-blocked`, or `user-merge-review` — is active alongside
|
|
161
|
+
it. Issue labels are canonical and the PR mirrors them; every poll repairs a
|
|
162
|
+
mismatch, issuing an edit only when the sets actually differ.
|
|
163
|
+
|
|
164
|
+
A PR with no linked `agent-running` issue is ignored entirely — manual
|
|
165
|
+
`/github-issue` PRs and unrelated `agent/*` branches are never touched. More
|
|
166
|
+
than one PR closing an issue, a branch that disagrees with its closing
|
|
167
|
+
reference, or an `agent-review` issue with no PR all fail closed: an actionable
|
|
168
|
+
comment plus `agent-blocked`.
|
|
169
|
+
|
|
170
|
+
### Capacity
|
|
171
|
+
|
|
172
|
+
Two reserved implementation slots plus one reviewer slot that is never borrowed
|
|
173
|
+
for implementation. The pools are separate tmux window namespaces — `issue-<n>`
|
|
174
|
+
and `review-<n>` — each counting only its own live windows. The oldest eligible
|
|
175
|
+
`agent-review` PR gets a fresh reviewer window running:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
ccode --print --permission-mode auto --model claude-opus-4-8 "/review-pr <pr>"
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The reviewer slot frees as soon as that window exits. Waiting on CI holds no
|
|
182
|
+
slot, because it is pure label state with no window.
|
|
183
|
+
|
|
184
|
+
### Review marker
|
|
185
|
+
|
|
186
|
+
The reviewer (`Sadotu/agent-skills`, `review-pr`) records each pass in one
|
|
187
|
+
immutable PR comment carrying a single-line marker:
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
<!-- review-pr:v1 {"fingerprint":"<64-hex>","head":"<40-hex>","base":"<40-hex>","issueUpdatedAt":"<ISO>","prUpdatedAt":"<ISO>","issue":41,"pr":72,"pass":1,"verdict":"PASS"} -->
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The reviewer is the producer and owns this format; this repository only consumes
|
|
194
|
+
it. Verdicts are exactly `PASS` and `BLOCKING`.
|
|
195
|
+
|
|
196
|
+
A marker is applied only when all three hold:
|
|
197
|
+
|
|
198
|
+
- **Authorship** — the comment has `viewerDidAuthor: true`, so the same GitHub
|
|
199
|
+
App identity the supervisor authenticates as wrote it. Every fingerprint input
|
|
200
|
+
is public, so without this check any commenter could forge a `PASS`.
|
|
201
|
+
- **Identity** — its `issue` and `pr` are the managed pair being gated.
|
|
202
|
+
- **Freshness** — its `fingerprint` equals the live fingerprint,
|
|
203
|
+
`sha256(head 0x1e base 0x1e issue-body 0x1e pr-body)`. Bodies decide
|
|
204
|
+
freshness, not `updatedAt`: a new comment or a label edit moves the timestamps
|
|
205
|
+
while reviewing the same content, whereas any real change to the head, the
|
|
206
|
+
base, the issue text, or the PR text invalidates the pass. `issueUpdatedAt`
|
|
207
|
+
and `prUpdatedAt` are recorded for audit only.
|
|
208
|
+
|
|
209
|
+
Comparing against live state rather than a snapshot captured at launch needs no
|
|
210
|
+
persistence, so the same content is handled once even across a supervisor
|
|
211
|
+
restart or a crash between the comment and the label transition. A marker that
|
|
212
|
+
fails any check is discarded silently: nothing is posted, nothing transitions,
|
|
213
|
+
and the PR simply queues a fresh pass. Parsing fails closed on an unsupported
|
|
214
|
+
tag, malformed JSON, or a missing or ill-typed field. A trusted, applicable
|
|
215
|
+
marker carrying an unsupported *verdict* is the one case that is not discarded —
|
|
216
|
+
it blocks, so a contract break surfaces instead of looping.
|
|
217
|
+
|
|
218
|
+
### Transitions
|
|
219
|
+
|
|
220
|
+
**`BLOCKING`** — the PR is kept or returned to draft and `agent-review` becomes
|
|
221
|
+
`agent-blocked` on issue and PR. The reviewer's findings stand alone in its own
|
|
222
|
+
comment; the supervisor adds nothing and stops. There is no automatic repair.
|
|
223
|
+
|
|
224
|
+
**`PASS`** — the PR is marked ready for review first, so repositories whose CI
|
|
225
|
+
triggers on `ready_for_review` start their checks. Checks are read on the
|
|
226
|
+
following poll, never in the same one that opened the PR:
|
|
227
|
+
|
|
228
|
+
| Checks | Outcome |
|
|
229
|
+
|---|---|
|
|
230
|
+
| None configured | Green → `user-merge-review` |
|
|
231
|
+
| All complete and successful, neutral, or skipped | Green → `user-merge-review` |
|
|
232
|
+
| Any still running | Keep `agent-review`; reviewer slot stays free |
|
|
233
|
+
| Any failed | Return to draft, `agent-blocked`, post the failing check names and URLs |
|
|
234
|
+
| Data unreadable | Transition nothing; retry next poll |
|
|
235
|
+
|
|
236
|
+
Every check GitHub reports is treated as required. "No checks configured" and
|
|
237
|
+
"could not read the checks" are deliberately distinct: only the former counts as
|
|
238
|
+
green.
|
|
239
|
+
|
|
240
|
+
### GitHub authentication
|
|
241
|
+
|
|
242
|
+
`gh` does not auto-consume the GitHub App credential, so the supervisor mints a
|
|
243
|
+
short-lived App installation token (via `gh-app-token.sh`, overridable with
|
|
244
|
+
`GH_APP_TOKEN_SCRIPT`) and injects it as `GH_TOKEN` for every `gh` call,
|
|
245
|
+
re-minting each poll. At startup it runs an authenticated `gh repo view` smoke
|
|
246
|
+
check and exits with a clear message if the App is not authenticated — rather
|
|
247
|
+
than churning on unauthenticated calls every poll.
|
|
248
|
+
|
|
249
|
+
### Configuration
|
|
250
|
+
|
|
251
|
+
The local concurrency guard is fixed at two implementation slots plus one
|
|
252
|
+
reviewer slot. Two knobs are environment-tunable:
|
|
253
|
+
|
|
254
|
+
| Var | Default | Meaning |
|
|
255
|
+
|-----|---------|---------|
|
|
256
|
+
| `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 |
|
|
257
|
+
| `POLL_MS` | `60000` | Poll interval |
|
|
258
|
+
|
|
259
|
+
Labels are created at startup — see the managed review gate below.
|
|
260
|
+
|
|
261
|
+
## Repo contents
|
|
262
|
+
|
|
263
|
+
| Path | Purpose |
|
|
264
|
+
|------|---------|
|
|
265
|
+
| `agents.toml` / `agents.lock` | [dotagents](https://github.com/Sadotu/agent-skills) manifest — declares which skills are installed and pins their source commits |
|
|
266
|
+
| `.agents/skills/` | Installed skills (`github-issue`, `setup`) — managed artifacts, restored from the manifest, not committed |
|
|
267
|
+
| `.claude/skills` | Symlink to `.agents/skills` so Claude Code picks the skills up |
|
|
268
|
+
| `CLAUDE.md` | Agent instructions and gotchas for working in this repo |
|
|
269
|
+
|
|
270
|
+
## Skills
|
|
271
|
+
|
|
272
|
+
- **`github-issue`** — runs a GitHub issue end to end: select issue, open a
|
|
273
|
+
draft PR immediately, self-resolve design decisions (logged to the PR),
|
|
274
|
+
implement in an isolated worktree, verify against the issue, mark ready.
|
|
275
|
+
- **`setup`** — connects the repo to the `container-coding-agent` GitHub App
|
|
276
|
+
and verifies `git`/`gh` authenticate as the App.
|
|
277
|
+
|
|
278
|
+
Both are sourced from [`Sadotu/agent-skills`](https://github.com/Sadotu/agent-skills).
|
|
279
|
+
|
|
280
|
+
## Releasing
|
|
281
|
+
|
|
282
|
+
`package.json` holds the version; the tag only confirms it. Bump it in a normal
|
|
283
|
+
pull request, then tag the merge commit:
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
git tag v0.2.0 && git push origin v0.2.0
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
`.github/workflows/publish.yml` publishes to npmjs via
|
|
290
|
+
[trusted publishing](https://docs.npmjs.com/trusted-publishers), so no npm
|
|
291
|
+
token is stored anywhere: npm exchanges the workflow's OIDC identity for a
|
|
292
|
+
short-lived credential. It refuses to publish unless the tag equals
|
|
293
|
+
`v<version>` and the tests pass. A published version cannot be republished;
|
|
294
|
+
bump and tag again.
|
|
295
|
+
|
|
296
|
+
Trusted publishing can only be configured on a package that already exists, so
|
|
297
|
+
`0.1.0` was published by hand (`npm login && npm publish`) to bootstrap it.
|
|
298
|
+
Every later release goes through the workflow.
|
|
299
|
+
|
|
300
|
+
Package visibility is set on the package, not inherited from this private
|
|
301
|
+
repository, and the setting appears only once a version exists. So after the
|
|
302
|
+
**first** release, set it once under **Sadotu → Packages →
|
|
303
|
+
issue-orchestrator → Package settings**.
|
|
304
|
+
|
|
305
|
+
## Development environment
|
|
306
|
+
|
|
307
|
+
Agents work on this repo from a shared, sandboxed devcontainer. That
|
|
308
|
+
environment is **not part of this app** — it lives in its own repo:
|
|
309
|
+
[`Sadotu/agent-devcontainer`](https://github.com/Sadotu/agent-devcontainer)
|
|
310
|
+
(image, setup scripts, security model, auth docs). A local `.devcontainer/`
|
|
311
|
+
folder pointing at that image may exist in a checkout but is gitignored.
|
|
312
|
+
|
|
313
|
+
GitHub access is via the scoped `container-coding-agent` GitHub App — never a
|
|
314
|
+
personal token, never `gh auth login`. See `CLAUDE.md` for the auth wiring.
|