next-issue 0.13.0 → 0.14.0

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
@@ -60,7 +60,12 @@ For each open issue, oldest first:
60
60
  reviewer leaves out of the findings thus gets an approval: only the findings
61
61
  count.
62
62
  11. With an open finding, run the fixer agent and go to step 6 again. The
63
- budget is `maxReviewRounds`.
63
+ budget is `maxReviewRounds`. A fixer that holds every finding to be wrong
64
+ changes nothing and gives its reason instead. The reason goes on the pull
65
+ request and into the next round, where it answers every finding of its own
66
+ round, so the reviewer withdraws them all or repeats them all. With only
67
+ some findings wrong, the fixer fixes the correct ones, and the next round
68
+ holds the rest.
64
69
 
65
70
  ## Stops for a loop
66
71
 
@@ -68,20 +73,35 @@ The harness hands the issue to a person, with the `status:needs-human` label,
68
73
  when:
69
74
 
70
75
  - the check budget or the review budget runs out;
71
- - a fixer round adds no commit, so there is no progress;
76
+ - a fixer round adds no commit and gives no reason, so there is no progress;
72
77
  - the fixer reports that the checks fail for a reason the change did not cause,
73
78
  such as a test already broken on the base branch or one that fails only under
74
79
  load. It changes nothing and the reason reaches the pull request, rather than
75
80
  the fixer spending its whole budget on a failure it cannot fix;
81
+ - a fixer disputes the findings and leaves a change behind, either in the
82
+ worktree or in a commit of its own. A dispute carries a reason, not a diff,
83
+ so the harness throws none of it away: an unclean worktree stays on the
84
+ disk, and a commit of the fixer stays on the local branch. Neither is
85
+ pushed, so the pull request shows no such change. It holds the reason of the
86
+ dispute, and the change is on the machine that ran the harness;
76
87
  - the reviewer repeats a finding set from an earlier round, which shows a
77
- ping-pong between the fixer and the reviewer;
88
+ ping-pong between the fixer and the reviewer, or a finding that the fixer
89
+ disputed and the reviewer holds to. The person then decides;
78
90
  - the reviewer gives no verdict, or the checks do not finish in time;
79
91
  - an agent does not finish in `agentTimeoutMinutes`;
80
92
  - the setup command fails, or does not finish in `setupTimeoutMinutes`;
81
93
  - a step of the run fails with an error.
82
94
 
83
- The fixer sees the findings of all earlier rounds, not only the last one. From
84
- round two, the reviewer judges only the earlier findings and any regression.
95
+ The fixer sees the findings of all earlier rounds, not only the last one, with
96
+ its own disputes next to them. From round two, the reviewer judges only the
97
+ earlier findings and any regression.
98
+
99
+ Each agent that writes to the pull request names itself in the first line of the
100
+ comment: `### Review: approved`, `### Review: changes requested` and
101
+ `### Fixer: no change`. The account is the same for all of them, so the heading,
102
+ not the author, tells them apart. The agents read the comments of the issue, not
103
+ of the pull request, so a comment of an agent never returns to a prompt: the
104
+ dispute of the fixer reaches the reviewer through the state file.
85
105
 
86
106
  ## Stop after the current issue
87
107
 
package/dist/pipeline.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { CODING, REVIEWING, runAgent } from "./agents.mjs";
2
2
  import { managedLabels } from "./config.mjs";
3
3
  import { run } from "./exec.mjs";
4
- import { addWorktree, branchName, commitAll, deleteBranch, diff, discardChanges, hasWorktree, push, removeWorktree, revision, worktreePath, } from "./git.mjs";
4
+ import { addWorktree, branchName, commitAll, deleteBranch, diff, discardChanges, hasWorktree, isDirty, push, removeWorktree, revision, worktreePath, } from "./git.mjs";
5
5
  import { 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";
@@ -20,6 +20,7 @@ export const PORTS = {
20
20
  failedCheckLogs,
21
21
  findPr,
22
22
  hasWorktree,
23
+ isDirty,
23
24
  issueComments,
24
25
  markPrReady,
25
26
  push,
@@ -280,8 +281,18 @@ async function reviewRound(job) {
280
281
  await writeState(repo, state);
281
282
  const fix = await fixRound(job, "The reviewer requested changes.", formatFindings(open), earlier, "review");
282
283
  if (!fix.committed) {
283
- await escalate(job, "The fixer added no commit for the review findings.");
284
- return { done: true, report: await finish(job, "needs-human", "no fix commit") };
284
+ const note = fix.unrelated;
285
+ if (note === undefined) {
286
+ await escalate(job, "The fixer added no commit for the review findings.");
287
+ return { done: true, report: await finish(job, "needs-human", "no fix commit") };
288
+ }
289
+ if (fix.stray === true) {
290
+ await escalate(job, `The fixer disputed the findings and left a change behind: ${note}`);
291
+ return { done: true, report: await finish(job, "needs-human", "dispute with changes") };
292
+ }
293
+ state.reviewLog.at(-1).dispute = note;
294
+ await writeState(repo, state);
295
+ await ports.commentOnPr(repo, pr, `### Fixer: no change\n\n${note}`);
285
296
  }
286
297
  return { done: false };
287
298
  }
@@ -322,8 +333,9 @@ async function fixRound(job, reason, detail, earlier, kind) {
322
333
  }));
