next-issue 0.8.0 → 0.8.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
@@ -24,22 +24,27 @@ For each open issue, oldest first:
24
24
  the claim. An issue with saved state under `.next-issue/` resumes instead,
25
25
  whatever its labels, unless another person is now the assignee.
26
26
  2. Assign the issue to you and add `status:in-progress`.
27
- 3. Fetch the default branch, fast-forward the local copy of it when that is
28
- safe, then add a git worktree on a new `issue-<n>` branch from it.
27
+ 3. Fetch the remote, fast-forward the local copy of the default branch when
28
+ that is safe, then add a git worktree at `../<repo>-issue-<n>` on the
29
+ `issue-<n>` branch. A branch that exists on the remote is the start, and a
30
+ local branch that fell behind the remote is fast-forwarded.
29
31
  4. Let the implementer agent do the work, then commit and push.
30
32
  5. Open a draft pull request that closes the issue.
31
- 6. Wait for the checks. A pending state waits again, up to
32
- `checkTimeoutMinutes`. A repository that reports no check keeps asking for
33
- `checkGraceSeconds`, because a new pull request often shows no check yet,
34
- and only then goes straight to the review.
33
+ 6. Wait for the checks, once the pull request shows the pushed commit. A
34
+ pending state waits again, up to `checkTimeoutMinutes`. A read that fails
35
+ is tried three times before the run stops. A repository that reports no
36
+ check keeps asking for `checkGraceSeconds`, because a new pull request often
37
+ shows no check yet, and only then goes straight to the review. The later
38
+ gates of the same issue ask once.
35
39
  7. On a red build, give the failed job logs to the fixer agent, then go to step
36
40
  6 again. The budget is `maxCiFixes`.
37
41
  8. Let the reviewer agent judge the diff, at most `diffMaxChars` of it. The
38
42
  reviewer returns a structured verdict that rates each finding `blocking` or
39
43
  `minor`. The harness puts the result on the pull request.
40
44
  9. No blocking finding means approval: mark the pull request ready for review
41
- and set `status:done`. The merge stays with you. A blocking finding always
42
- stops the approval, even when the reviewer also says approve.
45
+ and set `status:done`. The merge stays with you. The worktree, the local
46
+ branch and the state file go away. The remote branch stays. A blocking
47
+ finding always stops the approval, even when the reviewer also says approve.
43
48
  10. With a blocking finding, run the fixer agent and go to step 6 again. The
44
49
  budget is `maxReviewRounds`.
45
50
 
@@ -110,14 +115,14 @@ A short text summary of the same data goes to standard output. Use `--json` to
110
115
  get the full summary object there instead, so the harness fits in a pipe. All
111
116
  progress goes to standard error.
112
117
 
113
- Recorded for each step: `claim`, `comments`, `worktree`, `implement`, `push`,
114
- `pull-request`, `checks`, `review` and `fix`, each with a `.start`, `.end` or
115
- `.error` event and a duration in `ms`. Every `git` and `gh` command is recorded
116
- with its exit code and duration. A `tool` event holds the tool name, and for a
117
- `Bash` tool the command too. Each agent call adds a `usage` event with the
118
- tokens, the cost estimate in dollars, the turn count, the tool-call count, the
119
- model and the session id, so you can open the session again and read the full
120
- transcript.
118
+ Recorded for each step: `claim`, `comments`, `worktree`, `setup`, `implement`,
119
+ `push`, `pull-request`, `checks`, `review` and `fix`, each with a `.start`,
120
+ `.end` or `.error` event and a duration in `ms`. Every `git` and `gh` command
121
+ is recorded with its exit code and duration. A `tool` event holds the tool
122
+ name, and for a `Bash` tool the command too. Each agent call adds a `usage`
123
+ event with the tokens, the cost estimate in dollars, the turn count, the
124
+ tool-call count, the model and the session id, so you can open the session
125
+ again and read the full transcript.
121
126
 
