@ravshansbox/pi-next-issue 2.6.0 → 2.8.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
@@ -13,6 +13,7 @@ It needs `git`, `npx` and an authenticated [`gh`](https://cli.github.com/). It w
13
13
  Run this in pi, inside a git repository:
14
14
 
15
15
  - `/next-issue` starts a run of this repository when none is going, and stops the run when one is going. A stop takes effect after the current round, and the issue keeps its saved state, so the next run resumes it.
16
+ - The `next_issue_start` tool lets the model start a run. When no run is going, it starts a run like `/next-issue`. When a run is going, it starts nothing and reports the current run, which picks up new issues by itself. It never stops a run, and it does not ask for confirmation. Its result says whether it started a run.
16
17
 
17
18
  The footer shows the issue, the stage, the number of tool calls and the time spent on the issue, for example `next-issue #12 · review 1 · 37 tools · 4m12s`. After a run ends, it keeps the last summary. In the terminal, it uses the dim colour of the theme, like the rest of the footer. A note appears when an issue is done, needs a human or fails while pi is open. Pi does not repeat the notes of a run that ended before it opened.
18
19
 
@@ -20,16 +21,18 @@ The footer shows the issue, the stage, the number of tool calls and the time spe
20
21
 
21
22
  The package ships the `create-issue` skill. Ask pi to write an issue, or run `/skill:create-issue <problem>`. Pi then:
22
23
 
23
- 1. restates the problem and reads the code when the problem touches it;
24
+ 1. restates the problem, reads the code when the problem touches it, and finds the open issues that must be done first;
24
25
  2. asks numbered questions until none is open;
25
- 3. shows the title and the body, and waits for you to confirm;
26
- 4. runs `gh issue create --body-file` with the body from [`template.md`](skills/create-issue/template.md) and the `status:todo` label.
26
+ 3. shows the title, the body and those blockers, and asks once whether to create the issue and start a run;
27
+ 4. runs `gh issue create --body-file` with the body from [`template.md`](skills/create-issue/template.md) and the `status:todo` label;
28
+ 5. links each blocker with `gh issue edit --add-blocked-by`, so the worker waits for it;
29
+ 6. calls `next_issue_start` when you asked for a run.
27
30
 
28
31
  Pi does not implement the issue in that session. The worker does the work. The body holds the sections of the [issue format](../next-issue/README.md#issue-format).
29
32
 
30
33
  ## Worker
31
34
 
32
- The extension does no work itself. On `/next-issue` it connects to the `next-issue` worker on the socket file `~/.pi/agent/next-issue.sock`. When no worker answers, it starts one with `npx -y next-issue@<version> serve`, where `<version>` is the version of the extension, or `latest` when the extension runs from a source checkout with version `0.0.0`, in the background, and writes the output of the worker to `~/.pi/agent/next-issue-worker.log`.
35
+ The extension does no work itself. On `/next-issue` or `next_issue_start` it connects to the `next-issue` worker on the socket file `~/.pi/agent/next-issue.sock`. When no worker answers, it starts one with `npx -y next-issue@<version> serve`, where `<version>` is the version of the extension, or `latest` when the extension runs from a source checkout with version `0.0.0`, in the background, and writes the output of the worker to `~/.pi/agent/next-issue-worker.log`.
33
36
 
34
37
  - Every pi on the machine shares the one worker, and every pi in a repository shows the run of that repository, whoever started it.
35
38
  - Opening pi does not start a worker. It only shows a run when a worker is going.
package/index.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  type ExtensionContext,
6
6
  getAgentDir,
7
7
  } from '@earendil-works/pi-coding-agent';
8
+ import { Type } from 'typebox';
8
9
  import { repoRoot } from './src/git.ts';
9
10
  import { type Progress, spawnWorker, WorkerClient, type WorkerEvent } from './src/worker.ts';
10
11
 
@@ -144,7 +145,7 @@ export function createExtension(options: Options) {
144
145
  render();
145
146
  } else if (event.type === 'runs') {
146
147
  onProgress(root === undefined ? undefined : event.runs[root]);
147
- } else if (event.root === root) {
148
+ } else if (event.type === 'error' && event.root === root) {
148
149
  ui?.notify(`next-issue cannot start: ${event.message}`, 'error');
149
150
  }
150
151
  };
@@ -189,19 +190,84 @@ export function createExtension(options: Options) {
189
190
  ui = undefined;
190
191
  });
191
192
 
