next-issue 0.7.0 → 0.8.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
@@ -50,6 +50,10 @@ when:
50
50
 
51
51
  - the check budget or the review budget runs out;
52
52
  - a fixer round adds no commit, so there is no progress;
53
+ - the fixer reports that the checks fail for a reason the change did not cause,
54
+ such as a test already broken on the base branch or one that fails only under
55
+ load. It changes nothing and the reason reaches the pull request, rather than
56
+ the fixer spending its whole budget on a failure it cannot fix;
53
57
  - the reviewer repeats a finding set from an earlier round, which shows a
54
58
  ping-pong between the fixer and the reviewer;
55
59
  - the reviewer gives no verdict, or the checks do not finish in time;
package/dist/pipeline.mjs CHANGED
@@ -4,7 +4,7 @@ import { run } from "./exec.mjs";
4
4
  import { addWorktree, branchName, commitAll, diff, hasWorktree, push, removeWorktree, 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
- import { capDiff, fixPrompt, implementPrompt, parseCommitSubject, reviewPrompt } from "./prompts.mjs";
7
+ import { capDiff, fixPrompt, implementPrompt, parseCommitSubject, parseUnrelated, reviewPrompt, } from "./prompts.mjs";
8
8
  import { readState, writeState } from "./state.mjs";
9
9
  import { blockingFindings, fingerprint, formatFindings, formatVerdict, isApproved, readVerdict, VERDICT_SCHEMA, } from "./verdict.mjs";
10
10
  export const PORTS = {
@@ -172,10 +172,16 @@ async function work(context, issue, log, state) {
172
172
  await writeState(repo, state);
173
173
  const logs = await ports.failedCheckLogs(repo, branch, config.logMaxChars);
174
174
  log.event("checks.logs", { chars: logs.length });
175
- const committed = await fixRound(job, "The continuous integration checks failed.", logs, history(state.reviewLog));
176
- if (!committed) {
177
- await escalate("The fixer added no commit for the failed checks.");
178
- return { done: true, report: await finish("needs-human", "no fix commit") };
175
+ const fix = await fixRound(job, "The continuous integration checks failed.", logs, history(state.reviewLog), "checks");
176
+ if (!fix.committed) {
177
+ const note = fix.unrelated;
178
+ await escalate(note === undefined
179
+ ? "The fixer added no commit for the failed checks."
180
+ : `The checks fail for a reason this change did not cause: ${note}`);
181
+ return {
182
+ done: true,
183
+ report: await finish("needs-human", note === undefined ? "no fix commit" : "unrelated failure"),
184
+ };
179
185
  }
180
186
  return { done: false, fixed: true };
181
187
  };
@@ -248,32 +254,37 @@ async function work(context, issue, log, state) {
248
254
  const earlier = history(state.reviewLog);
249
255
  state.reviewLog.push({ round: state.reviewRounds, fingerprint: mark, findings: formatFindings(blocking) });
250
256
  await writeState(repo, state);
251
- const committed = await fixRound(job, "The reviewer requested changes.", formatFindings(blocking), earlier);
252
- if (!committed) {
257
+ const fix = await fixRound(job, "The reviewer requested changes.", formatFindings(blocking), earlier, "review");
258
+ if (!fix.committed) {
253
259
  await escalate("The fixer added no commit for the review findings.");
254
260
  return finish("needs-human", "no fix commit");
255
261
  }
256
262
  }
257
263
  }
258
- async function fixRound(job, reason, detail, earlier) {
264
+ async function fixRound(job, reason, detail, earlier, kind) {
259
265
  const { context, issue, log, worktree, branch } = job;
260
266
  const { ports } = context;
261
267
  const result = await log.step("fix", { reason }, () => ports.runAgent(log, {
262
268
  name: "fixer",
263
269
  cwd: worktree,
264
- prompt: fixPrompt(issue, reason, detail, earlier),
270
+ prompt: fixPrompt(issue, reason, detail, earlier, kind),
265
271
  profile: CODING,
266
272
  model: context.config.models.fixer,
267
273
  timeoutMs: context.config.agentTimeoutMinutes * 60_000,
268
274
  }));
275
+ const unrelated = parseUnrelated(result.text);
276
+ if (unrelated !== undefined) {
277
+ log.event("fix.unrelated", { reason, note: unrelated }, "quiet");
278
+ return { committed: false, unrelated };
279
+ }
269
280
  const subject = parseCommitSubject(result.text, `fix: address feedback on issue #${issue.number}`);
270
281
  if (!(await ports.commitAll(worktree, `${subject}\n\nRefs #${issue.number}`))) {
271
282
  log.event("commit.empty", { reason }, "quiet");
272
- return false;
283
+ return { committed: false };
273
284
  }
274
285
  log.event("commit", { subject });
275
286
  await ports.push(worktree, context.config.remote, branch);
276
- return true;
287
+ return { committed: true };
277
288
  }
278
289
  async function cleanUp(context, log, issue) {
279
290
  const path = worktreePath(context.repo, issue);
package/dist/prompts.mjs CHANGED
@@ -49,7 +49,12 @@ export function reviewPrompt(issue, comments, diff, round, earlier) {
49
49
  "Give the result as the structured output: the verdict, a short summary and one entry per finding.",
50
50
  ].join("\n\n");
51
51
  }
52
- export function fixPrompt(issue, reason, detail, earlier) {
52
+ const UNRELATED_RULE = [
53
+ "The checks can fail for a reason this change did not cause: a test that is already broken on the base branch, one that exercises code the diff does not touch, or one that fails only under load.",
54
+ "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>`.",
55
+ "A person then reads it. Do not weaken, skip or re-record a test to make the checks pass.",
56
+ ].join("\n");
57
+ export function fixPrompt(issue, reason, detail, earlier, kind) {
53
58
  return [
54
59
  `You are the fixer. Correct the work for issue #${issue.number}: ${issue.title}.`,
55
60
  `## Reason\n${reason}`,
@@ -63,11 +68,17 @@ export function fixPrompt(issue, reason, detail, earlier) {
63
68
  : "",
64
69
  "## Rules",
65
70
  "Fix the cause, not the symptom. Keep the change minimal.",
71
+ kind === "checks" ? UNRELATED_RULE : "",
66
72
  COMMIT_RULES,
67
73
  ]
68
74
  .filter((part) => part.length > 0)
69
75
  .join("\n\n");
70
76
  }
77
+ export function parseUnrelated(text) {
78
+ const match = /^unrelated:\s*(.+)$/im.exec(text);
79
+ const note = match?.[1]?.trim();
80
+ return note !== undefined && note.length > 0 ? note : undefined;
81
+ }
71
82
  export function parseCommitSubject(text, fallback) {
72
83
  const match = /^commit:\s*(.+)$/im.exec(text);
73
84
  const subject = match?.[1]?.trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-issue",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Autonomous issue harness driven by the Claude Agent SDK",
5
5
  "keywords": [
6
6
  "github",