next-issue 2.4.4 → 2.4.6

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,14 +30,18 @@ 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
39
42
  the agent replaces the subject, so a hook that rejects the subject can pass.
40
- The budget is `maxCommitFixes`.
43
+ The budget is `maxCommitFixes`. Any other git failure in the commit stops
44
+ the run with an error.
41
45
  5. Open a draft pull request that closes the issue. Its title is the subject
42
46
  of the commit that starts it, so a squash merge gives a conventional
43
47
  commit.
package/dist/git.mjs CHANGED
@@ -1,4 +1,5 @@
1
- import { appendFile, readFile } from 'node:fs/promises';
1
+ import { appendFile, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
2
3
  import { join } from 'node:path';
3
4
  import { must, run } from './exec.mjs';
4
5
  const REMOTE_PATTERNS = [
@@ -28,7 +29,7 @@ export const WORKTREE_DIR = '.worktrees';
28
29
  export function worktreePath(repo, issue) {
29
30
  return join(repo.root, WORKTREE_DIR, String(issue));
30
31
  }
31
- export async function addWorktree(repo, issue, branch, base, remote) {
32
+ export async function addWorktree(repo, issue, branch, base, remote, start) {
32
33
  const upstream = `${remote}/${branch}`;
33
34
  await must('git', ['fetch', remote], { cwd: repo.root });
34
35
  const baseUpdated = await updateBase(repo, base, remote);
@@ -36,12 +37,16 @@ export async function addWorktree(repo, issue, branch, base, remote) {
36
37
  const pushed = await hasBranch(repo, upstream, '--remotes');
37
38
  const existing = await findWorktree(repo, branch);
38
39
  const path = existing ?? worktreePath(repo, issue);
40
+ const target = pushed ? upstream : `${remote}/${base}`;
39
41
  if (existing === undefined) {
40
- const args = (await hasBranch(repo, branch))
42
+ const args = !start && (await hasBranch(repo, branch))
41
43
  ? ['worktree', 'add', path, branch]
42
- : ['worktree', 'add', '-b', branch, path, pushed ? upstream : `${remote}/${base}`];
44
+ : ['worktree', 'add', '-B', branch, path, target];
43
45
  await must('git', args, { cwd: repo.root });
44
46
  }
47
+ else if (start) {
48
+ await discardChanges(path, target);
49
+ }
45
50
  if (pushed) {
46
51
  await run('git', ['merge', '--ff-only', upstream], { cwd: path });
47
52
  }
@@ -97,14 +102,34 @@ export async function isDirty(cwd) {
97
102
  export async function diff(cwd, base, remote) {
98
103
  return must('git', ['diff', `${remote}/${base}...HEAD`], { cwd });
99
104
  }
105
+ export class HookError extends Error {
106
+ }
100
107
  export async function commitAll(cwd, message) {
101
108
  if (!(await isDirty(cwd))) {
102
109
  return false;
103
110
  }
104
111
  await must('git', ['add', '-A'], { cwd });
105
- await must('git', ['commit', '-m', message], { cwd });
112
+ const folder = await mkdtemp(join(tmpdir(), 'next-issue-message-'));
113
+ const file = join(folder, 'COMMIT_EDITMSG');
114
+ try {
115
+ await writeFile(file, `${message}\n`);
116
+ await runHook(cwd, 'pre-commit');
117
+ await runHook(cwd, 'commit-msg', file);
118
+ await must('git', ['commit', '--no-verify', '-F', file], { cwd });
119
+ }
120
+ finally {
121
+ await rm(folder, { recursive: true, force: true });
122
+ }
106
123
  return true;
107
124
  }
125
+ async function runHook(cwd, name, ...args) {
126
+ try {
127
+ await must('git', ['hook', 'run', '--ignore-missing', name, '--', ...args], { cwd });
128
+ }
129
+ catch (error) {
130
+ throw new HookError(error instanceof Error ? error.message : String(error));
131
+ }
132
+ }
108
133
  export async function discardChanges(cwd, target) {
109
134
  if (!(await isDirty(cwd)) && (await revision(cwd)) === target) {
110
135
  return false;
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, disputeBranches, findWorktree, isDirty, push, removeWorktree, resetToBase, revision, setAside, } from './git.mjs';
4
+ import { addWorktree, branchName, commitAll, deleteBranch, diff, discardChanges, disputeBranches, findWorktree, HookError, isDirty, push, removeWorktree, resetToBase, revision, setAside, } from './git.mjs';
5
5
  import { commentOnPr, createPr, failedBaseLogs, failedCheckLogs, findPr, issueComments, markPrReady, mergeCommit, mergePr, openBlockers, openIssues, 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';
@@ -132,7 +132,7 @@ export async function processIssue(context, issue) {
132
132
  };
133
133
  const state = saved === undefined ? fresh : context.reset ? resetState(saved) : saved;
134
134
  try {
135
- const report = await work(context, issue, log, state);
135
+ const report = await work(context, issue, log, state, saved === undefined);
136
136
  log.event('issue.end', { outcome: report.outcome, reason: report.reason, ms: report.ms }, 'quiet');
137
137
  return report;
138
138
  }
@@ -159,7 +159,7 @@ export async function processIssue(context, issue) {
159
159
  function skipped(issue) {
160
160
  return { issue: issue.number, outcome: 'skipped', ciFixes: 0, reviewRounds: 0, ms: 0 };
161
161
  }
162
- async function work(context, issue, log, state) {
162
+ async function work(context, issue, log, state, start) {
163
163
  const started = Date.now();
164
164
  const { repo, config, ports } = context;
165
165
  const branch = state.branch;
@@ -168,7 +168,7 @@ async function work(context, issue, log, state) {
168
168
  await writeState(repo, state);
169
169
  });
170
170
  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));
171
+ const added = await log.step('worktree', { branch }, () => ports.addWorktree(repo, issue.number, branch, context.base, config.remote, start));
172
172
  const worktree = added.path;
173
173
  if (!added.baseUpdated) {
174
174
  log.event('base.behind', { base: context.base }, 'quiet');
@@ -570,7 +570,7 @@ async function commitWork(job, before, subject, role) {
570
570
  }
571
571
  catch (error) {
572
572
  const used = state.commitFixes ?? 0;
573
- if (used >= config.maxCommitFixes) {
573
+ if (!(error instanceof HookError) || used >= config.maxCommitFixes) {
574
574
  throw error;
575
575
  }
576
576
  state.commitFixes = used + 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-issue",
3
- "version": "2.4.4",
3
+ "version": "2.4.6",
4
4
  "description": "Autonomous issue harness driven by the pi SDK",
5
5
  "keywords": [
6
6
  "github",