@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 CHANGED
@@ -1,204 +1,207 @@
1
1
  # issue-orchestrator
2
2
 
3
- A small, supervised issue queue. A single Node supervisor picks up open issues
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 each in its own
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-opus-5 "/github-issue <number>"
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 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.
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 are enforced by
19
+ Usage limits belong to
18
20
  [Usage Sentinel](https://github.com/Sadotu/usage-sentinel), which pauses this
19
- orchestrator's entire Docker container supervisor and workers together rather
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 it automatically.
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 abstract
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/current-repository usage
31
+ ### Installed usage
31
32
 
32
- The package is published publicly on
33
- [npmjs](https://www.npmjs.com/package/@nickysagan/issue-orchestrator), so no
34
- registry configuration or token is needed:
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
- The shared [`agent-devcontainer`](https://github.com/Sadotu/agent-devcontainer)
50
- also exposes that command through the literal alias `start work`.
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 `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.
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
- Only one supervisor per repository per Linux network namespace may run at a
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.
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 the supervisor or an
67
- LLM. Work begins only when a user runs `issue-orchestrator` or `start work`.
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 # node --test over test/
73
- node bin/supervisor.mjs # start the supervisor loop
74
- tmux attach -t orchestrator # watch the workers directly
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 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.
84
- - **Claim** the lowest-numbered `agent-ready` issue has its label swapped
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.
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 timeand 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
- window and no legacy window (below) is live.
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` 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.
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 finish by marking the PR ready.
118
- Human review and merge begin there. The supervisor exposes no approve or merge
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 are allowed to finish without being
122
- restarted or killed; both keep the supervisor active, and repairs consume an implementation slot.
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 (via `gh-app-token.sh`, overridable with
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. 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.
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
- The local concurrency guard is fixed at two implementation slots. Two knobs
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` | 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 |
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 — see the managed review gate above.
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 — declares which skills are installed and pins their source commits |
150
- | `.agents/skills/` | Installed skills (`address-review`, `github-issue`, `github-pr-cleanup`, `review-pr`, `setup`) — managed artifacts, restored from the manifest, not committed |
151
- | `.claude/skills` | Symlink to `.agents/skills` so Claude Code picks the skills up |
152
- | `CLAUDE.md` | Agent instructions and gotchas for working in this repo |
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, opens a draft PR, implements
157
- in an isolated worktree, verifies the result, and marks the PR ready for the
158
- owner.
159
- - **`setup`** connects the repo to the `container-coding-agent` GitHub App
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 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.
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
- The standalone `review-pr` and `address-review` skills remain installed for
169
- manual use, but the production supervisor route does not invoke them.
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. 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.
181
-
182
- Publishing goes to npmjs via
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**.
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 on this repo from a shared, sandboxed devcontainer. That
198
- environment is **not part of this app** — it lives in its own repo:
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
- folder pointing at that image may exist in a checkout but is gitignored.
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.
@@ -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
- const command = `ccode --print --permission-mode auto --model claude-opus-5 "/github-issue ${n}"`;
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nickysagan/issue-orchestrator",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "A small, supervised GitHub issue queue that keeps autonomous coding workers alive.",
5
5
  "license": "MIT",
6
6
  "repository": {