next-issue 2.4.3 → 2.4.5
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 +12 -5
- package/dist/git.mjs +35 -2
- package/dist/pipeline.mjs +16 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,7 +37,8 @@ and exits with code 1. For each issue:
|
|
|
37
37
|
hook stops a commit, the agent that made the change gets the hook output,
|
|
38
38
|
fixes it, and the harness commits again. A `commit:` line in the reply of
|
|
39
39
|
the agent replaces the subject, so a hook that rejects the subject can pass.
|
|
40
|
-
The budget is `maxCommitFixes`.
|
|
40
|
+
The budget is `maxCommitFixes`. Any other git failure in the commit stops
|
|
41
|
+
the run with an error.
|
|
41
42
|
5. Open a draft pull request that closes the issue. Its title is the subject
|
|
42
43
|
of the commit that starts it, so a squash merge gives a conventional
|
|
43
44
|
commit.
|
|
@@ -100,13 +101,19 @@ when:
|
|
|
100
101
|
- the fixer reports that the checks fail for a reason the change did not cause,
|
|
101
102
|
such as a test already broken on the base branch or one that fails only under
|
|
102
103
|
load. It changes nothing and the reason reaches the pull request, rather than
|
|
103
|
-
the fixer spending its whole budget on a failure it cannot fix
|
|
104
|
+
the fixer spending its whole budget on a failure it cannot fix. A commit it
|
|
105
|
+
makes anyway moves aside, as for a dispute of the findings;
|
|
104
106
|
- a fixer disputes the findings and leaves a change behind, either in the
|
|
105
107
|
worktree or in a commit of its own. A dispute carries a reason, not a diff,
|
|
106
108
|
so the harness throws none of it away: an unclean worktree stays on the
|
|
107
|
-
disk, and a commit of the fixer
|
|
108
|
-
|
|
109
|
-
|
|
109
|
+
disk, and a commit of the fixer moves to a new local
|
|
110
|
+
`<n>-dispute-<commit>` branch, so the `<n>` branch keeps only what the pull
|
|
111
|
+
request holds. The worktree drops the change of that commit, unless the fixer
|
|
112
|
+
edited the worktree after the commit: then the worktree keeps both and stays
|
|
113
|
+
on the disk. Neither is pushed, so the pull request shows no such change. It
|
|
114
|
+
holds the reason of the dispute, and the change is on the machine that ran
|
|
115
|
+
the harness. The `<n>-dispute-<commit>` branches go away with the `<n>`
|
|
116
|
+
branch when the issue is done;
|
|
110
117
|
- the reviewer repeats a finding set from an earlier round, which shows a
|
|
111
118
|
ping-pong between the fixer and the reviewer, or a finding that the fixer
|
|
112
119
|
disputed and the reviewer holds to. The person then decides;
|
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 = [
|
|
@@ -97,14 +98,34 @@ export async function isDirty(cwd) {
|
|
|
97
98
|
export async function diff(cwd, base, remote) {
|
|
98
99
|
return must('git', ['diff', `${remote}/${base}...HEAD`], { cwd });
|
|
99
100
|
}
|
|
101
|
+
export class HookError extends Error {
|
|
102
|
+
}
|
|
100
103
|
export async function commitAll(cwd, message) {
|
|
101
104
|
if (!(await isDirty(cwd))) {
|
|
102
105
|
return false;
|
|
103
106
|
}
|
|
104
107
|
await must('git', ['add', '-A'], { cwd });
|
|
105
|
-
await
|
|
108
|
+
const folder = await mkdtemp(join(tmpdir(), 'next-issue-message-'));
|
|
109
|
+
const file = join(folder, 'COMMIT_EDITMSG');
|
|
110
|
+
try {
|
|
111
|
+
await writeFile(file, `${message}\n`);
|
|
112
|
+
await runHook(cwd, 'pre-commit');
|
|
113
|
+
await runHook(cwd, 'commit-msg', file);
|
|
114
|
+
await must('git', ['commit', '--no-verify', '-F', file], { cwd });
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
await rm(folder, { recursive: true, force: true });
|
|
118
|
+
}
|
|
106
119
|
return true;
|
|
107
120
|
}
|
|
121
|
+
async function runHook(cwd, name, ...args) {
|
|
122
|
+
try {
|
|
123
|
+
await must('git', ['hook', 'run', '--ignore-missing', name, '--', ...args], { cwd });
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
throw new HookError(error instanceof Error ? error.message : String(error));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
108
129
|
export async function discardChanges(cwd, target) {
|
|
109
130
|
if (!(await isDirty(cwd)) && (await revision(cwd)) === target) {
|
|
110
131
|
return false;
|
|
@@ -113,6 +134,18 @@ export async function discardChanges(cwd, target) {
|
|
|
113
134
|
await must('git', ['clean', '-fd'], { cwd });
|
|
114
135
|
return true;
|
|
115
136
|
}
|
|
137
|
+
export async function setAside(cwd, target, branch) {
|
|
138
|
+
await must('git', ['branch', branch], { cwd });
|
|
139
|
+
if (await isDirty(cwd)) {
|
|
140
|
+
await must('git', ['reset', '--soft', target], { cwd });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
await discardChanges(cwd, target);
|
|
144
|
+
}
|
|
145
|
+
export async function disputeBranches(repo, branch) {
|
|
146
|
+
const list = await must('git', ['for-each-ref', '--format=%(refname:short)', `refs/heads/${branch}-dispute-*`], { cwd: repo.root });
|
|
147
|
+
return list.split('\n').filter((line) => line.length > 0);
|
|
148
|
+
}
|
|
116
149
|
export async function push(cwd, remote, branch, force = false) {
|
|
117
150
|
await must('git', ['push', '-u', ...(force ? ['--force'] : []), remote, branch], { cwd });
|
|
118
151
|
}
|
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, findWorktree, isDirty, push, removeWorktree, resetToBase, revision, } 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';
|
|
@@ -17,6 +17,7 @@ export const PORTS = {
|
|
|
17
17
|
detectSetupCommand,
|
|
18
18
|
diff,
|
|
19
19
|
discardChanges,
|
|
20
|
+
disputeBranches,
|
|
20
21
|
failedBaseLogs,
|
|
21
22
|
failedCheckLogs,
|
|
22
23
|
findPr,
|
|
@@ -36,6 +37,7 @@ export const PORTS = {
|
|
|
36
37
|
revision,
|
|
37
38
|
runAgent,
|
|
38
39
|
runSetup: async (command, cwd, timeoutMs) => run('bash', ['-lc', command], { cwd, timeoutMs }),
|
|
40
|
+
setAside,
|
|
39
41
|
setLabel,
|
|
40
42
|
viewIssue,
|
|
41
43
|
waitForBaseChecks,
|
|
@@ -501,8 +503,10 @@ async function finish(job, outcome, reason) {
|
|
|
501
503
|
const { repo, ports } = context;
|
|
502
504
|
await cleanUp(context, log, branch);
|
|
503
505
|
if (outcome === 'done') {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
+
for (const name of [branch, ...(await ports.disputeBranches(repo, branch))]) {
|
|
507
|
+
if (!(await ports.deleteBranch(repo, name))) {
|
|
508
|
+
log.event('branch.kept', { branch: name }, 'quiet');
|
|
509
|
+
}
|
|
506
510
|
}
|
|
507
511
|
await dropState(repo, issue.number);
|
|
508
512
|
}
|
|
@@ -531,7 +535,14 @@ async function fixRound(job, reason, detail, earlier, kind, force = false, from)
|
|
|
531
535
|
}));
|
|
532
536
|
const unrelated = parseUnrelated(result.text);
|
|
533
537
|
if (unrelated !== undefined) {
|
|
534
|
-
const
|
|
538
|
+
const disputed = await ports.revision(worktree);
|
|
539
|
+
const moved = disputed !== before;
|
|
540
|
+
if (moved) {
|
|
541
|
+
const aside = `${branch}-dispute-${disputed.slice(0, 7)}`;
|
|
542
|
+
await ports.setAside(worktree, before, aside);
|
|
543
|
+
log.event('fix.aside', { branch: aside }, 'quiet');
|
|
544
|
+
}
|
|
545
|
+
const stray = moved || (await ports.isDirty(worktree));
|
|
535
546
|
log.event('fix.unrelated', { reason, note: unrelated, stray }, 'quiet');
|
|
536
547
|
return { committed: false, unrelated, stray };
|
|
537
548
|
}
|
|
@@ -559,7 +570,7 @@ async function commitWork(job, before, subject, role) {
|
|
|
559
570
|
}
|
|
560
571
|
catch (error) {
|
|
561
572
|
const used = state.commitFixes ?? 0;
|
|
562
|
-
if (used >= config.maxCommitFixes) {
|
|
573
|
+
if (!(error instanceof HookError) || used >= config.maxCommitFixes) {
|
|
563
574
|
throw error;
|
|
564
575
|
}
|
|
565
576
|
state.commitFixes = used + 1;
|