next-issue 0.4.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
@@ -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,7 +86,8 @@ 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`,
@@ -195,6 +197,7 @@ A role without an entry in `models` uses the default model of the SDK. A
195
197
  | `--once` | Stop after the first handled issue |
196
198
  | `--max <n>` | Handle at most n issues |
197
199
  | `--reset` | Drop the saved budgets and findings first |
200
+ | `--json` | Print the machine summary instead of the text one |
198
201
  | `--verbose` | Show every command and all agent output |
199
202
  | `--quiet` | Show only the milestones and the summary |
200
203
  | `--help` | Show the option list |
@@ -213,10 +216,16 @@ compiled files, because Node does not strip types under `node_modules`.
213
216
 
214
217
  ## Releases
215
218
 
216
- Releases are automatic. Write [conventional
217
- commits](https://www.conventionalcommits.org/) on `main`. Release Please then
218
- keeps a release pull request open with the version bump and the changelog. Merge
219
- that pull request. The workflow tags the commit, makes the GitHub release and
220
- publishes the package to npm with a
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
221
226
  [trusted publish](https://docs.npmjs.com/trusted-publishers), so no token is
222
- stored.
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/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.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Autonomous issue harness driven by the Claude Agent SDK",
5
5
  "keywords": [
6
6
  "github",