next-issue 2.4.1 → 2.4.2

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/dist/prompts.mjs CHANGED
@@ -1,35 +1,35 @@
1
1
  const COMMIT_RULES = [
2
- "Do not run git, gh or any push command. The harness commits for you.",
3
- "End your final message with one line in the form `commit: <conventional commit subject>`.",
4
- ].join("\n");
2
+ 'Do not run git, gh or any push command. The harness commits for you.',
3
+ 'End your final message with one line in the form `commit: <conventional commit subject>`.',
4
+ ].join('\n');
5
5
  function issueBlock(issue, comments) {
6
6
  return [
7
7
  `# Issue #${issue.number}: ${issue.title}`,
8
- issue.body.length > 0 ? issue.body : "(no description)",
9
- comments.length > 0 ? `## Comments\n${comments.join("\n\n")}` : "",
8
+ issue.body.length > 0 ? issue.body : '(no description)',
9
+ comments.length > 0 ? `## Comments\n${comments.join('\n\n')}` : '',
10
10
  ]
11
11
  .filter((part) => part.length > 0)
12
- .join("\n\n");
12
+ .join('\n\n');
13
13
  }
14
14
  export function implementPrompt(issue, comments) {
15
15
  return [
16
- "You are the implementer. Solve the issue below in this worktree.",
16
+ 'You are the implementer. Solve the issue below in this worktree.',
17
17
  issueBlock(issue, comments),
18
- "## Rules",
19
- "Read the repository conventions first. Keep the change minimal and focused on the issue.",
20
- "Add or update tests when the repository has a test suite.",
18
+ '## Rules',
19
+ 'Read the repository conventions first. Keep the change minimal and focused on the issue.',
20
+ 'Add or update tests when the repository has a test suite.',
21
21
  COMMIT_RULES,
22
- ].join("\n\n");
22
+ ].join('\n\n');
23
23
  }
24
24
  export function hookPrompt(issue, comments, role, output) {
25
25
  return [
26
26
  `You are the ${role}. The harness could not commit your work for the issue below, because a git hook failed.`,
27
27
  issueBlock(issue, comments),
28
28
  `## Hook output\n${output}`,
29
- "## Rules",
30
- "Fix what the hook reports and keep the rest of the work in this worktree. Do not skip or weaken the hook.",
29
+ '## Rules',
30
+ 'Fix what the hook reports and keep the rest of the work in this worktree. Do not skip or weaken the hook.',
31
31
  COMMIT_RULES,
32
- ].join("\n\n");
32
+ ].join('\n\n');
33
33
  }
34
34
  export function capDiff(diff, maxChars) {
35
35
  return diff.length <= maxChars
@@ -38,42 +38,42 @@ export function capDiff(diff, maxChars) {
38
38
  }
39
39
  export function reviewPrompt(issue, comments, diff, earlier) {
40
40
  const scope = earlier.length === 0
41
- ? "Check correctness, scope, repository conventions and test cover."
41
+ ? 'Check correctness, scope, repository conventions and test cover.'
42
42
  : [
43
- "This is a later round. Judge two things only:",
44
- "whether the earlier findings below are now fixed, and whether the new commits broke something.",
45
- "Do not raise a new point of taste. Do not repeat a finding that is fixed.",
46
- "A `The fixer disputed every finding of this round` line answers all of the findings of that round, not one of them.",
47
- "Withdraw every finding of that round if the dispute is correct. Repeat them all if the dispute is wrong.",
48
- `## Earlier findings\n${earlier.join("\n")}`,
49
- ].join("\n");
43
+ 'This is a later round. Judge two things only:',
44
+ 'whether the earlier findings below are now fixed, and whether the new commits broke something.',
45
+ 'Do not raise a new point of taste. Do not repeat a finding that is fixed.',
46
+ 'A `The fixer disputed every finding of this round` line answers all of the findings of that round, not one of them.',
47
+ 'Withdraw every finding of that round if the dispute is correct. Repeat them all if the dispute is wrong.',
48
+ `## Earlier findings\n${earlier.join('\n')}`,
49
+ ].join('\n');
50
50
  return [
51
- "You are the reviewer. Judge whether the diff solves the issue correctly.",
51
+ 'You are the reviewer. Judge whether the diff solves the issue correctly.',
52
52
  issueBlock(issue, comments),
53
- "## Diff",
54
- "```diff",
53
+ '## Diff',
54
+ '```diff',
55
55
  diff,
56
- "```",
57
- "## Rules",
58
- "Read files and run commands to prove what you say: the tests, the type check or the linter.",
59
- "The worktree is ready to use. Do not change a file. Do not write with git or gh.",
60
- "The harness discards each change that you make, so a change is only lost work.",
56
+ '```',
57
+ '## Rules',
58
+ 'Read files and run commands to prove what you say: the tests, the type check or the linter.',
59
+ 'The worktree is ready to use. Do not change a file. Do not write with git or gh.',
60
+ 'The harness discards each change that you make, so a change is only lost work.',
61
61
  scope,
62
- "Mark a finding blocking only for a wrong result, a missing part of the issue, a regression or a broken convention.",
63
- "Give the result with the submit_result tool: a short summary and one entry per finding. The findings decide the outcome, so a problem that you leave out counts as no problem.",
64
- ].join("\n\n");
62
+ 'Mark a finding blocking only for a wrong result, a missing part of the issue, a regression or a broken convention.',
63
+ 'Give the result with the submit_result tool: a short summary and one entry per finding. The findings decide the outcome, so a problem that you leave out counts as no problem.',
64
+ ].join('\n\n');
65
65
  }
