next-issue 0.14.1 → 0.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 -7
- package/dist/agents.mjs +6 -1
- package/dist/args.mjs +9 -1
- package/dist/cli.mjs +1 -0
- package/dist/config.mjs +8 -0
- package/dist/exec.mjs +1 -0
- package/dist/github.mjs +13 -0
- package/dist/pi.mjs +159 -0
- package/dist/pipeline.mjs +23 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
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. Run it inside a git
|
|
4
|
+
the Claude Agent SDK, or with [pi](https://pi.dev). Run it inside a git
|
|
5
|
+
repository.
|
|
5
6
|
|
|
6
7
|
```bash
|
|
7
8
|
npx -y next-issue@latest
|
|
@@ -21,8 +22,10 @@ For each open issue, oldest first:
|
|
|
21
22
|
1. Claim the issue only when it holds the `status:todo` label and holds no
|
|
22
23
|
`status:in-progress` or `status:in-review` label. A `status:done`,
|
|
23
24
|
`status:needs-human` or `status:blocked` label always stops the claim, a
|
|
24
|
-
resume too. An issue
|
|
25
|
-
|
|
25
|
+
resume too. An issue that GitHub lists as blocked by an open issue waits,
|
|
26
|
+
because its blocker closes only when the pull request of the blocker merges.
|
|
27
|
+
An issue with saved state under `.next-issue/` resumes instead, whatever its
|
|
28
|
+
other labels and blockers.
|
|
26
29
|
2. Add `status:in-progress` to the issue.
|
|
27
30
|
3. Fetch the remote, fast-forward the local copy of the default branch when
|
|
28
31
|
that is safe, then add a git worktree at `../<repo>-issue-<n>` on the
|
|
@@ -131,7 +134,8 @@ verdict.
|
|
|
131
134
|
|
|
132
135
|
Every agent can run a shell command. The implementer and the fixer run with the
|
|
133
136
|
permission checks off; the reviewer runs with a smaller tool set, but its shell
|
|
134
|
-
has no limit on the commands.
|
|
137
|
+
has no limit on the commands. pi has no permission checks, so with the `pi`
|
|
138
|
+
agent the reviewer only runs without the `edit` and `write` tools. They all start in the worktree, but nothing holds
|
|
135
139
|
them there: they can run any command, with your rights, anywhere on the machine.
|
|
136
140
|
The prompt holds the title, the body and the comments of the issue, which come
|
|
137
141
|
from GitHub.
|
|
@@ -186,6 +190,7 @@ Three levels of console output:
|
|
|
186
190
|
- `gh`, authenticated with write access to the repository
|
|
187
191
|
- Anthropic credentials for the Claude Agent SDK: `ANTHROPIC_API_KEY`, or a
|
|
188
192
|
Claude subscription login
|
|
193
|
+
- With the `pi` agent: `pi` on the `PATH`, set up with a model instead
|
|
189
194
|
|
|
190
195
|
## Configuration
|
|
191
196
|
|
|
@@ -206,6 +211,7 @@ does not know all stop the run, so a typo cannot pass without a word.
|
|
|
206
211
|
|
|
207
212
|
```json
|
|
208
213
|
{
|
|
214
|
+
"agent": "claude",
|
|
209
215
|
"remote": "origin",
|
|
210
216
|
"issueLimit": 100,
|
|
211
217
|
"maxCiFixes": 5,
|
|
@@ -238,6 +244,7 @@ does not know all stop the run, so a typo cannot pass without a word.
|
|
|
238
244
|
|
|
239
245
|
| Field | Default | Effect |
|
|
240
246
|
| --- | --- | --- |
|
|
247
|
+
| `agent` | `claude` | The agent that does the work: `claude` or `pi` |
|
|
241
248
|
| `remote` | `origin` | The git remote for the repository and the branches |
|
|
242
249
|
| `issueLimit` | `100` | The maximum number of open issues to read |
|
|
243
250
|
| `maxCiFixes` | `5` | The budget for check fixes per issue |
|
|
@@ -259,11 +266,27 @@ does not know all stop the run, so a typo cannot pass without a word.
|
|
|
259
266
|
An empty `labels.ready` turns the ready requirement off. The harness then
|
|
260
267
|
claims every open issue that no other label holds back.
|
|
261
268
|
|
|
262
|
-
The log gives the reason for a skipped issue as `stop-label`, `not-ready
|
|
263
|
-
`in-flight
|
|
269
|
+
The log gives the reason for a skipped issue as `stop-label`, `not-ready`,
|
|
270
|
+
`in-flight` or `blocked`, the last one with the open blockers.
|
|
264
271
|
|
|
265
272
|
A role set to `null`, or with no entry in `models`, uses the default model of
|
|
266
|
-
the
|
|
273
|
+
the agent.
|
|
274
|
+
|
|
275
|
+
## The pi agent
|
|
276
|
+
|
|
277
|
+
With `"agent": "pi"` or `--agent pi`, each agent call runs `pi --mode json` in
|
|
278
|
+
the worktree with the prompt on standard input. pi loads your own settings,
|
|
279
|
+
extensions, skills and context files, as it does when you run it yourself. The
|
|
280
|
+
flag wins over the config file.
|
|
281
|
+
|
|
282
|
+
- The implementer and the fixer use your default pi tools.
|
|
283
|
+
- The reviewer runs with `--tools -edit,-write`, so it keeps `bash`.
|
|
284
|
+
- A model in `models` goes to `pi --model` as it is, so use a name that pi
|
|
285
|
+
knows.
|
|
286
|
+
- pi has no structured output, so the reviewer is asked to end its reply with
|
|
287
|
+
a JSON verdict. A reply with no valid verdict hands the issue to a person.
|
|
288
|
+
- A run fails when its last reply ends with an error, when pi exits with an
|
|
289
|
+
error, or when pi stops before the run settles.
|
|
267
290
|
|
|
268
291
|
Without a `setupCommand`, the harness looks in the new worktree for a lock file
|
|
269
292
|
and runs the install command that goes with it:
|
|
@@ -311,6 +334,7 @@ that a label still holds back stays as it was.
|
|
|
311
334
|
| `--force` | Replace an existing config file, with `init` |
|
|
312
335
|
| `--once` | Stop after the first handled issue |
|
|
313
336
|
| `--max <n>` | Handle at most n issues |
|
|
337
|
+
| `--agent <a>` | Run the agents with `claude` or `pi`, whatever the config file says |
|
|
314
338
|
| `--reset` | Drop the saved budgets and findings, and keep the rest of the state |
|
|
315
339
|
| `--json` | Print the machine summary instead of the text one |
|
|
316
340
|
| `--verbose` | Show every command and all agent output |
|
package/dist/agents.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
2
2
|
import { emptyUsage } from "./observe.mjs";
|
|
3
|
+
import { runPi } from "./pi.mjs";
|
|
3
4
|
const REVIEWING = {
|
|
4
5
|
tools: ["Read", "Grep", "Glob", "Bash"],
|
|
5
6
|
permissionMode: "dontAsk",
|
|
7
|
+
piTools: "-edit,-write",
|
|
6
8
|
};
|
|
7
9
|
const CODING = {
|
|
8
10
|
tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"],
|
|
@@ -11,6 +13,9 @@ const CODING = {
|
|
|
11
13
|
};
|
|
12
14
|
export { CODING, REVIEWING };
|
|
13
15
|
export async function runAgent(recorder, request) {
|
|
16
|
+
if (request.agent === "pi") {
|
|
17
|
+
return runPi(recorder, request);
|
|
18
|
+
}
|
|
14
19
|
const log = recorder.scope({ role: request.name });
|
|
15
20
|
const usage = emptyUsage();
|
|
16
21
|
const toolNames = new Map();
|
|
@@ -138,7 +143,7 @@ function main(models) {
|
|
|
138
143
|
return name;
|
|
139
144
|
}
|
|
140
145
|
export function toolFields(name, input) {
|
|
141
|
-
if (name !== "Bash" || typeof input !== "object" || input === null) {
|
|
146
|
+
if ((name !== "Bash" && name !== "bash") || typeof input !== "object" || input === null) {
|
|
142
147
|
return { tool: name };
|
|
143
148
|
}
|
|
144
149
|
const { command } = input;
|
package/dist/args.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { CONFIG_FILE } from "./config.mjs";
|
|
1
|
+
import { AGENT_NAMES, 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
|
|
8
9
|
--reset drop the saved budgets and findings first
|
|
9
10
|
--json print the machine summary instead of the text one
|
|
10
11
|
--force replace an existing config file, with init
|
|
@@ -51,6 +52,13 @@ export function parseArgs(argv) {
|
|
|
51
52
|
else if (arg === "--quiet") {
|
|
52
53
|
args.level = "quiet";
|
|
53
54
|
}
|
|
55
|
+
else if (arg === "--agent") {
|
|
56
|
+
const value = argv[++index];
|
|
57
|
+
if (value === undefined || !AGENT_NAMES.includes(value)) {
|
|
58
|
+
throw new Error("--agent needs claude or pi");
|
|
59
|
+
}
|
|
60
|
+
args.agent = value;
|
|
61
|
+
}
|
|
54
62
|
else if (arg === "--max") {
|
|
55
63
|
args.max = whole(argv[++index], arg);
|
|
56
64
|
}
|
package/dist/cli.mjs
CHANGED
|
@@ -28,6 +28,7 @@ async function main() {
|
|
|
28
28
|
return init(root, args.force);
|
|
29
29
|
}
|
|
30
30
|
const config = await loadConfig(root);
|
|
31
|
+
config.agent = args.agent ?? config.agent;
|
|
31
32
|
setDefaultTimeout(config.commandTimeoutMinutes * 60_000);
|
|
32
33
|
const repo = await detectRepo(root, config.remote);
|
|
33
34
|
const recorder = await Recorder.create(repo.root, args.level);
|
package/dist/config.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
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"];
|
|
4
5
|
const DEFAULTS = {
|
|
6
|
+
agent: "claude",
|
|
5
7
|
remote: "origin",
|
|
6
8
|
issueLimit: 100,
|
|
7
9
|
maxCiFixes: 5,
|
|
@@ -91,6 +93,12 @@ function parseConfig(value) {
|
|
|
91
93
|
}
|
|
92
94
|
}
|
|
93
95
|
const config = defaults();
|
|
96
|
+
if (file.agent !== undefined) {
|
|
97
|
+
if (typeof file.agent !== "string" || !AGENT_NAMES.includes(file.agent)) {
|
|
98
|
+
throw new Error(`${CONFIG_FILE}: agent must be claude or pi.`);
|
|
99
|
+
}
|
|
100
|
+
config.agent = file.agent;
|
|
101
|
+
}
|
|
94
102
|
if (file.remote !== undefined) {
|
|
95
103
|
config.remote = text(file.remote, "remote", false);
|
|
96
104
|
}
|
package/dist/exec.mjs
CHANGED
|
@@ -64,6 +64,7 @@ export function run(command, args, options = {}) {
|
|
|
64
64
|
child.stdout?.setEncoding("utf8");
|
|
65
65
|
child.stderr?.setEncoding("utf8");
|
|
66
66
|
child.stdout?.on("data", (chunk) => {
|
|
67
|
+
options.onStdout?.(chunk);
|
|
67
68
|
stdout += chunk;
|
|
68
69
|
if (options.tailChars !== undefined && stdout.length > options.tailChars) {
|
|
69
70
|
stdout = stdout.slice(-options.tailChars);
|
package/dist/github.mjs
CHANGED
|
@@ -44,6 +44,19 @@ export async function issueComments(repo, issue) {
|
|
|
44
44
|
const parsed = JSON.parse(raw);
|
|
45
45
|
return parsed.comments.map((comment) => `@${comment.author.login}: ${comment.body}`);
|
|
46
46
|
}
|
|
47
|
+
export async function openBlockers(repo, issue) {
|
|
48
|
+
const raw = await must("gh", [
|
|
49
|
+
"api",
|
|
50
|
+
"--paginate",
|
|
51
|
+
`repos/${slug(repo)}/issues/${issue}/dependencies/blocked_by`,
|
|
52
|
+
"--jq",
|
|
53
|
+
'.[] | select(.state == "open") | (.repository_url | split("/") | .[-2:] | join("/")) + "#" + (.number | tostring)',
|
|
54
|
+
], { cwd: repo.root });
|
|
55
|
+
return raw
|
|
56
|
+
.split("\n")
|
|
57
|
+
.filter((line) => line.length > 0)
|
|
58
|
+
.map((ref) => (ref.startsWith(`${slug(repo)}#`) ? ref.slice(slug(repo).length) : ref));
|
|
59
|
+
}
|
|
47
60
|
async function labelsOf(repo, kind, number) {
|
|
48
61
|
const raw = await gh(repo, [kind, "view", String(number), "--json", "labels"]);
|
|
49
62
|
const parsed = JSON.parse(raw);
|
package/dist/pi.mjs
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
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
|
+
}
|
package/dist/pipeline.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { CODING, REVIEWING, runAgent } from "./agents.mjs";
|
|
|
2
2
|
import { managedLabels } from "./config.mjs";
|
|
3
3
|
import { run } from "./exec.mjs";
|
|
4
4
|
import { addWorktree, branchName, commitAll, deleteBranch, diff, discardChanges, hasWorktree, isDirty, push, removeWorktree, revision, worktreePath, } from "./git.mjs";
|
|
5
|
-
import { commentOnPr, createPr, failedCheckLogs, findPr, issueComments, markPrReady, setLabel, waitForChecks, } from "./github.mjs";
|
|
5
|
+
import { commentOnPr, createPr, failedCheckLogs, findPr, issueComments, markPrReady, openBlockers, setLabel, waitForChecks, } from "./github.mjs";
|
|
6
6
|
import { message } from "./observe.mjs";
|
|
7
7
|
import { capDiff, fixPrompt, implementPrompt, parseCommitSubject, parseUnrelated, reviewPrompt, } from "./prompts.mjs";
|
|
8
8
|
import { detectSetupCommand } from "./setup.mjs";
|
|
@@ -23,6 +23,7 @@ export const PORTS = {
|
|
|
23
23
|
isDirty,
|
|
24
24
|
issueComments,
|
|
25
25
|
markPrReady,
|
|
26
|
+
openBlockers,
|
|
26
27
|
push,
|
|
27
28
|
removeWorktree,
|
|
28
29
|
revision,
|
|
@@ -55,7 +56,21 @@ export async function processIssue(context, issue) {
|
|
|
55
56
|
const skip = skipReason(issue, context.config, saved !== undefined);
|
|
56
57
|
if (skip !== undefined) {
|
|
57
58
|
log.event("issue.skip", { title: issue.title, reason: skip });
|
|
58
|
-
return
|
|
59
|
+
return skipped(issue);
|
|
60
|
+
}
|
|
61
|
+
if (saved === undefined) {
|
|
62
|
+
let blockers;
|
|
63
|
+
try {
|
|
64
|
+
blockers = await context.ports.openBlockers(context.repo, issue.number);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
log.event("issue.error", { error: message(error), ms: Date.now() - started }, "quiet");
|
|
68
|
+
return { ...skipped(issue), outcome: "error", reason: message(error), ms: Date.now() - started };
|
|
69
|
+
}
|
|
70
|
+
if (blockers.length > 0) {
|
|
71
|
+
log.event("issue.skip", { title: issue.title, reason: "blocked", blockers });
|
|
72
|
+
return skipped(issue);
|
|
73
|
+
}
|
|
59
74
|
}
|
|
60
75
|
log.event("issue.start", { title: issue.title, createdAt: issue.createdAt }, "quiet");
|
|
61
76
|
const fresh = {
|
|
@@ -92,6 +107,9 @@ export async function processIssue(context, issue) {
|
|
|
92
107
|
};
|
|
93
108
|
}
|
|
94
109
|
}
|
|
110
|
+
function skipped(issue) {
|
|
111
|
+
return { issue: issue.number, outcome: "skipped", ciFixes: 0, reviewRounds: 0, ms: 0 };
|
|
112
|
+
}
|
|
95
113
|
async function work(context, issue, log, state) {
|
|
96
114
|
const started = Date.now();
|
|
97
115
|
const { repo, config, ports } = context;
|
|
@@ -137,6 +155,7 @@ async function work(context, issue, log, state) {
|
|
|
137
155
|
prompt: implementPrompt(issue, comments),
|
|
138
156
|
profile: CODING,
|
|
139
157
|
model: config.models.implementer,
|
|
158
|
+
agent: config.agent,
|
|
140
159
|
timeoutMs: config.agentTimeoutMinutes * 60_000,
|
|
141
160
|
}));
|
|
142
161
|
const subject = parseCommitSubject(result.text, `fix: resolve issue #${issue.number}`);
|
|
@@ -236,6 +255,7 @@ async function reviewRound(job) {
|
|
|
236
255
|
prompt: reviewPrompt(issue, comments, patch, history(state.reviewLog)),
|
|
237
256
|
profile: REVIEWING,
|
|
238
257
|
model: config.models.reviewer,
|
|
258
|
+
agent: config.agent,
|
|
239
259
|
timeoutMs: config.agentTimeoutMinutes * 60_000,
|
|
240
260
|
outputSchema: VERDICT_SCHEMA,
|
|
241
261
|
}));
|
|
@@ -329,6 +349,7 @@ async function fixRound(job, reason, detail, earlier, kind) {
|
|
|
329
349
|
prompt: fixPrompt(issue, reason, detail, earlier, kind),
|
|
330
350
|
profile: CODING,
|
|
331
351
|
model: context.config.models.fixer,
|
|
352
|
+
agent: context.config.agent,
|
|
332
353
|
timeoutMs: context.config.agentTimeoutMinutes * 60_000,
|
|
333
354
|
}));
|
|
334
355
|
const unrelated = parseUnrelated(result.text);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "next-issue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Autonomous issue harness driven by the Claude Agent SDK",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"github",
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
"prepublishOnly": "npm run typecheck && npm test"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@types/node": "^26.
|
|
40
|
+
"@types/node": "^26.6.5",
|
|
41
41
|
"typescript": "^7.0.2"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@anthropic-ai/claude-agent-sdk": "^0.3.
|
|
44
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.310"
|
|
45
45
|
}
|
|
46
46
|
}
|