next-issue 0.4.0 → 0.5.1
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 +17 -8
- package/dist/cli.mjs +7 -1
- package/dist/git.mjs +10 -0
- package/dist/observe.mjs +1 -1
- package/dist/summary.mjs +47 -0
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
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.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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}`);
|
package/dist/observe.mjs
CHANGED
|
@@ -121,7 +121,7 @@ export function message(error) {
|
|
|
121
121
|
return error instanceof Error ? error.message : String(error);
|
|
122
122
|
}
|
|
123
123
|
function render(record) {
|
|
124
|
-
const time = String(record.ts).slice(
|
|
124
|
+
const time = new Date(String(record.ts)).toTimeString().slice(0, 8);
|
|
125
125
|
const issue = record.issue === undefined ? "" : ` #${record.issue}`;
|
|
126
126
|
const extras = Object.entries(record)
|
|
127
127
|
.filter(([key]) => !["ts", "runId", "kind", "issue"].includes(key))
|
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
|
+
}
|