next-issue 2.4.1 → 2.4.3
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 +70 -69
- package/dist/agents.mjs +32 -31
- package/dist/args.mjs +17 -17
- package/dist/cli.mjs +16 -14
- package/dist/config.mjs +42 -42
- package/dist/exec.mjs +26 -26
- package/dist/git.mjs +40 -40
- package/dist/github.mjs +107 -78
- package/dist/interrupt.mjs +7 -7
- package/dist/loop.mjs +13 -6
- package/dist/observe.mjs +19 -19
- package/dist/pipeline.mjs +129 -113
- package/dist/progress.mjs +22 -22
- package/dist/prompts.mjs +52 -52
- package/dist/run.mjs +28 -28
- package/dist/serve.mjs +49 -45
- package/dist/setup.mjs +13 -13
- package/dist/state.mjs +20 -20
- package/dist/summary.mjs +7 -7
- package/dist/verdict.mjs +30 -25
- package/dist/version.mjs +3 -3
- package/package.json +7 -1
package/dist/prompts.mjs
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
1
|
const COMMIT_RULES = [
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
].join(
|
|
2
|
+
'Do not run git, gh or any push command. The harness commits for you.',
|
|
3
|
+
'End your final message with one line in the form `commit: <conventional commit subject>`.',
|
|
4
|
+
].join('\n');
|
|
5
5
|
function issueBlock(issue, comments) {
|
|
6
6
|
return [
|
|
7
7
|
`# Issue #${issue.number}: ${issue.title}`,
|
|
8
|
-
issue.body.length > 0 ? issue.body :
|
|
9
|
-
comments.length > 0 ? `## Comments\n${comments.join(
|
|
8
|
+
issue.body.length > 0 ? issue.body : '(no description)',
|
|
9
|
+
comments.length > 0 ? `## Comments\n${comments.join('\n\n')}` : '',
|
|
10
10
|
]
|
|
11
11
|
.filter((part) => part.length > 0)
|
|
12
|
-
.join(
|
|
12
|
+
.join('\n\n');
|
|
13
13
|
}
|
|
14
14
|
export function implementPrompt(issue, comments) {
|
|
15
15
|
return [
|
|
16
|
-
|
|
16
|
+
'You are the implementer. Solve the issue below in this worktree.',
|
|
17
17
|
issueBlock(issue, comments),
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
'## Rules',
|
|
19
|
+
'Read the repository conventions first. Keep the change minimal and focused on the issue.',
|
|
20
|
+
'Add or update tests when the repository has a test suite.',
|
|
21
21
|
COMMIT_RULES,
|
|
22
|
-
].join(
|
|
22
|
+
].join('\n\n');
|
|
23
23
|
}
|
|
24
24
|
export function hookPrompt(issue, comments, role, output) {
|
|
25
25
|
return [
|
|
26
26
|
`You are the ${role}. The harness could not commit your work for the issue below, because a git hook failed.`,
|
|
27
27
|
issueBlock(issue, comments),
|
|
28
28
|
`## Hook output\n${output}`,
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
'## Rules',
|
|
30
|
+
'Fix what the hook reports and keep the rest of the work in this worktree. Do not skip or weaken the hook.',
|
|
31
31
|
COMMIT_RULES,
|
|
32
|
-
].join(
|
|
32
|
+
].join('\n\n');
|
|
33
33
|
}
|
|
34
34
|
export function capDiff(diff, maxChars) {
|
|
35
35
|
return diff.length <= maxChars
|
|
@@ -38,42 +38,42 @@ export function capDiff(diff, maxChars) {
|
|
|
38
38
|
}
|
|
39
39
|
export function reviewPrompt(issue, comments, diff, earlier) {
|
|
40
40
|
const scope = earlier.length === 0
|
|
41
|
-
?
|
|
41
|
+
? 'Check correctness, scope, repository conventions and test cover.'
|
|
42
42
|
: [
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
`## Earlier findings\n${earlier.join(
|
|
49
|
-
].join(
|
|
43
|
+
'This is a later round. Judge two things only:',
|
|
44
|
+
'whether the earlier findings below are now fixed, and whether the new commits broke something.',
|
|
45
|
+
'Do not raise a new point of taste. Do not repeat a finding that is fixed.',
|
|
46
|
+
'A `The fixer disputed every finding of this round` line answers all of the findings of that round, not one of them.',
|
|
47
|
+
'Withdraw every finding of that round if the dispute is correct. Repeat them all if the dispute is wrong.',
|
|
48
|
+
`## Earlier findings\n${earlier.join('\n')}`,
|
|
49
|
+
].join('\n');
|
|
50
50
|
return [
|
|
51
|
-
|
|
51
|
+
'You are the reviewer. Judge whether the diff solves the issue correctly.',
|
|
52
52
|
issueBlock(issue, comments),
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
'## Diff',
|
|
54
|
+
'```diff',
|
|
55
55
|
diff,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
'```',
|
|
57
|
+
'## Rules',
|
|
58
|
+
'Read files and run commands to prove what you say: the tests, the type check or the linter.',
|
|
59
|
+
'The worktree is ready to use. Do not change a file. Do not write with git or gh.',
|
|
60
|
+
'The harness discards each change that you make, so a change is only lost work.',
|
|
61
61
|
scope,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
].join(
|
|
62
|
+
'Mark a finding blocking only for a wrong result, a missing part of the issue, a regression or a broken convention.',
|
|
63
|
+
'Give the result with the submit_result tool: a short summary and one entry per finding. The findings decide the outcome, so a problem that you leave out counts as no problem.',
|
|
64
|
+
].join('\n\n');
|
|
65
65
|
}
|
|
66
66
|
const UNRELATED_RULE = [
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
].join(
|
|
67
|
+
'The checks can fail for a reason this change did not cause: a test that is already broken on the base branch, one that exercises code the diff does not touch, or one that fails only under load.',
|
|
68
|
+
'Establish that before you fix anything. If the failure is not yours, change nothing and end your final message with one line in the form `unrelated: <one sentence on what fails and why it is not this change>`.',
|
|
69
|
+
'A person then reads it. Do not weaken, skip or re-record a test to make the checks pass.',
|
|
70
|
+
].join('\n');
|
|
71
71
|
const DISPUTE_RULE = [
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
].join(
|
|
72
|
+
'A finding can be wrong: it can miss a convention of this repository, ask for work that the issue does not want, or describe behaviour that the code does not have.',
|
|
73
|
+
'Establish that before you change anything. If every finding is wrong, change nothing and end your final message with one line in the form `unrelated: <one sentence on why the findings are wrong>`.',
|
|
74
|
+
'If only some of the findings are wrong, fix the correct ones and change nothing for the wrong ones. Write no `unrelated` line in that round. The next round holds only the findings that are left, and you can dispute them there.',
|
|
75
|
+
'The reviewer reads that line in the next round. Do not make a change that you hold to be wrong only to end the round.',
|
|
76
|
+
].join('\n');
|
|
77
77
|
export function fixPrompt(issue, reason, detail, earlier, kind) {
|
|
78
78
|
return [
|
|
79
79
|
`You are the fixer. Correct the work for issue #${issue.number}: ${issue.title}.`,
|
|
@@ -81,18 +81,18 @@ export function fixPrompt(issue, reason, detail, earlier, kind) {
|
|
|
81
81
|
`## Detail\n${detail}`,
|
|
82
82
|
earlier.length > 0
|
|
83
83
|
? [
|
|
84
|
-
|
|
85
|
-
earlier.join(
|
|
86
|
-
|
|
87
|
-
].join(
|
|
88
|
-
:
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
kind ===
|
|
84
|
+
'## Earlier rounds',
|
|
85
|
+
earlier.join('\n'),
|
|
86
|
+
'Keep those earlier fixes. Do not undo work that a later round did not question.',
|
|
87
|
+
].join('\n')
|
|
88
|
+
: '',
|
|
89
|
+
'## Rules',
|
|
90
|
+
'Fix the cause, not the symptom. Keep the change minimal.',
|
|
91
|
+
kind === 'checks' ? UNRELATED_RULE : DISPUTE_RULE,
|
|
92
92
|
COMMIT_RULES,
|
|
93
93
|
]
|
|
94
94
|
.filter((part) => part.length > 0)
|
|
95
|
-
.join(
|
|
95
|
+
.join('\n\n');
|
|
96
96
|
}
|
|
97
97
|
export function parseUnrelated(text) {
|
|
98
98
|
const match = [...text.matchAll(/^unrelated:\s*(.+)$/gim)].at(-1);
|
|
@@ -103,7 +103,7 @@ export function parseCommitSubject(text, fallback) {
|
|
|
103
103
|
const match = [...text.matchAll(/^commit:\s*(.+)$/gim)].at(-1);
|
|
104
104
|
const subject = match?.[1]
|
|
105
105
|
?.trim()
|
|
106
|
-
.replace(/^`+|`+$/g,
|
|
106
|
+
.replace(/^`+|`+$/g, '')
|
|
107
107
|
.trim();
|
|
108
108
|
return subject && subject.length > 0 ? subject : fallback;
|
|
109
109
|
}
|
package/dist/run.mjs
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
import { writeFile } from
|
|
2
|
-
import { join, sep } from
|
|
3
|
-
import { loadConfig } from
|
|
4
|
-
import { setCommandObserver, setDefaultTimeout } from
|
|
5
|
-
import { detectRepo } from
|
|
6
|
-
import { defaultBranch } from
|
|
7
|
-
import { exitCode, runIssues, watchIssues } from
|
|
8
|
-
import { message, pruneRuns, Recorder } from
|
|
9
|
-
import { pendingIssues, PORTS, processIssue } from
|
|
10
|
-
import { STATE_DIR } from
|
|
11
|
-
import { packageVersion } from
|
|
1
|
+
import { writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join, sep } from 'node:path';
|
|
3
|
+
import { loadConfig } from './config.mjs';
|
|
4
|
+
import { setCommandObserver, setDefaultTimeout } from './exec.mjs';
|
|
5
|
+
import { detectRepo } from './git.mjs';
|
|
6
|
+
import { defaultBranch } from './github.mjs';
|
|
7
|
+
import { exitCode, runIssues, watchIssues } from './loop.mjs';
|
|
8
|
+
import { message, pruneRuns, Recorder } from './observe.mjs';
|
|
9
|
+
import { pendingIssues, PORTS, processIssue } from './pipeline.mjs';
|
|
10
|
+
import { STATE_DIR } from './state.mjs';
|
|
11
|
+
import { packageVersion } from './version.mjs';
|
|
12
12
|
const recorders = new Map();
|
|
13
13
|
setCommandObserver((record) => {
|
|
14
14
|
for (const [root, recorder] of recorders) {
|
|
15
15
|
if (record.cwd !== undefined && record.cwd !== root && !record.cwd.startsWith(root + sep)) {
|
|
16
16
|
continue;
|
|
17
17
|
}
|
|
18
|
-
recorder.event(record.code === 0 ?
|
|
19
|
-
cmd: `${record.command} ${record.args.join(
|
|
18
|
+
recorder.event(record.code === 0 ? 'command' : 'command.fail', {
|
|
19
|
+
cmd: `${record.command} ${record.args.join(' ')}`,
|
|
20
20
|
code: record.code,
|
|
21
21
|
ms: record.ms,
|
|
22
22
|
stderr: record.stderr.length > 0 ? record.stderr : undefined,
|
|
23
23
|
timedOut: record.timedOut ? true : undefined,
|
|
24
|
-
}, record.code === 0 ?
|
|
24
|
+
}, record.code === 0 ? 'verbose' : 'quiet');
|
|
25
25
|
}
|
|
26
26
|
});
|
|
27
27
|
export async function runRepo(root, args, controls) {
|
|
@@ -31,7 +31,7 @@ export async function runRepo(root, args, controls) {
|
|
|
31
31
|
const recorder = await Recorder.create(repo.root, args.level, controls.onEvent);
|
|
32
32
|
const pruned = await pruneRuns(repo.root, config.keepRunDays);
|
|
33
33
|
if (pruned > 0) {
|
|
34
|
-
recorder.event(
|
|
34
|
+
recorder.event('runs.pruned', { files: pruned, keepDays: config.keepRunDays }, 'verbose');
|
|
35
35
|
}
|
|
36
36
|
recorders.set(repo.root, recorder);
|
|
37
37
|
try {
|
|
@@ -55,16 +55,16 @@ async function handle(args, config, repo, recorder, controls) {
|
|
|
55
55
|
recorder,
|
|
56
56
|
ports: PORTS,
|
|
57
57
|
};
|
|
58
|
-
recorder.event(
|
|
58
|
+
recorder.event('run.start', {
|
|
59
59
|
version: await packageVersion(),
|
|
60
60
|
repo: `${repo.owner}/${repo.name}`,
|
|
61
61
|
base,
|
|
62
62
|
log: recorder.logFile,
|
|
63
|
-
},
|
|
63
|
+
}, 'quiet');
|
|
64
64
|
const loop = {
|
|
65
65
|
issues: async () => {
|
|
66
66
|
const issues = await pendingIssues(context);
|
|
67
|
-
recorder.event(
|
|
67
|
+
recorder.event('issues', { open: issues.length }, 'quiet');
|
|
68
68
|
return issues;
|
|
69
69
|
},
|
|
70
70
|
process: (issue) => processIssue(context, issue),
|
|
@@ -76,7 +76,7 @@ async function handle(args, config, repo, recorder, controls) {
|
|
|
76
76
|
? await watchIssues(args, {
|
|
77
77
|
...loop,
|
|
78
78
|
wait: (minutes) => {
|
|
79
|
-
recorder.event(
|
|
79
|
+
recorder.event('watch.wait', { minutes }, 'quiet');
|
|
80
80
|
passStart = recorder.summary();
|
|
81
81
|
return interrupts.sleep(minutes * 60_000);
|
|
82
82
|
},
|
|
@@ -84,20 +84,20 @@ async function handle(args, config, repo, recorder, controls) {
|
|
|
84
84
|
})
|
|
85
85
|
: await runIssues(args, loop);
|
|
86
86
|
if (stoppedAt !== undefined) {
|
|
87
|
-
recorder.event(
|
|
87
|
+
recorder.event('run.stop', { issue: stoppedAt }, 'quiet');
|
|
88
88
|
}
|
|
89
89
|
if (error !== undefined) {
|
|
90
|
-
recorder.event(
|
|
90
|
+
recorder.event('issues.fail', { error: message(error) }, 'quiet');
|
|
91
91
|
}
|
|
92
|
-
recorder.event(
|
|
93
|
-
done: count(reports,
|
|
94
|
-
needsHuman: count(reports,
|
|
95
|
-
failed: count(reports,
|
|
96
|
-
skipped: count(reports,
|
|
92
|
+
recorder.event('run.end', {
|
|
93
|
+
done: count(reports, 'done'),
|
|
94
|
+
needsHuman: count(reports, 'needs-human'),
|
|
95
|
+
failed: count(reports, 'error'),
|
|
96
|
+
skipped: count(reports, 'skipped'),
|
|
97
97
|
tokens: recorder.summary().total.total,
|
|
98
|
-
},
|
|
98
|
+
}, 'quiet');
|
|
99
99
|
const summary = { ...recorder.summary(), issues: reports };
|
|
100
|
-
const summaryFile = join(repo.root, STATE_DIR,
|
|
100
|
+
const summaryFile = join(repo.root, STATE_DIR, 'runs', `${recorder.runId}.summary.json`);
|
|
101
101
|
await writeFile(summaryFile, `${JSON.stringify(summary, null, 2)}\n`);
|
|
102
102
|
if (!args.watch) {
|
|
103
103
|
await print(summary);
|
package/dist/serve.mjs
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
import { rmSync } from
|
|
2
|
-
import { chmod, readFile, rm, writeFile } from
|
|
3
|
-
import { createServer } from
|
|
4
|
-
import { join } from
|
|
5
|
-
import { getAgentDir } from
|
|
6
|
-
import { WebSocketServer } from
|
|
7
|
-
import { parseArgs } from
|
|
8
|
-
import { stopChildren } from
|
|
9
|
-
import { message } from
|
|
10
|
-
import { applyEvent, startProgress } from
|
|
11
|
-
import { runRepo } from
|
|
12
|
-
import { packageVersion } from
|
|
1
|
+
import { rmSync } from 'node:fs';
|
|
2
|
+
import { chmod, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { createServer } from 'node:http';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { getAgentDir } from '@earendil-works/pi-coding-agent';
|
|
6
|
+
import { WebSocketServer } from 'ws';
|
|
7
|
+
import { parseArgs } from './args.mjs';
|
|
8
|
+
import { stopChildren } from './exec.mjs';
|
|
9
|
+
import { message } from './observe.mjs';
|
|
10
|
+
import { applyEvent, startProgress } from './progress.mjs';
|
|
11
|
+
import { runRepo } from './run.mjs';
|
|
12
|
+
import { packageVersion } from './version.mjs';
|
|
13
13
|
export function socketPaths(agentDir = getAgentDir()) {
|
|
14
14
|
return {
|
|
15
|
-
socketPath: join(agentDir,
|
|
16
|
-
lockPath: join(agentDir,
|
|
15
|
+
socketPath: join(agentDir, 'next-issue.sock'),
|
|
16
|
+
lockPath: join(agentDir, 'next-issue.lock'),
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
19
|
export function isNewer(candidate, current) {
|
|
20
|
-
const left = candidate.split(
|
|
21
|
-
const right = current.split(
|
|
20
|
+
const left = candidate.split('.').map(Number);
|
|
21
|
+
const right = current.split('.').map(Number);
|
|
22
22
|
for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
|
|
23
23
|
const difference = (left[index] ?? 0) - (right[index] ?? 0);
|
|
24
24
|
if (difference !== 0) {
|
|
@@ -33,28 +33,28 @@ function alive(pid) {
|
|
|
33
33
|
return true;
|
|
34
34
|
}
|
|
35
35
|
catch (error) {
|
|
36
|
-
return error.code ===
|
|
36
|
+
return error.code === 'EPERM';
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
async function holder(lockPath) {
|
|
40
|
-
const pid = Number((await readFile(lockPath,
|
|
40
|
+
const pid = Number((await readFile(lockPath, 'utf8').catch(() => '')).trim());
|
|
41
41
|
return pid > 0 && alive(pid) ? pid : undefined;
|
|
42
42
|
}
|
|
43
43
|
export async function stopWorker(lockPath) {
|
|
44
44
|
const pid = await holder(lockPath);
|
|
45
45
|
if (pid !== undefined) {
|
|
46
|
-
process.kill(pid,
|
|
46
|
+
process.kill(pid, 'SIGTERM');
|
|
47
47
|
}
|
|
48
48
|
return pid;
|
|
49
49
|
}
|
|
50
50
|
async function lock(path) {
|
|
51
51
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
52
52
|
try {
|
|
53
|
-
await writeFile(path, `${process.pid}\n`, { flag:
|
|
53
|
+
await writeFile(path, `${process.pid}\n`, { flag: 'wx', mode: 0o600 });
|
|
54
54
|
return true;
|
|
55
55
|
}
|
|
56
56
|
catch (error) {
|
|
57
|
-
if (error.code !==
|
|
57
|
+
if (error.code !== 'EEXIST') {
|
|
58
58
|
throw error;
|
|
59
59
|
}
|
|
60
60
|
if ((await holder(path)) !== undefined) {
|
|
@@ -79,7 +79,7 @@ export async function startWorker(options) {
|
|
|
79
79
|
const server = createServer();
|
|
80
80
|
const sockets = new WebSocketServer({ server });
|
|
81
81
|
const snapshot = () => ({
|
|
82
|
-
type:
|
|
82
|
+
type: 'runs',
|
|
83
83
|
runs: Object.fromEntries([...runs].map(([root, run]) => [root, run.progress])),
|
|
84
84
|
});
|
|
85
85
|
const send = (socket, value) => {
|
|
@@ -91,7 +91,7 @@ export async function startWorker(options) {
|
|
|
91
91
|
send(socket, value);
|
|
92
92
|
}
|
|
93
93
|
};
|
|
94
|
-
const busy = () => [...runs.values()].some((run) => run.progress.status ===
|
|
94
|
+
const busy = () => [...runs.values()].some((run) => run.progress.status === 'running' || run.progress.status === 'stopping');
|
|
95
95
|
const close = async () => {
|
|
96
96
|
for (const socket of sockets.clients) {
|
|
97
97
|
socket.terminate();
|
|
@@ -109,12 +109,12 @@ export async function startWorker(options) {
|
|
|
109
109
|
};
|
|
110
110
|
const start = (socket, root, merge) => {
|
|
111
111
|
const current = runs.get(root);
|
|
112
|
-
if (current?.progress.status ===
|
|
113
|
-
send(socket, { type:
|
|
112
|
+
if (current?.progress.status === 'running' || current?.progress.status === 'stopping') {
|
|
113
|
+
send(socket, { type: 'error', root, message: 'a run is going in this repository already' });
|
|
114
114
|
return;
|
|
115
115
|
}
|
|
116
116
|
if (restart) {
|
|
117
|
-
send(socket, { type:
|
|
117
|
+
send(socket, { type: 'error', root, message: 'the worker is restarting, try again soon' });
|
|
118
118
|
return;
|
|
119
119
|
}
|
|
120
120
|
const run = { progress: startProgress(), stop: false };
|
|
@@ -145,25 +145,29 @@ export async function startWorker(options) {
|
|
|
145
145
|
onEvent: (record) => update(applyEvent(run.progress, record)),
|
|
146
146
|
})
|
|
147
147
|
.then(() => {
|
|
148
|
-
const status = run.stop
|
|
148
|
+
const status = run.stop
|
|
149
|
+
? 'stopped'
|
|
150
|
+
: run.progress.error === undefined
|
|
151
|
+
? 'finished'
|
|
152
|
+
: 'failed';
|
|
149
153
|
update({ ...run.progress, status });
|
|
150
|
-
}, (error) => update({ ...run.progress, status:
|
|
154
|
+
}, (error) => update({ ...run.progress, status: 'failed', error: message(error) }))
|
|
151
155
|
.finally(restartWhenIdle);
|
|
152
156
|
};
|
|
153
157
|
const stop = (root) => {
|
|
154
158
|
const run = runs.get(root);
|
|
155
|
-
if (run === undefined || run.progress.status !==
|
|
159
|
+
if (run === undefined || run.progress.status !== 'running') {
|
|
156
160
|
return;
|
|
157
161
|
}
|
|
158
162
|
run.stop = true;
|
|
159
163
|
run.wake?.();
|
|
160
|
-
run.progress = { ...run.progress, status:
|
|
164
|
+
run.progress = { ...run.progress, status: 'stopping' };
|
|
161
165
|
broadcast();
|
|
162
166
|
};
|
|
163
|
-
sockets.on(
|
|
164
|
-
send(socket, { type:
|
|
167
|
+
sockets.on('connection', (socket) => {
|
|
168
|
+
send(socket, { type: 'hello', version: options.version });
|
|
165
169
|
send(socket, snapshot());
|
|
166
|
-
socket.on(
|
|
170
|
+
socket.on('message', (data) => {
|
|
167
171
|
let value;
|
|
168
172
|
try {
|
|
169
173
|
value = JSON.parse(String(data));
|
|
@@ -171,24 +175,24 @@ export async function startWorker(options) {
|
|
|
171
175
|
catch {
|
|
172
176
|
return;
|
|
173
177
|
}
|
|
174
|
-
if (value.type ===
|
|
178
|
+
if (value.type === 'hello') {
|
|
175
179
|
if (isNewer(value.version, options.version)) {
|
|
176
180
|
restart = true;
|
|
177
181
|
restartWhenIdle();
|
|
178
182
|
}
|
|
179
183
|
}
|
|
180
|
-
else if (value.type ===
|
|
184
|
+
else if (value.type === 'start') {
|
|
181
185
|
start(socket, value.root, value.merge === true);
|
|
182
186
|
}
|
|
183
|
-
else if (value.type ===
|
|
187
|
+
else if (value.type === 'stop') {
|
|
184
188
|
stop(value.root);
|
|
185
189
|
}
|
|
186
190
|
});
|
|
187
191
|
});
|
|
188
192
|
await new Promise((resolve, reject) => {
|
|
189
|
-
server.once(
|
|
193
|
+
server.once('error', reject);
|
|
190
194
|
server.listen(options.socketPath, () => {
|
|
191
|
-
server.off(
|
|
195
|
+
server.off('error', reject);
|
|
192
196
|
resolve();
|
|
193
197
|
});
|
|
194
198
|
});
|
|
@@ -203,20 +207,20 @@ export async function serve() {
|
|
|
203
207
|
runRepo,
|
|
204
208
|
});
|
|
205
209
|
if (worker === undefined) {
|
|
206
|
-
process.stderr.write(
|
|
210
|
+
process.stderr.write('next-issue: another worker is running\n');
|
|
207
211
|
return 1;
|
|
208
212
|
}
|
|
209
213
|
const end = (signal) => {
|
|
210
|
-
process.off(
|
|
211
|
-
process.off(
|
|
214
|
+
process.off('SIGTERM', onTerminate);
|
|
215
|
+
process.off('SIGINT', onInterrupt);
|
|
212
216
|
rmSync(paths.socketPath, { force: true });
|
|
213
217
|
rmSync(paths.lockPath, { force: true });
|
|
214
218
|
stopChildren(signal);
|
|
215
219
|
};
|
|
216
|
-
const onTerminate = () => end(
|
|
217
|
-
const onInterrupt = () => end(
|
|
218
|
-
process.on(
|
|
219
|
-
process.on(
|
|
220
|
+
const onTerminate = () => end('SIGTERM');
|
|
221
|
+
const onInterrupt = () => end('SIGINT');
|
|
222
|
+
process.on('SIGTERM', onTerminate);
|
|
223
|
+
process.on('SIGINT', onInterrupt);
|
|
220
224
|
process.stderr.write(`next-issue: worker listening on ${paths.socketPath}\n`);
|
|
221
225
|
await worker.closed;
|
|
222
226
|
return 0;
|
package/dist/setup.mjs
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { access } from
|
|
2
|
-
import { join } from
|
|
1
|
+
import { access } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
3
|
const MARKERS = [
|
|
4
|
-
[
|
|
5
|
-
[
|
|
6
|
-
[
|
|
7
|
-
[
|
|
8
|
-
[
|
|
9
|
-
[
|
|
10
|
-
[
|
|
11
|
-
[
|
|
12
|
-
[
|
|
13
|
-
[
|
|
14
|
-
[
|
|
4
|
+
['pnpm-lock.yaml', 'pnpm install --frozen-lockfile'],
|
|
5
|
+
['package-lock.json', 'npm ci'],
|
|
6
|
+
['.yarnrc.yml', 'yarn install --immutable'],
|
|
7
|
+
['yarn.lock', 'yarn install --frozen-lockfile'],
|
|
8
|
+
['bun.lock', 'bun install --frozen-lockfile'],
|
|
9
|
+
['bun.lockb', 'bun install --frozen-lockfile'],
|
|
10
|
+
['Cargo.lock', 'cargo fetch'],
|
|
11
|
+
['go.sum', 'go mod download'],
|
|
12
|
+
['uv.lock', 'uv sync'],
|
|
13
|
+
['poetry.lock', 'poetry install'],
|
|
14
|
+
['Gemfile.lock', 'bundle install'],
|
|
15
15
|
];
|
|
16
16
|
export async function detectSetupCommand(root) {
|
|
17
17
|
for (const [file, command] of MARKERS) {
|
package/dist/state.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { mkdir, readdir, readFile, rm, writeFile } from
|
|
2
|
-
import { join } from
|
|
3
|
-
export const STATE_DIR =
|
|
1
|
+
import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export const STATE_DIR = '.next-issue';
|
|
4
4
|
function dir(repo) {
|
|
5
5
|
return join(repo.root, STATE_DIR);
|
|
6
6
|
}
|
|
@@ -11,10 +11,10 @@ export async function readState(repo, issue) {
|
|
|
11
11
|
const path = file(repo, issue);
|
|
12
12
|
let text;
|
|
13
13
|
try {
|
|
14
|
-
text = await readFile(path,
|
|
14
|
+
text = await readFile(path, 'utf8');
|
|
15
15
|
}
|
|
16
16
|
catch (error) {
|
|
17
|
-
if (error.code ===
|
|
17
|
+
if (error.code === 'ENOENT') {
|
|
18
18
|
return undefined;
|
|
19
19
|
}
|
|
20
20
|
throw error;
|
|
@@ -31,34 +31,34 @@ export async function readState(repo, issue) {
|
|
|
31
31
|
}
|
|
32
32
|
return parsed;
|
|
33
33
|
}
|
|
34
|
-
const PHASES = [
|
|
34
|
+
const PHASES = ['claimed', 'implemented', 'review'];
|
|
35
35
|
function isRecord(value) {
|
|
36
|
-
return typeof value ===
|
|
36
|
+
return typeof value === 'object' && value !== null;
|
|
37
37
|
}
|
|
38
38
|
function isOptional(value, type) {
|
|
39
39
|
return value === undefined || typeof value === type;
|
|
40
40
|
}
|
|
41
41
|
function isBaseFix(value) {
|
|
42
|
-
return isRecord(value) && typeof value.sha ===
|
|
42
|
+
return isRecord(value) && typeof value.sha === 'string' && typeof value.head === 'string';
|
|
43
43
|
}
|
|
44
44
|
function isReviewRound(value) {
|
|
45
45
|
return (isRecord(value) &&
|
|
46
|
-
typeof value.round ===
|
|
47
|
-
typeof value.fingerprint ===
|
|
48
|
-
typeof value.findings ===
|
|
49
|
-
isOptional(value.dispute,
|
|
46
|
+
typeof value.round === 'number' &&
|
|
47
|
+
typeof value.fingerprint === 'string' &&
|
|
48
|
+
typeof value.findings === 'string' &&
|
|
49
|
+
isOptional(value.dispute, 'string'));
|
|
50
50
|
}
|
|
51
51
|
function isState(value) {
|
|
52
52
|
return (isRecord(value) &&
|
|
53
|
-
typeof value.issue ===
|
|
53
|
+
typeof value.issue === 'number' &&
|
|
54
54
|
PHASES.includes(value.phase) &&
|
|
55
|
-
typeof value.branch ===
|
|
56
|
-
isOptional(value.pr,
|
|
57
|
-
isOptional(value.title,
|
|
58
|
-
typeof value.ciFixes ===
|
|
59
|
-
isOptional(value.commitFixes,
|
|
55
|
+
typeof value.branch === 'string' &&
|
|
56
|
+
isOptional(value.pr, 'number') &&
|
|
57
|
+
isOptional(value.title, 'string') &&
|
|
58
|
+
typeof value.ciFixes === 'number' &&
|
|
59
|
+
isOptional(value.commitFixes, 'number') &&
|
|
60
60
|
(value.baseFix === undefined || isBaseFix(value.baseFix)) &&
|
|
61
|
-
typeof value.reviewRounds ===
|
|
61
|
+
typeof value.reviewRounds === 'number' &&
|
|
62
62
|
Array.isArray(value.reviewLog) &&
|
|
63
63
|
value.reviewLog.every(isReviewRound));
|
|
64
64
|
}
|
|
@@ -68,7 +68,7 @@ export async function savedIssues(repo) {
|
|
|
68
68
|
names = await readdir(dir(repo));
|
|
69
69
|
}
|
|
70
70
|
catch (error) {
|
|
71
|
-
if (error.code ===
|
|
71
|
+
if (error.code === 'ENOENT') {
|
|
72
72
|
return [];
|
|
73
73
|
}
|
|
74
74
|
throw error;
|
package/dist/summary.mjs
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
const OUTCOMES = [
|
|
1
|
+
const OUTCOMES = ['done', 'needs-human', 'error', 'skipped', 'stopped'];
|
|
2
2
|
export function formatSummary(summary) {
|
|
3
|
-
const tally = OUTCOMES.map((outcome) => `${summary.issues.filter((report) => report.outcome === outcome).length} ${outcome}`).join(
|
|
3
|
+
const tally = OUTCOMES.map((outcome) => `${summary.issues.filter((report) => report.outcome === outcome).length} ${outcome}`).join(', ');
|
|
4
4
|
const lines = [`run ${summary.runId}`, `issues ${summary.issues.length}: ${tally}`];
|
|
5
5
|
for (const report of summary.issues) {
|
|
6
6
|
lines.push(` ${issueLine(report)}`);
|
|
7
7
|
}
|
|
8
8
|
const roles = Object.entries(summary.roles)
|
|
9
9
|
.map(([role, totals]) => `${role} ${tokens(totals.total)} in ${totals.calls}`)
|
|
10
|
-
.join(
|
|
11
|
-
lines.push(`tokens ${tokens(summary.total.total)}${roles.length > 0 ? ` (${roles})` :
|
|
10
|
+
.join(', ');
|
|
11
|
+
lines.push(`tokens ${tokens(summary.total.total)}${roles.length > 0 ? ` (${roles})` : ''}`);
|
|
12
12
|
lines.push(`log ${summary.logFile}`);
|
|
13
|
-
return `${lines.join(
|
|
13
|
+
return `${lines.join('\n')}\n`;
|
|
14
14
|
}
|
|
15
15
|
function issueLine(report) {
|
|
16
16
|
const parts = [`#${report.issue} ${report.outcome.padEnd(11)}`];
|
|
@@ -29,12 +29,12 @@ function issueLine(report) {
|
|
|
29
29
|
if (report.reason !== undefined) {
|
|
30
30
|
parts.push(`(${report.reason})`);
|
|
31
31
|
}
|
|
32
|
-
return parts.join(
|
|
32
|
+
return parts.join(' ').trimEnd();
|
|
33
33
|
}
|
|
34
34
|
function duration(ms) {
|
|
35
35
|
const seconds = Math.round(ms / 1000);
|
|
36
36
|
const minutes = Math.floor(seconds / 60);
|
|
37
|
-
return minutes === 0 ? `${seconds}s` : `${minutes}m${String(seconds % 60).padStart(2,
|
|
37
|
+
return minutes === 0 ? `${seconds}s` : `${minutes}m${String(seconds % 60).padStart(2, '0')}s`;
|
|
38
38
|
}
|
|
39
39
|
function tokens(value) {
|
|
40
40
|
if (value < 1000) {
|