122
127
  ```bash
123
128
  jq -r 'select(.kind=="usage") | [.issue,.role,.turns,.total] | @tsv' \
@@ -134,7 +139,7 @@ Three levels of console output:
134
139
 
135
140
  ## Requirements
136
141
 
137
- - Node 24 or later, because the command runs the TypeScript source directly
142
+ - Node 24 or later
138
143
  - `gh`, authenticated with write access to the repository
139
144
  - Anthropic credentials for the Claude Agent SDK: `ANTHROPIC_API_KEY`, or a
140
145
  Claude subscription login
@@ -221,7 +226,8 @@ A hand-over leaves the state file with the used budgets in it, so the next run
221
226
  would hand the issue over again at once. To give the issue another try, take the
222
227
  `status:needs-human` label off and run with `--reset`. The flag puts the budgets
223
228
  and the findings back to zero and keeps the branch, the pull request and the
224
- step that the last run reached, so the work carries on where it stopped.
229
+ step that the last run reached, so the work carries on where it stopped. A
230
+ commit that you pushed to the pull request in the meantime is fetched first.
225
231
 
226
232
  ## Commands and options
227
233
 
package/dist/cli.mjs CHANGED
@@ -6,6 +6,7 @@ import { loadConfig, writeDefaultConfig } from "./config.mjs";
6
6
  import { setCommandObserver, setDefaultTimeout } from "./exec.mjs";
7
7
  import { detectRepo, ensureIgnored, repoRoot } from "./git.mjs";
8
8
  import { currentLogin, defaultBranch, openIssues } from "./github.mjs";
9
+ import { exitCode, runIssues } from "./loop.mjs";
9
10
  import { message, Recorder } from "./observe.mjs";
10
11
  import { PORTS, processIssue } from "./pipeline.mjs";
11
12
  import { resetState, STATE_DIR, takeStop } from "./state.mjs";
@@ -59,25 +60,14 @@ async function handle(args, config, repo, recorder) {
59
60
  recorder.event("run.start", { repo: `${repo.owner}/${repo.name}`, base, login, log: recorder.logFile }, "quiet");
60
61
  const issues = await openIssues(repo, config.issueLimit);
61
62
  recorder.event("issues", { open: issues.length }, "quiet");
62
- await takeStop(repo);
63
- const reports = [];
64
- for (const issue of issues) {
65
- const handled = reports.filter((report) => report.outcome !== "skipped").length;
66
- if (args.max !== undefined && handled >= args.max) {
67
- break;
68
- }
69
- if (args.reset) {
70
- await resetState(repo, issue.number);
71
- }
72
- const report = await processIssue(context, issue);
73
- reports.push(report);
74
- if (await takeStop(repo)) {
75
- recorder.event("run.stop", { issue: issue.number }, "quiet");
76
- break;
77
- }
78
- if (args.once && report.outcome !== "skipped") {
79
- break;
80
- }
63
+ const { reports, stoppedAt } = await runIssues(args, {
64
+ issues,
65
+ process: (issue) => processIssue(context, issue),
66
+ reset: (issue) => resetState(repo, issue),
67
+ stop: () => takeStop(repo),
68
+ });
69
+ if (stoppedAt !== undefined) {
70
+ recorder.event("run.stop", { issue: stoppedAt }, "quiet");
81
71
  }
82
72
  const summary = { ...recorder.summary(), issues: reports };
83
73
  recorder.event("run.end", {
@@ -90,7 +80,7 @@ async function handle(args, config, repo, recorder) {
90
80
  const summaryFile = join(repo.root, STATE_DIR, "runs", `${recorder.runId}.summary.json`);
91
81
  await writeFile(summaryFile, `${JSON.stringify(summary, null, 2)}\n`);
92
82
  await write(process.stdout, args.json ? `${JSON.stringify(summary, null, 2)}\n` : formatSummary(summary));
93
- return count(reports, "needs-human") + count(reports, "error") > 0 ? 1 : 0;
83
+ return exitCode(reports);
94
84
  }
95
85
  function count(reports, outcome) {
96
86
  return reports.filter((report) => report.outcome === outcome).length;
package/dist/exec.mjs CHANGED
@@ -15,7 +15,7 @@ export function run(command, args, options = {}) {
15
15
  cwd: options.cwd,
16
16
  stdio: [
17
17
  options.input === undefined ? "ignore" : "pipe",
18
- options.inherit === true ? "inherit" : "pipe",
18
+ options.show === true ? 2 : "pipe",
19
19
  "pipe",
20
20
  ],
21
21
  });
package/dist/git.mjs CHANGED
@@ -30,21 +30,26 @@ export function worktreePath(repo, issue) {
30
30
  export async function addWorktree(repo, issue, base, remote) {
31
31
  const path = worktreePath(repo, issue);
32
32
  const branch = branchName(issue);
33
- await must("git", ["fetch", remote, base], { cwd: repo.root });
33
+ const upstream = `${remote}/${branch}`;
34
+ await must("git", ["fetch", remote], { cwd: repo.root });
34
35
  await updateBase(repo, base, remote);
35
36
  await must("git", ["worktree", "prune"], { cwd: repo.root });
36
- if (await hasWorktree(repo, path)) {
37
- return path;
37
+ const pushed = await hasBranch(repo, upstream, "--remotes");
38
+ if (!(await hasWorktree(repo, path))) {
39
+ const args = (await hasBranch(repo, branch))
40
+ ? ["worktree", "add", path, branch]
41
+ : ["worktree", "add", "-b", branch, path, pushed ? upstream : `${remote}/${base}`];
42
+ await must("git", args, { cwd: repo.root });
43
+ }
44
+ if (pushed) {
45
+ await run("git", ["merge", "--ff-only", upstream], { cwd: path });
38
46
  }
39
- const existing = await run("git", ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`], {
40
- cwd: repo.root,
41
- });
42
- const args = existing.code === 0
43
- ? ["worktree", "add", path, branch]
44
- : ["worktree", "add", "-b", branch, path, `${remote}/${base}`];
45
- await must("git", args, { cwd: repo.root });
46
47
  return path;
