next-issue 0.3.0 → 0.5.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 +26 -6
- package/dist/agents.mjs +8 -1
- package/dist/cli.mjs +7 -1
- package/dist/git.mjs +10 -0
- package/dist/summary.mjs +47 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,13 +4,13 @@ A small harness that works through the open issues of a GitHub repository with
|
|
|
4
4
|
the Claude Agent SDK. Run it inside a git repository.
|
|
5
5
|
|
|
6
6
|
```bash
|
|
7
|
-
npx next-issue
|
|
7
|
+
npx -y next-issue@latest
|
|
8
8
|
```
|
|
9
9
|
|
|
10
10
|
Or install it once:
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
npm install -g next-issue
|
|
13
|
+
npm install -g next-issue@latest
|
|
14
14
|
next-issue
|
|
15
15
|
```
|
|
16
16
|
|
|
@@ -24,7 +24,8 @@ For each open issue, oldest first:
|
|
|
24
24
|
the claim. An issue with saved state under `.next-issue/` resumes instead,
|
|
25
25
|
whatever its labels, unless another person is now the assignee.
|
|
26
26
|
2. Assign the issue to you and add `status:in-progress`.
|
|
27
|
-
3.
|
|
27
|
+
3. Fetch the default branch, fast-forward the local copy of it when that is
|
|
28
|
+
safe, then add a git worktree on a new `issue-<n>` branch from it.
|
|
28
29
|
4. Let the implementer agent do the work, then commit and push.
|
|
29
30
|
5. Open a draft pull request that closes the issue.
|
|
30
31
|
6. Wait for the checks. A pending state waits again, up to
|
|
@@ -85,13 +86,15 @@ Every run has an id, `<timestamp>`, and writes two files under
|
|
|
85
86
|
one report per issue with the outcome, the reason, the pull request number
|
|
86
87
|
and the budgets used.
|
|
87
88
|
|
|
88
|
-
|
|
89
|
+
A short text summary of the same data goes to standard output. Use `--json` to
|
|
90
|
+
get the full summary object there instead, so the harness fits in a pipe. All
|
|
89
91
|
progress goes to standard error.
|
|
90
92
|
|
|
91
93
|
Recorded for each step: `claim`, `comments`, `worktree`, `implement`, `push`,
|
|
92
94
|
`pull-request`, `checks`, `review` and `fix`, each with a `.start`, `.end` or
|
|
93
95
|
`.error` event and a duration in `ms`. Every `git` and `gh` command is recorded
|
|
94
|
-
with its exit code and duration.
|
|
96
|
+
with its exit code and duration. A `tool` event holds the tool name, and for a
|
|
97
|
+
`Bash` tool the command too. Each agent call adds a `usage` event with the
|
|
95
98
|
tokens, the cost estimate in dollars, the turn count, the tool-call count, the
|
|
96
99
|
model and the session id, so you can open the session again and read the full
|
|
97
100
|
transcript.
|
|
@@ -121,7 +124,7 @@ Three levels of console output:
|
|
|
121
124
|
To write the file with the defaults, run the `init` command in the repository:
|
|
122
125
|
|
|
123
126
|
```bash
|
|
124
|
-
npx next-issue init
|
|
127
|
+
npx -y next-issue@latest init
|
|
125
128
|
```
|
|
126
129
|
|
|
127
130
|
The command writes `next-issue.config.json` in the root of the repository and
|
|
@@ -194,6 +197,7 @@ A role without an entry in `models` uses the default model of the SDK. A
|
|
|
194
197
|
| `--once` | Stop after the first handled issue |
|
|
195
198
|
| `--max <n>` | Handle at most n issues |
|
|
196
199
|
| `--reset` | Drop the saved budgets and findings first |
|
|
200
|
+
| `--json` | Print the machine summary instead of the text one |
|
|
197
201
|
| `--verbose` | Show every command and all agent output |
|
|
198
202
|
| `--quiet` | Show only the milestones and the summary |
|
|
199
203
|
| `--help` | Show the option list |
|
|
@@ -209,3 +213,19 @@ npm run build
|
|
|
209
213
|
|
|
210
214
|
`npm run build` compiles `src/*.mts` to `dist/*.mjs`. The package holds the
|
|
211
215
|
compiled files, because Node does not strip types under `node_modules`.
|
|
216
|
+
|
|
217
|
+
## Releases
|
|
218
|
+
|
|
219
|
+
Releases are automatic and need no pull request. Push a
|
|
220
|
+
[conventional commit](https://www.conventionalcommits.org/) to `main`. The
|
|
221
|
+
workflow reads the commits since the last tag and picks the bump: `feat` gives a
|
|
222
|
+
minor, `fix` gives a patch, and a `!` mark or a `BREAKING CHANGE` body gives a
|
|
223
|
+
major. Any other type releases nothing.
|
|
224
|
+
|
|
225
|
+
The workflow then bumps the version and publishes the package to npm with a
|
|
226
|
+
[trusted publish](https://docs.npmjs.com/trusted-publishers), so no token is
|
|
227
|
+
stored. Only after the publish does it push the tag and make the GitHub release,
|
|
228
|
+
so a failed publish leaves no half-release behind. A manual run of the workflow
|
|
229
|
+
publishes the version in `package.json` and changes nothing else, which repairs
|
|
230
|
+
a release that reached GitHub but not npm. The release job stays on a
|
|
231
|
+
GitHub runner, because npm takes provenance only from a GitHub runner.
|
package/dist/agents.mjs
CHANGED
|
@@ -44,7 +44,7 @@ export async function runAgent(recorder, request) {
|
|
|
44
44
|
if (block.type === "tool_use") {
|
|
45
45
|
toolCalls += 1;
|
|
46
46
|
toolNames.set(block.id, block.name);
|
|
47
|
-
log.event("tool",
|
|
47
|
+
log.event("tool", toolFields(block.name, block.input));
|
|
48
48
|
}
|
|
49
49
|
if (block.type === "text") {
|
|
50
50
|
text += block.text;
|
|
@@ -120,3 +120,10 @@ function main(models) {
|
|
|
120
120
|
}
|
|
121
121
|
return name;
|
|
122
122
|
}
|
|
123
|
+
export function toolFields(name, input) {
|
|
124
|
+
if (name !== "Bash" || typeof input !== "object" || input === null) {
|
|
125
|
+
return { tool: name };
|
|
126
|
+
}
|
|
127
|
+
const { command } = input;
|
|
128
|
+
return typeof command === "string" ? { tool: name, command } : { tool: name };
|
|
129
|
+
}
|
package/dist/cli.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { currentLogin, defaultBranch, openIssues } from "./github.mjs";
|
|
|
8
8
|
import { message, Recorder } from "./observe.mjs";
|
|
9
9
|
import { processIssue } from "./pipeline.mjs";
|
|
10
10
|
import { clearState, STATE_DIR } from "./state.mjs";
|
|
11
|
+
import { formatSummary } from "./summary.mjs";
|
|
11
12
|
const USAGE = `next-issue [command] [options]
|
|
12
13
|
|
|
13
14
|
init write ${CONFIG_FILE} with the defaults
|
|
@@ -16,6 +17,7 @@ const USAGE = `next-issue [command] [options]
|
|
|
16
17
|
--once stop after the first handled issue
|
|
17
18
|
--max <n> handle at most n issues
|
|
18
19
|
--reset drop the saved budgets and findings first
|
|
20
|
+
--json print the machine summary instead of the text one
|
|
19
21
|
--force replace an existing config file, with init
|
|
20
22
|
--verbose show every command and all agent output
|
|
21
23
|
--quiet show only the milestones and the summary
|
|
@@ -26,6 +28,7 @@ function parseArgs(argv) {
|
|
|
26
28
|
command: "run",
|
|
27
29
|
once: false,
|
|
28
30
|
help: false,
|
|
31
|
+
json: false,
|
|
29
32
|
reset: false,
|
|
30
33
|
force: false,
|
|
31
34
|
level: "normal",
|
|
@@ -41,6 +44,9 @@ function parseArgs(argv) {
|
|
|
41
44
|
else if (arg === "--force") {
|
|
42
45
|
args.force = true;
|
|
43
46
|
}
|
|
47
|
+
else if (arg === "--json") {
|
|
48
|
+
args.json = true;
|
|
49
|
+
}
|
|
44
50
|
else if (arg === "--help") {
|
|
45
51
|
args.help = true;
|
|
46
52
|
}
|
|
@@ -147,7 +153,7 @@ async function handle(args, config, repo, recorder) {
|
|
|
147
153
|
}, "quiet");
|
|
148
154
|
const summaryFile = join(repo.root, STATE_DIR, "runs", `${recorder.runId}.summary.json`);
|
|
149
155
|
await writeFile(summaryFile, `${JSON.stringify(summary, null, 2)}\n`);
|
|
150
|
-
await write(process.stdout, `${JSON.stringify(summary, null, 2)}\n`);
|
|
156
|
+
await write(process.stdout, args.json ? `${JSON.stringify(summary, null, 2)}\n` : formatSummary(summary));
|
|
151
157
|
return count(reports, "needs-human") + count(reports, "error") > 0 ? 1 : 0;
|
|
152
158
|
}
|
|
153
159
|
function count(reports, outcome) {
|
package/dist/git.mjs
CHANGED
|
@@ -31,6 +31,7 @@ export async function addWorktree(repo, issue, base, remote) {
|
|
|
31
31
|
const path = worktreePath(repo, issue);
|
|
32
32
|
const branch = branchName(issue);
|
|
33
33
|
await must("git", ["fetch", remote, base], { cwd: repo.root });
|
|
34
|
+
await updateBase(repo, base, remote);
|
|
34
35
|
await must("git", ["worktree", "prune"], { cwd: repo.root });
|
|
35
36
|
if (await hasWorktree(repo, path)) {
|
|
36
37
|
return path;
|
|
@@ -42,6 +43,15 @@ export async function addWorktree(repo, issue, base, remote) {
|
|
|
42
43
|
await must("git", args, { cwd: repo.root });
|
|
43
44
|
return path;
|
|
44
45
|
}
|
|
46
|
+
export async function updateBase(repo, base, remote) {
|
|
47
|
+
const head = await run("git", ["symbolic-ref", "--quiet", "--short", "HEAD"], { cwd: repo.root });
|
|
48
|
+
if (head.code === 0 && head.stdout.trim() === base) {
|
|
49
|
+
const merge = await run("git", ["merge", "--ff-only", `${remote}/${base}`], { cwd: repo.root });
|
|
50
|
+
return merge.code === 0;
|
|
51
|
+
}
|
|
52
|
+
const fetch = await run("git", ["fetch", remote, `${base}:${base}`], { cwd: repo.root });
|
|
53
|
+
return fetch.code === 0;
|
|
54
|
+
}
|
|
45
55
|
export async function hasWorktree(repo, path) {
|
|
46
56
|
const list = await must("git", ["worktree", "list", "--porcelain"], { cwd: repo.root });
|
|
47
57
|
return list.split("\n").some((line) => line.trim() === `worktree ${path}`);
|
package/dist/summary.mjs
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const OUTCOMES = ["done", "needs-human", "error", "skipped"];
|
|
2
|
+
export function formatSummary(summary) {
|
|
3
|
+
const tally = OUTCOMES.map((outcome) => `${summary.issues.filter((report) => report.outcome === outcome).length} ${outcome}`).join(", ");
|
|
4
|
+
const lines = [`run ${summary.runId}`, `issues ${summary.issues.length}: ${tally}`];
|
|
5
|
+
for (const report of summary.issues) {
|
|
6
|
+
lines.push(` ${issueLine(report)}`);
|
|
7
|
+
}
|
|
8
|
+
const roles = Object.entries(summary.roles)
|
|
9
|
+
.map(([role, totals]) => `${role} ${tokens(totals.total)} in ${totals.calls}`)
|
|
10
|
+
.join(", ");
|
|
11
|
+
lines.push(`tokens ${tokens(summary.total.total)}${roles.length > 0 ? ` (${roles})` : ""}`);
|
|
12
|
+
lines.push(`log ${summary.logFile}`);
|
|
13
|
+
return `${lines.join("\n")}\n`;
|
|
14
|
+
}
|
|
15
|
+
function issueLine(report) {
|
|
16
|
+
const parts = [`#${report.issue} ${report.outcome.padEnd(11)}`];
|
|
17
|
+
if (report.pr !== undefined) {
|
|
18
|
+
parts.push(`pr ${report.pr}`);
|
|
19
|
+
}
|
|
20
|
+
if (report.ciFixes > 0) {
|
|
21
|
+
parts.push(`ci fixes ${report.ciFixes}`);
|
|
22
|
+
}
|
|
23
|
+
if (report.reviewRounds > 0) {
|
|
24
|
+
parts.push(`reviews ${report.reviewRounds}`);
|
|
25
|
+
}
|
|
26
|
+
if (report.ms > 0) {
|
|
27
|
+
parts.push(duration(report.ms));
|
|
28
|
+
}
|
|
29
|
+
if (report.reason !== undefined) {
|
|
30
|
+
parts.push(`(${report.reason})`);
|
|
31
|
+
}
|
|
32
|
+
return parts.join(" ").trimEnd();
|
|
33
|
+
}
|
|
34
|
+
function duration(ms) {
|
|
35
|
+
const seconds = Math.round(ms / 1000);
|
|
36
|
+
const minutes = Math.floor(seconds / 60);
|
|
37
|
+
return minutes === 0 ? `${seconds}s` : `${minutes}m${String(seconds % 60).padStart(2, "0")}s`;
|
|
38
|
+
}
|
|
39
|
+
function tokens(value) {
|
|
40
|
+
if (value < 1000) {
|
|
41
|
+
return String(value);
|
|
42
|
+
}
|
|
43
|
+
if (value < 1_000_000) {
|
|
44
|
+
return `${(value / 1000).toFixed(1)}k`;
|
|
45
|
+
}
|
|
46
|
+
return `${(value / 1_000_000).toFixed(2)}M`;
|
|
47
|
+
}
|