next-issue 2.11.0 → 2.13.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 +14 -3
- package/dist/args.mjs +3 -2
- package/dist/cli.mjs +13 -0
- package/dist/handover.mjs +3 -0
- package/dist/logs.mjs +99 -0
- package/dist/observe.mjs +1 -1
- package/dist/serve.mjs +9 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -248,6 +248,13 @@ jq -r 'select(.kind=="usage") | [.issue,.role,.turns,.total] | @tsv' \
|
|
|
248
248
|
.next-issue/runs/*.jsonl
|
|
249
249
|
```
|
|
250
250
|
|
|
251
|
+
`next-issue logs` shows the newest run log, one readable line per event, and
|
|
252
|
+
then prints new events as the run adds them, until `Ctrl-C`. In a terminal it
|
|
253
|
+
starts with the last events that fit the screen; in a pipe it starts with all
|
|
254
|
+
of them. It stops when the reader of the pipe closes it; on Linux it notices
|
|
255
|
+
this only at the next event. With `--json` it prints the raw JSON lines. This also shows a run of
|
|
256
|
+
the worker, whose progress goes to no terminal.
|
|
257
|
+
|
|
251
258
|
Three levels of console output:
|
|
252
259
|
|
|
253
260
|
| Level | Shows |
|
|
@@ -408,13 +415,14 @@ state of an issue that a label still holds back stays as it was.
|
|
|
408
415
|
| `init` | Write the config file with the defaults |
|
|
409
416
|
| `serve` | Run the worker that pi extensions talk to, see [Worker](#worker) |
|
|
410
417
|
| `stop` | Stop the worker, see [Worker](#worker) |
|
|
418
|
+
| `logs` | Show the latest run log and follow it, see [Observability](#observability) |
|
|
411
419
|
| `--force` | Replace an existing config file, with `init` |
|
|
412
420
|
| `--once` | Stop after the first handled issue |
|
|
413
421
|
| `--max <n>` | Handle at most n issues |
|
|
414
422
|
| `--merge` | Squash-merge the pull request after the approval, delete the remote branch and fix a failure of the checks on the base branch |
|
|
415
423
|
| `--watch` | Keep running, and read the open issues again after each pass |
|
|
416
424
|
| `--reset` | Drop the saved budgets and findings, and keep the rest of the state |
|
|
417
|
-
| `--json` | Print the machine summary instead of the text one
|
|
425
|
+
| `--json` | Print the machine summary instead of the text one, or the raw JSON lines with `logs` |
|
|
418
426
|
| `--verbose` | Show every command and all agent output |
|
|
419
427
|
| `--quiet` | Show only the milestones and the summary |
|
|
420
428
|
| `--help` | Show the option list |
|
|
@@ -471,14 +479,17 @@ to it. The worker:
|
|
|
471
479
|
issues;
|
|
472
480
|
- sends every client a `questions` message for each repository: the open
|
|
473
481
|
issues with the `needsHuman` label, each with its number, title, the reason
|
|
474
|
-
of the hand-over
|
|
482
|
+
of the hand-over, the url of the comment that holds it, the `owner/name`
|
|
483
|
+
repository, the pull request when there is one, and the `needsHuman` and
|
|
484
|
+
`skip` labels. It reads them
|
|
475
485
|
from GitHub, so the list is the same after a restart. It sends them when a
|
|
476
486
|
client connects, for each repository it knows, after each issue of a run
|
|
477
487
|
ends, and when a client sends `{ "type": "questions", "root": "<path>" }`.
|
|
478
488
|
An issue that no longer has the label leaves the list. An issue with the
|
|
479
489
|
label but no hand-over comment is in the list with an empty reason and url.
|
|
480
490
|
The issues are read one at a time. A client never gets an older list after
|
|
481
|
-
a newer one. When the
|
|
491
|
+
a newer one. When the client adds an `"id"` to its message, the list that
|
|
492
|
+
answers it carries the same `id` and is read after the message arrived. When the list cannot be read, the client gets
|
|
482
493
|
`{ "type": "questions-error", "root": "<path>", "message": "<text>" }`;
|
|
483
494
|
- stops a run, when a client asks, like a second `Ctrl-C`: after the current
|
|
484
495
|
round, with the state kept;
|
package/dist/args.mjs
CHANGED
|
@@ -4,13 +4,14 @@ export const USAGE = `next-issue [command] [options]
|
|
|
4
4
|
init write ${CONFIG_FILE} with the defaults
|
|
5
5
|
serve run the worker that pi extensions talk to
|
|
6
6
|
stop stop the worker
|
|
7
|
+
logs show the latest run log and follow it
|
|
7
8
|
|
|
8
9
|
--once stop after the first handled issue
|
|
9
10
|
--max <n> handle at most n issues
|
|
10
11
|
--merge squash-merge the pull request after the approval
|
|
11
12
|
--watch keep running and read the issues again after each pass
|
|
12
13
|
--reset drop the saved budgets and findings first
|
|
13
|
-
--json print the machine summary instead of the text one
|
|
14
|
+
--json print the machine summary instead of the text one, or raw lines with logs
|
|
14
15
|
--force replace an existing config file, with init
|
|
15
16
|
--verbose show every command and all agent output
|
|
16
17
|
--quiet show only the milestones and the summary
|
|
@@ -31,7 +32,7 @@ export function parseArgs(argv) {
|
|
|
31
32
|
for (let index = 0; index < argv.length; index += 1) {
|
|
32
33
|
const arg = argv[index];
|
|
33
34
|
if (index === 0 && !arg.startsWith('-')) {
|
|
34
|
-
if (arg !== 'init' && arg !== 'serve' && arg !== 'stop') {
|
|
35
|
+
if (arg !== 'init' && arg !== 'serve' && arg !== 'stop' && arg !== 'logs') {
|
|
35
36
|
throw new Error(`Unknown command: ${arg}`);
|
|
36
37
|
}
|
|
37
38
|
args.command = arg;
|
package/dist/cli.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { parseArgs, USAGE } from './args.mjs';
|
|
|
3
3
|
import { writeDefaultConfig } from './config.mjs';
|
|
4
4
|
import { ensureIgnored, repoRoot, WORKTREE_DIR } from './git.mjs';
|
|
5
5
|
import { listenForInterrupts } from './interrupt.mjs';
|
|
6
|
+
import { followLog, newestRunLog } from './logs.mjs';
|
|
6
7
|
import { message } from './observe.mjs';
|
|
7
8
|
import { runRepo } from './run.mjs';
|
|
8
9
|
import { serve, socketPaths, stopWorker } from './serve.mjs';
|
|
@@ -33,12 +34,24 @@ async function main() {
|
|
|
33
34
|
if (args.command === 'init') {
|
|
34
35
|
return init(root, args.force);
|
|
35
36
|
}
|
|
37
|
+
if (args.command === 'logs') {
|
|
38
|
+
return logs(root, args.json);
|
|
39
|
+
}
|
|
36
40
|
const interrupts = listenForInterrupts((text) => void write(process.stderr, `${text}\n`));
|
|
37
41
|
return runRepo(root, args, {
|
|
38
42
|
interrupts,
|
|
39
43
|
print: (summary) => write(process.stdout, args.json ? `${JSON.stringify(summary, null, 2)}\n` : formatSummary(summary)),
|
|
40
44
|
});
|
|
41
45
|
}
|
|
46
|
+
async function logs(root, json) {
|
|
47
|
+
const file = await newestRunLog(root);
|
|
48
|
+
if (file === undefined) {
|
|
49
|
+
await write(process.stderr, `next-issue: no run log in ${STATE_DIR}/runs\n`);
|
|
50
|
+
return 1;
|
|
51
|
+
}
|
|
52
|
+
const tail = process.stdout.isTTY ? Math.max(1, (process.stdout.rows || 24) - 1) : undefined;
|
|
53
|
+
return followLog(file, { json, tail }, (text) => write(process.stdout, text));
|
|
54
|
+
}
|
|
42
55
|
async function init(root, force) {
|
|
43
56
|
const path = await writeDefaultConfig(root, force);
|
|
44
57
|
await write(process.stdout, `wrote ${path}\n`);
|
package/dist/handover.mjs
CHANGED
|
@@ -33,6 +33,9 @@ export async function waitingQuestions(repo, config, ports) {
|
|
|
33
33
|
title: issue.title,
|
|
34
34
|
reason: comment?.body.slice(HAND_OVER.length).trim() ?? '',
|
|
35
35
|
url: comment?.url ?? '',
|
|
36
|
+
repo: `${repo.owner}/${repo.name}`,
|
|
37
|
+
...(pr === undefined ? {} : { pr }),
|
|
38
|
+
labels: { needsHuman: config.labels.needsHuman, skip: config.labels.skip },
|
|
36
39
|
});
|
|
37
40
|
}
|
|
38
41
|
return questions;
|
package/dist/logs.mjs
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { writeSync } from 'node:fs';
|
|
2
|
+
import { open, readdir } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
5
|
+
import { render } from './observe.mjs';
|
|
6
|
+
import { STATE_DIR } from './state.mjs';
|
|
7
|
+
const POLL_MS = 200;
|
|
8
|
+
export async function newestRunLog(root) {
|
|
9
|
+
const dir = join(root, STATE_DIR, 'runs');
|
|
10
|
+
let names;
|
|
11
|
+
try {
|
|
12
|
+
names = await readdir(dir);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
const newest = names
|
|
18
|
+
.filter((name) => name.endsWith('.jsonl'))
|
|
19
|
+
.sort()
|
|
20
|
+
.at(-1);
|
|
21
|
+
return newest === undefined ? undefined : join(dir, newest);
|
|
22
|
+
}
|
|
23
|
+
export async function followLog(file, options, print) {
|
|
24
|
+
const handle = await open(file, 'r');
|
|
25
|
+
let stopped = false;
|
|
26
|
+
let wake;
|
|
27
|
+
const stop = () => {
|
|
28
|
+
stopped = true;
|
|
29
|
+
wake?.();
|
|
30
|
+
};
|
|
31
|
+
process.on('SIGINT', stop);
|
|
32
|
+
process.stdout.on('error', stop);
|
|
33
|
+
const show = (line) => {
|
|
34
|
+
if (options.json) {
|
|
35
|
+
return `${line}\n`;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
return render(JSON.parse(line));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return '';
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const decoder = new StringDecoder('utf8');
|
|
45
|
+
let position = 0;
|
|
46
|
+
let rest = '';
|
|
47
|
+
let first = true;
|
|
48
|
+
try {
|
|
49
|
+
while (!stopped) {
|
|
50
|
+
const buffer = Buffer.alloc(64 * 1024);
|
|
51
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, position);
|
|
52
|
+
if (bytesRead > 0) {
|
|
53
|
+
position += bytesRead;
|
|
54
|
+
rest += decoder.write(buffer.subarray(0, bytesRead));
|
|
55
|
+
if (bytesRead === buffer.length) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const lines = rest.split('\n');
|
|
60
|
+
rest = lines.pop();
|
|
61
|
+
let shown = lines.filter((line) => line.trim().length > 0);
|
|
62
|
+
if (first && options.tail !== undefined) {
|
|
63
|
+
shown = shown.slice(-options.tail);
|
|
64
|
+
}
|
|
65
|
+
first = false;
|
|
66
|
+
if (shown.length > 0) {
|
|
67
|
+
await print(shown.map(show).join(''));
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
writeSync(process.stdout.fd, '');
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
stop();
|
|
74
|
+
}
|
|
75
|
+
if (stopped) {
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
await new Promise((resolve) => {
|
|
79
|
+
const timer = setTimeout(resolve, POLL_MS);
|
|
80
|
+
wake = () => {
|
|
81
|
+
clearTimeout(timer);
|
|
82
|
+
resolve();
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
wake = undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (!stopped && error.code !== 'EPIPE') {
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
process.off('SIGINT', stop);
|
|
95
|
+
process.stdout.off('error', stop);
|
|
96
|
+
await handle.close();
|
|
97
|
+
}
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
package/dist/observe.mjs
CHANGED
|
@@ -171,7 +171,7 @@ export async function pruneRuns(root, keepDays) {
|
|
|
171
171
|
export function message(error) {
|
|
172
172
|
return error instanceof Error ? error.message : String(error);
|
|
173
173
|
}
|
|
174
|
-
function render(record) {
|
|
174
|
+
export function render(record) {
|
|
175
175
|
const time = new Date(String(record.ts)).toTimeString().slice(0, 8);
|
|
176
176
|
const issue = record.issue === undefined ? '' : ` #${record.issue}`;
|
|
177
177
|
const extras = Object.entries(record)
|
package/dist/serve.mjs
CHANGED
|
@@ -113,7 +113,7 @@ export async function startWorker(options) {
|
|
|
113
113
|
send(socket, value);
|
|
114
114
|
}
|
|
115
115
|
};
|
|
116
|
-
const sendQuestions = async (root, targets) => {
|
|
116
|
+
const sendQuestions = async (root, targets, id) => {
|
|
117
117
|
remember(root);
|
|
118
118
|
const read = (reads.get(root) ?? 0) + 1;
|
|
119
119
|
reads.set(root, read);
|
|
@@ -126,10 +126,14 @@ export async function startWorker(options) {
|
|
|
126
126
|
}
|
|
127
127
|
for (const socket of targets()) {
|
|
128
128
|
const seen = delivered.get(socket) ?? new Map();
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
const last = seen.get(root);
|
|
130
|
+
if (last === undefined || last.read < read) {
|
|
131
|
+
seen.set(root, { read, value });
|
|
131
132
|
delivered.set(socket, seen);
|
|
132
|
-
send(socket, value);
|
|
133
|
+
send(socket, id === undefined ? value : { ...value, id });
|
|
134
|
+
}
|
|
135
|
+
else if (id !== undefined) {
|
|
136
|
+
send(socket, { ...last.value, id });
|
|
133
137
|
}
|
|
134
138
|
}
|
|
135
139
|
};
|
|
@@ -240,7 +244,7 @@ export async function startWorker(options) {
|
|
|
240
244
|
stop(value.root);
|
|
241
245
|
}
|
|
242
246
|
else if (value.type === 'questions') {
|
|
243
|
-
void sendQuestions(value.root, () => [socket]);
|
|
247
|
+
void sendQuestions(value.root, () => [socket], value.id);
|
|
244
248
|
}
|
|
245
249
|
});
|
|
246
250
|
});
|