323
334
  const unrelated = parseUnrelated(result.text);
324
335
  if (unrelated !== undefined) {
325
- log.event("fix.unrelated", { reason, note: unrelated }, "quiet");
326
- return { committed: false, unrelated };
336
+ const stray = (await ports.revision(worktree)) !== before || (await ports.isDirty(worktree));
337
+ log.event("fix.unrelated", { reason, note: unrelated, stray }, "quiet");
338
+ return { committed: false, unrelated, stray };
327
339
  }
328
340
  const subject = parseCommitSubject(result.text, `fix: address feedback on issue #${issue.number}`);
329
341
  if (!(await commitWork(job, before, subject))) {
@@ -370,5 +382,10 @@ async function setStatus(context, issue, pr, label) {
370
382
  }
371
383
  }
372
384
  function history(rounds) {
373
- return rounds.map((round) => `Round ${round.round}:\n${round.findings}`);
385
+ return rounds.map((round) => {
386
+ const head = `Round ${round.round}:\n${round.findings}`;
387
+ return round.dispute === undefined
388
+ ? head
389
+ : `${head}\nThe fixer disputed every finding of this round: ${round.dispute}`;
390
+ });
374
391
  }
package/dist/prompts.mjs CHANGED
@@ -33,6 +33,8 @@ export function reviewPrompt(issue, comments, diff, earlier) {
33
33
  "This is a later round. Judge two things only:",
34
34
  "whether the earlier findings below are now fixed, and whether the new commits broke something.",
35
35
  "Do not raise a new point of taste. Do not repeat a finding that is fixed.",
36
+ "A `The fixer disputed every finding of this round` line answers all of the findings of that round, not one of them.",
37
+ "Withdraw every finding of that round if the dispute is correct. Repeat them all if the dispute is wrong.",
36
38
  `## Earlier findings\n${earlier.join("\n")}`,
37
39
  ].join("\n");
38
40
  return [
@@ -56,6 +58,12 @@ const UNRELATED_RULE = [
56
58
  "Establish that before you fix anything. If the failure is not yours, change nothing and end your final message with one line in the form `unrelated: <one sentence on what fails and why it is not this change>`.",
57
59
  "A person then reads it. Do not weaken, skip or re-record a test to make the checks pass.",
58
60
  ].join("\n");
61
+ const DISPUTE_RULE = [
62
+ "A finding can be wrong: it can miss a convention of this repository, ask for work that the issue does not want, or describe behaviour that the code does not have.",
63
+ "Establish that before you change anything. If every finding is wrong, change nothing and end your final message with one line in the form `unrelated: <one sentence on why the findings are wrong>`.",
64
+ "If only some of the findings are wrong, fix the correct ones and change nothing for the wrong ones. Write no `unrelated` line in that round. The next round holds only the findings that are left, and you can dispute them there.",
65
+ "The reviewer reads that line in the next round. Do not make a change that you hold to be wrong only to end the round.",
66
+ ].join("\n");
59
67
  export function fixPrompt(issue, reason, detail, earlier, kind) {
60
68
  return [
61
69
  `You are the fixer. Correct the work for issue #${issue.number}: ${issue.title}.`,
@@ -70,7 +78,7 @@ export function fixPrompt(issue, reason, detail, earlier, kind) {
70
78
  : "",
71
79
  "## Rules",
72
80
  "Fix the cause, not the symptom. Keep the change minimal.",
73
- kind === "checks" ? UNRELATED_RULE : "",
81
+ kind === "checks" ? UNRELATED_RULE : DISPUTE_RULE,
74
82
  COMMIT_RULES,
75
83
  ]
76
84
  .filter((part) => part.length > 0)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-issue",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Autonomous issue harness driven by the Claude Agent SDK",
5
5
  "keywords": [
6
6
  "github",