next-issue 2.12.0 → 2.14.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
@@ -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 |
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/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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-issue",
3
- "version": "2.12.0",
3
+ "version": "2.14.0",
4
4
  "description": "Autonomous issue harness driven by the pi SDK",
5
5
  "keywords": [
6
6
  "github",