66
66
  const UNRELATED_RULE = [
67
- "The checks can fail for a reason this change did not cause: a test that is already broken on the base branch, one that exercises code the diff does not touch, or one that fails only under load.",
68
- "Establish that before you fix anything. If the failure is not yours, change nothing and end your final message with one line in the form `unrelated: <one sentence on what fails and why it is not this change>`.",
69
- "A person then reads it. Do not weaken, skip or re-record a test to make the checks pass.",
70
- ].join("\n");
67
+ 'The checks can fail for a reason this change did not cause: a test that is already broken on the base branch, one that exercises code the diff does not touch, or one that fails only under load.',
68
+ 'Establish that before you fix anything. If the failure is not yours, change nothing and end your final message with one line in the form `unrelated: <one sentence on what fails and why it is not this change>`.',
69
+ 'A person then reads it. Do not weaken, skip or re-record a test to make the checks pass.',
70
+ ].join('\n');
71
71
  const DISPUTE_RULE = [
72
- "A finding can be wrong: it can miss a convention of this repository, ask for work that the issue does not want, or describe behaviour that the code does not have.",
73
- "Establish that before you change anything. If every finding is wrong, change nothing and end your final message with one line in the form `unrelated: <one sentence on why the findings are wrong>`.",
74
- "If only some of the findings are wrong, fix the correct ones and change nothing for the wrong ones. Write no `unrelated` line in that round. The next round holds only the findings that are left, and you can dispute them there.",
75
- "The reviewer reads that line in the next round. Do not make a change that you hold to be wrong only to end the round.",
76
- ].join("\n");
72
+ 'A finding can be wrong: it can miss a convention of this repository, ask for work that the issue does not want, or describe behaviour that the code does not have.',
73
+ 'Establish that before you change anything. If every finding is wrong, change nothing and end your final message with one line in the form `unrelated: <one sentence on why the findings are wrong>`.',
74
+ 'If only some of the findings are wrong, fix the correct ones and change nothing for the wrong ones. Write no `unrelated` line in that round. The next round holds only the findings that are left, and you can dispute them there.',
75
+ 'The reviewer reads that line in the next round. Do not make a change that you hold to be wrong only to end the round.',
76
+ ].join('\n');
77
77
  export function fixPrompt(issue, reason, detail, earlier, kind) {
78
78
  return [
79
79
  `You are the fixer. Correct the work for issue #${issue.number}: ${issue.title}.`,
@@ -81,18 +81,18 @@ export function fixPrompt(issue, reason, detail, earlier, kind) {
81
81
  `## Detail\n${detail}`,
82
82
  earlier.length > 0
83
83
  ? [
84
- "## Earlier rounds",
85
- earlier.join("\n"),
86
- "Keep those earlier fixes. Do not undo work that a later round did not question.",
87
- ].join("\n")
88
- : "",
89
- "## Rules",
90
- "Fix the cause, not the symptom. Keep the change minimal.",
91
- kind === "checks" ? UNRELATED_RULE : DISPUTE_RULE,
84
+ '## Earlier rounds',
85
+ earlier.join('\n'),
86
+ 'Keep those earlier fixes. Do not undo work that a later round did not question.',
87
+ ].join('\n')
88
+ : '',
89
+ '## Rules',
90
+ 'Fix the cause, not the symptom. Keep the change minimal.',
91
+ kind === 'checks' ? UNRELATED_RULE : DISPUTE_RULE,
92
92
  COMMIT_RULES,
93
93
  ]
94
94
  .filter((part) => part.length > 0)
95
- .join("\n\n");
95
+ .join('\n\n');
96
96
  }