193
+ const connect = async (ctx: ExtensionContext) => {
194
+ root = await options.root(ctx.cwd);
195
+ if (ctx.hasUI) {
196
+ show(ctx);
197
+ follow();
198
+ }
199
+ const client = shared(options.client);
200
+ await client.ensure();
201
+ return { client, root, progress: client.runs[root] };
202
+ };
203
+
204
+ const going = (progress: Progress | undefined): progress is Progress =>
205
+ progress?.status === 'running' || progress?.status === 'stopping';
206
+
207
+ const accepted = (client: WorkerClient, root: string, signal: AbortSignal | undefined) =>
208
+ new Promise<void>((resolve, reject) => {
209
+ const finish = (error?: Error) => {
210
+ stop();
211
+ signal?.removeEventListener('abort', abort);
212
+ if (error === undefined) {
213
+ resolve();
214
+ } else {
215
+ reject(error);
216
+ }
217
+ };
218
+ const abort = () => finish(new Error('next_issue_start was cancelled'));
219
+ const stop = client.subscribe((event) => {
220
+ if (event.type === 'error' && event.root === root) {
221
+ finish(new Error(event.message));
222
+ } else if (event.type === 'closed') {
223
+ finish(new Error('the worker closed the connection'));
224
+ } else if (event.type === 'runs' && going(event.runs[root])) {
225
+ finish();
226
+ }
227
+ });
228
+ if (signal?.aborted) {
229
+ abort();
230
+ } else {
231
+ signal?.addEventListener('abort', abort, { once: true });
232
+ }
233
+ });
234
+
235
+ pi.registerTool({
236
+ name: 'next_issue_start',
237
+ label: 'Start next-issue',
238
+ description:
239
+ 'Start a next-issue run of the GitHub issues of this repository when none is going. Starts the worker when needed. When a run is going, starts nothing and reports that run, which picks up new issues by itself.',
240
+ promptSnippet: 'Start a next-issue run of this repository when none is going',
241
+ parameters: Type.Object({}),
242
+ async execute(_toolCallId, _params, signal, _onUpdate, ctx) {
243
+ const { client, root, progress } = await connect(ctx);
244
+ if (going(progress)) {
245
+ return {
246
+ content: [
247
+ {
248
+ type: 'text',
249
+ text: `A run of ${root} is going already, so nothing started: ${formatStatus(progress, Date.now())}. It picks up new issues by itself.`,
250
+ },
251
+ ],
252
+ details: { started: false, root },
253
+ };
254
+ }
255
+ const accepting = accepted(client, root, signal);
256
+ client.send({ type: 'start', root, merge: true });
257
+ await accepting;
258
+ return {
259
+ content: [{ type: 'text', text: `Started a next-issue run of ${root}.` }],
260
+ details: { started: true, root },
261
+ };
262
+ },
263
+ });
264
+
192
265
  pi.registerCommand('next-issue', {
193
266
  description: 'Start or stop working through the GitHub issues of this repository',
194
267
  handler: async (_args, ctx) => {
195
268
  try {
196
- root = await options.root(ctx.cwd);
197
- if (ctx.hasUI) {
198
- show(ctx);
199
- follow();
200
- }
201
- const client = shared(options.client);
202
- await client.ensure();
203
- const progress = client.runs[root];
204
- if (progress?.status === 'running' || progress?.status === 'stopping') {
269
+ const { client, root, progress } = await connect(ctx);
270
+ if (going(progress)) {
205
271
  client.send({ type: 'stop', root });
206
272
  } else {
207
273
  client.send({ type: 'start', root, merge: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ravshansbox/pi-next-issue",
3
- "version": "2.6.0",
3
+ "version": "2.8.0",
4
4
  "description": "Pi extension that works through GitHub issues with the next-issue worker",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -38,7 +38,8 @@
38
38
  "check": "npm run typecheck && npm run lint && npm run format:check && npm test"
39
39
  },
40
40
  "peerDependencies": {
41
- "@earendil-works/pi-coding-agent": "*"
41
+ "@earendil-works/pi-coding-agent": "*",
42
+ "typebox": "*"
42
43
  },
43
44
  "devDependencies": {
44
45
  "@types/node": "^26.6.5",
@@ -8,19 +8,26 @@ description: Turn a problem into a clear GitHub issue that the next-issue worker
8
8
  Turn the problem of the user into one GitHub issue in the format of `template.md`, next to this file.
9
9
 
10
10
  1. Understand the problem. Restate it in your own words.
11
- 2. Investigate the code when the problem touches it. Read the relevant files and note what you find, with file paths.
11
+ 2. Investigate the code when the problem touches it. Read the relevant files and note what you find, with file paths. Run `gh issue list` and note each open issue that must be done first, including the issues you create in the same session.
12
12
  3. Ask numbered questions about anything that is unclear: the scope, the expected behaviour, the acceptance criteria. Label options with letters and recommend one. Ask again until no question is open.
13
13
  4. Write the body from `template.md` into a temporary file. Keep every heading of the template and its order. Leave `Open questions` empty.
14
- 5. Show the title and the body to the user, and ask for confirmation. Change the draft and show it again until the user confirms.
15
- 6. Only after the user confirms, create the issue:
14
+ 5. Show the title, the body and the blockers to the user. Ask once whether to create the issue and start a run: a) create the issue and start a run, b) create the issue only, c) change the draft. Change the draft and show it again until the user picks a or b.
15
+ 6. Only after the user picks a or b, create the issue:
16
16
 
17
17
  ```bash
18
18
  gh issue create --title "<title>" --body-file <file> --label status:todo
19
19
  ```
20
20
 
21
- 7. Tell the user the issue url.
21
+ 7. Link each blocker:
22
22
 
23
- Once the issue exists, do not implement it in this session. The next-issue worker does the work.
23
+ ```bash
24
+ gh issue edit <number> --add-blocked-by <blocker>
25
+ ```
26
+
27
+ 8. Tell the user the issue url.
28
+ 9. Only when the user picked a, call the `next_issue_start` tool, and tell the user what it reports.
29
+
30
+ Once the issue exists, do not implement it in this session. The next-issue worker does the work. Do not ask about a run a second time.
24
31
 
25
32
  ## Sections
26
33
 
package/src/worker.ts CHANGED
@@ -31,7 +31,8 @@ export type WorkerMessage =
31
31
 
32
32
  export type WorkerEvent =
33
33
  | { type: 'runs'; runs: Record<string, Progress> }
34
- | { type: 'error'; root: string; message: string };
34
+ | { type: 'error'; root: string; message: string }
35
+ | { type: 'closed' };
35
36
 
36
37
  export type WorkerOptions = {
37
38
  socketPath: string;
@@ -227,6 +228,7 @@ export class WorkerClient {
227
228
  socket.once('close', () => {
228
229
  if (this.socket === socket) {
229
230
  this.socket = undefined;
231
+ this.emit({ type: 'closed' });
230
232
  this.scheduleReconnect();
231
233
  }
232
234
  if (!hello) {