next-issue 0.20.0 → 1.0.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 +2 -24
- package/dist/agents.mjs +1 -6
- package/dist/args.mjs +1 -9
- package/dist/cli.mjs +0 -1
- package/dist/config.mjs +0 -8
- package/dist/pipeline.mjs +0 -4
- package/package.json +1 -1
- package/dist/pi.mjs +0 -159
package/README.md
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
# next-issue
|
|
2
2
|
|
|
3
3
|
A small harness that works through the open issues of a GitHub repository with
|
|
4
|
-
the Claude Agent SDK
|
|
5
|
-
repository.
|
|
4
|
+
the Claude Agent SDK. Run it inside a git repository.
|
|
6
5
|
|
|
7
6
|
```bash
|
|
8
7
|
npx -y next-issue@latest
|
|
@@ -159,8 +158,7 @@ verdict.
|
|
|
159
158
|
|
|
160
159
|
Every agent can run a shell command. The implementer and the fixer run with the
|
|
161
160
|
permission checks off; the reviewer runs with a smaller tool set, but its shell
|
|
162
|
-
has no limit on the commands.
|
|
163
|
-
agent the reviewer only runs without the `edit` and `write` tools. They all start in the worktree, but nothing holds
|
|
161
|
+
has no limit on the commands. They all start in the worktree, but nothing holds
|
|
164
162
|
them there: they can run any command, with your rights, anywhere on the machine.
|
|
165
163
|
The prompt holds the title, the body and the comments of the issue, which come
|
|
166
164
|
from GitHub.
|
|
@@ -215,7 +213,6 @@ Three levels of console output:
|
|
|
215
213
|
- `gh`, authenticated with write access to the repository
|
|
216
214
|
- Anthropic credentials for the Claude Agent SDK: `ANTHROPIC_API_KEY`, or a
|
|
217
215
|
Claude subscription login
|
|
218
|
-
- With the `pi` agent: `pi` on the `PATH`, set up with a model instead
|
|
219
216
|
|
|
220
217
|
## Configuration
|
|
221
218
|
|
|
@@ -236,7 +233,6 @@ does not know all stop the run, so a typo cannot pass without a word.
|
|
|
236
233
|
|
|
237
234
|
```json
|
|
238
235
|
{
|
|
239
|
-
"agent": "claude",
|
|
240
236
|
"remote": "origin",
|
|
241
237
|
"issueLimit": 100,
|
|
242
238
|
"maxCiFixes": 5,
|
|
@@ -270,7 +266,6 @@ does not know all stop the run, so a typo cannot pass without a word.
|
|
|
270
266
|
|
|
271
267
|
| Field | Default | Effect |
|
|
272
268
|
| --- | --- | --- |
|
|
273
|
-
| `agent` | `claude` | The agent that does the work: `claude` or `pi` |
|
|
274
269
|
| `remote` | `origin` | The git remote for the repository and the branches |
|
|
275
270
|
| `issueLimit` | `100` | The maximum number of open issues to read |
|
|
276
271
|
| `maxCiFixes` | `5` | The budget for check fixes per issue |
|
|
@@ -299,22 +294,6 @@ The log gives the reason for a skipped issue as `stop-label`, `not-ready`,
|
|
|
299
294
|
A role set to `null`, or with no entry in `models`, uses the default model of
|
|
300
295
|
the agent.
|
|
301
296
|
|
|
302
|
-
## The pi agent
|
|
303
|
-
|
|
304
|
-
With `"agent": "pi"` or `--agent pi`, each agent call runs `pi --mode json` in
|
|
305
|
-
the worktree with the prompt on standard input. pi loads your own settings,
|
|
306
|
-
extensions, skills and context files, as it does when you run it yourself. The
|
|
307
|
-
flag wins over the config file.
|
|
308
|
-
|
|
309
|
-
- The implementer and the fixer use your default pi tools.
|
|
310
|
-
- The reviewer runs with `--tools -edit,-write`, so it keeps `bash`.
|
|
311
|
-
- A model in `models` goes to `pi --model` as it is, so use a name that pi
|
|
312
|
-
knows.
|
|
313
|
-
- pi has no structured output, so the reviewer is asked to end its reply with
|
|
314
|
-
a JSON verdict. A reply with no valid verdict hands the issue to a person.
|
|
315
|
-
- A run fails when its last reply ends with an error, when pi exits with an
|
|
316
|
-
error, or when pi stops before the run settles.
|
|
317
|
-
|
|
318
297
|
Without a `setupCommand`, the harness looks in the new worktree for a lock file
|
|
319
298
|
and runs the install command that goes with it:
|
|
320
299
|
|
|
@@ -361,7 +340,6 @@ that a label still holds back stays as it was.
|
|
|
361
340
|
| `--force` | Replace an existing config file, with `init` |
|
|
362
341
|
| `--once` | Stop after the first handled issue |
|
|
363
342
|
| `--max <n>` | Handle at most n issues |
|
|
364
|
-
| `--agent <a>` | Run the agents with `claude` or `pi`, whatever the config file says |
|
|
365
343
|
| `--merge` | Squash-merge the pull request after the approval, delete the remote branch and fix a failure of the checks on the base branch |
|
|
366
344
|
| `--reset` | Drop the saved budgets and findings, and keep the rest of the state |
|
|
367
345
|
| `--json` | Print the machine summary instead of the text one |
|
package/dist/agents.mjs
CHANGED
|
@@ -2,11 +2,9 @@ import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { track } from "./exec.mjs";
|
|
4
4
|
import { emptyUsage } from "./observe.mjs";
|
|
5
|
-
import { runPi } from "./pi.mjs";
|
|
6
5
|
const REVIEWING = {
|
|
7
6
|
tools: ["Read", "Grep", "Glob", "Bash"],
|
|
8
7
|
permissionMode: "dontAsk",
|
|
9
|
-
piTools: "-edit,-write",
|
|
10
8
|
};
|
|
11
9
|
const CODING = {
|
|
12
10
|
tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"],
|
|
@@ -15,9 +13,6 @@ const CODING = {
|
|
|
15
13
|
};
|
|
16
14
|
export { CODING, REVIEWING };
|
|
17
15
|
export async function runAgent(recorder, request) {
|
|
18
|
-
if (request.agent === "pi") {
|
|
19
|
-
return runPi(recorder, request);
|
|
20
|
-
}
|
|
21
16
|
const log = recorder.scope({ role: request.name });
|
|
22
17
|
const usage = emptyUsage();
|
|
23
18
|
const toolNames = new Map();
|
|
@@ -146,7 +141,7 @@ function main(models) {
|
|
|
146
141
|
return name;
|
|
147
142
|
}
|
|
148
143
|
export function toolFields(name, input) {
|
|
149
|
-
if (
|
|
144
|
+
if (name !== "Bash" || typeof input !== "object" || input === null) {
|
|
150
145
|
return { tool: name };
|
|
151
146
|
}
|
|
152
147
|
const { command } = input;
|
package/dist/args.mjs
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CONFIG_FILE } from "./config.mjs";
|
|
2
2
|
export const USAGE = `next-issue [command] [options]
|
|
3
3
|
|
|
4
4
|
init write ${CONFIG_FILE} with the defaults
|
|
5
5
|
|
|
6
6
|
--once stop after the first handled issue
|
|
7
7
|
--max <n> handle at most n issues
|
|
8
|
-
--agent <a> run the agents with claude or pi
|
|
9
8
|
--merge squash-merge the pull request after the approval
|
|
10
9
|
--reset drop the saved budgets and findings first
|
|
11
10
|
--json print the machine summary instead of the text one
|
|
@@ -57,13 +56,6 @@ export function parseArgs(argv) {
|
|
|
57
56
|
else if (arg === "--quiet") {
|
|
58
57
|
args.level = "quiet";
|
|
59
58
|
}
|
|
60
|
-
else if (arg === "--agent") {
|
|
61
|
-
const value = argv[++index];
|
|
62
|
-
if (value === undefined || !AGENT_NAMES.includes(value)) {
|
|
63
|
-
throw new Error("--agent needs claude or pi");
|
|
64
|
-
}
|
|
65
|
-
args.agent = value;
|
|
66
|
-
}
|
|
67
59
|
else if (arg === "--max") {
|
|
68
60
|
args.max = whole(argv[++index], arg);
|
|
69
61
|
}
|
package/dist/cli.mjs
CHANGED
|
@@ -29,7 +29,6 @@ async function main() {
|
|
|
29
29
|
return init(root, args.force);
|
|
30
30
|
}
|
|
31
31
|
const config = await loadConfig(root);
|
|
32
|
-
config.agent = args.agent ?? config.agent;
|
|
33
32
|
setDefaultTimeout(config.commandTimeoutMinutes * 60_000);
|
|
34
33
|
const repo = await detectRepo(root, config.remote);
|
|
35
34
|
const recorder = await Recorder.create(repo.root, args.level);
|
package/dist/config.mjs
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { message } from "./observe.mjs";
|
|
4
|
-
export const AGENT_NAMES = ["claude", "pi"];
|
|
5
4
|
const DEFAULTS = {
|
|
6
|
-
agent: "claude",
|
|
7
5
|
remote: "origin",
|
|
8
6
|
issueLimit: 100,
|
|
9
7
|
maxCiFixes: 5,
|
|
@@ -95,12 +93,6 @@ function parseConfig(value) {
|
|
|
95
93
|
}
|
|
96
94
|
}
|
|
97
95
|
const config = defaults();
|
|
98
|
-
if (file.agent !== undefined) {
|
|
99
|
-
if (typeof file.agent !== "string" || !AGENT_NAMES.includes(file.agent)) {
|
|
100
|
-
throw new Error(`${CONFIG_FILE}: agent must be claude or pi.`);
|
|
101
|
-
}
|
|
102
|
-
config.agent = file.agent;
|
|
103
|
-
}
|
|
104
96
|
if (file.remote !== undefined) {
|
|
105
97
|
config.remote = text(file.remote, "remote", false);
|
|
106
98
|
}
|
package/dist/pipeline.mjs
CHANGED
|
@@ -162,7 +162,6 @@ async function work(context, issue, log, state) {
|
|
|
162
162
|
prompt: implementPrompt(issue, comments),
|
|
163
163
|
profile: CODING,
|
|
164
164
|
model: config.models.implementer,
|
|
165
|
-
agent: config.agent,
|
|
166
165
|
timeoutMs: config.agentTimeoutMinutes * 60_000,
|
|
167
166
|
}));
|
|
168
167
|
const subject = parseCommitSubject(result.text, `fix: resolve issue #${issue.number}`);
|
|
@@ -276,7 +275,6 @@ async function reviewRound(job) {
|
|
|
276
275
|
prompt: reviewPrompt(issue, comments, patch, history(state.reviewLog)),
|
|
277
276
|
profile: REVIEWING,
|
|
278
277
|
model: config.models.reviewer,
|
|
279
|
-
agent: config.agent,
|
|
280
278
|
timeoutMs: config.agentTimeoutMinutes * 60_000,
|
|
281
279
|
outputSchema: VERDICT_SCHEMA,
|
|
282
280
|
}));
|
|
@@ -440,7 +438,6 @@ async function fixRound(job, reason, detail, earlier, kind) {
|
|
|
440
438
|
prompt: fixPrompt(issue, reason, detail, earlier, kind),
|
|
441
439
|
profile: CODING,
|
|
442
440
|
model: context.config.models.fixer,
|
|
443
|
-
agent: context.config.agent,
|
|
444
441
|
timeoutMs: context.config.agentTimeoutMinutes * 60_000,
|
|
445
442
|
}));
|
|
446
443
|
const unrelated = parseUnrelated(result.text);
|
|
@@ -482,7 +479,6 @@ async function commitWork(job, before, subject, role) {
|
|
|
482
479
|
prompt: hookPrompt(issue, comments, role, output),
|
|
483
480
|
profile: CODING,
|
|
484
481
|
model: config.models[role],
|
|
485
|
-
agent: config.agent,
|
|
486
482
|
timeoutMs: config.agentTimeoutMinutes * 60_000,
|
|
487
483
|
}));
|
|
488
484
|
}
|
package/package.json
CHANGED
package/dist/pi.mjs
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import { toolFields } from "./agents.mjs";
|
|
2
|
-
import { run } from "./exec.mjs";
|
|
3
|
-
import { emptyUsage } from "./observe.mjs";
|
|
4
|
-
const NO_LIMIT = 2 ** 31 - 1;
|
|
5
|
-
const OUTPUT_TAIL = 200_000;
|
|
6
|
-
export async function runPi(recorder, request) {
|
|
7
|
-
const log = recorder.scope({ role: request.name });
|
|
8
|
-
const usage = emptyUsage();
|
|
9
|
-
let text = "";
|
|
10
|
-
let toolCalls = 0;
|
|
11
|
-
let turns = 0;
|
|
12
|
-
let costUsd = 0;
|
|
13
|
-
let model = request.model;
|
|
14
|
-
let sessionId;
|
|
15
|
-
let settled = false;
|
|
16
|
-
let last;
|
|
17
|
-
const read = (line) => {
|
|
18
|
-
let event;
|
|
19
|
-
try {
|
|
20
|
-
event = JSON.parse(line);
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
if (event.type === "session") {
|
|
26
|
-
sessionId = event.id;
|
|
27
|
-
}
|
|
28
|
-
else if (event.type === "message_end" && event.message?.role === "assistant") {
|
|
29
|
-
const message = event.message;
|
|
30
|
-
last = message;
|
|
31
|
-
turns += 1;
|
|
32
|
-
model = message.model ?? model;
|
|
33
|
-
const input = message.usage?.input ?? 0;
|
|
34
|
-
const output = message.usage?.output ?? 0;
|
|
35
|
-
const cacheRead = message.usage?.cacheRead ?? 0;
|
|
36
|
-
const cacheWrite = message.usage?.cacheWrite ?? 0;
|
|
37
|
-
usage.input += input;
|
|
38
|
-
usage.output += output;
|
|
39
|
-
usage.cacheRead += cacheRead;
|
|
40
|
-
usage.cacheWrite += cacheWrite;
|
|
41
|
-
usage.total += input + output + cacheRead + cacheWrite;
|
|
42
|
-
costUsd += message.usage?.cost?.total ?? 0;
|
|
43
|
-
const said = textOf(message.content);
|
|
44
|
-
if (said.length > 0) {
|
|
45
|
-
text = said;
|
|
46
|
-
if (log.level === "verbose") {
|
|
47
|
-
process.stderr.write(said);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
else if (event.type === "tool_execution_start") {
|
|
52
|
-
toolCalls += 1;
|
|
53
|
-
log.event("tool", toolFields(event.toolName ?? "unknown", event.args));
|
|
54
|
-
}
|
|
55
|
-
else if (event.type === "tool_execution_end" && event.isError === true) {
|
|
56
|
-
log.event("tool.error", { tool: event.toolName }, "quiet");
|
|
57
|
-
}
|
|
58
|
-
else if (event.type === "agent_settled") {
|
|
59
|
-
settled = true;
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
const started = Date.now();
|
|
63
|
-
log.event("agent.start", { model: request.model, timeoutMs: request.timeoutMs, agent: "pi" }, "normal");
|
|
64
|
-
let rest = "";
|
|
65
|
-
const result = await run("pi", piArgs(request), {
|
|
66
|
-
cwd: request.cwd,
|
|
67
|
-
input: piPrompt(request),
|
|
68
|
-
timeoutMs: request.timeoutMs ?? NO_LIMIT,
|
|
69
|
-
tailChars: OUTPUT_TAIL,
|
|
70
|
-
onStdout: (chunk) => {
|
|
71
|
-
rest += chunk;
|
|
72
|
-
let end = rest.indexOf("\n");
|
|
73
|
-
while (end !== -1) {
|
|
74
|
-
read(rest.slice(0, end).replace(/\r$/, ""));
|
|
75
|
-
rest = rest.slice(end + 1);
|
|
76
|
-
end = rest.indexOf("\n");
|
|
77
|
-
}
|
|
78
|
-
},
|
|
79
|
-
});
|
|
80
|
-
if (rest.length > 0) {
|
|
81
|
-
read(rest);
|
|
82
|
-
}
|
|
83
|
-
const failed = (reason) => new Error(`The ${request.name} agent failed: ${reason}`);
|
|
84
|
-
if (result.timedOut) {
|
|
85
|
-
throw new Error(`The ${request.name} agent did not finish in ${request.timeoutMs} ms.`);
|
|
86
|
-
}
|
|
87
|
-
if (last?.stopReason === "error" || last?.stopReason === "aborted") {
|
|
88
|
-
throw failed(last.errorMessage ?? last.stopReason);
|
|
89
|
-
}
|
|
90
|
-
if (result.code !== 0) {
|
|
91
|
-
throw failed(`pi exited with code ${result.code}: ${result.stderr.trim()}`);
|
|
92
|
-
}
|
|
93
|
-
if (!settled) {
|
|
94
|
-
throw failed("pi stopped before the run settled");
|
|
95
|
-
}
|
|
96
|
-
if (last?.stopReason === "length") {
|
|
97
|
-
log.event("agent.incomplete", { subtype: "length" }, "quiet");
|
|
98
|
-
}
|
|
99
|
-
const ms = Date.now() - started;
|
|
100
|
-
recorder.usage(request.name, usage, ms, {
|
|
101
|
-
turns,
|
|
102
|
-
toolCalls,
|
|
103
|
-
model,
|
|
104
|
-
session: sessionId,
|
|
105
|
-
costUsd,
|
|
106
|
-
});
|
|
107
|
-
return {
|
|
108
|
-
text: text.trim(),
|
|
109
|
-
usage,
|
|
110
|
-
turns,
|
|
111
|
-
toolCalls,
|
|
112
|
-
ms,
|
|
113
|
-
costUsd,
|
|
114
|
-
model,
|
|
115
|
-
sessionId,
|
|
116
|
-
structured: request.outputSchema === undefined ? undefined : jsonOf(text),
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
function piArgs(request) {
|
|
120
|
-
return [
|
|
121
|
-
"--mode",
|
|
122
|
-
"json",
|
|
123
|
-
...(request.model === undefined ? [] : ["--model", request.model]),
|
|
124
|
-
...(request.profile.piTools === undefined ? [] : ["--tools", request.profile.piTools]),
|
|
125
|
-
];
|
|
126
|
-
}
|
|
127
|
-
function piPrompt(request) {
|
|
128
|
-
if (request.outputSchema === undefined) {
|
|
129
|
-
return request.prompt;
|
|
130
|
-
}
|
|
131
|
-
return [
|
|
132
|
-
request.prompt,
|
|
133
|
-
"",
|
|
134
|
-
"End your reply with only a JSON object in a json code block. The object must match this JSON schema:",
|
|
135
|
-
JSON.stringify(request.outputSchema),
|
|
136
|
-
].join("\n");
|
|
137
|
-
}
|
|
138
|
-
function textOf(content) {
|
|
139
|
-
if (typeof content === "string") {
|
|
140
|
-
return content;
|
|
141
|
-
}
|
|
142
|
-
if (!Array.isArray(content)) {
|
|
143
|
-
return "";
|
|
144
|
-
}
|
|
145
|
-
return content
|
|
146
|
-
.filter((block) => block?.type === "text" && typeof block.text === "string")
|
|
147
|
-
.map((block) => block.text)
|
|
148
|
-
.join("");
|
|
149
|
-
}
|
|
150
|
-
function jsonOf(text) {
|
|
151
|
-
const fenced = [...text.matchAll(/```(?:json)?[ \t]*\n([\s\S]*?)```/g)].at(-1)?.[1];
|
|
152
|
-
const candidate = fenced ?? text.slice(text.indexOf("{"), text.lastIndexOf("}") + 1);
|
|
153
|
-
try {
|
|
154
|
-
return JSON.parse(candidate);
|
|
155
|
-
}
|
|
156
|
-
catch {
|
|
157
|
-
return undefined;
|
|
158
|
-
}
|
|
159
|
-
}
|