next-issue 2.14.0 → 2.15.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 +37 -24
- package/dist/config.mjs +2 -0
- package/dist/github.mjs +1 -1
- package/dist/handover.mjs +17 -3
- package/dist/interrupt.mjs +2 -2
- package/dist/job.mjs +6 -0
- package/dist/loop.mjs +43 -17
- package/dist/pipeline.mjs +4 -4
- package/dist/progress.mjs +26 -14
- package/dist/review.mjs +1 -7
- package/dist/run.mjs +1 -0
- package/dist/state.mjs +0 -1
- package/dist/verdict.mjs +0 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,8 +12,16 @@ npx -y next-issue@latest
|
|
|
12
12
|
Before each issue, fetch the open issues again and take the oldest one not yet
|
|
13
13
|
seen in this run. An issue seen once in a run, also a skipped or failed one, is
|
|
14
14
|
not tried again in that run. The run ends when no unseen issue is left. When
|
|
15
|
-
the fetch fails, the run
|
|
16
|
-
and exits with code 1.
|
|
15
|
+
the fetch fails, the run claims no new issue, ends once the running issues end,
|
|
16
|
+
writes the summary of the issues it handled and exits with code 1.
|
|
17
|
+
|
|
18
|
+
A run handles up to `concurrency` issues at the same time, 1 by default. When an
|
|
19
|
+
issue ends, the run fetches the open issues again and claims the oldest unseen
|
|
20
|
+
one for the free slot. Each issue has its own worktree and branch. Step 3 below
|
|
21
|
+
and the removal of a worktree or branch run for one issue at a time, so two
|
|
22
|
+
issues never change the git refs or worktrees of the repository at once. An issue blocked by a running issue is skipped and not tried again in
|
|
23
|
+
that run, even when the blocker closes before the run ends. `--max <n>` counts the handled issues across the run, and with `--once`
|
|
24
|
+
the run handles one issue. For each issue:
|
|
17
25
|
|
|
18
26
|
1. Claim the issue only when it holds the `status:todo` label and holds no
|
|
19
27
|
`status:in-progress` or `status:in-review` label. A `status:done`,
|
|
@@ -104,7 +112,7 @@ and exits with code 1. For each issue:
|
|
|
104
112
|
|
|
105
113
|
## Issue format
|
|
106
114
|
|
|
107
|
-
Every agent prompt holds the body and the comments of the
|
|
115
|
+
Every agent prompt holds the body of the issue and the comments of users with write or admin access to the repository. An issue reads best with these sections, in this order:
|
|
108
116
|
|
|
109
117
|
1. `Problem`
|
|
110
118
|
2. `Context and findings`
|
|
@@ -120,7 +128,9 @@ The harness hands the issue to a person, with the `status:needs-human` label,
|
|
|
120
128
|
and comments the reason on the pull request, or on the issue when there is no
|
|
121
129
|
pull request yet, when:
|
|
122
130
|
|
|
123
|
-
- the check budget or the review budget runs out
|
|
131
|
+
- the check budget or the review budget runs out. A finding that the reviewer
|
|
132
|
+
repeats, even one that the fixer disputed, goes back to the fixer until the
|
|
133
|
+
review budget runs out;
|
|
124
134
|
- a fixer round adds no commit and gives no reason, so there is no progress;
|
|
125
135
|
- the fixer reports that the checks fail for a reason the change did not cause,
|
|
126
136
|
such as a test already broken on the base branch or one that fails only under
|
|
@@ -138,9 +148,6 @@ pull request yet, when:
|
|
|
138
148
|
holds the reason of the dispute, and the change is on the machine that ran
|
|
139
149
|
the harness. The `<n>-dispute-<commit>` branches go away with the `<n>`
|
|
140
150
|
branch when the issue is done;
|
|
141
|
-
- the reviewer repeats a finding set from an earlier round, which shows a
|
|
142
|
-
ping-pong between the fixer and the reviewer, or a finding that the fixer
|
|
143
|
-
disputed and the reviewer holds to. The person then decides;
|
|
144
151
|
- the reviewer gives no verdict, or the checks do not finish in time;
|
|
145
152
|
- someone pushes to the pull request while the harness waits for the checks,
|
|
146
153
|
so the pull request no longer shows the commit that the harness pushed;
|
|
@@ -150,7 +157,8 @@ pull request yet, when:
|
|
|
150
157
|
the pull request closes without a merge;
|
|
151
158
|
- with `--merge`, the checks on the base branch fail after the merge and the
|
|
152
159
|
fixer cannot fix them, or they do not finish in time. The base branch is
|
|
153
|
-
then broken, so the run
|
|
160
|
+
then broken, so the run claims no new issue and ends once the running
|
|
161
|
+
issues end;
|
|
154
162
|
- an agent does not finish in `agentTimeoutMinutes`;
|
|
155
163
|
- the setup command fails, or does not finish in `setupTimeoutMinutes`;
|
|
156
164
|
- a step of the run fails with an error. A `gh` call that is safe to repeat,
|
|
@@ -175,12 +183,12 @@ exception is the answer to a hand-over, see
|
|
|
175
183
|
|
|
176
184
|
Press `Ctrl-C` to stop the run step by step:
|
|
177
185
|
|
|
178
|
-
1. The first press stops
|
|
179
|
-
|
|
180
|
-
2. The second press stops
|
|
181
|
-
one checks round (wait, then fix) or one review round
|
|
182
|
-
issue ends as `stopped`. Its state, worktree and
|
|
183
|
-
carries on from that point.
|
|
186
|
+
1. The first press stops new claims. The harness finishes every issue that it
|
|
187
|
+
holds and then writes the summary as usual.
|
|
188
|
+
2. The second press stops every running issue after its current round: the
|
|
189
|
+
implementation, one checks round (wait, then fix) or one review round
|
|
190
|
+
(review, then fix). The issue ends as `stopped`. Its state, worktree and
|
|
191
|
+
labels stay, so the next run carries on from that point.
|
|
184
192
|
3. The third press stops every agent, kills the command that runs and every
|
|
185
193
|
child of it, so the run dies in the middle of a step. The saved state lets the next run continue the
|
|
186
194
|
issue, but the issue keeps `status:in-progress` and the run writes no
|
|
@@ -208,12 +216,15 @@ Every agent can run a shell command. The implementer and the fixer run with the
|
|
|
208
216
|
permission checks off; the reviewer runs with a smaller tool set, but its shell
|
|
209
217
|
has no limit on the commands. They all start in the worktree, but nothing holds
|
|
210
218
|
them there: they can run any command, with your rights, anywhere on the machine.
|
|
211
|
-
The prompt holds the title
|
|
212
|
-
|
|
219
|
+
The prompt holds the title and the body of the issue, which come from GitHub.
|
|
220
|
+
It also holds the comments of the issue, but only those by users with write or
|
|
221
|
+
admin access to the repository. The harness leaves out every other comment and
|
|
222
|
+
writes a `comment.skip` event with its author to the run log. If an access check
|
|
223
|
+
fails for a reason other than a 404, the issue stops with an error.
|
|
213
224
|
|
|
214
|
-
Thus a person who can write an issue or a comment can try to
|
|
215
|
-
instruction to the agent. Use the harness only on a repository where you
|
|
216
|
-
the issue authors, or run it in a container.
|
|
225
|
+
Thus a person who can write an issue, or a writer who can comment, can try to
|
|
226
|
+
give an instruction to the agent. Use the harness only on a repository where you
|
|
227
|
+
trust the issue authors, or run it in a container.
|
|
217
228
|
|
|
218
229
|
## Observability
|
|
219
230
|
|
|
@@ -298,6 +309,7 @@ does not know all stop the run, so a typo cannot pass without a word.
|
|
|
298
309
|
{
|
|
299
310
|
"remote": "origin",
|
|
300
311
|
"issueLimit": 100,
|
|
312
|
+
"concurrency": 1,
|
|
301
313
|
"maxCiFixes": 5,
|
|
302
314
|
"maxCommitFixes": 10,
|
|
303
315
|
"maxReviewRounds": 5,
|
|
@@ -331,6 +343,7 @@ does not know all stop the run, so a typo cannot pass without a word.
|
|
|
331
343
|
| ----------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
332
344
|
| `remote` | `origin` | The git remote for the repository and the branches |
|
|
333
345
|
| `issueLimit` | `100` | The maximum number of open issues to read. With `labels.ready` set, only issues with the ready, in-progress or in-review label count |
|
|
346
|
+
| `concurrency` | `1` | The most issues that a run handles at the same time |
|
|
334
347
|
| `maxCiFixes` | `5` | The budget for check fixes per issue |
|
|
335
348
|
| `maxCommitFixes` | `10` | The budget for fixes of a commit that a git hook stopped, per issue |
|
|
336
349
|
| `maxReviewRounds` | `5` | The budget for review rounds per issue |
|
|
@@ -453,7 +466,7 @@ and `--once` is the same as `--max 1`.
|
|
|
453
466
|
The watch stops:
|
|
454
467
|
|
|
455
468
|
- at the first `Ctrl-C` during a wait, at once;
|
|
456
|
-
- at the first `Ctrl-C` during an issue, after
|
|
469
|
+
- at the first `Ctrl-C` during an issue, after the running issues;
|
|
457
470
|
- after a pass that halts the run, for example when the base branch is broken
|
|
458
471
|
after `--merge`;
|
|
459
472
|
- when the open issues cannot be read.
|
|
@@ -475,8 +488,8 @@ to it. The worker:
|
|
|
475
488
|
once. Each run is like `next-issue` without `--watch`, and the client says
|
|
476
489
|
whether it merges;
|
|
477
490
|
- sends every client the progress of every run when it connects and on every
|
|
478
|
-
change: the issue, the step, the tool count
|
|
479
|
-
issues;
|
|
491
|
+
change: for each running issue the issue, the step, the tool count and the
|
|
492
|
+
last tool, and the handled issues;
|
|
480
493
|
- sends every client a `questions` message for each repository: the open
|
|
481
494
|
issues with the `needsHuman` label, each with its number, title, the reason
|
|
482
495
|
of the hand-over, the url of the comment that holds it, the `owner/name`
|
|
@@ -491,8 +504,8 @@ to it. The worker:
|
|
|
491
504
|
a newer one. When the client adds an `"id"` to its message, the list that
|
|
492
505
|
answers it carries the same `id` and is read after the message arrived. When the list cannot be read, the client gets
|
|
493
506
|
`{ "type": "questions-error", "root": "<path>", "message": "<text>" }`;
|
|
494
|
-
- stops a run, when a client asks, like a second `Ctrl-C`:
|
|
495
|
-
round, with the state kept;
|
|
507
|
+
- stops a run, when a client asks, like a second `Ctrl-C`: each running issue
|
|
508
|
+
after its current round, with the state kept;
|
|
496
509
|
- stays when all clients close;
|
|
497
510
|
- closes once no run is going when a client with a newer version connects, so
|
|
498
511
|
that the client can start the newer worker;
|
package/dist/config.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { message } from './observe.mjs';
|
|
|
4
4
|
const DEFAULTS = {
|
|
5
5
|
remote: 'origin',
|
|
6
6
|
issueLimit: 100,
|
|
7
|
+
concurrency: 1,
|
|
7
8
|
maxCiFixes: 5,
|
|
8
9
|
maxCommitFixes: 10,
|
|
9
10
|
maxReviewRounds: 5,
|
|
@@ -33,6 +34,7 @@ function defaults() {
|
|
|
33
34
|
const FIELDS = [...Object.keys(DEFAULTS), 'setupCommand'];
|
|
34
35
|
const WHOLE_FIELDS = [
|
|
35
36
|
['issueLimit', 1],
|
|
37
|
+
['concurrency', 1],
|
|
36
38
|
['maxCiFixes', 0],
|
|
37
39
|
['maxCommitFixes', 0],
|
|
38
40
|
['maxReviewRounds', 1],
|
package/dist/github.mjs
CHANGED
|
@@ -72,7 +72,7 @@ export async function viewIssue(repo, issue) {
|
|
|
72
72
|
export async function issueComments(repo, issue) {
|
|
73
73
|
const raw = await gh(repo, ['issue', 'view', String(issue), '--json', 'comments']);
|
|
74
74
|
const parsed = JSON.parse(raw);
|
|
75
|
-
return parsed.comments.map((comment) =>
|
|
75
|
+
return parsed.comments.map((comment) => ({ author: comment.author.login, body: comment.body }));
|
|
76
76
|
}
|
|
77
77
|
export async function threadComments(repo, number) {
|
|
78
78
|
const raw = await repeatable([
|
package/dist/handover.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { setStatus } from './job.mjs';
|
|
1
|
+
import { oneAtATime, setStatus, } from './job.mjs';
|
|
2
2
|
import { message } from './observe.mjs';
|
|
3
3
|
import { dropState, readState, writeState } from './state.mjs';
|
|
4
4
|
export const HAND_OVER = 'next-issue needs help:';
|
|
@@ -40,6 +40,20 @@ export async function waitingQuestions(repo, config, ports) {
|
|
|
40
40
|
}
|
|
41
41
|
return questions;
|
|
42
42
|
}
|
|
43
|
+
export async function writerComments(context, log, issue) {
|
|
44
|
+
const { repo, ports } = context;
|
|
45
|
+
const writer = writers(repo, ports);
|
|
46
|
+
const kept = [];
|
|
47
|
+
for (const comment of await ports.issueComments(repo, issue)) {
|
|
48
|
+
if (await writer(comment.author)) {
|
|
49
|
+
kept.push(`@${comment.author}: ${comment.body}`);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
log.event('comment.skip', { author: comment.author }, 'quiet');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return kept;
|
|
56
|
+
}
|
|
43
57
|
export async function handOverReply(context, issue, pr) {
|
|
44
58
|
const { repo, ports } = context;
|
|
45
59
|
const comments = await ports.threadComments(repo, pr ?? issue);
|
|
@@ -77,7 +91,7 @@ export async function finish(job, outcome, reason) {
|
|
|
77
91
|
await cleanUp(context, log, branch);
|
|
78
92
|
if (outcome === 'done') {
|
|
79
93
|
for (const name of [branch, ...(await ports.disputeBranches(repo, branch))]) {
|
|
80
|
-
if (!(await ports.deleteBranch(repo, name))) {
|
|
94
|
+
if (!(await oneAtATime(repo.root, () => ports.deleteBranch(repo, name)))) {
|
|
81
95
|
log.event('branch.kept', { branch: name }, 'quiet');
|
|
82
96
|
}
|
|
83
97
|
}
|
|
@@ -100,7 +114,7 @@ export async function cleanUp(context, log, branch) {
|
|
|
100
114
|
if (path === undefined) {
|
|
101
115
|
return;
|
|
102
116
|
}
|
|
103
|
-
if (!(await context.ports.removeWorktree(context.repo, path))) {
|
|
117
|
+
if (!(await oneAtATime(context.repo.root, () => context.ports.removeWorktree(context.repo, path)))) {
|
|
104
118
|
log.event('worktree.kept', { path }, 'quiet');
|
|
105
119
|
}
|
|
106
120
|
}
|
package/dist/interrupt.mjs
CHANGED
|
@@ -9,10 +9,10 @@ export function listenForInterrupts(say) {
|
|
|
9
9
|
wake();
|
|
10
10
|
}
|
|
11
11
|
else if (presses === 1) {
|
|
12
|
-
say('Stopping after
|
|
12
|
+
say('Stopping after the running issues. Press Ctrl-C again to stop after the current round.');
|
|
13
13
|
}
|
|
14
14
|
else if (presses === 2) {
|
|
15
|
-
say('Stopping after
|
|
15
|
+
say('Stopping after the current round. Press Ctrl-C again to stop now.');
|
|
16
16
|
}
|
|
17
17
|
else {
|
|
18
18
|
process.off('SIGINT', press);
|
package/dist/job.mjs
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { managedLabels } from './config.mjs';
|
|
2
2
|
import { writeState } from './state.mjs';
|
|
3
|
+
const worktreeQueues = new Map();
|
|
4
|
+
export function oneAtATime(root, work) {
|
|
5
|
+
const result = (worktreeQueues.get(root) ?? Promise.resolve()).then(work);
|
|
6
|
+
worktreeQueues.set(root, result.catch(() => undefined));
|
|
7
|
+
return result;
|
|
8
|
+
}
|
|
3
9
|
export async function openPr(job) {
|
|
4
10
|
const { context, issue, log, state, branch } = job;
|
|
5
11
|
const { repo, config, ports } = context;
|
package/dist/loop.mjs
CHANGED
|
@@ -1,29 +1,55 @@
|
|
|
1
1
|
export const BACK_OFF_MINUTES = [1, 2, 3, 5, 10];
|
|
2
2
|
export async function runIssues(args, loop, seen = new Set()) {
|
|
3
|
+
const limit = args.once ? Math.min(1, args.max ?? 1) : args.max;
|
|
4
|
+
const concurrency = loop.concurrency ?? 1;
|
|
3
5
|
const reports = [];
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
const running = new Map();
|
|
7
|
+
let stoppedAt;
|
|
8
|
+
let error;
|
|
9
|
+
let crash;
|
|
10
|
+
let claiming = true;
|
|
11
|
+
const room = () => running.size < concurrency && (limit === undefined || handled(reports) + running.size < limit);
|
|
12
|
+
for (;;) {
|
|
13
|
+
if (claiming && room()) {
|
|
14
|
+
try {
|
|
15
|
+
const issues = await loop.issues();
|
|
16
|
+
for (const issue of issues.filter((candidate) => !seen.has(candidate.number))) {
|
|
17
|
+
if (!room()) {
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
seen.add(issue.number);
|
|
21
|
+
running.set(issue.number, loop.process(issue).then((report) => ({ issue: issue.number, report }), (failure) => ({ issue: issue.number, failure })));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch (failure) {
|
|
25
|
+
error = failure;
|
|
26
|
+
claiming = false;
|
|
27
|
+
}
|
|
11
28
|
}
|
|
12
|
-
|
|
13
|
-
if (issue === undefined) {
|
|
29
|
+
if (running.size === 0) {
|
|
14
30
|
break;
|
|
15
31
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
32
|
+
const { issue, report, failure } = await Promise.race(running.values());
|
|
33
|
+
running.delete(issue);
|
|
34
|
+
if (report === undefined) {
|
|
35
|
+
crash ??= { failure };
|
|
36
|
+
claiming = false;
|
|
37
|
+
continue;
|
|
21
38
|
}
|
|
22
|
-
|
|
23
|
-
|
|
39
|
+
reports.push(report);
|
|
40
|
+
if (claiming && (loop.stop() || report.halt === true)) {
|
|
41
|
+
stoppedAt = issue;
|
|
42
|
+
claiming = false;
|
|
24
43
|
}
|
|
25
44
|
}
|
|
26
|
-
|
|
45
|
+
if (crash !== undefined) {
|
|
46
|
+
throw crash.failure;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
reports,
|
|
50
|
+
...(stoppedAt === undefined ? {} : { stoppedAt }),
|
|
51
|
+
...(error === undefined ? {} : { error }),
|
|
52
|
+
};
|
|
27
53
|
}
|
|
28
54
|
export async function watchIssues(args, watch) {
|
|
29
55
|
const limit = args.once ? 1 : args.max;
|
package/dist/pipeline.mjs
CHANGED
|
@@ -2,8 +2,8 @@ import { CODING } from './agents.mjs';
|
|
|
2
2
|
import { commitWork } from './fix.mjs';
|
|
3
3
|
import { checkGate } from './gate.mjs';
|
|
4
4
|
import { branchName } from './git.mjs';
|
|
5
|
-
import { cleanUp, escalate, finish, handOver, handOverReply, skipped, stop } from './handover.mjs';
|
|
6
|
-
import { openPr, setStatus } from './job.mjs';
|
|
5
|
+
import { cleanUp, escalate, finish, handOver, handOverReply, skipped, stop, writerComments, } from './handover.mjs';
|
|
6
|
+
import { oneAtATime, openPr, setStatus } from './job.mjs';
|
|
7
7
|
import { afterMerge, fixBase } from './merge.mjs';
|
|
8
8
|
import { message } from './observe.mjs';
|
|
9
9
|
import { implementPrompt, parseCommitSubject } from './prompts.mjs';
|
|
@@ -144,11 +144,11 @@ async function work(context, issue, log, state, start, handedOver) {
|
|
|
144
144
|
await setStatus(context, issue.number, state.pr, config.labels.inProgress);
|
|
145
145
|
await writeState(repo, state);
|
|
146
146
|
});
|
|
147
|
-
const comments = await log.step('comments', {}, () =>
|
|
147
|
+
const comments = await log.step('comments', {}, () => writerComments(context, log, issue.number));
|
|
148
148
|
const reply = handedOver
|
|
149
149
|
? await log.step('reply', {}, () => handOverReply(context, issue.number, state.pr))
|
|
150
150
|
: [];
|
|
151
|
-
const added = await log.step('worktree', { branch }, () => ports.addWorktree(repo, issue.number, branch, context.base, config.remote, start));
|
|
151
|
+
const added = await log.step('worktree', { branch }, () => oneAtATime(repo.root, () => ports.addWorktree(repo, issue.number, branch, context.base, config.remote, start)));
|
|
152
152
|
const worktree = added.path;
|
|
153
153
|
if (!added.baseUpdated) {
|
|
154
154
|
log.event('base.behind', { base: context.base }, 'quiet');
|
package/dist/progress.mjs
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
const TOOL_LINE_MAX = 80;
|
|
2
2
|
const NOT_STAGES = new Set(['issue.start', 'agent.start', 'run.start']);
|
|
3
3
|
export function startProgress() {
|
|
4
|
-
return { status: 'running',
|
|
5
|
-
}
|
|
6
|
-
function idle(progress) {
|
|
7
|
-
const { issue: _issue, title: _title, stage: _stage, lastTool: _lastTool, issueStartedAt: _at, ...rest } = progress;
|
|
8
|
-
return { ...rest, tools: 0 };
|
|
4
|
+
return { status: 'running', issues: [], handled: [] };
|
|
9
5
|
}
|
|
10
6
|
function toolLine(record) {
|
|
11
7
|
const tool = String(record['tool']);
|
|
@@ -13,36 +9,52 @@ function toolLine(record) {
|
|
|
13
9
|
const line = typeof command === 'string' ? `${tool} ${command.split('\n')[0]}` : tool;
|
|
14
10
|
return line.length > TOOL_LINE_MAX ? `${line.slice(0, TOOL_LINE_MAX - 1)}…` : line;
|
|
15
11
|
}
|
|
12
|
+
function change(progress, issue, update) {
|
|
13
|
+
return {
|
|
14
|
+
...progress,
|
|
15
|
+
issues: progress.issues.map((current) => (current.issue === issue ? update(current) : current)),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
16
18
|
export function applyEvent(progress, record) {
|
|
17
19
|
const kind = String(record['kind']);
|
|
20
|
+
const issue = Number(record['issue']);
|
|
18
21
|
if (kind === 'issue.start') {
|
|
19
22
|
const next = {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
issue,
|
|
24
|
+
tools: 0,
|
|
25
|
+
startedAt: Date.parse(String(record['ts'])),
|
|
23
26
|
};
|
|
24
27
|
if (typeof record['title'] === 'string') {
|
|
25
28
|
next.title = record['title'];
|
|
26
29
|
}
|
|
27
|
-
|
|
30
|
+
const others = progress.issues.filter((current) => current.issue !== issue);
|
|
31
|
+
return { ...progress, issues: [...others, next] };
|
|
28
32
|
}
|
|
29
33
|
if (kind === 'tool') {
|
|
30
|
-
return
|
|
34
|
+
return change(progress, issue, (current) => ({
|
|
35
|
+
...current,
|
|
36
|
+
tools: current.tools + 1,
|
|
37
|
+
lastTool: toolLine(record),
|
|
38
|
+
}));
|
|
31
39
|
}
|
|
32
40
|
if (kind === 'issue.end' || kind === 'issue.error') {
|
|
33
41
|
const outcome = kind === 'issue.error' ? 'error' : record['outcome'];
|
|
34
42
|
const handled = outcome === 'done' || outcome === 'needs-human' || outcome === 'error'
|
|
35
|
-
? [...progress.handled, { issue
|
|
43
|
+
? [...progress.handled, { issue, outcome }]
|
|
36
44
|
: progress.handled;
|
|
37
|
-
|
|
45
|
+
const issues = progress.issues.filter((current) => current.issue !== issue);
|
|
46
|
+
return { ...progress, issues, handled };
|
|
38
47
|
}
|
|
39
48
|
if (kind === 'issues.fail') {
|
|
40
49
|
return { ...progress, error: String(record['error']) };
|
|
41
50
|
}
|
|
42
|
-
if (kind.endsWith('.start') && !NOT_STAGES.has(kind)
|
|
51
|
+
if (kind.endsWith('.start') && !NOT_STAGES.has(kind)) {
|
|
43
52
|
const stage = kind.slice(0, -'.start'.length);
|
|
44
53
|
const round = record['round'];
|
|
45
|
-
return
|
|
54
|
+
return change(progress, issue, (current) => ({
|
|
55
|
+
...current,
|
|
56
|
+
stage: typeof round === 'number' ? `${stage} ${round}` : stage,
|
|
57
|
+
}));
|
|
46
58
|
}
|
|
47
59
|
return progress;
|
|
48
60
|
}
|
package/dist/review.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { history, setStatus } from './job.mjs';
|
|
|
6
6
|
import { afterMerge } from './merge.mjs';
|
|
7
7
|
import { capDiff, reviewPrompt } from './prompts.mjs';
|
|
8
8
|
import { writeState } from './state.mjs';
|
|
9
|
-
import { blockingFindings,
|
|
9
|
+
import { blockingFindings, formatFindings, formatVerdict, openFindings, parseVerdict, } from './verdict.mjs';
|
|
10
10
|
export async function reviewRound(job) {
|
|
11
11
|
const { context, issue, log, state, worktree, comments } = job;
|
|
12
12
|
const { repo, config, ports } = context;
|
|
@@ -73,15 +73,9 @@ export async function reviewRound(job) {
|
|
|
73
73
|
await setStatus(context, issue.number, pr, config.labels.done);
|
|
74
74
|
return { done: true, report: await finish(job, 'done') };
|
|
75
75
|
}
|
|
76
|
-
const mark = fingerprint(open);
|
|
77
|
-
if (state.reviewLog.some((round) => round.fingerprint === mark)) {
|
|
78
|
-
await escalate(job, 'The reviewer repeated findings that an earlier round did not fix.');
|
|
79
|
-
return { done: true, report: await finish(job, 'needs-human', 'repeated findings') };
|
|
80
|
-
}
|
|
81
76
|
const earlier = history(state.reviewLog);
|
|
82
77
|
state.reviewLog.push({
|
|
83
78
|
round: state.reviewRounds,
|
|
84
|
-
fingerprint: mark,
|
|
85
79
|
findings: formatFindings(open),
|
|
86
80
|
});
|
|
87
81
|
await writeState(repo, state);
|
package/dist/run.mjs
CHANGED
|
@@ -68,6 +68,7 @@ async function handle(args, config, repo, recorder, controls) {
|
|
|
68
68
|
},
|
|
69
69
|
process: (issue) => processIssue(context, issue),
|
|
70
70
|
stop: interrupts.afterIssue,
|
|
71
|
+
concurrency: config.concurrency,
|
|
71
72
|
};
|
|
72
73
|
const print = controls.print ?? (async () => undefined);
|
|
73
74
|
let passStart = recorder.summary();
|
package/dist/state.mjs
CHANGED
package/dist/verdict.mjs
CHANGED
|
@@ -54,12 +54,3 @@ export function formatVerdict(verdict, open) {
|
|
|
54
54
|
export function formatFindings(findings) {
|
|
55
55
|
return findings.map((finding) => `- ${finding.detail}`).join('\n');
|
|
56
56
|
}
|
|
57
|
-
export function fingerprint(findings) {
|
|
58
|
-
return findings
|
|
59
|
-
.map((finding) => finding.detail
|
|
60
|
-
.toLowerCase()
|
|
61
|
-
.replace(/[^a-z0-9]+/g, ' ')
|
|
62
|
-
.trim())
|
|
63
|
-
.sort()
|
|
64
|
-
.join('|');
|
|
65
|
-
}
|