next-issue 2.4.5 → 2.4.7

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
@@ -30,9 +30,12 @@ and exits with code 1. For each issue:
30
30
  state keeps the branch name that the state holds, and a worktree that
31
31
  holds that branch already stays in use. A branch that exists on the
32
32
  remote is the start, and a local branch that fell behind the remote is
33
- fast-forwarded. A fast-forward of the base that is not safe leaves the
34
- local base branch as it is and writes a `base.behind` event. A new branch
35
- then starts from the remote base.
33
+ fast-forwarded. An issue with no saved state does not reuse a local
34
+ `<n>` branch: it resets that branch, and any worktree that holds it, to
35
+ the remote branch, or to the remote base when there is none. A
36
+ fast-forward of the base that is not safe leaves the local base branch as
37
+ it is and writes a `base.behind` event. A new branch then starts from the
38
+ remote base.
36
39
  4. Let the implementer agent do the work, then commit and push. When a git
37
40
  hook stops a commit, the agent that made the change gets the hook output,
38
41
  fixes it, and the harness commits again. A `commit:` line in the reply of
@@ -48,7 +51,10 @@ and exits with code 1. For each issue:
48
51
  while no check failed counts as such a read, not as a red build. A repository that reports no
49
52
  check keeps asking for `checkGraceSeconds`, because a new pull request often
50
53
  shows no check yet, and only then goes straight to the review. The later
51
- 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`.
52
58
  7. On a red build, give the failed job logs to the fixer agent, then go to step
53
59
  6 again. The budget is `maxCiFixes`.
54
60
  8. Let the reviewer agent judge the diff, at most `diffMaxChars` of it. The
package/dist/git.mjs CHANGED
@@ -29,7 +29,7 @@ export const WORKTREE_DIR = '.worktrees';
29
29
  export function worktreePath(repo, issue) {
30
30
  return join(repo.root, WORKTREE_DIR, String(issue));
31
31
  }
32
- export async function addWorktree(repo, issue, branch, base, remote) {
32
+ export async function addWorktree(repo, issue, branch, base, remote, start) {
33
33
  const upstream = `${remote}/${branch}`;
34
34
  await must('git', ['fetch', remote], { cwd: repo.root });
35
35
  const baseUpdated = await updateBase(repo, base, remote);
@@ -37,12 +37,16 @@ export async function addWorktree(repo, issue, branch, base, remote) {
37
37
  const pushed = await hasBranch(repo, upstream, '--remotes');
38
38
  const existing = await findWorktree(repo, branch);
39
39
  const path = existing ?? worktreePath(repo, issue);
40
+ const target = pushed ? upstream : `${remote}/${base}`;
40
41
  if (existing === undefined) {
41
- const args = (await hasBranch(repo, branch))
42
+ const args = !start && (await hasBranch(repo, branch))
42
43
  ? ['worktree', 'add', path, branch]
43
- : ['worktree', 'add', '-b', branch, path, pushed ? upstream : `${remote}/${base}`];
44
+ : ['worktree', 'add', '-B', branch, path, target];
44
45
  await must('git', args, { cwd: repo.root });
45
46
  }
47
+ else if (start) {
48
+ await discardChanges(path, target);
49
+ }
46
50
  if (pushed) {
47
51
  await run('git', ['merge', '--ff-only', upstream], { cwd: path });
48
52
  }
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');
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 { 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';
@@ -29,6 +29,7 @@ export const PORTS = {
29
29
  mergePr,
30
30
  openBlockers,
31
31
  openIssues,
32
+ prConflicts,
32
33
  prMerged,
33
34
  push,
34
35
  removeWorktree,
@@ -132,7 +133,7 @@ export async function processIssue(context, issue) {
132
133
  };
133
134
  const state = saved === undefined ? fresh : context.reset ? resetState(saved) : saved;
134
135
  try {
135
- const report = await work(context, issue, log, state);
136
+ const report = await work(context, issue, log, state, saved === undefined);
136
137
  log.event('issue.end', { outcome: report.outcome, reason: report.reason, ms: report.ms }, 'quiet');
137
138
  return report;
138
139
  }
@@ -159,7 +160,7 @@ export async function processIssue(context, issue) {
159
160
  function skipped(issue) {
160
161
  return { issue: issue.number, outcome: 'skipped', ciFixes: 0, reviewRounds: 0, ms: 0 };
161
162
  }
162
- async function work(context, issue, log, state) {
163
+ async function work(context, issue, log, state, start) {
163
164
  const started = Date.now();
164
165
  const { repo, config, ports } = context;
165
166
  const branch = state.branch;
@@ -168,7 +169,7 @@ async function work(context, issue, log, state) {
168
169
  await writeState(repo, state);
169
170
  });
170
171
  const comments = await log.step('comments', {}, () => ports.issueComments(repo, issue.number));
171
- const added = await log.step('worktree', { branch }, () => ports.addWorktree(repo, issue.number, branch, context.base, config.remote));
172
+ const added = await log.step('worktree', { branch }, () => ports.addWorktree(repo, issue.number, branch, context.base, config.remote, start));
172
173
  const worktree = added.path;
173
174
  if (!added.baseUpdated) {
174
175
  log.event('base.behind', { base: context.base }, 'quiet');
@@ -292,6 +293,14 @@ async function checkGate(job) {
292
293
  await escalate(job, 'The checks did not finish in time.');
293
294
  return { done: true, report: await finish(job, 'needs-human', 'checks timeout') };
294
295
  }
296
+ if (checks === 'none' &&
297
+ (await ports.prConflicts(repo, state.pr, {
298
+ intervalSeconds: config.checkIntervalSeconds,
299
+ timeoutMs: config.checkTimeoutMinutes * 60_000,
300
+ }))) {
301
+ await escalate(job, 'The pull request conflicts with its base, so no checks ran.');
302
+ return { done: true, report: await finish(job, 'needs-human', 'merge conflict') };
303
+ }
295
304
  if (checks !== 'fail') {
296
305
  return { done: false, fixed: false };
297
306
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-issue",
3
- "version": "2.4.5",
3
+ "version": "2.4.7",
4
4
  "description": "Autonomous issue harness driven by the pi SDK",
5
5
  "keywords": [
6
6
  "github",