@nickysagan/issue-orchestrator 0.2.0 → 0.2.1
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 +130 -127
- package/bin/supervisor.mjs +11 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,204 +1,207 @@
|
|
|
1
1
|
# issue-orchestrator
|
|
2
2
|
|
|
3
|
-
A small, supervised issue queue.
|
|
3
|
+
A small, supervised issue queue. One Node supervisor picks up open issues
|
|
4
4
|
labeled `agent-ready` (ascending) and keeps at most **two** autonomous
|
|
5
|
-
subscription-authenticated `ccode` implementation workers alive
|
|
5
|
+
subscription-authenticated `ccode` implementation workers alive, each in its own
|
|
6
6
|
tmux window running:
|
|
7
7
|
|
|
8
8
|
```bash
|
|
9
|
-
ccode --print --permission-mode auto --model claude-
|
|
9
|
+
ccode --print --permission-mode auto --model claude-sonnet-5 --effort medium "/github-issue <number>"
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
The full model name pins
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
The full model name pins workers to Sonnet 5 rather than the Claude Code default
|
|
13
|
+
or a moving alias, and `--effort medium` states the reasoning effort instead of
|
|
14
|
+
inheriting one. Together they are the production default for routine coding work;
|
|
15
|
+
an exceptional issue is run manually with Opus rather than escalated automatically.
|
|
16
|
+
Print mode skips the workspace-trust prompt, and each process exits — closing its
|
|
17
|
+
tmux window — when its command finishes.
|
|
16
18
|
|
|
17
|
-
Usage limits
|
|
19
|
+
Usage limits belong to
|
|
18
20
|
[Usage Sentinel](https://github.com/Sadotu/usage-sentinel), which pauses this
|
|
19
|
-
orchestrator's
|
|
20
|
-
than this repository stopping workers itself.
|
|
21
|
+
orchestrator's whole Docker container; this repo never kills a worker for usage.
|
|
21
22
|
|
|
22
23
|
A finished implementation PR is marked ready for the repository owner. The
|
|
23
|
-
supervisor does not review, repair, approve, or merge
|
|
24
|
+
supervisor does not review, repair, approve, or merge.
|
|
24
25
|
|
|
25
26
|
## Orchestrator
|
|
26
27
|
|
|
27
|
-
Zero-dependency Node ≥20.8.0 on Linux (the supervisor guard uses a Linux
|
|
28
|
-
Unix socket). No build step.
|
|
28
|
+
Zero-dependency Node ≥20.8.0 on Linux (the supervisor guard uses a Linux
|
|
29
|
+
abstract Unix socket). No build step.
|
|
29
30
|
|
|
30
|
-
### Installed
|
|
31
|
+
### Installed usage
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
[npmjs](https://www.npmjs.com/package/@nickysagan/issue-orchestrator)
|
|
34
|
-
registry configuration or token
|
|
33
|
+
Published publicly on
|
|
34
|
+
[npmjs](https://www.npmjs.com/package/@nickysagan/issue-orchestrator) — no
|
|
35
|
+
registry configuration or token needed. The scope names the package; the
|
|
36
|
+
installed command is `issue-orchestrator`.
|
|
35
37
|
|
|
36
38
|
```bash
|
|
37
39
|
npm install -g @nickysagan/issue-orchestrator@latest
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
The scope names the package; the installed command is `issue-orchestrator`.
|
|
41
|
-
|
|
42
|
-
Run it from the target project repository, not from the package checkout:
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
cd /path/to/target-project
|
|
40
|
+
cd /path/to/target-project # run from the target repo, not the package checkout
|
|
46
41
|
issue-orchestrator
|
|
47
42
|
```
|
|
48
43
|
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
[`agent-devcontainer`](https://github.com/Sadotu/agent-devcontainer) also exposes
|
|
45
|
+
that command as the alias `start work`.
|
|
51
46
|
|
|
52
|
-
The supervisor resolves the
|
|
53
|
-
authentication, registers its
|
|
54
|
-
|
|
55
|
-
cannot resolve its container ID or
|
|
56
|
-
run unenforced.
|
|
47
|
+
The supervisor resolves the working directory's `origin`, checks GitHub App
|
|
48
|
+
authentication, registers its container with Sentinel, and starts the workers
|
|
49
|
+
itself — do not start Claude separately. It exits nonzero rather than run
|
|
50
|
+
unenforced if it cannot resolve its container ID or lease.
|
|
57
51
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
network namespaces and therefore independent guards; containers sharing a
|
|
64
|
-
namespace share this guard.
|
|
52
|
+
One supervisor per repository per Linux network namespace: a second invocation
|
|
53
|
+
for the same `owner/repo` exits nonzero with `already running` before auth, the
|
|
54
|
+
lease, tmux, or workers. Ownership is a kernel-owned abstract Unix socket,
|
|
55
|
+
released on exit or process death. Devcontainers normally have distinct
|
|
56
|
+
namespaces, so their guards are independent.
|
|
65
57
|
|
|
66
|
-
Neither the image build nor devcontainer startup launches
|
|
67
|
-
LLM
|
|
58
|
+
Neither the image build nor devcontainer startup launches a supervisor or an
|
|
59
|
+
LLM; work begins only at `issue-orchestrator` / `start work`.
|
|
68
60
|
|
|
69
61
|
### Repository-checkout usage
|
|
70
62
|
|
|
71
63
|
```bash
|
|
72
|
-
npm test
|
|
73
|
-
node bin/supervisor.mjs
|
|
74
|
-
tmux attach -t orchestrator # watch the workers
|
|
64
|
+
npm test # node --test over test/
|
|
65
|
+
node bin/supervisor.mjs # start the supervisor loop
|
|
66
|
+
tmux attach -t orchestrator # watch the workers
|
|
75
67
|
```
|
|
76
68
|
|
|
77
69
|
### How it works
|
|
78
70
|
|
|
79
|
-
- **Admission** — before
|
|
80
|
-
with
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
the
|
|
89
|
-
- **
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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.
|
|
71
|
+
- **Admission** — before any claim, the primary worktree is checked read-only
|
|
72
|
+
with `git status --porcelain`. Pending changes, or an unreadable check, means
|
|
73
|
+
no claim and no worker that poll; nothing is stashed, reset, cleaned, or
|
|
74
|
+
committed. Live workers keep running.
|
|
75
|
+
- **Claim** — lowest-numbered `agent-ready` issue gets its label swapped to
|
|
76
|
+
`agent-running`, then a tmux window `issue-<n>` opens.
|
|
77
|
+
- **Complete** — Phase 6 marks the PR ready. The next poll closes the worker
|
|
78
|
+
window, removes `agent-running`, and frees the slot.
|
|
79
|
+
- **Vanished** — a worker lost while its PR is still draft or absent releases
|
|
80
|
+
the claim and preserves the draft/worktree. No automatic restart.
|
|
81
|
+
- **Worker logs** — `<git-common-dir>/issue-orchestrator/logs/issue-<n>.<attempt>.log`,
|
|
82
|
+
latest three attempts kept, secrets scrubbed from surfaced tails.
|
|
83
|
+
- **Worker usage** — implementation workers run under Claude Code's streaming
|
|
84
|
+
JSON output, so each attempt log ends with the run's own usage record.
|
|
85
|
+
- **Pause notices** — the heartbeat carries Sentinel's enforcement state. A
|
|
86
|
+
pending pause prints one flushed line — triggering window, both observed
|
|
87
|
+
percentages, reset time — and is then acknowledged so Sentinel may pause; the
|
|
88
|
+
first heartbeat back to `running` prints one resume line. Ordinary refreshes
|
|
89
|
+
stay quiet, and a malformed payload is reported rather than guessed at.
|
|
103
90
|
- **Stop** — the supervisor exits when nothing is queued and no implementation
|
|
104
|
-
|
|
91
|
+
or legacy window is live.
|
|
105
92
|
|
|
106
93
|
Live container pause/unpause check:
|
|
107
94
|
[docs/smoke-checks/README.md](docs/smoke-checks/README.md).
|
|
108
95
|
|
|
96
|
+
### Inspecting one worker attempt's usage
|
|
97
|
+
|
|
98
|
+
An implementation worker streams its session into its attempt log as JSON
|
|
99
|
+
events. The run's own `result` event is the usage record:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
log="$(git rev-parse --git-common-dir)/issue-orchestrator/logs/issue-<n>.<attempt>.log"
|
|
103
|
+
grep -h '"type":"result"' "$log" |
|
|
104
|
+
jq '{session_id, num_turns, duration_ms, total_cost_usd, usage, modelUsage}'
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The issue and attempt numbers come from the path, the model from `modelUsage`
|
|
108
|
+
(and the log's opening `init` event), and completion from the log's final
|
|
109
|
+
`[orchestrator <timestamp>] Worker finished for #<n>` line. An interrupted
|
|
110
|
+
worker has no `result` event — its partial events are still in the file, and
|
|
111
|
+
`jq -cR 'fromjson? | select(.type)' "$log"` shows how far it got (the `-R
|
|
112
|
+
fromjson?` form skips the appended orchestrator stamp line, which is not JSON).
|
|
113
|
+
|
|
114
|
+
Reviewer and repair workers keep the plain-text output; only implementation
|
|
115
|
+
attempts carry a usage record.
|
|
116
|
+
|
|
109
117
|
## Implementation-only handoff
|
|
110
118
|
|
|
111
|
-
`agent-running`
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
reviewer or repair worker and does not transition issues through them.
|
|
119
|
+
`agent-running` is the ownership marker: applied at claim, removed only after the
|
|
120
|
+
PR is ready or a vanished/failed launch is released. Review labels in other repos
|
|
121
|
+
are left untouched, but no reviewer or repair worker is created and issues are
|
|
122
|
+
not transitioned through them.
|
|
116
123
|
|
|
117
|
-
Manual and managed `/github-issue` runs both
|
|
118
|
-
|
|
119
|
-
operation.
|
|
124
|
+
Manual and managed `/github-issue` runs both end at "PR ready". Human review and
|
|
125
|
+
merge start there; the supervisor exposes no approve or merge.
|
|
120
126
|
|
|
121
|
-
Legacy `review-<n>` and `repair-<n>` windows
|
|
122
|
-
|
|
127
|
+
Legacy `review-<n>` and `repair-<n>` windows finish uninterrupted — they keep the
|
|
128
|
+
supervisor active, and repairs consume an implementation slot.
|
|
123
129
|
|
|
124
130
|
## GitHub authentication
|
|
125
131
|
|
|
126
132
|
`gh` does not auto-consume the GitHub App credential, so the supervisor mints a
|
|
127
|
-
short-lived installation token (
|
|
133
|
+
short-lived installation token (`gh-app-token.sh`, overridable with
|
|
128
134
|
`GH_APP_TOKEN_SCRIPT`) and injects it as `GH_TOKEN` for every `gh` call,
|
|
129
|
-
re-minting each poll.
|
|
130
|
-
|
|
131
|
-
on unauthenticated calls every poll.
|
|
135
|
+
re-minting each poll. A startup `gh repo view` smoke check exits with a clear
|
|
136
|
+
message when the App is not authenticated, instead of churning every poll.
|
|
132
137
|
|
|
133
138
|
### Configuration
|
|
134
139
|
|
|
135
|
-
|
|
136
|
-
are environment-tunable:
|
|
140
|
+
Concurrency is fixed at two implementation slots. Two environment knobs:
|
|
137
141
|
|
|
138
142
|
| Var | Default | Meaning |
|
|
139
143
|
|-----|---------|---------|
|
|
140
|
-
| `SENTINEL_URL` | `http://usage-sentinel:4317` |
|
|
144
|
+
| `SENTINEL_URL` | `http://usage-sentinel:4317` | Sentinel base URL for the managed-container lease, on the shared container network; set explicitly (e.g. `http://host.docker.internal:4317`) only when Sentinel is exposed on the host |
|
|
141
145
|
| `POLL_MS` | `60000` | Poll interval |
|
|
142
146
|
|
|
143
|
-
Labels are created at startup
|
|
147
|
+
Labels are created at startup.
|
|
144
148
|
|
|
145
149
|
## Repo contents
|
|
146
150
|
|
|
147
151
|
| Path | Purpose |
|
|
148
152
|
|------|---------|
|
|
149
|
-
| `agents.toml` / `agents.lock` | [dotagents](https://github.com/Sadotu/agent-skills) manifest —
|
|
150
|
-
| `.agents/skills/` | Installed skills
|
|
151
|
-
| `.claude/skills` | Symlink to `.agents/skills` so Claude Code
|
|
152
|
-
| `CLAUDE.md` | Agent instructions and gotchas
|
|
153
|
+
| `agents.toml` / `agents.lock` | [dotagents](https://github.com/Sadotu/agent-skills) manifest — which skills are installed, pinned to source commits |
|
|
154
|
+
| `.agents/skills/` | Installed skills — managed artifacts, restored from the manifest, not committed |
|
|
155
|
+
| `.claude/skills` | Symlink to `.agents/skills` so Claude Code sees them |
|
|
156
|
+
| `CLAUDE.md` | Agent instructions and gotchas |
|
|
153
157
|
|
|
154
158
|
## Skills
|
|
155
159
|
|
|
156
|
-
- **`github-issue`** — runs an issue end to end
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
and verifies `git`/`gh` authenticate as the App.
|
|
160
|
+
- **`github-issue`** — runs an issue end to end: draft PR, implementation in an
|
|
161
|
+
isolated worktree, verification, PR marked ready.
|
|
162
|
+
- **`setup`** — connects the repo to the `container-coding-agent` GitHub App and
|
|
163
|
+
verifies `git`/`gh` authenticate as the App.
|
|
161
164
|
- **`github-pr-cleanup`** — cleans the worktree, branch and session artifacts of
|
|
162
|
-
one merged or closed pull request. Worktree Warden
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
checkout, and so it can also be run by hand for a single pull request.
|
|
165
|
+
one merged or closed pull request. Worktree Warden calls it automatically when
|
|
166
|
+
a pull request goes terminal, so cleanup happens outside this supervisor, which
|
|
167
|
+
ends at "PR ready". Installed here so the script exists in a checkout and can
|
|
168
|
+
be run by hand for a single pull request.
|
|
167
169
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
+
`review-pr` and `address-review` stay installed for manual use; the production
|
|
171
|
+
supervisor route does not invoke them.
|
|
172
|
+
|
|
173
|
+
Skills are pinned by `agents.lock` and do **not** update on their own — the
|
|
174
|
+
devcontainer installs `--frozen` whenever a lock exists. To take a new upstream
|
|
175
|
+
version, run `/opt/agent-devcontainer/dotagents-install.sh --upgrade "$PWD" /opt/agent-devcontainer`
|
|
176
|
+
and commit the rewritten lock.
|
|
170
177
|
|
|
171
178
|
## Releasing
|
|
172
179
|
|
|
173
|
-
`package.json` holds the version, and merging the bump is the whole release.
|
|
174
|
-
it in a normal
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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**.
|
|
180
|
+
`package.json` holds the version, and merging the bump is the whole release.
|
|
181
|
+
Bump it in a normal PR; on `main`, `.github/workflows/publish.yml` sees a version
|
|
182
|
+
the registry lacks, runs the tests, publishes, and pushes the `v<version>` tag. A
|
|
183
|
+
merge without a bump exits without releasing. A hand-pushed `v*` tag takes the
|
|
184
|
+
same path with one extra guard: it must equal `v<version>`. Published versions
|
|
185
|
+
cannot be republished — bump and merge again.
|
|
186
|
+
|
|
187
|
+
Publishing uses npmjs [trusted publishing](https://docs.npmjs.com/trusted-publishers),
|
|
188
|
+
so no npm token is stored anywhere. Tagging is a separate job, so the job holding
|
|
189
|
+
an npm credential cannot write to this repo and the job that can push a tag holds
|
|
190
|
+
no npm credential.
|
|
191
|
+
|
|
192
|
+
Two one-time bootstraps, both needing a package that already exists: `0.1.0` was
|
|
193
|
+
published by hand (`npm login && npm publish`) to enable trusted publishing, and
|
|
194
|
+
package visibility — set on the package, not inherited from this private repo —
|
|
195
|
+
must be set once under **Sadotu → Packages → issue-orchestrator → Package
|
|
196
|
+
settings**.
|
|
194
197
|
|
|
195
198
|
## Development environment
|
|
196
199
|
|
|
197
|
-
Agents work
|
|
198
|
-
|
|
200
|
+
Agents work from a shared, sandboxed devcontainer that is **not part of this
|
|
201
|
+
app** — it lives in
|
|
199
202
|
[`Sadotu/agent-devcontainer`](https://github.com/Sadotu/agent-devcontainer)
|
|
200
203
|
(image, setup scripts, security model, auth docs). A local `.devcontainer/`
|
|
201
|
-
|
|
204
|
+
pointing at that image may exist in a checkout but is gitignored.
|
|
202
205
|
|
|
203
206
|
GitHub access is via the scoped `container-coding-agent` GitHub App — never a
|
|
204
207
|
personal token, never `gh auth login`. See `CLAUDE.md` for the auth wiring.
|
package/bin/supervisor.mjs
CHANGED
|
@@ -339,7 +339,17 @@ export function createTmux({ exec, session = SESSION, logs = NO_WORKER_LOGS }) {
|
|
|
339
339
|
// window command via a non-interactive shell that never sources .bashrc,
|
|
340
340
|
// so the alias would silently fail to resolve without `bash -ic`, which
|
|
341
341
|
// forces alias expansion regardless of login/interactive invocation.
|
|
342
|
-
|
|
342
|
+
//
|
|
343
|
+
// `stream-json` — not `json` — carries the run's session id and usage into
|
|
344
|
+
// the attempt log. `json` buffers the whole session and writes one object
|
|
345
|
+
// at exit, so a worker killed or lost mid-run would leave an *empty* log
|
|
346
|
+
// and destroy the diagnostic tail a vanished worker is reported with;
|
|
347
|
+
// streamed events land as they happen, exactly as text output does today,
|
|
348
|
+
// and the terminal `result` event still carries the full usage record.
|
|
349
|
+
// `--verbose` is mandatory: the CLI refuses the combination without it.
|
|
350
|
+
// Nothing here reads the result — measurement is passive, the log is the
|
|
351
|
+
// record.
|
|
352
|
+
const command = `ccode --print --permission-mode auto --model claude-sonnet-5 --effort medium --output-format stream-json --verbose "/github-issue ${n}"`;
|
|
343
353
|
// No PR to name: the worker opens one mid-run, long after this launch.
|
|
344
354
|
return newWindow("issue", n, `issue-${n}`, command, `Worker finished for #${n}`);
|
|
345
355
|
}
|