@nickysagan/issue-orchestrator 0.1.2 → 0.2.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 +99 -215
- package/bin/supervisor.mjs +497 -29
- package/package.json +1 -1
- package/src/enforcementNotice.mjs +36 -0
- package/src/managedContainer.mjs +51 -4
- package/src/repairGate.mjs +57 -0
- package/src/repairMarker.mjs +248 -0
- package/src/reviewGate.mjs +18 -8
- package/src/workerLogs.mjs +14 -10
package/README.md
CHANGED
|
@@ -6,18 +6,21 @@ subscription-authenticated `ccode` implementation workers alive — each in its
|
|
|
6
6
|
tmux window running:
|
|
7
7
|
|
|
8
8
|
```bash
|
|
9
|
-
ccode --print --permission-mode auto --model claude-opus-
|
|
9
|
+
ccode --print --permission-mode auto --model claude-opus-5 "/github-issue <number>"
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
+
The full model name pins implementation workers to Opus 5 rather than the
|
|
13
|
+
Claude Code default or the moving `opus` alias. Non-interactive print mode
|
|
14
|
+
skips the workspace-trust prompt, and each process exits — closing its tmux
|
|
15
|
+
window — once its command finishes.
|
|
16
|
+
|
|
12
17
|
Usage limits are enforced by
|
|
13
18
|
[Usage Sentinel](https://github.com/Sadotu/usage-sentinel), which pauses this
|
|
14
19
|
orchestrator's entire Docker container — supervisor and workers together — rather
|
|
15
|
-
than this repository stopping workers itself.
|
|
16
|
-
workers to Opus 4.8 instead of the Claude Code default or the moving `opus`
|
|
17
|
-
alias.
|
|
20
|
+
than this repository stopping workers itself.
|
|
18
21
|
|
|
19
|
-
A finished PR is
|
|
20
|
-
|
|
22
|
+
A finished implementation PR is marked ready for the repository owner. The
|
|
23
|
+
supervisor does not review, repair, approve, or merge it automatically.
|
|
21
24
|
|
|
22
25
|
## Orchestrator
|
|
23
26
|
|
|
@@ -44,37 +47,24 @@ issue-orchestrator
|
|
|
44
47
|
```
|
|
45
48
|
|
|
46
49
|
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
|
-
```
|
|
50
|
+
also exposes that command through the literal alias `start work`.
|
|
60
51
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
52
|
+
The supervisor resolves the `origin` of that working directory, checks GitHub App
|
|
53
|
+
authentication, registers its own container with Sentinel, and starts the
|
|
54
|
+
autonomous workers itself. Do not start Claude separately. A supervisor that
|
|
55
|
+
cannot resolve its container ID or register its lease exits nonzero rather than
|
|
56
|
+
run unenforced.
|
|
65
57
|
|
|
66
58
|
Only one supervisor per repository per Linux network namespace may run at a
|
|
67
|
-
time
|
|
68
|
-
`
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
an LLM. Work begins only when a user explicitly runs `issue-orchestrator` or
|
|
77
|
-
its `start work` alias.
|
|
59
|
+
time; a second invocation for the same resolved `owner/repo` exits nonzero with
|
|
60
|
+
`already running` before authentication, the lease, tmux, or worker startup.
|
|
61
|
+
Ownership is a kernel-owned Linux abstract Unix socket, released on normal exit
|
|
62
|
+
and automatically when the process dies. Devcontainers normally have distinct
|
|
63
|
+
network namespaces and therefore independent guards; containers sharing a
|
|
64
|
+
namespace share this guard.
|
|
65
|
+
|
|
66
|
+
Neither the image build nor devcontainer startup launches the supervisor or an
|
|
67
|
+
LLM. Work begins only when a user runs `issue-orchestrator` or `start work`.
|
|
78
68
|
|
|
79
69
|
### Repository-checkout usage
|
|
80
70
|
|
|
@@ -86,227 +76,121 @@ tmux attach -t orchestrator # watch the workers directly
|
|
|
86
76
|
|
|
87
77
|
### How it works
|
|
88
78
|
|
|
79
|
+
- **Admission** — before anything is claimed, the primary worktree is checked
|
|
80
|
+
with a read-only `git status --porcelain`. If it has pending changes — or if
|
|
81
|
+
that check cannot be read — no issue is claimed and no worker starts for that
|
|
82
|
+
poll; nothing is stashed, reset, cleaned, or committed. Live workers keep
|
|
83
|
+
running and are reconciled as usual.
|
|
89
84
|
- **Claim** — the lowest-numbered `agent-ready` issue has its label swapped
|
|
90
|
-
`agent-ready` → `agent-running`, then a tmux window `issue-<n>` opens
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
`<git-common-dir>/issue-orchestrator/logs/<issue|review>-<n>.<attempt>.log`,
|
|
111
|
-
keeping the latest three attempts. Surfaced tails are scrubbed of secrets. A
|
|
112
|
-
run that reaches its end stamps a final `[orchestrator <ISO>] ... finished ...`
|
|
113
|
-
line into its log, so a tail can be placed in time without the file's mtime.
|
|
114
|
-
- **Active-container lease** — at startup the supervisor resolves its own exact
|
|
115
|
-
64-character Docker container ID (from `/proc/self/mountinfo`; the short
|
|
116
|
-
`hostname` form is not accepted) and registers it with
|
|
117
|
-
`PUT /managed-containers/<id>` on Sentinel. Registration is a lease, not an
|
|
118
|
-
admission decision: Sentinel never allows or denies a start. Every later poll
|
|
119
|
-
re-PUTs the same ID as its heartbeat, well inside the five-minute lease; a
|
|
120
|
-
clean exit `DELETE`s it as best-effort cleanup, and an abrupt exit is handled
|
|
121
|
-
by Sentinel's lease expiry. A failed heartbeat is logged and retried on the
|
|
122
|
-
next poll. The supervisor keeps a local guard of two implementation workers
|
|
123
|
-
plus one reviewer.
|
|
124
|
-
- **Usage enforcement lives in Sentinel** — when Claude Code's five-hour or
|
|
125
|
-
weekly window reaches 95%, Sentinel `docker pause`s every registered container,
|
|
126
|
-
freezing this supervisor and every worker process in place, and unpauses only
|
|
127
|
-
after a fresh valid reading puts both windows below 95%. This repository reads
|
|
128
|
-
no usage telemetry, applies no thresholds, calls no Docker API, and never kills
|
|
129
|
-
a worker because of usage.
|
|
130
|
-
- **Stop** — the supervisor exits when nothing is queued, no worker or reviewer
|
|
131
|
-
window is live, and every managed issue rests in `agent-blocked` or
|
|
132
|
-
`user-merge-review`. Merge and post-merge cleanup monitoring are not yet
|
|
133
|
-
implemented.
|
|
85
|
+
`agent-ready` → `agent-running`, then a tmux window `issue-<n>` opens.
|
|
86
|
+
- **Complete** — Phase 6 marks the implementation PR ready for the owner. The
|
|
87
|
+
next poll closes any remaining worker window, removes `agent-running` from
|
|
88
|
+
the issue, and frees the slot.
|
|
89
|
+
- **Vanished** — if a worker disappears while its PR is still draft or absent,
|
|
90
|
+
the claim is released and the draft/worktree is preserved. It is not
|
|
91
|
+
restarted automatically.
|
|
92
|
+
- **Worker logs** — every implementation launch writes combined output to
|
|
93
|
+
`<git-common-dir>/issue-orchestrator/logs/issue-<n>.<attempt>.log`, keeping
|
|
94
|
+
the latest three attempts and scrubbing secrets from surfaced tails.
|
|
95
|
+
- **Usage enforcement** — Usage Sentinel owns pause/unpause thresholds. This
|
|
96
|
+
repository reads no usage telemetry and never kills a worker for usage.
|
|
97
|
+
- **Pause notices** — the managed-container heartbeat carries Sentinel's
|
|
98
|
+
enforcement state. A pending pause prints one flushed line naming the
|
|
99
|
+
triggering window, both observed percentages and the reset time, and is then
|
|
100
|
+
acknowledged so Sentinel may pause; the first heartbeat back to `running`
|
|
101
|
+
prints one resume line. Ordinary refreshes stay quiet, and a payload that is
|
|
102
|
+
present but malformed is reported rather than guessed at.
|
|
103
|
+
- **Stop** — the supervisor exits when nothing is queued and no implementation
|
|
104
|
+
window and no legacy window (below) is live.
|
|
134
105
|
|
|
135
106
|
Live container pause/unpause check:
|
|
136
107
|
[docs/smoke-checks/README.md](docs/smoke-checks/README.md).
|
|
137
108
|
|
|
138
|
-
##
|
|
139
|
-
|
|
140
|
-
A managed PR receives an independent automated review and then either stops with
|
|
141
|
-
documented blockers or becomes ready for the owner. The supervisor never
|
|
142
|
-
approves and never merges.
|
|
143
|
-
|
|
144
|
-
### Labels
|
|
145
|
-
|
|
146
|
-
At startup — after the repository is resolved and the GitHub App auth check
|
|
147
|
-
passes, and before the first poll — the supervisor creates any of these labels
|
|
148
|
-
that the repository is missing. Labels that already exist are never renamed,
|
|
149
|
-
recoloured, or re-described, and no GitHub mutation ever happens at image-build
|
|
150
|
-
time (the build has neither a target repository nor runtime credentials).
|
|
109
|
+
## Implementation-only handoff
|
|
151
110
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
| `agent-blocked` | `d73a4a` | Automated workflow blocked; findings posted |
|
|
158
|
-
| `user-merge-review` | `fbca04` | Automated review passed; waiting for owner review and merge |
|
|
111
|
+
`agent-running` remains the active ownership marker: it is applied when an
|
|
112
|
+
issue is claimed and removed only after the implementation PR is ready for the
|
|
113
|
+
owner, or when a vanished/failed launch is safely released. Existing review
|
|
114
|
+
labels are left untouched in repositories, but the supervisor does not create a
|
|
115
|
+
reviewer or repair worker and does not transition issues through them.
|
|
159
116
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
it. Issue labels are canonical and the PR mirrors them; every poll repairs a
|
|
164
|
-
mismatch, issuing an edit only when the sets actually differ.
|
|
117
|
+
Manual and managed `/github-issue` runs both finish by marking the PR ready.
|
|
118
|
+
Human review and merge begin there. The supervisor exposes no approve or merge
|
|
119
|
+
operation.
|
|
165
120
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
than one PR closing an issue, a branch that disagrees with its closing
|
|
169
|
-
reference, or an `agent-review` issue with no PR all fail closed: an actionable
|
|
170
|
-
comment plus `agent-blocked`.
|
|
171
|
-
|
|
172
|
-
### Capacity
|
|
173
|
-
|
|
174
|
-
Two reserved implementation slots plus one reviewer slot that is never borrowed
|
|
175
|
-
for implementation. The pools are separate tmux window namespaces — `issue-<n>`
|
|
176
|
-
and `review-<n>` — each counting only its own live windows. The oldest eligible
|
|
177
|
-
`agent-review` PR gets a fresh reviewer window running:
|
|
178
|
-
|
|
179
|
-
```bash
|
|
180
|
-
ccode --print --permission-mode auto --model claude-opus-4-8 "/review-pr <pr>"
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
The reviewer slot frees as soon as that window exits. Waiting on CI holds no
|
|
184
|
-
slot, because it is pure label state with no window.
|
|
185
|
-
|
|
186
|
-
### Review marker
|
|
187
|
-
|
|
188
|
-
The reviewer (`Sadotu/agent-skills`, `review-pr`) records each pass in one
|
|
189
|
-
immutable PR comment carrying a single-line marker:
|
|
190
|
-
|
|
191
|
-
```
|
|
192
|
-
<!-- review-pr:v1 {"fingerprint":"<64-hex>","head":"<40-hex>","base":"<40-hex>","issueUpdatedAt":"<ISO>","prUpdatedAt":"<ISO>","issue":41,"pr":72,"pass":1,"verdict":"PASS"} -->
|
|
193
|
-
```
|
|
121
|
+
Legacy `review-<n>` and `repair-<n>` windows are allowed to finish without being
|
|
122
|
+
restarted or killed; both keep the supervisor active, and repairs consume an implementation slot.
|
|
194
123
|
|
|
195
|
-
|
|
196
|
-
it. Verdicts are exactly `PASS` and `BLOCKING`.
|
|
197
|
-
|
|
198
|
-
A marker is applied only when all three hold:
|
|
199
|
-
|
|
200
|
-
- **Authorship** — the comment has `viewerDidAuthor: true`, so the same GitHub
|
|
201
|
-
App identity the supervisor authenticates as wrote it. Every fingerprint input
|
|
202
|
-
is public, so without this check any commenter could forge a `PASS`.
|
|
203
|
-
- **Identity** — its `issue` and `pr` are the managed pair being gated.
|
|
204
|
-
- **Freshness** — its `fingerprint` equals the live fingerprint,
|
|
205
|
-
`sha256(head 0x1e base 0x1e issue-body 0x1e pr-body)`. Bodies decide
|
|
206
|
-
freshness, not `updatedAt`: a new comment or a label edit moves the timestamps
|
|
207
|
-
while reviewing the same content, whereas any real change to the head, the
|
|
208
|
-
base, the issue text, or the PR text invalidates the pass. `issueUpdatedAt`
|
|
209
|
-
and `prUpdatedAt` are recorded for audit only.
|
|
210
|
-
|
|
211
|
-
Comparing against live state rather than a snapshot captured at launch needs no
|
|
212
|
-
persistence, so the same content is handled once even across a supervisor
|
|
213
|
-
restart or a crash between the comment and the label transition. A marker that
|
|
214
|
-
fails any check is discarded silently: nothing is posted, nothing transitions,
|
|
215
|
-
and the PR simply queues a fresh pass. Parsing fails closed on an unsupported
|
|
216
|
-
tag, malformed JSON, or a missing or ill-typed field. A trusted, applicable
|
|
217
|
-
marker carrying an unsupported *verdict* is the one case that is not discarded —
|
|
218
|
-
it blocks, so a contract break surfaces instead of looping.
|
|
219
|
-
|
|
220
|
-
### Transitions
|
|
221
|
-
|
|
222
|
-
**`BLOCKING`** — the PR is kept or returned to draft and `agent-review` becomes
|
|
223
|
-
`agent-blocked` on issue and PR. The reviewer's findings stand alone in its own
|
|
224
|
-
comment; the supervisor adds nothing and stops. There is no automatic repair.
|
|
225
|
-
|
|
226
|
-
**`PASS`** — the PR is marked ready for review first, so repositories whose CI
|
|
227
|
-
triggers on `ready_for_review` start their checks. Checks are read on the
|
|
228
|
-
following poll, never in the same one that opened the PR:
|
|
229
|
-
|
|
230
|
-
| Checks | Outcome |
|
|
231
|
-
|---|---|
|
|
232
|
-
| None configured | Green → `user-merge-review` |
|
|
233
|
-
| All complete and successful, neutral, or skipped | Green → `user-merge-review` |
|
|
234
|
-
| Any still running | Keep `agent-review`; reviewer slot stays free |
|
|
235
|
-
| Any failed | Return to draft, `agent-blocked`, post the failing check names and URLs |
|
|
236
|
-
| Data unreadable | Transition nothing; retry next poll |
|
|
237
|
-
|
|
238
|
-
Every check GitHub reports is treated as required. "No checks configured" and
|
|
239
|
-
"could not read the checks" are deliberately distinct: only the former counts as
|
|
240
|
-
green.
|
|
241
|
-
|
|
242
|
-
### GitHub authentication
|
|
124
|
+
## GitHub authentication
|
|
243
125
|
|
|
244
126
|
`gh` does not auto-consume the GitHub App credential, so the supervisor mints a
|
|
245
|
-
short-lived
|
|
127
|
+
short-lived installation token (via `gh-app-token.sh`, overridable with
|
|
246
128
|
`GH_APP_TOKEN_SCRIPT`) and injects it as `GH_TOKEN` for every `gh` call,
|
|
247
|
-
re-minting each poll. At startup
|
|
248
|
-
|
|
249
|
-
|
|
129
|
+
re-minting each poll. At startup an authenticated `gh repo view` smoke check
|
|
130
|
+
exits with a clear message if the App is not authenticated, rather than churning
|
|
131
|
+
on unauthenticated calls every poll.
|
|
250
132
|
|
|
251
133
|
### Configuration
|
|
252
134
|
|
|
253
|
-
The local concurrency guard is fixed at two implementation slots
|
|
254
|
-
|
|
135
|
+
The local concurrency guard is fixed at two implementation slots. Two knobs
|
|
136
|
+
are environment-tunable:
|
|
255
137
|
|
|
256
138
|
| Var | Default | Meaning |
|
|
257
139
|
|-----|---------|---------|
|
|
258
140
|
| `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 |
|
|
259
141
|
| `POLL_MS` | `60000` | Poll interval |
|
|
260
142
|
|
|
261
|
-
Labels are created at startup — see the managed review gate
|
|
143
|
+
Labels are created at startup — see the managed review gate above.
|
|
262
144
|
|
|
263
145
|
## Repo contents
|
|
264
146
|
|
|
265
147
|
| Path | Purpose |
|
|
266
148
|
|------|---------|
|
|
267
149
|
| `agents.toml` / `agents.lock` | [dotagents](https://github.com/Sadotu/agent-skills) manifest — declares which skills are installed and pins their source commits |
|
|
268
|
-
| `.agents/skills/` | Installed skills (`github-issue`, `setup`) — managed artifacts, restored from the manifest, not committed |
|
|
150
|
+
| `.agents/skills/` | Installed skills (`address-review`, `github-issue`, `github-pr-cleanup`, `review-pr`, `setup`) — managed artifacts, restored from the manifest, not committed |
|
|
269
151
|
| `.claude/skills` | Symlink to `.agents/skills` so Claude Code picks the skills up |
|
|
270
152
|
| `CLAUDE.md` | Agent instructions and gotchas for working in this repo |
|
|
271
153
|
|
|
272
154
|
## Skills
|
|
273
155
|
|
|
274
|
-
- **`github-issue`** — runs
|
|
275
|
-
|
|
276
|
-
|
|
156
|
+
- **`github-issue`** — runs an issue end to end, opens a draft PR, implements
|
|
157
|
+
in an isolated worktree, verifies the result, and marks the PR ready for the
|
|
158
|
+
owner.
|
|
277
159
|
- **`setup`** — connects the repo to the `container-coding-agent` GitHub App
|
|
278
160
|
and verifies `git`/`gh` authenticate as the App.
|
|
161
|
+
- **`github-pr-cleanup`** — cleans the worktree, branch and session artifacts of
|
|
162
|
+
one merged or closed pull request. Worktree Warden is the automatic caller: it
|
|
163
|
+
watches for terminal pull requests and runs the skill's cleanup script, so
|
|
164
|
+
cleanup happens outside this repository's supervisor, which ends at "PR ready
|
|
165
|
+
for the owner". The skill is installed here so that script exists in a
|
|
166
|
+
checkout, and so it can also be run by hand for a single pull request.
|
|
279
167
|
|
|
280
|
-
|
|
168
|
+
The standalone `review-pr` and `address-review` skills remain installed for
|
|
169
|
+
manual use, but the production supervisor route does not invoke them.
|
|
281
170
|
|
|
282
171
|
## Releasing
|
|
283
172
|
|
|
284
|
-
`package.json` holds the version, and merging the bump is the whole release.
|
|
285
|
-
|
|
286
|
-
`.github/workflows/publish.yml` sees a version the registry does not carry,
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
173
|
+
`package.json` holds the version, and merging the bump is the whole release. Bump
|
|
174
|
+
it in a normal pull request; when that lands on `main`,
|
|
175
|
+
`.github/workflows/publish.yml` sees a version the registry does not carry, runs
|
|
176
|
+
the tests, publishes, and pushes the matching `v<version>` tag. A merge without a
|
|
177
|
+
bump finds its version already published and exits without releasing. Pushing a
|
|
178
|
+
`v*` tag by hand takes the same path, with one extra guard: the tag must equal
|
|
179
|
+
`v<version>` or the run fails. A published version cannot be republished; bump
|
|
180
|
+
and merge again.
|
|
290
181
|
|
|
291
182
|
Publishing goes to npmjs via
|
|
292
|
-
[trusted publishing](https://docs.npmjs.com/trusted-publishers), so no npm
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
`0.1.0` was published by hand (`npm login && npm publish`) to bootstrap it.
|
|
304
|
-
Every later release goes through the workflow.
|
|
305
|
-
|
|
306
|
-
Package visibility is set on the package, not inherited from this private
|
|
307
|
-
repository, and the setting appears only once a version exists. So after the
|
|
308
|
-
**first** release, set it once under **Sadotu → Packages →
|
|
309
|
-
issue-orchestrator → Package settings**.
|
|
183
|
+
[trusted publishing](https://docs.npmjs.com/trusted-publishers), so no npm token
|
|
184
|
+
is stored anywhere — npm exchanges the workflow's OIDC identity for a short-lived
|
|
185
|
+
credential. Tagging is a separate job, so the job holding an npm credential
|
|
186
|
+
cannot write to this repository and the job that can push a tag holds no npm
|
|
187
|
+
credential.
|
|
188
|
+
|
|
189
|
+
Two one-time bootstraps, both because they need a package that already exists:
|
|
190
|
+
`0.1.0` was published by hand (`npm login && npm publish`) to enable trusted
|
|
191
|
+
publishing, and package visibility — set on the package, not inherited from this
|
|
192
|
+
private repository — must be set once after the first release under **Sadotu →
|
|
193
|
+
Packages → issue-orchestrator → Package settings**.
|
|
310
194
|
|
|
311
195
|
## Development environment
|
|
312
196
|
|