@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 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
- ## Install
15
+ ## Quick install
16
16
 
17
17
  ```bash
18
18
  npm i -g @armstrongnate/april
19
- # or: pnpm add -g @armstrongnate/april
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
- Then:
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
- april init # writes ~/.config/april/config.yaml + skill + env file; checks prereqs
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
- april install # registers the user service and starts it
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
- `april init` and the daemon both verify that the `cli/gh-webhook` extension is installed and refuse to proceed without it.
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. After editing, `april restart`.
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
- - On Linux, the systemd unit references it via `EnvironmentFile=-`. Changes take effect on the next restart.
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
- ### Linux: keep april running after logout
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
- User services stop when you log out unless linger is enabled. On a server you SSH into:
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
- sudo loginctl enable-linger $USER
173
+ april upgrade
72
174
  ```
73
175
 
74
- `april install` reminds you if it sees linger is off.
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
- ### Node version managers
178
+ Manual equivalent if you want to do it yourself:
77
179
 
78
- `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.
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
 
@@ -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
- # claudeAllowedTools: # optional, defaults to Edit, Write, Bash(*)
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 claudeAllowedTools = Array.isArray(parsed.claudeAllowedTools)
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, claudeAllowedTools, port, repos };
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 (failure ${state.consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES})`);
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 allowedTools = [
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 allowedToolsArgs = allowedTools.map((t) => `--allowedTools '${t}'`).join(" ");
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@armstrongnate/april",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "She does all the work so you don't have to. Watches GitHub issues and spawns Claude Code sessions to work them.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: issue-worker
3
- description: Autonomously work a GitHub issue end-to-end — read, implement, and open a PR with no human input required.
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 and simplify
28
+ ## 4. Review
29
29
 
30
- Run `git diff` and review your own changes. Fix any issues before committing.
30
+ Run `/simplify` to review your changes for reuse, quality, and efficiency, and fix anything it surfaces.
31
31
 
32
- - **Correctness:** Does it fully address the issue? Missing edge cases?
33
- - **Simplify:** Can anything be combined, inlined, or removed? Prefer fewer files, less indirection, and no unnecessary abstractions.
34
- - **Reuse:** Are you duplicating logic that already exists in the codebase? Use existing helpers and patterns.
35
- - **Cleanup:** Remove leftover debug code, TODOs, unused imports, and dead code.
36
- - **Style:** Match the conventions of the surrounding code.
37
- - **Tests:** Are the tests meaningful, not just testing mocks?
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
- Then update the issue labels:
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`