@ravshansbox/pi-next-issue 1.0.1 → 1.0.3

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
@@ -22,7 +22,7 @@ The run does not belong to a session: it goes on after `/new`, `/resume`, a fork
22
22
 
23
23
  ## What it does
24
24
 
25
- Each loop fetches the open issues and takes the oldest one with saved state, else the oldest one with the `status:todo` label and none of `status:in-progress`, `status:done`, `status:needs-human` or `status:blocked`. The run ends when no such issue is left, or at the first error. For each issue:
25
+ Each loop fetches the 200 oldest open issues with the `status:todo` label and none of `status:in-progress`, `status:done`, `status:needs-human` or `status:blocked`, and the open issues with saved state that it can read. It takes the oldest one with saved state, else the oldest one with the `status:todo` label and none of `status:in-progress`, `status:done`, `status:needs-human` or `status:blocked`. The run ends when no such issue is left, or at the first error. For each issue:
26
26
 
27
27
  1. Swap `status:todo` for `status:in-progress`.
28
28
  2. Add a git worktree at `.worktrees/<n>` on the `<n>` branch, from the remote default branch.
@@ -35,6 +35,8 @@ Each loop fetches the open issues and takes the oldest one with saved state, els
35
35
 
36
36
  An issue that cannot go on gets `status:needs-human` and a comment with the reason. Its worktree stays for you to look at.
37
37
 
38
+ The implementer and the reviewer read the issue title, the body and the comments. They get only comments by the owner, members and collaborators of the repository. The body is passed as it is, and its author can edit it after you add the label, so check the body before you add the label.
39
+
38
40
  Each agent is a pi session in the worktree. It uses the model and thinking level from your pi settings, and loads all of your extensions except this one.
39
41
 
40
42
  The state of each issue is saved in `.worktrees/<n>.json`. The extension adds `/.worktrees/` to `.git/info/exclude`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ravshansbox/pi-next-issue",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Pi extension that works through GitHub issues in the background",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/github.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { setTimeout as sleep } from 'node:timers/promises';
2
2
  import { must, run } from './exec.ts';
3
- import type { ChecksResult, Issue } from './runner.ts';
3
+ import { LABELS, STOP_LABELS, type ChecksResult, type Issue } from './runner.ts';
4
4
 
5
5
  export type Repo = { root: string; slug: string };
6
6
 
@@ -9,6 +9,7 @@ type Check = { name: string; bucket: string; link: string };
9
9
  const POLL_MS = 15_000;
10
10
  const GRACE_MS = 60_000;
11
11
  const READ_TRIES = 3;
12
+ const TRUSTED = ['OWNER', 'MEMBER', 'COLLABORATOR'];
12
13
 
13
14
  function gh(repo: Repo, args: string[], input?: string): Promise<string> {
14
15
  return must('gh', [...args, '--repo', repo.slug], {
@@ -27,25 +28,48 @@ export async function defaultBranch(repo: Repo): Promise<string> {
27
28
  );
28
29
  }
29
30
 
30
- export async function openIssues(repo: Repo): Promise<Issue[]> {
31
+ type RawIssue = {
32
+ number: number;
33
+ title: string;
34
+ body: string | null;
35
+ createdAt: string;
36
+ labels: Array<{ name: string }>;
37
+ };
38
+
39
+ const ISSUE_FIELDS = 'number,title,body,labels,createdAt';
40
+
41
+ export async function openIssues(repo: Repo, saved: number[]): Promise<Issue[]> {
31
42
  const raw = await gh(repo, [
32
43
  'issue',
33
44
  'list',
34
45
  '--state',
35
46
  'open',
47
+ '--label',
48
+ LABELS.ready,
49
+ '--search',
50
+ ['sort:created-asc', ...STOP_LABELS.map((label) => `-label:${label}`)].join(' '),
36
51
  '--limit',
37
52
  '200',
38
53
  '--json',
39
- 'number,title,body,labels,createdAt',
54
+ ISSUE_FIELDS,
40
55
  ]);
41
- const parsed = JSON.parse(raw) as Array<{
42
- number: number;
43
- title: string;
44
- body: string | null;
45
- createdAt: string;
46
- labels: Array<{ name: string }>;
47
- }>;
48
- return parsed
56
+ const listed = JSON.parse(raw) as RawIssue[];
57
+ const known = new Set(listed.map((item) => item.number));
58
+ for (const number of saved.filter((item) => !known.has(item))) {
59
+ const view = await run(
60
+ 'gh',
61
+ ['issue', 'view', String(number), '--json', `${ISSUE_FIELDS},state`, '--repo', repo.slug],
62
+ { cwd: repo.root },
63
+ );
64
+ if (view.code !== 0) {
65
+ continue;
66
+ }
67
+ const item = JSON.parse(view.stdout) as RawIssue & { state: string };
68
+ if (item.state === 'OPEN') {
69
+ listed.push(item);
70
+ }
71
+ }
72
+ return listed
49
73
  .sort((a, b) => a.createdAt.localeCompare(b.createdAt))
50
74
  .map((item) => ({
51
75
  number: item.number,
@@ -58,9 +82,11 @@ export async function openIssues(repo: Repo): Promise<Issue[]> {
58
82
  export async function comments(repo: Repo, issue: number): Promise<string[]> {
59
83
  const raw = await gh(repo, ['issue', 'view', String(issue), '--json', 'comments']);
60
84
  const parsed = JSON.parse(raw) as {
61
- comments: Array<{ author: { login: string }; body: string }>;
85
+ comments: Array<{ author: { login: string }; authorAssociation: string; body: string }>;
62
86
  };
63
- return parsed.comments.map((comment) => `@${comment.author.login}: ${comment.body}`);
87
+ return parsed.comments
88
+ .filter((comment) => TRUSTED.includes(comment.authorAssociation))
89
+ .map((comment) => `@${comment.author.login}: ${comment.body}`);
64
90
  }
65
91
 
66
92
  export async function setLabel(
package/src/ports.ts CHANGED
@@ -10,7 +10,7 @@ export async function createPorts(root: string, config: Config): Promise<Ports>
10
10
  const base = await github.defaultBranch(repo);
11
11
  const timeoutMs = config.checkTimeoutMinutes * 60_000;
12
12
  return {
13
- openIssues: () => github.openIssues(repo),
13
+ openIssues: async () => github.openIssues(repo, await state.savedIssues(root)),
14
14
  comments: (issue) => github.comments(repo, issue),
15
15
  setLabel: (issue, add, remove) => github.setLabel(repo, issue, add, remove),
16
16
  commentOnIssue: (issue, body) => github.commentOnIssue(repo, issue, body),
package/src/runner.ts CHANGED
@@ -86,7 +86,7 @@ export const LABELS = {
86
86
  blocked: 'status:blocked',
87
87
  };
88
88
 
89
- const STOP_LABELS = [LABELS.inProgress, LABELS.done, LABELS.needsHuman, LABELS.blocked];
89
+ export const STOP_LABELS = [LABELS.inProgress, LABELS.done, LABELS.needsHuman, LABELS.blocked];
90
90
 
91
91
  function ready(issue: Issue): boolean {
92
92
  return (