97
97
  export function parseUnrelated(text) {
98
98
  const match = [...text.matchAll(/^unrelated:\s*(.+)$/gim)].at(-1);
@@ -103,7 +103,7 @@ export function parseCommitSubject(text, fallback) {
103
103
  const match = [...text.matchAll(/^commit:\s*(.+)$/gim)].at(-1);
104
104
  const subject = match?.[1]
105
105
  ?.trim()
106
- .replace(/^`+|`+$/g, "")
106
+ .replace(/^`+|`+$/g, '')
107
107
  .trim();
108
108
  return subject && subject.length > 0 ? subject : fallback;
109
109
  }
package/dist/run.mjs CHANGED
@@ -1,27 +1,27 @@
1
- import { writeFile } from "node:fs/promises";
2
- import { join, sep } from "node:path";
3
- import { loadConfig } from "./config.mjs";
4
- import { setCommandObserver, setDefaultTimeout } from "./exec.mjs";
5
- import { detectRepo } from "./git.mjs";
6
- import { defaultBranch } from "./github.mjs";
7
- import { exitCode, runIssues, watchIssues } from "./loop.mjs";
8
- import { message, pruneRuns, Recorder } from "./observe.mjs";
9
- import { pendingIssues, PORTS, processIssue } from "./pipeline.mjs";
10
- import { STATE_DIR } from "./state.mjs";
11
- import { packageVersion } from "./version.mjs";
1
+ import { writeFile } from 'node:fs/promises';
2
+ import { join, sep } from 'node:path';
3
+ import { loadConfig } from './config.mjs';
4
+ import { setCommandObserver, setDefaultTimeout } from './exec.mjs';
5
+ import { detectRepo } from './git.mjs';
6
+ import { defaultBranch } from './github.mjs';
7
+ import { exitCode, runIssues, watchIssues } from './loop.mjs';
8
+ import { message, pruneRuns, Recorder } from './observe.mjs';
9
+ import { pendingIssues, PORTS, processIssue } from './pipeline.mjs';
10
+ import { STATE_DIR } from './state.mjs';
11
+ import { packageVersion } from './version.mjs';
12
12
  const recorders = new Map();
13
13
  setCommandObserver((record) => {
14
14
  for (const [root, recorder] of recorders) {
15
15
  if (record.cwd !== undefined && record.cwd !== root && !record.cwd.startsWith(root + sep)) {
16
16
  continue;
17
17
  }
18
- recorder.event(record.code === 0 ? "command" : "command.fail", {
19
- cmd: `${record.command} ${record.args.join(" ")}`,
18
+ recorder.event(record.code === 0 ? 'command' : 'command.fail', {
19
+ cmd: `${record.command} ${record.args.join(' ')}`,
20
20
  code: record.code,
21
21
  ms: record.ms,
22
22
  stderr: record.stderr.length > 0 ? record.stderr : undefined,
23
23
  timedOut: record.timedOut ? true : undefined,
24
- }, record.code === 0 ? "verbose" : "quiet");
24
+ }, record.code === 0 ? 'verbose' : 'quiet');
25
25
  }
26
26
  });
27
27
  export async function runRepo(root, args, controls) {
@@ -31,7 +31,7 @@ export async function runRepo(root, args, controls) {
31
31
  const recorder = await Recorder.create(repo.root, args.level, controls.onEvent);
32
32
  const pruned = await pruneRuns(repo.root, config.keepRunDays);
33
33
  if (pruned > 0) {
34
- recorder.event("runs.pruned", { files: pruned, keepDays: config.keepRunDays }, "verbose");
34
+ recorder.event('runs.pruned', { files: pruned, keepDays: config.keepRunDays }, 'verbose');
35
35
  }
36
36
  recorders.set(repo.root, recorder);
37
37
  try {
@@ -55,16 +55,16 @@ async function handle(args, config, repo, recorder, controls) {
55
55
  recorder,
56
56
  ports: PORTS,
57
57
  };
58
- recorder.event("run.start", {
58
+ recorder.event('run.start', {
59
59
  version: await packageVersion(),
60
60
  repo: `${repo.owner}/${repo.name}`,
61
61
  base,
62
62
  log: recorder.logFile,
63
- }, "quiet");
63
+ }, 'quiet');
64
64
  const loop = {
65
65
  issues: async () => {
66
66
  const issues = await pendingIssues(context);
67
- recorder.event("issues", { open: issues.length }, "quiet");
67
+ recorder.event('issues', { open: issues.length }, 'quiet');
68
68
  return issues;
69
69
  },
70
70
  process: (issue) => processIssue(context, issue),
@@ -76,7 +76,7 @@ async function handle(args, config, repo, recorder, controls) {
76
76
  ? await watchIssues(args, {
77
77
  ...loop,
78
78
  wait: (minutes) => {
79
- recorder.event("watch.wait", { minutes }, "quiet");
79
+ recorder.event('watch.wait', { minutes }, 'quiet');
80
80
  passStart = recorder.summary();
81
81
  return interrupts.sleep(minutes * 60_000);
82
82
  },
@@ -84,20 +84,20 @@ async function handle(args, config, repo, recorder, controls) {
84
84
  })
85
85
  : await runIssues(args, loop);
86
86
  if (stoppedAt !== undefined) {
87
- recorder.event("run.stop", { issue: stoppedAt }, "quiet");
87
+ recorder.event('run.stop', { issue: stoppedAt }, 'quiet');
88
88
  }
89
89
  if (error !== undefined) {
90
- recorder.event("issues.fail", { error: message(error) }, "quiet");
90
+ recorder.event('issues.fail', { error: message(error) }, 'quiet');
91
91
  }
92
- recorder.event("run.end", {
93
- done: count(reports, "done"),
94
- needsHuman: count(reports, "needs-human"),
95
- failed: count(reports, "error"),
96
- skipped: count(reports, "skipped"),
92
+ recorder.event('run.end', {
93
+ done: count(reports, 'done'),
94
+ needsHuman: count(reports, 'needs-human'),
95
+ failed: count(reports, 'error'),
96
+ skipped: count(reports, 'skipped'),
97
97
  tokens: recorder.summary().total.total,
98
- }, "quiet");
98
+ }, 'quiet');
99
99
  const summary = { ...recorder.summary(), issues: reports };
100
- const summaryFile = join(repo.root, STATE_DIR, "runs", `${recorder.runId}.summary.json`);
100
+ const summaryFile = join(repo.root, STATE_DIR, 'runs', `${recorder.runId}.summary.json`);
101
101
  await writeFile(summaryFile, `${JSON.stringify(summary, null, 2)}\n`);
102
102
  if (!args.watch) {
103
103
  await print(summary);
package/dist/serve.mjs CHANGED
@@ -1,24 +1,24 @@
1
- import { rmSync } from "node:fs";
2
- import { chmod, readFile, rm, writeFile } from "node:fs/promises";
3
- import { createServer } from "node:http";
4
- import { join } from "node:path";
5
- import { getAgentDir } from "@earendil-works/pi-coding-agent";
6
- import { WebSocketServer } from "ws";
7
- import { parseArgs } from "./args.mjs";
8
- import { stopChildren } from "./exec.mjs";
9
- import { message } from "./observe.mjs";
10
- import { applyEvent, startProgress } from "./progress.mjs";
11
- import { runRepo } from "./run.mjs";
12
- import { packageVersion } from "./version.mjs";
1
+ import { rmSync } from 'node:fs';
2
+ import { chmod, readFile, rm, writeFile } from 'node:fs/promises';
3
+ import { createServer } from 'node:http';
4
+ import { join } from 'node:path';
5
+ import { getAgentDir } from '@earendil-works/pi-coding-agent';
6
+ import { WebSocketServer } from 'ws';
7
+ import { parseArgs } from './args.mjs';
8
+ import { stopChildren } from './exec.mjs';
9
+ import { message } from './observe.mjs';
10
+ import { applyEvent, startProgress } from './progress.mjs';
11
+ import { runRepo } from './run.mjs';
12
+ import { packageVersion } from './version.mjs';
13
13
  export function socketPaths(agentDir = getAgentDir()) {
14
14
  return {
15
- socketPath: join(agentDir, "next-issue.sock"),
16
- lockPath: join(agentDir, "next-issue.lock"),
15
+ socketPath: join(agentDir, 'next-issue.sock'),
16
+ lockPath: join(agentDir, 'next-issue.lock'),
17
17
  };
18
18
  }
19
19
  export function isNewer(candidate, current) {
20
- const left = candidate.split(".").map(Number);
21
- const right = current.split(".").map(Number);
20
+ const left = candidate.split('.').map(Number);
21
+ const right = current.split('.').map(Number);
22
22
  for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
23
23
  const difference = (left[index] ?? 0) - (right[index] ?? 0);
24
24
  if (difference !== 0) {
@@ -33,28 +33,28 @@ function alive(pid) {
33
33
  return true;
34
34
  }
35
35
  catch (error) {
36
- return error.code === "EPERM";
36
+ return error.code === 'EPERM';
37
37
  }
38
38
  }
39
39
  async function holder(lockPath) {
40
- const pid = Number((await readFile(lockPath, "utf8").catch(() => "")).trim());
40
+ const pid = Number((await readFile(lockPath, 'utf8').catch(() => '')).trim());
41
41
  return pid > 0 && alive(pid) ? pid : undefined;
42
42
  }
43
43
  export async function stopWorker(lockPath) {
44
44
  const pid = await holder(lockPath);
45
45
  if (pid !== undefined) {
46
- process.kill(pid, "SIGTERM");
46
+ process.kill(pid, 'SIGTERM');
47
47
  }
48
48
  return pid;
49
49
  }
50
50
  async function lock(path) {
51
51
  for (let attempt = 0; attempt < 2; attempt += 1) {
52
52
  try {
53
- await writeFile(path, `${process.pid}\n`, { flag: "wx", mode: 0o600 });
53
+ await writeFile(path, `${process.pid}\n`, { flag: 'wx', mode: 0o600 });
54
54
  return true;
55
55
  }
56
56
  catch (error) {
57
- if (error.code !== "EEXIST") {
57
+ if (error.code !== 'EEXIST') {
58
58
  throw error;
59
59
  }
60
60
  if ((await holder(path)) !== undefined) {
@@ -79,7 +79,7 @@ export async function startWorker(options) {
79
79
  const server = createServer();
80
80
  const sockets = new WebSocketServer({ server });
81
81
  const snapshot = () => ({
82
- type: "runs",
82
+ type: 'runs',
83
83
  runs: Object.fromEntries([...runs].map(([root, run]) => [root, run.progress])),
84
84
  });
85
85
  const send = (socket, value) => {
@@ -91,7 +91,7 @@ export async function startWorker(options) {
91
91
  send(socket, value);
92
92
  }
93
93
  };
94
- const busy = () => [...runs.values()].some((run) => run.progress.status === "running" || run.progress.status === "stopping");
94
+ const busy = () => [...runs.values()].some((run) => run.progress.status === 'running' || run.progress.status === 'stopping');
95
95
  const close = async () => {
96
96
  for (const socket of sockets.clients) {
97
97
  socket.terminate();
@@ -109,12 +109,12 @@ export async function startWorker(options) {
109
109
  };
110
110
  const start = (socket, root, merge) => {
111
111
  const current = runs.get(root);
112
- if (current?.progress.status === "running" || current?.progress.status === "stopping") {
113
- send(socket, { type: "error", root, message: "a run is going in this repository already" });
112
+ if (current?.progress.status === 'running' || current?.progress.status === 'stopping') {
113
+ send(socket, { type: 'error', root, message: 'a run is going in this repository already' });
114
114
  return;
115
115
  }
116
116
  if (restart) {
117
- send(socket, { type: "error", root, message: "the worker is restarting, try again soon" });
117
+ send(socket, { type: 'error', root, message: 'the worker is restarting, try again soon' });
118
118
  return;
119
119
  }
120
120
  const run = { progress: startProgress(), stop: false };
@@ -145,25 +145,29 @@ export async function startWorker(options) {
145
145
  onEvent: (record) => update(applyEvent(run.progress, record)),
146
146
  })
147
147
  .then(() => {
148
- const status = run.stop ? "stopped" : run.progress.error === undefined ? "finished" : "failed";
148
+ const status = run.stop
149
+ ? 'stopped'
150
+ : run.progress.error === undefined
151
+ ? 'finished'
152
+ : 'failed';
149
153
  update({ ...run.progress, status });
150
- }, (error) => update({ ...run.progress, status: "failed", error: message(error) }))
154
+ }, (error) => update({ ...run.progress, status: 'failed', error: message(error) }))
151
155
  .finally(restartWhenIdle);
152
156
  };
153
157
  const stop = (root) => {
154
158
  const run = runs.get(root);
155
- if (run === undefined || run.progress.status !== "running") {
159
+ if (run === undefined || run.progress.status !== 'running') {
156
160
  return;
157
161
  }
158
162
  run.stop = true;
159
163
  run.wake?.();
160
- run.progress = { ...run.progress, status: "stopping" };
164
+ run.progress = { ...run.progress, status: 'stopping' };
161
165
  broadcast();
162
166
  };
163
- sockets.on("connection", (socket) => {
164
- send(socket, { type: "hello", version: options.version });
167
+ sockets.on('connection', (socket) => {
168
+ send(socket, { type: 'hello', version: options.version });
165
169
  send(socket, snapshot());
166
- socket.on("message", (data) => {
170
+ socket.on('message', (data) => {
167
171
  let value;
168
172
  try {
169
173
  value = JSON.parse(String(data));
@@ -171,24 +175,24 @@ export async function startWorker(options) {
171
175
  catch {
172
176
  return;
173
177
  }
174
- if (value.type === "hello") {
178
+ if (value.type === 'hello') {
175
179
  if (isNewer(value.version, options.version)) {
176
180
  restart = true;
177
181
  restartWhenIdle();
178
182
  }
179
183
  }
180
- else if (value.type === "start") {
184
+ else if (value.type === 'start') {
181
185
  start(socket, value.root, value.merge === true);
182
186
  }
183
- else if (value.type === "stop") {
187
+ else if (value.type === 'stop') {
184
188
  stop(value.root);
185
189
  }
186
190
  });
187
191
  });
188
192
  await new Promise((resolve, reject) => {
189
- server.once("error", reject);
193
+ server.once('error', reject);
190
194
  server.listen(options.socketPath, () => {
191
- server.off("error", reject);
195
+ server.off('error', reject);
192
196
  resolve();
193
197
  });
194
198
  });
@@ -203,20 +207,20 @@ export async function serve() {
203
207
  runRepo,
204
208
  });
205
209
  if (worker === undefined) {
206
- process.stderr.write("next-issue: another worker is running\n");
210
+ process.stderr.write('next-issue: another worker is running\n');
207
211
  return 1;
208
212
  }
209
213
  const end = (signal) => {
210
- process.off("SIGTERM", onTerminate);
211
- process.off("SIGINT", onInterrupt);
214
+ process.off('SIGTERM', onTerminate);
215
+ process.off('SIGINT', onInterrupt);
212
216
  rmSync(paths.socketPath, { force: true });
213
217
  rmSync(paths.lockPath, { force: true });
214
218
  stopChildren(signal);
215
219
  };
216
- const onTerminate = () => end("SIGTERM");
217
- const onInterrupt = () => end("SIGINT");
218
- process.on("SIGTERM", onTerminate);
219
- process.on("SIGINT", onInterrupt);
220
+ const onTerminate = () => end('SIGTERM');
221
+ const onInterrupt = () => end('SIGINT');
222
+ process.on('SIGTERM', onTerminate);
223
+ process.on('SIGINT', onInterrupt);
220
224
  process.stderr.write(`next-issue: worker listening on ${paths.socketPath}\n`);
221
225
  await worker.closed;
222
226
  return 0;
package/dist/setup.mjs CHANGED
@@ -1,17 +1,17 @@
1
- import { access } from "node:fs/promises";
2
- import { join } from "node:path";
1
+ import { access } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
3
  const MARKERS = [
4
- ["pnpm-lock.yaml", "pnpm install --frozen-lockfile"],
5
- ["package-lock.json", "npm ci"],
6
- [".yarnrc.yml", "yarn install --immutable"],
7
- ["yarn.lock", "yarn install --frozen-lockfile"],
8
- ["bun.lock", "bun install --frozen-lockfile"],
9
- ["bun.lockb", "bun install --frozen-lockfile"],
10
- ["Cargo.lock", "cargo fetch"],
11
- ["go.sum", "go mod download"],
12
- ["uv.lock", "uv sync"],
13
- ["poetry.lock", "poetry install"],
14
- ["Gemfile.lock", "bundle install"],
4
+ ['pnpm-lock.yaml', 'pnpm install --frozen-lockfile'],
5
+ ['package-lock.json', 'npm ci'],
6
+ ['.yarnrc.yml', 'yarn install --immutable'],
7
+ ['yarn.lock', 'yarn install --frozen-lockfile'],
8
+ ['bun.lock', 'bun install --frozen-lockfile'],
9
+ ['bun.lockb', 'bun install --frozen-lockfile'],
10
+ ['Cargo.lock', 'cargo fetch'],
11
+ ['go.sum', 'go mod download'],
12
+ ['uv.lock', 'uv sync'],
13
+ ['poetry.lock', 'poetry install'],
14
+ ['Gemfile.lock', 'bundle install'],
15
15
  ];
16
16
  export async function detectSetupCommand(root) {
17
17
  for (const [file, command] of MARKERS) {
package/dist/state.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
2
- import { join } from "node:path";
3
- export const STATE_DIR = ".next-issue";
1
+ import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ export const STATE_DIR = '.next-issue';
4
4
  function dir(repo) {
5
5
  return join(repo.root, STATE_DIR);
6
6
  }
@@ -11,10 +11,10 @@ export async function readState(repo, issue) {
11
11
  const path = file(repo, issue);
12
12
  let text;
13
13
  try {
14
- text = await readFile(path, "utf8");
14
+ text = await readFile(path, 'utf8');
15
15
  }
16
16
  catch (error) {
17
- if (error.code === "ENOENT") {
17
+ if (error.code === 'ENOENT') {
18
18
  return undefined;
19
19
  }
20
20
  throw error;
@@ -31,34 +31,34 @@ export async function readState(repo, issue) {
31
31
  }
32
32
  return parsed;
33
33
  }
34
- const PHASES = ["claimed", "implemented", "review"];
34
+ const PHASES = ['claimed', 'implemented', 'review'];
35
35
  function isRecord(value) {
36
- return typeof value === "object" && value !== null;
36
+ return typeof value === 'object' && value !== null;
37
37
  }
38
38
  function isOptional(value, type) {
39
39
  return value === undefined || typeof value === type;
40
40
  }
41
41
  function isBaseFix(value) {
42
- return isRecord(value) && typeof value.sha === "string" && typeof value.head === "string";
42
+ return isRecord(value) && typeof value.sha === 'string' && typeof value.head === 'string';
43
43
  }
44
44
  function isReviewRound(value) {
45
45
  return (isRecord(value) &&
46
- typeof value.round === "number" &&
47
- typeof value.fingerprint === "string" &&
48
- typeof value.findings === "string" &&
49
- isOptional(value.dispute, "string"));
46
+ typeof value.round === 'number' &&
47
+ typeof value.fingerprint === 'string' &&
48
+ typeof value.findings === 'string' &&
49
+ isOptional(value.dispute, 'string'));
50
50
  }
51
51
  function isState(value) {
52
52
  return (isRecord(value) &&
53
- typeof value.issue === "number" &&
53
+ typeof value.issue === 'number' &&
54
54
  PHASES.includes(value.phase) &&
55
- typeof value.branch === "string" &&
56
- isOptional(value.pr, "number") &&
57
- isOptional(value.title, "string") &&
58
- typeof value.ciFixes === "number" &&
59
- isOptional(value.commitFixes, "number") &&
55
+ typeof value.branch === 'string' &&
56
+ isOptional(value.pr, 'number') &&
57
+ isOptional(value.title, 'string') &&
58
+ typeof value.ciFixes === 'number' &&
59
+ isOptional(value.commitFixes, 'number') &&
60
60
  (value.baseFix === undefined || isBaseFix(value.baseFix)) &&
61
- typeof value.reviewRounds === "number" &&
61
+ typeof value.reviewRounds === 'number' &&
62
62
  Array.isArray(value.reviewLog) &&
63
63
  value.reviewLog.every(isReviewRound));
64
64
  }
@@ -68,7 +68,7 @@ export async function savedIssues(repo) {
68
68
  names = await readdir(dir(repo));
69
69
  }
70
70
  catch (error) {
71
- if (error.code === "ENOENT") {
71
+ if (error.code === 'ENOENT') {
72
72
  return [];
73
73
  }
74
74
  throw error;
package/dist/summary.mjs CHANGED
@@ -1,16 +1,16 @@
1
- const OUTCOMES = ["done", "needs-human", "error", "skipped", "stopped"];
1
+ const OUTCOMES = ['done', 'needs-human', 'error', 'skipped', 'stopped'];
2
2
  export function formatSummary(summary) {
3
- const tally = OUTCOMES.map((outcome) => `${summary.issues.filter((report) => report.outcome === outcome).length} ${outcome}`).join(", ");
3
+ const tally = OUTCOMES.map((outcome) => `${summary.issues.filter((report) => report.outcome === outcome).length} ${outcome}`).join(', ');
4
4
  const lines = [`run ${summary.runId}`, `issues ${summary.issues.length}: ${tally}`];
5
5
  for (const report of summary.issues) {
6
6
  lines.push(` ${issueLine(report)}`);
7
7
  }
8
8
  const roles = Object.entries(summary.roles)
9
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})` : ""}`);
10
+ .join(', ');
11
+ lines.push(`tokens ${tokens(summary.total.total)}${roles.length > 0 ? ` (${roles})` : ''}`);
12
12
  lines.push(`log ${summary.logFile}`);
13
- return `${lines.join("\n")}\n`;
13
+ return `${lines.join('\n')}\n`;
14
14
  }
15
15
  function issueLine(report) {
16
16
  const parts = [`#${report.issue} ${report.outcome.padEnd(11)}`];
@@ -29,12 +29,12 @@ function issueLine(report) {
29
29
  if (report.reason !== undefined) {
30
30
  parts.push(`(${report.reason})`);
31
31
  }
32
- return parts.join(" ").trimEnd();
32
+ return parts.join(' ').trimEnd();
33
33
  }
34
34
  function duration(ms) {
35
35
  const seconds = Math.round(ms / 1000);
36
36
  const minutes = Math.floor(seconds / 60);
37
- return minutes === 0 ? `${seconds}s` : `${minutes}m${String(seconds % 60).padStart(2, "0")}s`;
37
+ return minutes === 0 ? `${seconds}s` : `${minutes}m${String(seconds % 60).padStart(2, '0')}s`;
38
38
  }
39
39
  function tokens(value) {
40
40
  if (value < 1000) {