@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 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.