47
48
  }
49
+ async function hasBranch(repo, name, ...flags) {
50
+ const list = await must("git", ["branch", "--list", ...flags, name], { cwd: repo.root });
51
+ return list.length > 0;
52
+ }
48
53
  export async function updateBase(repo, base, remote) {
49
54
  const head = await run("git", ["symbolic-ref", "--quiet", "--short", "HEAD"], { cwd: repo.root });
50
55
  const args = head.code === 0 && head.stdout.trim() === base
@@ -98,3 +103,10 @@ export async function commitAll(cwd, message) {
98
103
  export async function push(cwd, remote, branch) {
99
104
  await must("git", ["push", "-u", remote, branch], { cwd });
100
105
  }
106
+ export async function revision(cwd) {
107
+ return must("git", ["rev-parse", "HEAD"], { cwd });
108
+ }
109
+ export async function deleteBranch(repo, branch) {
110
+ const result = await run("git", ["branch", "-D", branch], { cwd: repo.root });
111
+ return result.code === 0;
112
+ }
package/dist/github.mjs CHANGED
@@ -1,5 +1,7 @@
1
1
  import { must, run } from "./exec.mjs";
2
2
  const CHECK_PENDING_CODE = 8;
3
+ const READ_TRIES = 3;
4
+ const KNOWN_LABELS = new Map();
3
5
  const NO_CHECKS = /no checks reported/i;
4
6
  function slug(repo) {
5
7
  return `${repo.owner}/${repo.name}`;
@@ -22,6 +24,8 @@ export async function openIssues(repo, limit) {
22
24
  "list",
23
25
  "--state",
24
26
  "open",
27
+ "--search",
28
+ "sort:created-asc",
25
29
  "--limit",
26
30
  String(limit),
27
31
  "--json",
@@ -65,7 +69,17 @@ export async function setLabel(repo, kind, number, add, remove) {
65
69
  await gh(repo, [kind, "edit", String(number), ...labelArgs(add, remove, current)]);
66
70
  }
67
71
  export async function ensureLabel(repo, label) {
72
+ let known = KNOWN_LABELS.get(slug(repo));
73
+ if (known === undefined) {
74
+ const raw = await gh(repo, ["label", "list", "--limit", "1000", "--json", "name"]);
75
+ known = new Set(JSON.parse(raw).map((item) => item.name));
76
+ KNOWN_LABELS.set(slug(repo), known);
77
+ }
78
+ if (known.has(label)) {
79
+ return;
80
+ }
68
81
  await run("gh", ["label", "create", label, "--repo", slug(repo)], { cwd: repo.root });
82
+ known.add(label);
69
83
  }
70
84
  export async function findPr(repo, branch) {
71
85
  const result = await run("gh", ["pr", "list", "--head", branch, "--state", "open", "--json", "number", "--repo", slug(repo)], { cwd: repo.root });
@@ -95,6 +109,9 @@ export async function createPr(repo, branch, base, title, body, draft) {
95
109
  }
96
110
  return number;
97
111
  }
112
+ async function prHead(repo, branch) {
113
+ return gh(repo, ["pr", "view", branch, "--json", "headRefOid", "--jq", ".headRefOid"]);
114
+ }
98
115
  async function readChecks(repo, branch) {
99
116
  const result = await run("gh", ["pr", "checks", branch, "--json", "name,bucket,link,description", "--repo", slug(repo)], { cwd: repo.root });
100
117
  const output = result.stdout.trim();
@@ -107,8 +124,8 @@ async function readChecks(repo, branch) {
107
124
  const checks = JSON.parse(output);
108
125
  return checks.length === 0 ? "none" : checks;
109
126
  }
110
- async function watchOnce(repo, branch, intervalSeconds, timeoutMs, inherit) {
111
- const watch = await run("gh", ["pr", "checks", branch, "--watch", "--fail-fast", "--interval", String(intervalSeconds), "--repo", slug(repo)], { cwd: repo.root, inherit, timeoutMs });
127
+ async function watchOnce(repo, branch, intervalSeconds, timeoutMs, show) {
128
+ const watch = await run("gh", ["pr", "checks", branch, "--watch", "--fail-fast", "--interval", String(intervalSeconds), "--repo", slug(repo)], { cwd: repo.root, show, timeoutMs });
112
129
  if (watch.timedOut) {
113
130
  return "timeout";
114
131
  }
@@ -125,31 +142,53 @@ const REAL_CLOCK = {
125
142
  sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
126
143
  };
127
144
  export async function pollChecks(probe, options, clock = REAL_CLOCK) {
128
- const started = clock.now();
129
- const deadline = started + options.timeoutMs;
145
+ const deadline = clock.now() + options.timeoutMs;
130
146
  const rest = () => Math.max(0, deadline - clock.now());
147
+ let shown;
148
+ let failures = 0;
131
149
  for (;;) {
132
150
  if (rest() === 0) {
133
151
  return "timeout";
134
152
  }
135
- if ((await probe.list()) === "none") {
136
- if (clock.now() - started >= options.graceMs) {
137
- return "none";
153
+ const state = await read(probe, options.head, rest(), () => (shown ??= clock.now()));
154
+ if (state instanceof Error) {
155
+ failures += 1;
156
+ if (failures >= READ_TRIES) {
157
+ throw state;
138
158
  }
139
159
  }
140
160
  else {
141
- const state = await probe.watch(rest());
142
- if (state !== "pending") {
161
+ failures = 0;
162
+ if (state === "none" && clock.now() - (shown ?? 0) >= options.graceMs) {
163
+ return "none";
164
+ }
165
+ if (state !== "none" && state !== "behind" && state !== "pending") {
143
166
  return state;
144
167
  }
145
168
  }
146
169
  await clock.sleep(Math.min(options.intervalSeconds * 1000, rest()));
147
170
  }
148
171
  }
172
+ async function read(probe, head, timeoutMs, shown) {
173
+ try {
174
+ if (head !== undefined && (await probe.head()) !== head) {
175
+ return "behind";
176
+ }
177
+ shown();
178
+ if ((await probe.list()) === "none") {
179
+ return "none";
180
+ }
181
+ }
182
+ catch (error) {
183
+ return error instanceof Error ? error : new Error(String(error));
184
+ }
185
+ return probe.watch(timeoutMs);
186
+ }
149
187
  export function waitForChecks(repo, branch, options) {
150
188
  return pollChecks({
189
+ head: () => prHead(repo, branch),
151
190
  list: async () => ((await readChecks(repo, branch)) === "none" ? "none" : "some"),
152
- watch: (timeoutMs) => watchOnce(repo, branch, options.intervalSeconds, timeoutMs, options.inherit),
191
+ watch: (timeoutMs) => watchOnce(repo, branch, options.intervalSeconds, timeoutMs, options.show),
153
192
  }, options);
154
193
  }
155
194
  export async function failedCheckLogs(repo, branch, maxChars) {
package/dist/loop.mjs ADDED
@@ -0,0 +1,27 @@
1
+ export async function runIssues(args, loop) {
2
+ await loop.stop();
3
+ const reports = [];
4
+ for (const issue of loop.issues) {
5
+ if (args.max !== undefined && handled(reports) >= args.max) {
6
+ break;
7
+ }
8
+ if (args.reset) {
9
+ await loop.reset(issue.number);
10
+ }
11
+ const report = await loop.process(issue);
12
+ reports.push(report);
13
+ if (await loop.stop()) {
14
+ return { reports, stoppedAt: issue.number };
15
+ }
16
+ if (args.once && report.outcome !== "skipped") {
17
+ break;
18
+ }
19
+ }
20
+ return { reports };
21
+ }
22
+ export function exitCode(reports) {
23
+ return reports.some((report) => report.outcome === "needs-human" || report.outcome === "error") ? 1 : 0;
24
+ }
25
+ function handled(reports) {
26
+ return reports.filter((report) => report.outcome !== "skipped").length;
27
+ }
package/dist/pipeline.mjs CHANGED
@@ -1,11 +1,11 @@
1
1
  import { CODING, READ_ONLY, runAgent } from "./agents.mjs";
2
2
  import { managedLabels } from "./config.mjs";
3
3
  import { run } from "./exec.mjs";
4
- import { addWorktree, branchName, commitAll, diff, hasWorktree, push, removeWorktree, worktreePath, } from "./git.mjs";
4
+ import { addWorktree, branchName, commitAll, deleteBranch, diff, hasWorktree, push, removeWorktree, revision, worktreePath, } from "./git.mjs";
5
5
  import { assignIssue, commentOnPr, createPr, failedCheckLogs, findPr, issueComments, markPrReady, setLabel, waitForChecks, } from "./github.mjs";
6
6
  import { message } from "./observe.mjs";
7
7
  import { capDiff, fixPrompt, implementPrompt, parseCommitSubject, parseUnrelated, reviewPrompt, } from "./prompts.mjs";
8
- import { readState, writeState } from "./state.mjs";
8
+ import { dropState, readState, writeState } from "./state.mjs";
9
9
  import { blockingFindings, fingerprint, formatFindings, formatVerdict, isApproved, readVerdict, VERDICT_SCHEMA, } from "./verdict.mjs";
10
10
  export const PORTS = {
11
11
  addWorktree,
@@ -13,6 +13,7 @@ export const PORTS = {
13
13
  commentOnPr,
14
14
  commitAll,
15
15
  createPr,
16
+ deleteBranch,
16
17
  diff,
17
18
  failedCheckLogs,
18
19
  findPr,
@@ -21,6 +22,7 @@ export const PORTS = {
21
22
  markPrReady,
22
23
  push,
23
24
  removeWorktree,
25
+ revision,
24
26
  runAgent,
25
27
  runSetup: async (command, cwd, timeoutMs) => run("bash", ["-lc", command], { cwd, timeoutMs }),
26
28
  setLabel,
@@ -101,6 +103,12 @@ async function work(context, issue, log, state) {
101
103
  const escalate = (reason) => handOver(context, log, issue.number, state, reason);
102
104
  const finish = async (outcome, reason) => {
103
105
  await cleanUp(context, log, issue.number);
106
+ if (outcome === "done") {
107
+ if (!(await ports.deleteBranch(repo, branch))) {
108
+ log.event("branch.kept", { branch }, "quiet");
109
+ }
110
+ await dropState(repo, issue.number);
111
+ }
104
112
  return {
105
113
  issue: issue.number,
106
114
  outcome,
@@ -123,6 +131,7 @@ async function work(context, issue, log, state) {
123
131
  }
124
132
  }
125
133
  if (state.phase === "claimed") {
134
+ const before = await ports.revision(worktree);
126
135
  const result = await log.step("implement", {}, () => ports.runAgent(log, {
127
136
  name: "implementer",
128
137
  cwd: worktree,
@@ -132,11 +141,10 @@ async function work(context, issue, log, state) {
132
141
  timeoutMs: config.agentTimeoutMinutes * 60_000,
133
142
  }));
134
143
  const subject = parseCommitSubject(result.text, `fix: resolve issue #${issue.number}`);
135
- if (!(await ports.commitAll(worktree, `${subject}\n\nRefs #${issue.number}`))) {
144
+ if (!(await commitWork(job, before, subject))) {
136
145
  await escalate("The implementer produced no change.");
137
146
  return finish("needs-human", "no implementation commit");
138
147
  }
139
- log.event("commit", { subject });
140
148
  state.phase = "implemented";
141
149
  await writeState(repo, state);
142
150
  }
@@ -149,14 +157,18 @@ async function work(context, issue, log, state) {
149
157
  state.phase = "review";
150
158
  await writeState(repo, state);
151
159
  log.event("pull-request.ready", { pr: state.pr }, "quiet");
160
+ let noChecks = false;
152
161
  const gate = async () => {
153
- const checks = await log.step("checks", { pr: state.pr }, () => ports.waitForChecks(repo, branch, {
162
+ const head = await ports.revision(worktree);
163
+ const checks = await log.step("checks", { pr: state.pr, head }, () => ports.waitForChecks(repo, branch, {
164
+ head,
154
165
  intervalSeconds: config.checkIntervalSeconds,
155
- graceMs: config.checkGraceSeconds * 1000,
166
+ graceMs: noChecks ? 0 : config.checkGraceSeconds * 1000,
156
167
  timeoutMs: config.checkTimeoutMinutes * 60_000,
157
- inherit: log.level === "verbose",
168
+ show: log.level === "verbose",
158
169
  }));
159
170
  log.event("checks.state", { state: checks });
171
+ noChecks ||= checks === "none";
160
172
  if (checks === "timeout") {
161
173
  await escalate("The checks did not finish in time.");
162
174
  return { done: true, report: await finish("needs-human", "checks timeout") };
@@ -210,7 +222,7 @@ async function work(context, issue, log, state) {
210
222
  const review = await log.step("review", fields, () => ports.runAgent(log, {
211
223
  name: "reviewer",
212
224
  cwd: worktree,
213
- prompt: reviewPrompt(issue, comments, patch, state.reviewRounds, history(state.reviewLog)),
225
+ prompt: reviewPrompt(issue, comments, patch, history(state.reviewLog)),
214
226
  profile: READ_ONLY,
215
227
  model: config.models.reviewer,
216
228
  timeoutMs: config.agentTimeoutMinutes * 60_000,
@@ -242,8 +254,6 @@ async function work(context, issue, log, state) {
242
254
  log.event("pull-request.undraft.fail", { pr: state.pr }, "quiet");
243
255
  }
244
256
  await setStatus(context, issue.number, state.pr, config.labels.done);
245
- state.phase = "done";
246
- await writeState(repo, state);
247
257
  return finish("done");
248
258
  }
249
259
  const mark = fingerprint(blocking);
@@ -264,6 +274,7 @@ async function work(context, issue, log, state) {
264
274
  async function fixRound(job, reason, detail, earlier, kind) {
265
275
  const { context, issue, log, worktree, branch } = job;
266
276
  const { ports } = context;
277
+ const before = await ports.revision(worktree);
267
278
  const result = await log.step("fix", { reason }, () => ports.runAgent(log, {
268
279
  name: "fixer",
269
280
  cwd: worktree,
@@ -278,14 +289,20 @@ async function fixRound(job, reason, detail, earlier, kind) {
278
289
  return { committed: false, unrelated };
279
290
  }
280
291
  const subject = parseCommitSubject(result.text, `fix: address feedback on issue #${issue.number}`);
281
- if (!(await ports.commitAll(worktree, `${subject}\n\nRefs #${issue.number}`))) {
292
+ if (!(await commitWork(job, before, subject))) {
282
293
  log.event("commit.empty", { reason }, "quiet");
283
294
  return { committed: false };
284
295
  }
285
- log.event("commit", { subject });
286
296
  await ports.push(worktree, context.config.remote, branch);
287
297
  return { committed: true };
288
298
  }
299
+ async function commitWork(job, before, subject) {
300
+ const { context, issue, log, worktree } = job;
301
+ if (await context.ports.commitAll(worktree, `${subject}\n\nRefs #${issue.number}`)) {
302
+ log.event("commit", { subject });
303
+ }
304
+ return (await context.ports.revision(worktree)) !== before;
305
+ }
289
306
  async function cleanUp(context, log, issue) {
290
307
  const path = worktreePath(context.repo, issue);
291
308
  try {
package/dist/prompts.mjs CHANGED
@@ -26,8 +26,8 @@ export function capDiff(diff, maxChars) {
26
26
  ? diff
27
27
  : `${diff.slice(0, maxChars)}\n... The diff is cut here, after ${maxChars} characters.`;
28
28
  }
29
- export function reviewPrompt(issue, comments, diff, round, earlier) {
30
- const scope = round === 1
29
+ export function reviewPrompt(issue, comments, diff, earlier) {
30
+ const scope = earlier.length === 0
31
31
  ? "Check correctness, scope, repository conventions and test cover."
32
32
  : [
33
33
  "This is a later round. Judge two things only:",
package/dist/state.mjs CHANGED
@@ -38,3 +38,6 @@ export async function takeStop(repo) {
38
38
  return false;
39
39
  }
40
40
  }
41
+ export async function dropState(repo, issue) {
42
+ await rm(file(repo, issue), { force: true });
43
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-issue",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Autonomous issue harness driven by the Claude Agent SDK",
5
5
  "keywords": [
6
6
  "github",