@armstrongnate/april 0.0.2 → 0.0.4
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 +167 -17
- package/config.example.yaml +1 -4
- package/dist/cli.js +6 -0
- package/dist/commands/upgrade.js +55 -0
- package/dist/config.js +2 -4
- package/dist/processes.js +1 -6
- package/dist/spawner.js +2 -6
- package/package.json +1 -1
- package/skills/issue-worker/SKILL.md +32 -15
package/README.md
CHANGED
|
@@ -8,27 +8,105 @@ april watches for GitHub issues assigned to you with a specific label, then spin
|
|
|
8
8
|
|
|
9
9
|
- [Node.js](https://nodejs.org/) >= 22
|
|
10
10
|
- [gh](https://cli.github.com/) (authenticated)
|
|
11
|
-
- The `gh-webhook` extension: `gh extension install cli/gh-webhook`
|
|
11
|
+
- The [`gh-webhook` extension](https://github.com/cli/gh-webhook): `gh extension install cli/gh-webhook`
|
|
12
12
|
- [tmux](https://github.com/tmux/tmux)
|
|
13
13
|
- [Claude Code](https://claude.ai/claude-code) CLI
|
|
14
14
|
|
|
15
|
-
##
|
|
15
|
+
## Quick install
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
npm i -g @armstrongnate/april
|
|
19
|
-
#
|
|
19
|
+
april init # writes ~/.config/april/config.yaml + skill + env file; checks prereqs
|
|
20
|
+
$EDITOR ~/.config/april/config.yaml
|
|
21
|
+
april install # registers the user service and starts it
|
|
22
|
+
april logs -f
|
|
20
23
|
```
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
`april init` and the daemon both verify that the `cli/gh-webhook` extension is installed and refuse to proceed without it.
|
|
26
|
+
|
|
27
|
+
## Server install (full playbook)
|
|
28
|
+
|
|
29
|
+
The minimal flow above leaves out auth setup and a few server-specific gotchas. This is the end-to-end recipe for a fresh Linux server.
|
|
30
|
+
|
|
31
|
+
### 1. System prerequisites
|
|
23
32
|
|
|
24
33
|
```bash
|
|
25
|
-
|
|
34
|
+
# Install node 22+, tmux, gh, claude code via your usual route, then:
|
|
35
|
+
gh extension install cli/gh-webhook
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 2. Create a Personal Access Token
|
|
39
|
+
|
|
40
|
+
The systemd user service runs in a stripped-down environment — no shell config, no keyring/DBus, no credential helpers. Whatever clever auth you have set up in your interactive shell almost certainly will not be available to the daemon. **You need an explicit token in the env file.**
|
|
41
|
+
|
|
42
|
+
Generate one on your GitHub host's web UI: Settings → Developer settings → Personal access tokens.
|
|
43
|
+
|
|
44
|
+
**Classic PAT scopes:**
|
|
45
|
+
- `repo` — issue read/write, label updates, PR creation, code access (no smaller scope works for private repo issues)
|
|
46
|
+
- `admin:repo_hook` — needed for `gh webhook forward` to register and clean up its temporary `cli` webhook
|
|
47
|
+
- `workflow` — only if Claude might modify `.github/workflows/*` files
|
|
48
|
+
|
|
49
|
+
**Fine-grained PAT** (GHES 3.10+):
|
|
50
|
+
- Repository access: select the repos
|
|
51
|
+
- Permissions: Issues R/W, Pull requests R/W, Contents R/W, Webhooks R/W, Metadata R, Workflows R/W (last one optional)
|
|
52
|
+
|
|
53
|
+
The daemon's own needs are smaller (Issues R/W + Webhooks R/W + Metadata R), but Claude inherits the same env when it runs inside tmux, so the token has to cover both — see [Token inheritance](#token-inheritance) below.
|
|
54
|
+
|
|
55
|
+
### 3. Install the package
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm i -g @armstrongnate/april
|
|
59
|
+
april init
|
|
26
60
|
$EDITOR ~/.config/april/config.yaml
|
|
27
|
-
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 4. Configure auth in the env file
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
$EDITOR ~/.config/april/env
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
For a **GitHub Enterprise Server** host:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
GH_HOST=your.ghes.host
|
|
73
|
+
GH_ENTERPRISE_TOKEN=ghp_...
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
For **github.com**:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
GH_TOKEN=ghp_...
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`gh` is host-aware about which env var it reads: `GH_TOKEN` is github.com-only; `GH_ENTERPRISE_TOKEN` covers any other host (used together with `GH_HOST`). Setting `GH_TOKEN` while `GH_HOST` points elsewhere will silently fail.
|
|
83
|
+
|
|
84
|
+
### 5. Install and start the service
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
april install
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
This writes `~/.config/systemd/user/april.service`, enables it, and starts it. The unit references the env file via `EnvironmentFile=-~/.config/april/env`, so anything you put there flows through on each restart.
|
|
91
|
+
|
|
92
|
+
### 6. Enable linger (Linux servers)
|
|
93
|
+
|
|
94
|
+
systemd user services stop when you log out unless linger is enabled. On any server you SSH out of:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
sudo loginctl enable-linger $USER
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`april install` warns you if it sees this is off.
|
|
101
|
+
|
|
102
|
+
### 7. Verify
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
april status
|
|
28
106
|
april logs -f
|
|
29
107
|
```
|
|
30
108
|
|
|
31
|
-
|
|
109
|
+
Healthy logs include `Starting webhook forwarder for <repo>` and no immediate errors. Then label an issue with `agent:todo` and watch it kick off.
|
|
32
110
|
|
|
33
111
|
## Commands
|
|
34
112
|
|
|
@@ -48,34 +126,106 @@ The daemon reads extra env vars from `~/.config/april/env`. One `KEY=VALUE` per
|
|
|
48
126
|
|
|
49
127
|
```sh
|
|
50
128
|
# ~/.config/april/env
|
|
129
|
+
GH_HOST=your.ghes.host
|
|
130
|
+
GH_ENTERPRISE_TOKEN=ghp_...
|
|
51
131
|
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/...
|
|
52
132
|
APRIL_DEBUG=1
|
|
53
|
-
GREETING="hello world"
|
|
54
133
|
```
|
|
55
134
|
|
|
56
|
-
This file is seeded by `april install` (and `april init`) and is **never overwritten by reinstalls** — safe to put long-lived secrets here.
|
|
135
|
+
This file is seeded by `april install` (and `april init`) and is **never overwritten by reinstalls** — safe to put long-lived secrets here.
|
|
136
|
+
|
|
137
|
+
After editing:
|
|
138
|
+
- **Linux:** `april restart`. The systemd unit re-reads the env file on each start.
|
|
139
|
+
- **macOS:** `april install && april restart`. launchd has no `EnvironmentFile=` equivalent, so values are inlined into the plist at install time; you have to regenerate it after editing.
|
|
140
|
+
|
|
141
|
+
### Token inheritance
|
|
57
142
|
|
|
58
|
-
-
|
|
59
|
-
- On macOS, launchd has no equivalent directive, so values are read at install time and inlined into the plist. After editing the env file, run `april install` to regenerate the plist, then `april restart`.
|
|
143
|
+
`spawner.ts` runs Claude inside tmux with `tmux new-session -d <claudeCommand>` — no login shell. So Claude inherits the daemon's env directly, including any `GH_TOKEN` / `GH_ENTERPRISE_TOKEN` you set in the env file. Practical implication: the PAT you provide has to cover what *both* april and Claude need, not just april. If you want Claude to fall back to your shell's auth (a credential helper, network-level auth, etc.), you'd need to wrap the tmux command in a login shell — not currently supported.
|
|
60
144
|
|
|
61
145
|
## Service backend
|
|
62
146
|
|
|
63
147
|
- **Linux** uses systemd user services at `~/.config/systemd/user/april.service`. Logs go to the journal (`journalctl --user -u april`).
|
|
64
148
|
- **macOS** uses launchd LaunchAgents at `~/Library/LaunchAgents/dev.april.daemon.plist`. Logs go to `~/Library/Logs/april/april.log`.
|
|
65
149
|
|
|
66
|
-
###
|
|
150
|
+
### Node version managers
|
|
151
|
+
|
|
152
|
+
`april install` captures the absolute path of the `node` binary it was invoked with (e.g. `~/.nvm/versions/node/v22.x.x/bin/node`) and bakes it into the unit/plist. If you later remove or change that node version, the service will fail to start — re-run `april install` after switching.
|
|
67
153
|
|
|
68
|
-
|
|
154
|
+
## GitHub Enterprise Server caveat
|
|
155
|
+
|
|
156
|
+
`gh webhook forward` relies on GitHub's hosted webhook-relay infrastructure. **That relay is github.com-only — it is not part of GHES.** If your repos live on a GHES instance, you will see this in the logs after the daemon authenticates fine:
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
Error: you do not have access to this feature
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The webhook extension itself works against GHES (it can authenticate, list extensions, etc.), but the actual `forward` subcommand has no relay to talk to.
|
|
163
|
+
|
|
164
|
+
Workarounds (none currently shipped — file an issue if you need them wired up):
|
|
165
|
+
|
|
166
|
+
1. **Direct webhook delivery.** Expose april's port publicly (reverse proxy + TLS, or a tunnel like cloudflared / tailscale funnel / ngrok), and configure a webhook on the GHES repo pointing at `https://your-server/webhook/github`. Skip `gh webhook forward` entirely.
|
|
167
|
+
2. **Polling.** Replace the forwarder with a periodic poll of open issues. april already has reconciliation-on-startup; turning it into a timer is small.
|
|
168
|
+
3. **Third-party relay** like smee.io.
|
|
169
|
+
|
|
170
|
+
## Upgrading
|
|
69
171
|
|
|
70
172
|
```bash
|
|
71
|
-
|
|
173
|
+
april upgrade
|
|
72
174
|
```
|
|
73
175
|
|
|
74
|
-
`april
|
|
176
|
+
This runs the package install, regenerates the unit/plist, and restarts the service in one go. Pass a specific version (`april upgrade 0.0.5`) to pin, or `--with pnpm|yarn` if `april upgrade` picks the wrong package manager.
|
|
75
177
|
|
|
76
|
-
|
|
178
|
+
Manual equivalent if you want to do it yourself:
|
|
77
179
|
|
|
78
|
-
|
|
180
|
+
```bash
|
|
181
|
+
npm i -g @armstrongnate/april@latest
|
|
182
|
+
april install # regenerates the unit/plist with any template changes (also runs daemon-reload on Linux)
|
|
183
|
+
april restart
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**If you skip `april install` after upgrading, new template features (`EnvironmentFile=`, env-var changes, etc.) will not appear in your existing unit file** — `npm` only updates the package, not anything systemd has on disk.
|
|
187
|
+
|
|
188
|
+
## Troubleshooting
|
|
189
|
+
|
|
190
|
+
### `Required gh extension not installed: cli/gh-webhook`
|
|
191
|
+
|
|
192
|
+
Install it as the same user that runs the service:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
gh extension install cli/gh-webhook
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### `gh auth token not found for host "..."`
|
|
199
|
+
|
|
200
|
+
`gh-webhook` shells out to `gh auth token` to extract a Bearer token, and your gh setup doesn't have an extractable one (you might be relying on a credential helper, network-level auth like Cloudflare Access, or a wrapper script). Add an explicit `GH_TOKEN` / `GH_ENTERPRISE_TOKEN` to `~/.config/april/env`, then `april restart`.
|
|
201
|
+
|
|
202
|
+
### `you do not have access to this feature` on `gh webhook forward`
|
|
203
|
+
|
|
204
|
+
Your host (likely GHES) doesn't expose the webhook-forwarding relay. See [GitHub Enterprise Server caveat](#github-enterprise-server-caveat).
|
|
205
|
+
|
|
206
|
+
### Service starts but env vars in `~/.config/april/env` aren't applied
|
|
207
|
+
|
|
208
|
+
You're running a unit that was generated before `EnvironmentFile=` support landed (anything from before v0.0.2). Confirm:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
systemctl --user cat april | grep -i environment
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
If you don't see `EnvironmentFile=`, regenerate:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
april install
|
|
218
|
+
systemctl --user daemon-reload
|
|
219
|
+
april restart
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### `Token:` is empty in `gh auth status` but `gh` commands work
|
|
223
|
+
|
|
224
|
+
Means your auth is provided by something other than a stored token (credential helper, network auth, wrapper). The webhook extension still needs an actual token — it doesn't matter how `gh` does its other API calls. Add `GH_TOKEN` / `GH_ENTERPRISE_TOKEN` to the env file.
|
|
225
|
+
|
|
226
|
+
### Service can't find `gh` / `tmux` / `claude` even though they're on your shell PATH
|
|
227
|
+
|
|
228
|
+
`april install` captures `$PATH` at install time and bakes it into the unit. If you installed any of those tools after running `april install`, re-run `april install` to recapture PATH.
|
|
79
229
|
|
|
80
230
|
## Usage
|
|
81
231
|
|
package/config.example.yaml
CHANGED
|
@@ -2,10 +2,7 @@ assignee: "your-github-username"
|
|
|
2
2
|
label: "agent:todo"
|
|
3
3
|
claudeSkill: "issue-worker"
|
|
4
4
|
# claudeModel: "opus" # optional, defaults to opus
|
|
5
|
-
#
|
|
6
|
-
# - "Edit"
|
|
7
|
-
# - "Write"
|
|
8
|
-
# - "Bash(*)"
|
|
5
|
+
# claudePermissionMode: "auto" # optional, defaults to auto (others: default, acceptEdits, plan, bypassPermissions)
|
|
9
6
|
port: 7890
|
|
10
7
|
repos:
|
|
11
8
|
- owner: "org"
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { backend } from "./service/index.js";
|
|
3
3
|
import { run as runInit } from "./commands/init.js";
|
|
4
|
+
import { run as runUpgrade } from "./commands/upgrade.js";
|
|
4
5
|
const HELP = `april — issue worker
|
|
5
6
|
|
|
6
7
|
Usage:
|
|
@@ -9,6 +10,8 @@ Usage:
|
|
|
9
10
|
Commands:
|
|
10
11
|
init Copy bundled config + skill to ~/.config/april and ~/.claude
|
|
11
12
|
install [--print] Install and start the user service. --print emits the unit/plist to stdout instead.
|
|
13
|
+
upgrade [VER] Upgrade the npm package, regenerate the unit, and restart. VER defaults to "latest".
|
|
14
|
+
Pass --with npm|pnpm|yarn to override the auto-detected package manager.
|
|
12
15
|
uninstall Stop and remove the user service
|
|
13
16
|
start Start the service
|
|
14
17
|
stop Stop the service
|
|
@@ -70,6 +73,9 @@ async function main() {
|
|
|
70
73
|
if (cmd === "init") {
|
|
71
74
|
return runInit(rest);
|
|
72
75
|
}
|
|
76
|
+
if (cmd === "upgrade") {
|
|
77
|
+
return runUpgrade(rest);
|
|
78
|
+
}
|
|
73
79
|
if (cmd === "daemon") {
|
|
74
80
|
// Run the long-running process inline. Importing index.js triggers main();
|
|
75
81
|
// we then hang forever and let its SIGINT/SIGTERM handlers terminate the process.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
const PACKAGE = "@armstrongnate/april";
|
|
4
|
+
function detectPackageManager() {
|
|
5
|
+
// Where this script lives reveals which global install dir it's in.
|
|
6
|
+
const here = fileURLToPath(import.meta.url);
|
|
7
|
+
if (/[\\/]\.?pnpm[\\/]|[\\/]Library[\\/]pnpm[\\/]/.test(here))
|
|
8
|
+
return "pnpm";
|
|
9
|
+
if (/[\\/]\.config[\\/]yarn[\\/]|[\\/]\.yarn[\\/]/.test(here))
|
|
10
|
+
return "yarn";
|
|
11
|
+
return "npm";
|
|
12
|
+
}
|
|
13
|
+
function pmInstallArgs(pm, ref) {
|
|
14
|
+
switch (pm) {
|
|
15
|
+
case "pnpm":
|
|
16
|
+
return ["add", "-g", ref];
|
|
17
|
+
case "yarn":
|
|
18
|
+
return ["global", "add", ref];
|
|
19
|
+
case "npm":
|
|
20
|
+
return ["install", "-g", ref];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function step(name, cmd, args) {
|
|
24
|
+
console.log(`\n→ ${name}`);
|
|
25
|
+
console.log(` $ ${cmd} ${args.join(" ")}`);
|
|
26
|
+
const res = spawnSync(cmd, args, { stdio: "inherit" });
|
|
27
|
+
if ((res.status ?? 1) !== 0) {
|
|
28
|
+
throw new Error(`${name} failed (exit ${res.status})`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function run(args) {
|
|
32
|
+
let pm = detectPackageManager();
|
|
33
|
+
// --with <pm> override
|
|
34
|
+
const withIdx = args.indexOf("--with");
|
|
35
|
+
if (withIdx >= 0) {
|
|
36
|
+
const v = args[withIdx + 1];
|
|
37
|
+
if (v !== "npm" && v !== "pnpm" && v !== "yarn") {
|
|
38
|
+
console.error(`--with must be one of: npm, pnpm, yarn`);
|
|
39
|
+
return 2;
|
|
40
|
+
}
|
|
41
|
+
pm = v;
|
|
42
|
+
}
|
|
43
|
+
let ref = `${PACKAGE}@latest`;
|
|
44
|
+
// Allow `april upgrade <version>` to pin
|
|
45
|
+
const positional = args.filter((a, i) => !a.startsWith("--") && args[i - 1] !== "--with");
|
|
46
|
+
if (positional[0])
|
|
47
|
+
ref = `${PACKAGE}@${positional[0]}`;
|
|
48
|
+
console.log(`april upgrade — using ${pm}, target ${ref}`);
|
|
49
|
+
step(`Installing ${ref}`, pm, pmInstallArgs(pm, ref));
|
|
50
|
+
// From here on, `april` resolves to the freshly installed binary on PATH.
|
|
51
|
+
step("Regenerating service unit (april install)", "april", ["install"]);
|
|
52
|
+
step("Restarting service (april restart)", "april", ["restart"]);
|
|
53
|
+
console.log("\n✓ Upgrade complete. Tail logs with: april logs -f");
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -63,9 +63,7 @@ export function loadConfig() {
|
|
|
63
63
|
const label = validateString(parsed, "label", "config");
|
|
64
64
|
const claudeSkill = validateString(parsed, "claudeSkill", "config");
|
|
65
65
|
const claudeModel = typeof parsed.claudeModel === "string" ? parsed.claudeModel.trim() : undefined;
|
|
66
|
-
const
|
|
67
|
-
? parsed.claudeAllowedTools.filter((t) => typeof t === "string" && t.trim().length > 0).map((t) => t.trim())
|
|
68
|
-
: undefined;
|
|
66
|
+
const claudePermissionMode = typeof parsed.claudePermissionMode === "string" ? parsed.claudePermissionMode.trim() : undefined;
|
|
69
67
|
const port = Number(parsed.port);
|
|
70
68
|
if (!Number.isInteger(port) || port < 1024 || port > 65535) {
|
|
71
69
|
throw new Error(`config: "port" must be an integer between 1024 and 65535, got: ${parsed.port}`);
|
|
@@ -96,7 +94,7 @@ export function loadConfig() {
|
|
|
96
94
|
: undefined;
|
|
97
95
|
return { owner, name, path: resolvedPath, defaultBranch, slackChannel, postWorktreeHook };
|
|
98
96
|
});
|
|
99
|
-
const config = { assignee, label, claudeSkill, claudeModel,
|
|
97
|
+
const config = { assignee, label, claudeSkill, claudeModel, claudePermissionMode, port, repos };
|
|
100
98
|
log.info(`Config loaded: assignee=${assignee}, label=${label}, repos=${repos.map((r) => `${r.owner}/${r.name}`).join(", ")}`);
|
|
101
99
|
return config;
|
|
102
100
|
}
|
package/dist/processes.js
CHANGED
|
@@ -30,7 +30,6 @@ function cleanupStaleWebhooks(repoKey) {
|
|
|
30
30
|
const INITIAL_BACKOFF_MS = 1000;
|
|
31
31
|
const MAX_BACKOFF_MS = 30_000;
|
|
32
32
|
const UPTIME_RESET_MS = 60_000;
|
|
33
|
-
const MAX_CONSECUTIVE_FAILURES = 5;
|
|
34
33
|
const forwarders = [];
|
|
35
34
|
function spawnForwarder(config, repoKey, url) {
|
|
36
35
|
const state = {
|
|
@@ -82,12 +81,8 @@ function spawnForwarder(config, repoKey, url) {
|
|
|
82
81
|
state.consecutiveFailures++;
|
|
83
82
|
state.backoffMs = Math.min(state.backoffMs * 2, MAX_BACKOFF_MS);
|
|
84
83
|
}
|
|
85
|
-
if (state.consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
|
|
86
|
-
log.error(`Forwarder for ${repoKey} failed ${MAX_CONSECUTIVE_FAILURES} consecutive times, giving up`);
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
84
|
log.warn(`Forwarder for ${repoKey} exited (code=${code}, signal=${signal}), ` +
|
|
90
|
-
`restarting in ${state.backoffMs}ms (
|
|
85
|
+
`restarting in ${state.backoffMs}ms (consecutive failures: ${state.consecutiveFailures})`);
|
|
91
86
|
setTimeout(() => start(), state.backoffMs);
|
|
92
87
|
});
|
|
93
88
|
}
|
package/dist/spawner.js
CHANGED
|
@@ -149,15 +149,11 @@ export function spawnClaude(config, repo, issue, worktreePath, sessionName) {
|
|
|
149
149
|
// Session does not exist, proceed
|
|
150
150
|
}
|
|
151
151
|
const model = config.claudeModel || "opus";
|
|
152
|
-
const
|
|
153
|
-
...(config.claudeAllowedTools ?? ["Read", "Search", "Edit", "Write", "Bash(*)"]),
|
|
154
|
-
...(repo.slackChannel ? ["mcp__plugin_slack_slack__*"] : []),
|
|
155
|
-
];
|
|
152
|
+
const permissionMode = config.claudePermissionMode || "auto";
|
|
156
153
|
const slackPart = repo.slackChannel ? ` Post the PR to Slack channel #${repo.slackChannel}.` : "";
|
|
157
154
|
const prompt = `/${config.claudeSkill} Read GitHub issue #${issue.number} on ${repo.owner}/${repo.name} using the gh CLI. Implement it and open a PR.${slackPart}`;
|
|
158
155
|
log.debug(`Prompt: ${prompt}`);
|
|
159
|
-
const
|
|
160
|
-
const claudeCommand = `claude --model ${model} ${allowedToolsArgs}`;
|
|
156
|
+
const claudeCommand = `claude --model ${model} --permission-mode ${permissionMode}`;
|
|
161
157
|
log.info(`Spawning tmux session "${sessionName}" with claude`);
|
|
162
158
|
execSync(`tmux new-session -d -s ${JSON.stringify(sessionName)} -c ${JSON.stringify(worktreePath)} ${JSON.stringify(claudeCommand)}`);
|
|
163
159
|
// Send the prompt via send-keys after Claude starts
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: issue-worker
|
|
3
|
-
description: Autonomously work a GitHub issue end-to-end — read, implement,
|
|
3
|
+
description: Autonomously work a GitHub issue end-to-end — read, implement, open a PR, and monitor CI/review feedback.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# issue-worker
|
|
7
7
|
|
|
8
|
-
You have been assigned a GitHub issue. Work it to completion autonomously. Do not stop to ask for approval or confirmation — go straight from reading the issue to opening a PR.
|
|
8
|
+
You have been assigned a GitHub issue. Work it to completion autonomously. Do not stop to ask for approval or confirmation — go straight from reading the issue to opening a PR, then monitor and respond to CI failures and review feedback.
|
|
9
9
|
|
|
10
10
|
## 1. Read the issue
|
|
11
11
|
|
|
@@ -25,16 +25,17 @@ gh issue view {issue_number} --repo {owner}/{repo} --comments
|
|
|
25
25
|
- Write or update tests as appropriate
|
|
26
26
|
- Ensure the code builds/lints/passes tests
|
|
27
27
|
|
|
28
|
-
## 4. Review
|
|
28
|
+
## 4. Review
|
|
29
29
|
|
|
30
|
-
Run `
|
|
30
|
+
Run `/simplify` to review your changes for reuse, quality, and efficiency, and fix anything it surfaces.
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
- **
|
|
35
|
-
- **
|
|
36
|
-
- **
|
|
37
|
-
|
|
32
|
+
Then do a quick manual pass on things `/simplify` won't catch:
|
|
33
|
+
|
|
34
|
+
- **Correctness:** Does the change fully address the issue? Any missing edge cases?
|
|
35
|
+
- **Tests:** Are the tests meaningful, or are they just asserting on mocks?
|
|
36
|
+
- **Cleanup:** Any leftover debug code, TODOs, or commented-out lines?
|
|
37
|
+
|
|
38
|
+
Fix anything you find before moving on.
|
|
38
39
|
|
|
39
40
|
## 5. Commit, push, and open a PR
|
|
40
41
|
|
|
@@ -42,12 +43,28 @@ Run `git diff` and review your own changes. Fix any issues before committing.
|
|
|
42
43
|
gh pr create --title "..." --body "..."
|
|
43
44
|
```
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
## 6. Post to Slack (if instructed)
|
|
47
|
+
|
|
48
|
+
If the prompt specifies a Slack channel, use the Slack MCP tool to post a message with a link to the PR. Format: `<pr_url|PR> repo-name: title of the pr`
|
|
49
|
+
|
|
50
|
+
## 7. Monitor CI and review feedback
|
|
51
|
+
|
|
52
|
+
After creating the PR, monitor it until all checks pass and all review feedback is addressed.
|
|
53
|
+
|
|
54
|
+
Loop:
|
|
55
|
+
1. Sleep for 3 minutes (`sleep 180`)
|
|
56
|
+
2. Check CI status: `gh pr checks {pr_number} --repo {owner}/{repo}`
|
|
57
|
+
3. If any checks failed, read the failure logs, fix the issue, commit, and push
|
|
58
|
+
4. Check for review comments: `gh pr view {pr_number} --repo {owner}/{repo} --comments`
|
|
59
|
+
5. If there are new or unresolved comments, address them, commit, and push
|
|
60
|
+
6. Repeat from step 1
|
|
61
|
+
|
|
62
|
+
Stop when:
|
|
63
|
+
- All CI checks pass AND
|
|
64
|
+
- No unresolved review comments remain
|
|
65
|
+
|
|
66
|
+
Once everything is green, update the issue labels:
|
|
46
67
|
|
|
47
68
|
```
|
|
48
69
|
gh issue edit {issue_number} --repo {owner}/{repo} --add-label agent:review --remove-label agent:wip
|
|
49
70
|
```
|
|
50
|
-
|
|
51
|
-
## 6. Post to Slack (if instructed)
|
|
52
|
-
|
|
53
|
-
If the prompt specifies a Slack channel, use the Slack MCP tool to post a message with a link to the PR. Format: `<pr_url|PR> title of the pr`
|