next-issue 2.4.6 → 2.4.8

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
@@ -51,7 +51,10 @@ and exits with code 1. For each issue:
51
51
  while no check failed counts as such a read, not as a red build. A repository that reports no
52
52
  check keeps asking for `checkGraceSeconds`, because a new pull request often
53
53
  shows no check yet, and only then goes straight to the review. The later
54
- gates of the same issue ask once.
54
+ gates of the same issue ask once. GitHub runs no check on a pull request
55
+ that conflicts with its base, so no check and a conflict hand the issue to
56
+ a person. While GitHub still works out whether the pull request can merge,
57
+ the run asks again until `checkTimeoutMinutes`.
55
58
  7. On a red build, give the failed job logs to the fixer agent, then go to step
56
59
  6 again. The budget is `maxCiFixes`.
57
60
  8. Let the reviewer agent judge the diff, at most `diffMaxChars` of it. The
@@ -97,7 +100,8 @@ and exits with code 1. For each issue:
97
100
  ## Stops for a loop
98
101
 
99
102
  The harness hands the issue to a person, with the `status:needs-human` label,
100
- when:
103
+ and comments the reason on the pull request, or on the issue when there is no
104
+ pull request yet, when:
101
105
 
102
106
  - the check budget or the review budget runs out;
103
107
  - a fixer round adds no commit and gives no reason, so there is no progress;
@@ -132,8 +136,8 @@ when:
132
136
  - the setup command fails, or does not finish in `setupTimeoutMinutes`;
133
137
  - a step of the run fails with an error. A `gh` call that is safe to repeat,
134
138
  such as a read or a label edit, is tried up to three times first when the
135
- network fails. A `gh pr create` or `gh pr comment` is tried once, so a lost
136
- reply never makes a duplicate.
139
+ network fails. A `gh pr create`, `gh pr comment` or `gh issue comment` is
140
+ tried once, so a lost reply never makes a duplicate.
137
141
 
138
142
  The fixer sees the findings of all earlier rounds, not only the last one, with
139
143
  its own disputes next to them. From round two, the reviewer judges only the
package/dist/github.mjs CHANGED
@@ -330,6 +330,17 @@ export async function mergePr(repo, pr) {
330
330
  const result = await run('gh', ['pr', 'merge', String(pr), '--squash', '--delete-branch', '--repo', slug(repo)], { cwd: repo.root });
331
331
  return result.code === 0;
332
332
  }
