next-issue 2.14.1 → 2.16.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 +31 -18
- package/dist/config.mjs +2 -0
- package/dist/handover.mjs +3 -3
- package/dist/interrupt.mjs +2 -2
- package/dist/job.mjs +6 -0
- package/dist/loop.mjs +43 -17
- package/dist/pipeline.mjs +2 -2
- 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`,
|
|
@@ -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
|
|
@@ -218,6 +226,9 @@ Thus a person who can write an issue, or a writer who can comment, can try to
|
|
|
218
226
|
give an instruction to the agent. Use the harness only on a repository where you
|
|
219
227
|
trust the issue authors, or run it in a container.
|
|
220
228
|
|
|
229
|
+
[ADR 0001](https://github.com/ravshansbox/next-issue/blob/main/docs/adr/0001-reviewer-isolation.md) records why the reviewer
|
|
230
|
+
keeps a full shell, and which other options were considered.
|
|
231
|
+
|
|
221
232
|
## Observability
|
|
222
233
|
|
|
223
234
|
Every run has an id, `<timestamp>`, and writes these under `.next-issue/runs/`:
|
|
@@ -301,6 +312,7 @@ does not know all stop the run, so a typo cannot pass without a word.
|
|
|
301
312
|
{
|
|
302
313
|
"remote": "origin",
|
|
303
314
|
"issueLimit": 100,
|
|
315
|
+
"concurrency": 1,
|
|
304
316
|
"maxCiFixes": 5,
|
|
305
317
|
"maxCommitFixes": 10,
|
|
306
318
|
"maxReviewRounds": 5,
|
|
@@ -334,6 +346,7 @@ does not know all stop the run, so a typo cannot pass without a word.
|
|
|
334
346
|
| ----------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
335
347
|
| `remote` | `origin` | The git remote for the repository and the branches |
|
|
336
348
|
| `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 |
|
|
349
|
+
| `concurrency` | `1` | The most issues that a run handles at the same time |
|
|
337
350
|
| `maxCiFixes` | `5` | The budget for check fixes per issue |
|
|
338
351
|
| `maxCommitFixes` | `10` | The budget for fixes of a commit that a git hook stopped, per issue |
|
|
339
352
|
| `maxReviewRounds` | `5` | The budget for review rounds per issue |
|
|
@@ -456,7 +469,7 @@ and `--once` is the same as `--max 1`.
|
|
|
456
469
|
The watch stops:
|
|
457
470
|
|
|
458
471
|
- at the first `Ctrl-C` during a wait, at once;
|
|
459
|
-
- at the first `Ctrl-C` during an issue, after
|
|
472
|
+
- at the first `Ctrl-C` during an issue, after the running issues;
|
|
460
473
|
- after a pass that halts the run, for example when the base branch is broken
|
|
461
474
|
after `--merge`;
|
|
462
475
|
- when the open issues cannot be read.
|
|
@@ -478,8 +491,8 @@ to it. The worker:
|
|
|
478
491
|
once. Each run is like `next-issue` without `--watch`, and the client says
|
|
479
492
|
whether it merges;
|
|
480
493
|
- sends every client the progress of every run when it connects and on every
|
|
481
|
-
change: the issue, the step, the tool count
|
|
482
|
-
issues;
|
|
494
|
+
change: for each running issue the issue, the step, the tool count and the
|
|
495
|
+
last tool, and the handled issues;
|
|
483
496
|
- sends every client a `questions` message for each repository: the open
|
|
484
497
|
issues with the `needsHuman` label, each with its number, title, the reason
|
|
485
498
|
of the hand-over, the url of the comment that holds it, the `owner/name`
|
|
@@ -494,8 +507,8 @@ to it. The worker:
|
|
|
494
507
|
a newer one. When the client adds an `"id"` to its message, the list that
|
|
495
508
|
answers it carries the same `id` and is read after the message arrived. When the list cannot be read, the client gets
|
|
496
509
|
`{ "type": "questions-error", "root": "<path>", "message": "<text>" }`;
|
|
497
|
-
- stops a run, when a client asks, like a second `Ctrl-C`:
|
|
498
|
-
round, with the state kept;
|
|
510
|
+
- stops a run, when a client asks, like a second `Ctrl-C`: each running issue
|
|
511
|
+
after its current round, with the state kept;
|
|
499
512
|
- stays when all clients close;
|
|
500
513
|
- closes once no run is going when a client with a newer version connects, so
|
|
501
514
|
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/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:';
|
|
@@ -91,7 +91,7 @@ export async function finish(job, outcome, reason) {
|
|
|
91
91
|
await cleanUp(context, log, branch);
|
|
92
92
|
if (outcome === 'done') {
|
|
93
93
|
for (const name of [branch, ...(await ports.disputeBranches(repo, branch))]) {
|
|
94
|
-
if (!(await ports.deleteBranch(repo, name))) {
|
|
94
|
+
if (!(await oneAtATime(repo.root, () => ports.deleteBranch(repo, name)))) {
|
|
95
95
|
log.event('branch.kept', { branch: name }, 'quiet');
|
|
96
96
|
}
|
|
97
97
|
}
|
|
@@ -114,7 +114,7 @@ export async function cleanUp(context, log, branch) {
|
|
|
114
114
|
if (path === undefined) {
|
|
115
115
|
return;
|
|
116
116
|
}
|
|
117
|
-
if (!(await context.ports.removeWorktree(context.repo, path))) {
|
|
117
|
+
if (!(await oneAtATime(context.repo.root, () => context.ports.removeWorktree(context.repo, path)))) {
|
|
118
118
|
log.event('worktree.kept', { path }, 'quiet');
|
|
119
119
|
}
|
|
120
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
|
@@ -3,7 +3,7 @@ import { commitWork } from './fix.mjs';
|
|
|
3
3
|
import { checkGate } from './gate.mjs';
|
|
4
4
|
import { branchName } from './git.mjs';
|
|
5
5
|
import { cleanUp, escalate, finish, handOver, handOverReply, skipped, stop, writerComments, } from './handover.mjs';
|
|
6
|
-
import { openPr, setStatus } from './job.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';
|
|
@@ -148,7 +148,7 @@ async function work(context, issue, log, state, start, handedOver) {
|
|
|
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
|
-
}
|