@ravshansbox/pi-next-issue 2.5.10 → 2.7.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 -1
- package/index.ts +76 -10
- package/package.json +9 -3
- package/skills/create-issue/SKILL.md +32 -0
- package/skills/create-issue/template.md +9 -0
- package/src/worker.ts +3 -1
package/README.md
CHANGED
|
@@ -13,12 +13,25 @@ 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
|
+
## Create an issue
|
|
21
|
+
|
|
22
|
+
The package ships the `create-issue` skill. Ask pi to write an issue, or run `/skill:create-issue <problem>`. Pi then:
|
|
23
|
+
|
|
24
|
+
1. restates the problem and reads the code when the problem touches it;
|
|
25
|
+
2. asks numbered questions until none is open;
|
|
26
|
+
3. shows the title and the body, 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. calls `next_issue_start` when you asked for a run.
|
|
29
|
+
|
|
30
|
+
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).
|
|
31
|
+
|
|
19
32
|
## Worker
|
|
20
33
|
|
|
21
|
-
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`.
|
|
34
|
+
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`.
|
|
22
35
|
|
|
23
36
|
- Every pi on the machine shares the one worker, and every pi in a repository shows the run of that repository, whoever started it.
|
|
24
37
|
- 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
|
|
197
|
-
if (
|
|
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.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"description": "Pi extension that works through GitHub issues with the next-issue worker",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -16,12 +16,16 @@
|
|
|
16
16
|
"keywords": [
|
|
17
17
|
"pi-package",
|
|
18
18
|
"pi-extension",
|
|
19
|
+
"pi-skill",
|
|
19
20
|
"github",
|
|
20
21
|
"issues"
|
|
21
22
|
],
|
|
22
23
|
"pi": {
|
|
23
24
|
"extensions": [
|
|
24
25
|
"./index.ts"
|
|
26
|
+
],
|
|
27
|
+
"skills": [
|
|
28
|
+
"./skills"
|
|
25
29
|
]
|
|
26
30
|
},
|
|
27
31
|
"scripts": {
|
|
@@ -34,7 +38,8 @@
|
|
|
34
38
|
"check": "npm run typecheck && npm run lint && npm run format:check && npm test"
|
|
35
39
|
},
|
|
36
40
|
"peerDependencies": {
|
|
37
|
-
"@earendil-works/pi-coding-agent": "*"
|
|
41
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
42
|
+
"typebox": "*"
|
|
38
43
|
},
|
|
39
44
|
"devDependencies": {
|
|
40
45
|
"@types/node": "^26.6.5",
|
|
@@ -45,7 +50,8 @@
|
|
|
45
50
|
},
|
|
46
51
|
"files": [
|
|
47
52
|
"index.ts",
|
|
48
|
-
"src"
|
|
53
|
+
"src",
|
|
54
|
+
"skills"
|
|
49
55
|
],
|
|
50
56
|
"publishConfig": {
|
|
51
57
|
"access": "public"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: create-issue
|
|
3
|
+
description: Turn a problem into a clear GitHub issue that the next-issue worker can claim. Use when the user wants to write, file or create an issue.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Create an issue
|
|
7
|
+
|
|
8
|
+
Turn the problem of the user into one GitHub issue in the format of `template.md`, next to this file.
|
|
9
|
+
|
|
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.
|
|
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
|
+
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. 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
|
+
|
|
17
|
+
```bash
|
|
18
|
+
gh issue create --title "<title>" --body-file <file> --label status:todo
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
7. Tell the user the issue url.
|
|
22
|
+
8. Only when the user picked a, call the `next_issue_start` tool, and tell the user what it reports.
|
|
23
|
+
|
|
24
|
+
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.
|
|
25
|
+
|
|
26
|
+
## Sections
|
|
27
|
+
|
|
28
|
+
- Problem: what is wrong or missing, and who it affects.
|
|
29
|
+
- Context and findings: what you found in the code and the docs, with paths.
|
|
30
|
+
- Acceptance criteria: a list of checks that show the issue is done, each one observable.
|
|
31
|
+
- Out of scope: what the issue does not cover.
|
|
32
|
+
- Open questions: empty when the issue is created.
|
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) {
|