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 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, or with [pi](https://pi.dev). Run it inside a git
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. pi has no permission checks, so with the `pi`
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 ((name !== "Bash" && name !== "bash") || typeof input !== "object" || input === null) {
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 { AGENT_NAMES, CONFIG_FILE } from "./config.mjs";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-issue",
3
- "version": "0.20.0",
3
+ "version": "1.0.0",
4
4
  "description": "Autonomous issue harness driven by the Claude Agent SDK",
5
5
  "keywords": [
6
6
  "github",
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
- }