next-issue 0.17.1 → 0.18.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 +18 -11
- package/dist/agents.mjs +14 -0
- package/dist/cli.mjs +14 -3
- package/dist/exec.mjs +7 -6
- package/dist/interrupt.mjs +22 -0
- package/dist/loop.mjs +1 -2
- package/dist/pipeline.mjs +21 -0
- package/dist/state.mjs +0 -9
- package/dist/summary.mjs +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -113,17 +113,24 @@ not the author, tells them apart. The agents read the comments of the issue, not
|
|
|
113
113
|
of the pull request, so a comment of an agent never returns to a prompt: the
|
|
114
114
|
dispute of the fixer reaches the reviewer through the state file.
|
|
115
115
|
|
|
116
|
-
## Stop
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
116
|
+
## Stop a run
|
|
117
|
+
|
|
118
|
+
Press `Ctrl-C` to stop the run step by step:
|
|
119
|
+
|
|
120
|
+
1. The first press stops the run after the current issue. The harness finishes
|
|
121
|
+
the issue that it holds and then writes the summary as usual.
|
|
122
|
+
2. The second press stops the run after the current round: the implementation,
|
|
123
|
+
one checks round (wait, then fix) or one review round (review, then fix). The
|
|
124
|
+
issue ends as `stopped`. Its state, worktree and labels stay, so the next run
|
|
125
|
+
carries on from that point.
|
|
126
|
+
3. The third press kills the command that runs and every child of it, so the run
|
|
127
|
+
dies in the middle of a step. The saved state lets the next run continue the
|
|
128
|
+
issue, but the issue keeps `status:in-progress` and the run writes no
|
|
129
|
+
summary.
|
|
130
|
+
|
|
131
|
+
Each of the first two presses prints a line that says what happens next. The
|
|
132
|
+
agents and commands run in a process group of their own, so a press reaches only
|
|
133
|
+
the harness.
|
|
127
134
|
|
|
128
135
|
A status label that the harness puts on the issue goes on the pull request too,
|
|
129
136
|
but only if the pull request exists at that moment. The `status:in-progress`
|
package/dist/agents.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { track } from "./exec.mjs";
|
|
2
4
|
import { emptyUsage } from "./observe.mjs";
|
|
3
5
|
import { runPi } from "./pi.mjs";
|
|
4
6
|
const REVIEWING = {
|
|
@@ -45,6 +47,7 @@ export async function runAgent(recorder, request) {
|
|
|
45
47
|
allowDangerouslySkipPermissions: request.profile.allowDangerouslySkipPermissions,
|
|
46
48
|
systemPrompt: { type: "preset", preset: "claude_code" },
|
|
47
49
|
abortController: controller,
|
|
50
|
+
spawnClaudeCodeProcess: spawnClaude,
|
|
48
51
|
outputFormat: request.outputSchema === undefined
|
|
49
52
|
? undefined
|
|
50
53
|
: { type: "json_schema", schema: request.outputSchema },
|
|
@@ -149,3 +152,14 @@ export function toolFields(name, input) {
|
|
|
149
152
|
const { command } = input;
|
|
150
153
|
return typeof command === "string" ? { tool: name, command } : { tool: name };
|
|
151
154
|
}
|
|
155
|
+
export function spawnClaude(options) {
|
|
156
|
+
const child = spawn(options.command, options.args, {
|
|
157
|
+
cwd: options.cwd,
|
|
158
|
+
env: options.env,
|
|
159
|
+
signal: options.signal,
|
|
160
|
+
detached: true,
|
|
161
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
162
|
+
});
|
|
163
|
+
track(child);
|
|
164
|
+
return child;
|
|
165
|
+
}
|
package/dist/cli.mjs
CHANGED
|
@@ -6,10 +6,11 @@ import { loadConfig, writeDefaultConfig } from "./config.mjs";
|
|
|
6
6
|
import { setCommandObserver, setDefaultTimeout } from "./exec.mjs";
|
|
7
7
|
import { detectRepo, ensureIgnored, repoRoot } from "./git.mjs";
|
|
8
8
|
import { defaultBranch, openIssues } from "./github.mjs";
|
|
9
|
+
import { listenForInterrupts } from "./interrupt.mjs";
|
|
9
10
|
import { exitCode, runIssues } from "./loop.mjs";
|
|
10
11
|
import { message, pruneRuns, Recorder } from "./observe.mjs";
|
|
11
12
|
import { PORTS, processIssue } from "./pipeline.mjs";
|
|
12
|
-
import { STATE_DIR
|
|
13
|
+
import { STATE_DIR } from "./state.mjs";
|
|
13
14
|
import { formatSummary } from "./summary.mjs";
|
|
14
15
|
import { packageVersion } from "./version.mjs";
|
|
15
16
|
function write(stream, text) {
|
|
@@ -61,8 +62,18 @@ async function init(root, force) {
|
|
|
61
62
|
return 0;
|
|
62
63
|
}
|
|
63
64
|
async function handle(args, config, repo, recorder) {
|
|
65
|
+
const interrupts = listenForInterrupts((text) => void write(process.stderr, `${text}\n`));
|
|
64
66
|
const base = await defaultBranch(repo);
|
|
65
|
-
const context = {
|
|
67
|
+
const context = {
|
|
68
|
+
repo,
|
|
69
|
+
config,
|
|
70
|
+
base,
|
|
71
|
+
reset: args.reset,
|
|
72
|
+
merge: args.merge,
|
|
73
|
+
stopAfterRound: interrupts.afterRound,
|
|
74
|
+
recorder,
|
|
75
|
+
ports: PORTS,
|
|
76
|
+
};
|
|
66
77
|
recorder.event("run.start", {
|
|
67
78
|
version: await packageVersion(),
|
|
68
79
|
repo: `${repo.owner}/${repo.name}`,
|
|
@@ -74,7 +85,7 @@ async function handle(args, config, repo, recorder) {
|
|
|
74
85
|
const { reports, stoppedAt } = await runIssues(args, {
|
|
75
86
|
issues,
|
|
76
87
|
process: (issue) => processIssue(context, issue),
|
|
77
|
-
stop:
|
|
88
|
+
stop: interrupts.afterIssue,
|
|
78
89
|
});
|
|
79
90
|
if (stoppedAt !== undefined) {
|
|
80
91
|
recorder.event("run.stop", { issue: stoppedAt }, "quiet");
|
package/dist/exec.mjs
CHANGED
|
@@ -3,7 +3,7 @@ let observer;
|
|
|
3
3
|
let defaultTimeoutMs = 10 * 60_000;
|
|
4
4
|
const STDERR_TAIL = 200_000;
|
|
5
5
|
const groups = new Set();
|
|
6
|
-
function
|
|
6
|
+
export function stopChildren(signal) {
|
|
7
7
|
for (const pid of groups) {
|
|
8
8
|
try {
|
|
9
9
|
process.kill(-pid, signal);
|
|
@@ -11,18 +11,15 @@ function forward(signal) {
|
|
|
11
11
|
catch { }
|
|
12
12
|
}
|
|
13
13
|
groups.clear();
|
|
14
|
-
process.off("SIGINT", onInterrupt);
|
|
15
14
|
process.off("SIGTERM", onTerminate);
|
|
16
15
|
process.kill(process.pid, signal);
|
|
17
16
|
}
|
|
18
|
-
const
|
|
19
|
-
const onTerminate = () => forward("SIGTERM");
|
|
17
|
+
const onTerminate = () => stopChildren("SIGTERM");
|
|
20
18
|
function watch(pid) {
|
|
21
19
|
if (pid === undefined) {
|
|
22
20
|
return;
|
|
23
21
|
}
|
|
24
22
|
if (groups.size === 0) {
|
|
25
|
-
process.on("SIGINT", onInterrupt);
|
|
26
23
|
process.on("SIGTERM", onTerminate);
|
|
27
24
|
}
|
|
28
25
|
groups.add(pid);
|
|
@@ -33,10 +30,14 @@ function unwatch(pid) {
|
|
|
33
30
|
}
|
|
34
31
|
groups.delete(pid);
|
|
35
32
|
if (groups.size === 0) {
|
|
36
|
-
process.off("SIGINT", onInterrupt);
|
|
37
33
|
process.off("SIGTERM", onTerminate);
|
|
38
34
|
}
|
|
39
35
|
}
|
|
36
|
+
export function track(child) {
|
|
37
|
+
watch(child.pid);
|
|
38
|
+
child.on("close", () => unwatch(child.pid));
|
|
39
|
+
child.on("error", () => unwatch(child.pid));
|
|
40
|
+
}
|
|
40
41
|
export function setCommandObserver(next) {
|
|
41
42
|
observer = next;
|
|
42
43
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { stopChildren } from "./exec.mjs";
|
|
2
|
+
export function listenForInterrupts(say) {
|
|
3
|
+
let presses = 0;
|
|
4
|
+
const press = () => {
|
|
5
|
+
presses += 1;
|
|
6
|
+
if (presses === 1) {
|
|
7
|
+
say("Stopping after this issue. Press Ctrl-C again to stop after this round.");
|
|
8
|
+
}
|
|
9
|
+
else if (presses === 2) {
|
|
10
|
+
say("Stopping after this round. Press Ctrl-C again to stop now.");
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
process.off("SIGINT", press);
|
|
14
|
+
stopChildren("SIGINT");
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
process.on("SIGINT", press);
|
|
18
|
+
return {
|
|
19
|
+
afterIssue: () => presses >= 1,
|
|
20
|
+
afterRound: () => presses >= 2,
|
|
21
|
+
};
|
|
22
|
+
}
|
package/dist/loop.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export async function runIssues(args, loop) {
|
|
2
|
-
await loop.stop();
|
|
3
2
|
const reports = [];
|
|
4
3
|
for (const issue of loop.issues) {
|
|
5
4
|
if (args.max !== undefined && handled(reports) >= args.max) {
|
|
@@ -7,7 +6,7 @@ export async function runIssues(args, loop) {
|
|
|
7
6
|
}
|
|
8
7
|
const report = await loop.process(issue);
|
|
9
8
|
reports.push(report);
|
|
10
|
-
if (
|
|
9
|
+
if (loop.stop()) {
|
|
11
10
|
return { reports, stoppedAt: issue.number };
|
|
12
11
|
}
|
|
13
12
|
if (args.once && report.outcome !== "skipped") {
|
package/dist/pipeline.mjs
CHANGED
|
@@ -167,6 +167,9 @@ async function work(context, issue, log, state) {
|
|
|
167
167
|
state.phase = "implemented";
|
|
168
168
|
await writeState(repo, state);
|
|
169
169
|
}
|
|
170
|
+
if (context.stopAfterRound()) {
|
|
171
|
+
return stop(job);
|
|
172
|
+
}
|
|
170
173
|
await log.step("push", { branch }, () => ports.push(worktree, config.remote, branch));
|
|
171
174
|
const pr = await log.step("pull-request", { branch }, async () => {
|
|
172
175
|
const existing = await ports.findPr(repo, branch);
|
|
@@ -178,6 +181,9 @@ async function work(context, issue, log, state) {
|
|
|
178
181
|
await writeState(repo, state);
|
|
179
182
|
log.event("pull-request.ready", { pr }, "quiet");
|
|
180
183
|
for (;;) {
|
|
184
|
+
if (context.stopAfterRound()) {
|
|
185
|
+
return stop(job);
|
|
186
|
+
}
|
|
181
187
|
const gate = await checkGate(job);
|
|
182
188
|
if (gate.done) {
|
|
183
189
|
return gate.report;
|
|
@@ -185,6 +191,9 @@ async function work(context, issue, log, state) {
|
|
|
185
191
|
if (gate.fixed) {
|
|
186
192
|
continue;
|
|
187
193
|
}
|
|
194
|
+
if (context.stopAfterRound()) {
|
|
195
|
+
return stop(job);
|
|
196
|
+
}
|
|
188
197
|
const round = await reviewRound(job);
|
|
189
198
|
if (round.done) {
|
|
190
199
|
return round.report;
|
|
@@ -324,6 +333,18 @@ async function reviewRound(job) {
|
|
|
324
333
|
function escalate(job, reason) {
|
|
325
334
|
return handOver(job.context, job.log, job.issue.number, job.state, reason);
|
|
326
335
|
}
|
|
336
|
+
function stop(job) {
|
|
337
|
+
const { issue, log, state } = job;
|
|
338
|
+
log.event("issue.stop", { phase: state.phase }, "quiet");
|
|
339
|
+
return {
|
|
340
|
+
issue: issue.number,
|
|
341
|
+
outcome: "stopped",
|
|
342
|
+
pr: state.pr,
|
|
343
|
+
ciFixes: state.ciFixes,
|
|
344
|
+
reviewRounds: state.reviewRounds,
|
|
345
|
+
ms: Date.now() - job.started,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
327
348
|
async function finish(job, outcome, reason) {
|
|
328
349
|
const { context, issue, log, state, branch } = job;
|
|
329
350
|
const { repo, ports } = context;
|
package/dist/state.mjs
CHANGED
|
@@ -22,15 +22,6 @@ 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
|
-
}
|
|
34
25
|
export async function dropState(repo, issue) {
|
|
35
26
|
await rm(file(repo, issue), { force: true });
|
|
36
27
|
}
|
package/dist/summary.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const OUTCOMES = ["done", "needs-human", "error", "skipped"];
|
|
1
|
+
const OUTCOMES = ["done", "needs-human", "error", "skipped", "stopped"];
|
|
2
2
|
export function formatSummary(summary) {
|
|
3
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}`];
|