333
+ export async function prConflicts(repo, pr, options, clock = REAL_CLOCK) {
334
+ const deadline = clock.now() + options.timeoutMs;
335
+ for (;;) {
336
+ const mergeable = (await gh(repo, ['pr', 'view', String(pr), '--json', 'mergeable', '--jq', '.mergeable'])).trim();
337
+ const rest = deadline - clock.now();
338
+ if (mergeable !== 'UNKNOWN' || rest <= 0) {
339
+ return mergeable === 'CONFLICTING';
340
+ }
341
+ await clock.sleep(Math.min(options.intervalSeconds * 1000, rest));
342
+ }
343
+ }
333
344
  export async function prMerged(repo, pr) {
334
345
  return ((await gh(repo, ['pr', 'view', String(pr), '--json', 'state', '--jq', '.state'])).trim() ===
335
346
  'MERGED');
@@ -352,6 +363,9 @@ export async function mergeCommit(repo, pr, options, clock = REAL_CLOCK) {
352
363
  export async function reopenIssue(repo, issue) {
353
364
  await gh(repo, ['issue', 'reopen', String(issue)]);
354
365
  }
366
+ export async function commentOnIssue(repo, issue, body) {
367
+ await ghOnce(repo, ['issue', 'comment', String(issue), '--body-file', '-'], body);
368
+ }
355
369
  export async function commentOnPr(repo, pr, body) {
356
370
  await ghOnce(repo, ['pr', 'comment', String(pr), '--body-file', '-'], body);
357
371
  }
package/dist/pipeline.mjs CHANGED
@@ -2,7 +2,7 @@ import { CODING, REVIEWING, runAgent } from './agents.mjs';
2
2
  import { managedLabels } from './config.mjs';
3
3
  import { run } from './exec.mjs';
4
4
  import { addWorktree, branchName, commitAll, deleteBranch, diff, discardChanges, disputeBranches, findWorktree, HookError, isDirty, push, removeWorktree, resetToBase, revision, setAside, } from './git.mjs';
5
- import { commentOnPr, createPr, failedBaseLogs, failedCheckLogs, findPr, issueComments, markPrReady, mergeCommit, mergePr, openBlockers, openIssues, prMerged, reopenIssue, setLabel, viewIssue, waitForBaseChecks, waitForChecks, } from './github.mjs';
5
+ import { commentOnIssue, commentOnPr, createPr, failedBaseLogs, failedCheckLogs, findPr, issueComments, markPrReady, mergeCommit, mergePr, openBlockers, openIssues, prConflicts, prMerged, reopenIssue, setLabel, viewIssue, waitForBaseChecks, waitForChecks, } from './github.mjs';
6
6
  import { message } from './observe.mjs';
7
7
  import { capDiff, fixPrompt, hookPrompt, implementPrompt, parseCommitSubject, parseUnrelated, reviewPrompt, } from './prompts.mjs';
8
8
  import { detectSetupCommand } from './setup.mjs';
@@ -10,6 +10,7 @@ import { dropState, readState, resetState, savedIssues, writeState, } from './st
10
10
  import { blockingFindings, fingerprint, formatFindings, formatVerdict, openFindings, readVerdict, VERDICT_SCHEMA, } from './verdict.mjs';
11
11
  export const PORTS = {
12
12
  addWorktree,
13
+ commentOnIssue,
13
14
  commentOnPr,
14
15
  commitAll,
15
16
  createPr,
@@ -29,6 +30,7 @@ export const PORTS = {
29
30
  mergePr,
30
31
  openBlockers,
31
32
  openIssues,
33
+ prConflicts,
32
34
  prMerged,
33
35
  push,
34
36
  removeWorktree,
@@ -292,6 +294,14 @@ async function checkGate(job) {
292
294
  await escalate(job, 'The checks did not finish in time.');
293
295
  return { done: true, report: await finish(job, 'needs-human', 'checks timeout') };
294
296
  }
297
+ if (checks === 'none' &&
298
+ (await ports.prConflicts(repo, state.pr, {
299
+ intervalSeconds: config.checkIntervalSeconds,
300
+ timeoutMs: config.checkTimeoutMinutes * 60_000,
301
+ }))) {
302
+ await escalate(job, 'The pull request conflicts with its base, so no checks ran.');
303
+ return { done: true, report: await finish(job, 'needs-human', 'merge conflict') };
304
+ }
295
305
  if (checks !== 'fail') {
296
306
  return { done: false, fixed: false };
297
307
  }
@@ -607,8 +617,12 @@ async function handOver(context, log, issue, state, reason) {
607
617
  log.event('hand-over', { reason }, 'quiet');
608
618
  await writeState(context.repo, state);
609
619
  await setStatus(context, issue, state.pr, context.config.labels.needsHuman);
610
- if (state.pr !== undefined) {
611
- await context.ports.commentOnPr(context.repo, state.pr, `next-issue needs help: ${reason}`);
620
+ const body = `next-issue needs help: ${reason}`;
621
+ if (state.pr === undefined) {
622
+ await context.ports.commentOnIssue(context.repo, issue, body);
623
+ }
624
+ else {
625
+ await context.ports.commentOnPr(context.repo, state.pr, body);
612
626
  }
613
627
  }
614
628
  async function setStatus(context, issue, pr, label) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-issue",
3
- "version": "2.4.6",
3
+ "version": "2.4.8",
4
4
  "description": "Autonomous issue harness driven by the pi SDK",
5
5
  "keywords": [
6
6
  "github",