next-issue 0.5.1 → 0.6.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 +11 -0
- package/dist/args.mjs +73 -0
- package/dist/cli.mjs +8 -74
- package/dist/pipeline.mjs +28 -13
- package/dist/state.mjs +9 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -56,6 +56,17 @@ when:
|
|
|
56
56
|
The fixer sees the findings of all earlier rounds, not only the last one. From
|
|
57
57
|
round two, the reviewer judges only the earlier findings and any regression.
|
|
58
58
|
|
|
59
|
+
## Stop after the current issue
|
|
60
|
+
|
|
61
|
+
Run `touch .next-issue/stop` in a second terminal. The harness reads the file
|
|
62
|
+
only between two issues, finishes the issue that it holds, deletes the file and
|
|
63
|
+
then writes the summary as usual. A file from an earlier run is dropped at the
|
|
64
|
+
start, so it stops nothing.
|
|
65
|
+
|
|
66
|
+
`Ctrl-C` is different: the terminal signals every child too, so the run dies in
|
|
67
|
+
the middle of a step. The saved state lets the next run continue the issue, but
|
|
68
|
+
the issue keeps `status:in-progress` and the run writes no summary.
|
|
69
|
+
|
|
59
70
|
Every status label that the harness puts on the issue goes on the pull request
|
|
60
71
|
too, from the first review round. Thus a draft pull request shows work that the
|
|
61
72
|
harness has not finished, and a `status:needs-human` pull request shows work
|
package/dist/args.mjs
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { CONFIG_FILE } from "./config.mjs";
|
|
2
|
+
export const USAGE = `next-issue [command] [options]
|
|
3
|
+
|
|
4
|
+
init write ${CONFIG_FILE} with the defaults
|
|
5
|
+
|
|
6
|
+
--issue <n> handle one issue only
|
|
7
|
+
--once stop after the first handled issue
|
|
8
|
+
--max <n> handle at most n issues
|
|
9
|
+
--reset drop the saved budgets and findings first
|
|
10
|
+
--json print the machine summary instead of the text one
|
|
11
|
+
--force replace an existing config file, with init
|
|
12
|
+
--verbose show every command and all agent output
|
|
13
|
+
--quiet show only the milestones and the summary
|
|
14
|
+
--help show this text
|
|
15
|
+
`;
|
|
16
|
+
export function parseArgs(argv) {
|
|
17
|
+
const args = {
|
|
18
|
+
command: "run",
|
|
19
|
+
once: false,
|
|
20
|
+
help: false,
|
|
21
|
+
json: false,
|
|
22
|
+
reset: false,
|
|
23
|
+
force: false,
|
|
24
|
+
level: "normal",
|
|
25
|
+
};
|
|
26
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
27
|
+
const arg = argv[index];
|
|
28
|
+
if (index === 0 && !arg.startsWith("-")) {
|
|
29
|
+
if (arg !== "init") {
|
|
30
|
+
throw new Error(`Unknown command: ${arg}`);
|
|
31
|
+
}
|
|
32
|
+
args.command = "init";
|
|
33
|
+
}
|
|
34
|
+
else if (arg === "--force") {
|
|
35
|
+
args.force = true;
|
|
36
|
+
}
|
|
37
|
+
else if (arg === "--json") {
|
|
38
|
+
args.json = true;
|
|
39
|
+
}
|
|
40
|
+
else if (arg === "--help") {
|
|
41
|
+
args.help = true;
|
|
42
|
+
}
|
|
43
|
+
else if (arg === "--reset") {
|
|
44
|
+
args.reset = true;
|
|
45
|
+
}
|
|
46
|
+
else if (arg === "--once") {
|
|
47
|
+
args.once = true;
|
|
48
|
+
}
|
|
49
|
+
else if (arg === "--verbose") {
|
|
50
|
+
args.level = "verbose";
|
|
51
|
+
}
|
|
52
|
+
else if (arg === "--quiet") {
|
|
53
|
+
args.level = "quiet";
|
|
54
|
+
}
|
|
55
|
+
else if (arg === "--issue") {
|
|
56
|
+
args.issue = whole(argv[++index], arg);
|
|
57
|
+
}
|
|
58
|
+
else if (arg === "--max") {
|
|
59
|
+
args.max = whole(argv[++index], arg);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return args;
|
|
66
|
+
}
|
|
67
|
+
function whole(value, option) {
|
|
68
|
+
const parsed = Number(value);
|
|
69
|
+
if (value === undefined || !Number.isInteger(parsed) || parsed < 1) {
|
|
70
|
+
throw new Error(`${option} needs a whole number of 1 or more`);
|
|
71
|
+
}
|
|
72
|
+
return parsed;
|
|
73
|
+
}
|
package/dist/cli.mjs
CHANGED
|
@@ -1,86 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { writeFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { parseArgs, USAGE } from "./args.mjs";
|
|
5
|
+
import { loadConfig, writeDefaultConfig } from "./config.mjs";
|
|
5
6
|
import { setCommandObserver } from "./exec.mjs";
|
|
6
7
|
import { detectRepo, ensureIgnored, repoRoot } from "./git.mjs";
|
|
7
8
|
import { currentLogin, defaultBranch, openIssues } from "./github.mjs";
|
|
8
9
|
import { message, Recorder } from "./observe.mjs";
|
|
9
10
|
import { processIssue } from "./pipeline.mjs";
|
|
10
|
-
import { clearState, STATE_DIR } from "./state.mjs";
|
|
11
|
+
import { clearState, STATE_DIR, takeStop } from "./state.mjs";
|
|
11
12
|
import { formatSummary } from "./summary.mjs";
|
|
12
|
-
const USAGE = `next-issue [command] [options]
|
|
13
|
-
|
|
14
|
-
init write ${CONFIG_FILE} with the defaults
|
|
15
|
-
|
|
16
|
-
--issue <n> handle one issue only
|
|
17
|
-
--once stop after the first handled issue
|
|
18
|
-
--max <n> handle at most n issues
|
|
19
|
-
--reset drop the saved budgets and findings first
|
|
20
|
-
--json print the machine summary instead of the text one
|
|
21
|
-
--force replace an existing config file, with init
|
|
22
|
-
--verbose show every command and all agent output
|
|
23
|
-
--quiet show only the milestones and the summary
|
|
24
|
-
--help show this text
|
|
25
|
-
`;
|
|
26
|
-
function parseArgs(argv) {
|
|
27
|
-
const args = {
|
|
28
|
-
command: "run",
|
|
29
|
-
once: false,
|
|
30
|
-
help: false,
|
|
31
|
-
json: false,
|
|
32
|
-
reset: false,
|
|
33
|
-
force: false,
|
|
34
|
-
level: "normal",
|
|
35
|
-
};
|
|
36
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
37
|
-
const arg = argv[index];
|
|
38
|
-
if (index === 0 && !arg.startsWith("-")) {
|
|
39
|
-
if (arg !== "init") {
|
|
40
|
-
throw new Error(`Unknown command: ${arg}`);
|
|
41
|
-
}
|
|
42
|
-
args.command = "init";
|
|
43
|
-
}
|
|
44
|
-
else if (arg === "--force") {
|
|
45
|
-
args.force = true;
|
|
46
|
-
}
|
|
47
|
-
else if (arg === "--json") {
|
|
48
|
-
args.json = true;
|
|
49
|
-
}
|
|
50
|
-
else if (arg === "--help") {
|
|
51
|
-
args.help = true;
|
|
52
|
-
}
|
|
53
|
-
else if (arg === "--reset") {
|
|
54
|
-
args.reset = true;
|
|
55
|
-
}
|
|
56
|
-
else if (arg === "--once") {
|
|
57
|
-
args.once = true;
|
|
58
|
-
}
|
|
59
|
-
else if (arg === "--verbose") {
|
|
60
|
-
args.level = "verbose";
|
|
61
|
-
}
|
|
62
|
-
else if (arg === "--quiet") {
|
|
63
|
-
args.level = "quiet";
|
|
64
|
-
}
|
|
65
|
-
else if (arg === "--issue") {
|
|
66
|
-
args.issue = whole(argv[++index], arg);
|
|
67
|
-
}
|
|
68
|
-
else if (arg === "--max") {
|
|
69
|
-
args.max = whole(argv[++index], arg);
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
throw new Error(`Unknown argument: ${arg}`);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return args;
|
|
76
|
-
}
|
|
77
|
-
function whole(value, option) {
|
|
78
|
-
const parsed = Number(value);
|
|
79
|
-
if (value === undefined || !Number.isInteger(parsed) || parsed < 1) {
|
|
80
|
-
throw new Error(`${option} needs a whole number of 1 or more`);
|
|
81
|
-
}
|
|
82
|
-
return parsed;
|
|
83
|
-
}
|
|
84
13
|
function write(stream, text) {
|
|
85
14
|
return new Promise((resolve, reject) => {
|
|
86
15
|
stream.write(text, (error) => (error === null || error === undefined ? resolve() : reject(error)));
|
|
@@ -128,6 +57,7 @@ async function handle(args, config, repo, recorder) {
|
|
|
128
57
|
recorder.event("run.start", { repo: `${repo.owner}/${repo.name}`, base, login, log: recorder.logFile }, "quiet");
|
|
129
58
|
const issues = (await openIssues(repo, config.issueLimit)).filter((issue) => args.issue === undefined || issue.number === args.issue);
|
|
130
59
|
recorder.event("issues", { open: issues.length }, "quiet");
|
|
60
|
+
await takeStop(repo);
|
|
131
61
|
const reports = [];
|
|
132
62
|
for (const issue of issues) {
|
|
133
63
|
const handled = reports.filter((report) => report.outcome !== "skipped").length;
|
|
@@ -139,6 +69,10 @@ async function handle(args, config, repo, recorder) {
|
|
|
139
69
|
}
|
|
140
70
|
const report = await processIssue(context, issue);
|
|
141
71
|
reports.push(report);
|
|
72
|
+
if (await takeStop(repo)) {
|
|
73
|
+
recorder.event("run.stop", { issue: issue.number }, "quiet");
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
142
76
|
if (args.once && report.outcome !== "skipped") {
|
|
143
77
|
break;
|
|
144
78
|
}
|
package/dist/pipeline.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CODING, READ_ONLY, runAgent } from "./agents.mjs";
|
|
2
2
|
import { managedLabels } from "./config.mjs";
|
|
3
3
|
import { run } from "./exec.mjs";
|
|
4
|
-
import { addWorktree, branchName, commitAll, diff, push, removeWorktree, worktreePath, } from "./git.mjs";
|
|
4
|
+
import { addWorktree, branchName, commitAll, diff, hasWorktree, push, removeWorktree, worktreePath, } from "./git.mjs";
|
|
5
5
|
import { assignIssue, commentOnPr, createPr, failedCheckLogs, findPr, issueComments, markPrReady, setLabel, waitForChecks, } from "./github.mjs";
|
|
6
6
|
import { message } from "./observe.mjs";
|
|
7
7
|
import { fixPrompt, implementPrompt, parseCommitSubject, reviewPrompt } from "./prompts.mjs";
|
|
@@ -55,6 +55,7 @@ export async function processIssue(context, issue) {
|
|
|
55
55
|
catch (failure) {
|
|
56
56
|
log.event("hand-over.error", { error: message(failure) }, "quiet");
|
|
57
57
|
}
|
|
58
|
+
await cleanUp(context, log, issue.number);
|
|
58
59
|
return {
|
|
59
60
|
issue: issue.number,
|
|
60
61
|
outcome: "error",
|
|
@@ -79,15 +80,18 @@ async function work(context, issue, log, state) {
|
|
|
79
80
|
const worktree = await log.step("worktree", { branch }, () => addWorktree(repo, issue.number, context.base, config.remote));
|
|
80
81
|
const job = { context, issue, log, state, branch, worktree, comments };
|
|
81
82
|
const escalate = (reason) => handOver(context, log, issue.number, state, reason);
|
|
82
|
-
const finish = (outcome, reason) =>
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
83
|
+
const finish = async (outcome, reason) => {
|
|
84
|
+
await cleanUp(context, log, issue.number);
|
|
85
|
+
return {
|
|
86
|
+
issue: issue.number,
|
|
87
|
+
outcome,
|
|
88
|
+
reason,
|
|
89
|
+
pr: state.pr,
|
|
90
|
+
ciFixes: state.ciFixes,
|
|
91
|
+
reviewRounds: state.reviewRounds,
|
|
92
|
+
ms: Date.now() - started,
|
|
93
|
+
};
|
|
94
|
+
};
|
|
91
95
|
if (config.setupCommand !== undefined) {
|
|
92
96
|
const setup = await log.step("setup", { cmd: config.setupCommand }, () => run("bash", ["-lc", config.setupCommand], { cwd: worktree }));
|
|
93
97
|
if (setup.code !== 0) {
|
|
@@ -182,9 +186,6 @@ async function work(context, issue, log, state) {
|
|
|
182
186
|
await setStatus(context, issue.number, state.pr, config.labels.done);
|
|
183
187
|
state.phase = "done";
|
|
184
188
|
await writeState(repo, state);
|
|
185
|
-
if (!(await removeWorktree(repo, issue.number))) {
|
|
186
|
-
log.event("worktree.kept", { path: worktreePath(repo, issue.number) }, "quiet");
|
|
187
|
-
}
|
|
188
189
|
return finish("done");
|
|
189
190
|
}
|
|
190
191
|
const mark = fingerprint(blocking);
|
|
@@ -220,6 +221,20 @@ async function fixRound(job, reason, detail, earlier = []) {
|
|
|
220
221
|
await push(worktree, context.config.remote, branch);
|
|
221
222
|
return true;
|
|
222
223
|
}
|
|
224
|
+
async function cleanUp(context, log, issue) {
|
|
225
|
+
const path = worktreePath(context.repo, issue);
|
|
226
|
+
try {
|
|
227
|
+
if (!(await hasWorktree(context.repo, path))) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (!(await removeWorktree(context.repo, issue))) {
|
|
231
|
+
log.event("worktree.kept", { path }, "quiet");
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
log.event("worktree.error", { error: message(error) }, "quiet");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
223
238
|
async function handOver(context, log, issue, state, reason) {
|
|
224
239
|
log.event("hand-over", { reason }, "quiet");
|
|
225
240
|
state.phase = "needs-human";
|
package/dist/state.mjs
CHANGED
|
@@ -22,3 +22,12 @@ export async function writeState(repo, state) {
|
|
|
22
22
|
await mkdir(dir(repo), { recursive: true });
|
|
23
23
|
await writeFile(file(repo, state.issue), `${JSON.stringify(state, null, 2)}\n`);
|
|
24
24
|
}
|
|
25
|
+
export async function takeStop(repo) {
|
|
26
|
+
try {
|
|
27
|
+
await rm(join(dir(repo), "stop"));
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|