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 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. Add a git worktree on a new `issue-<n>` branch from the default branch.
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
- The summary also goes to standard output, so the harness fits in a pipe. All
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. Each agent call adds a `usage` event with the
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", { tool: block.name });
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}`);
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-issue",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Autonomous issue harness driven by the Claude Agent SDK",
5
5
  "keywords": [
6
6
  "github",