@ravshansbox/pi-github 1.3.0 → 1.4.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
@@ -12,6 +12,7 @@ Watches open issues in a GitHub repo and answers new issues and comments with [p
12
12
  - Issues are handled in parallel, up to 3 at once. Comments on the same issue are handled one after another.
13
13
  - The agent's final message is posted as a comment that starts with `agent:`. Comments that start with `agent:` are ignored, so it does not answer itself.
14
14
  - If the agent made commits, the branch is pushed and a pull request is opened once. Later commits update the same pull request.
15
+ - When someone with write access confirms the implementation is good, the agent asks for a merge: the pull request is merged with the method the repo allows (squash, then rebase, then merge commit) and the issue is closed. Anyone without write access is refused.
15
16
  - State and sessions are kept in `.git/pi-github`.
16
17
 
17
18
  ## Requirements
package/dist/github.js CHANGED
@@ -17,6 +17,9 @@ export function createGitHub({ baseUrl, token, repo }) {
17
17
  }
18
18
  return response;
19
19
  }
20
+ async function getRepository() {
21
+ return (await (await request("GET", `/repos/${repo}`)).json());
22
+ }
20
23
  async function list(path) {
21
24
  const items = [];
22
25
  let url = path;
@@ -48,15 +51,31 @@ export function createGitHub({ baseUrl, token, repo }) {
48
51
  const path = target.kind === "issue" ? `issues/${target.id}` : `issues/comments/${target.id}`;
49
52
  await request("POST", `/repos/${repo}/${path}/reactions`, { content });
50
53
  },
51
- getDefaultBranch: async () => {
52
- const data = (await (await request("GET", `/repos/${repo}`)).json());
53
- return data.default_branch;
54
+ getDefaultBranch: async () => (await getRepository()).default_branch,
55
+ getMergeMethod: async () => {
56
+ const settings = await getRepository();
57
+ if (settings.allow_squash_merge)
58
+ return "squash";
59
+ if (settings.allow_rebase_merge)
60
+ return "rebase";
61
+ return "merge";
62
+ },
63
+ hasWriteAccess: async (user) => {
64
+ const response = await request("GET", `/repos/${repo}/collaborators/${encodeURIComponent(user)}/permission`);
65
+ const { permission } = (await response.json());
66
+ return ["admin", "maintain", "write"].includes(permission);
54
67
  },
55
68
  getIssue: async (issueNumber) => (await (await request("GET", `/repos/${repo}/issues/${issueNumber}`)).json()),
56
- hasOpenPullRequest: async (branch) => {
69
+ findOpenPullRequest: async (branch) => {
57
70
  const owner = repo.split("/")[0];
58
71
  const response = await request("GET", `/repos/${repo}/pulls?state=open&head=${encodeURIComponent(`${owner}:${branch}`)}`);
59
- return (await response.json()).length > 0;
72
+ return (await response.json())[0]?.number;
73
+ },
74
+ mergePullRequest: async (pullNumber, mergeMethod) => {
75
+ await request("PUT", `/repos/${repo}/pulls/${pullNumber}/merge`, { merge_method: mergeMethod });
76
+ },
77
+ closeIssue: async (issueNumber) => {
78
+ await request("PATCH", `/repos/${repo}/issues/${issueNumber}`, { state: "closed", state_reason: "completed" });
60
79
  },
61
80
  createPullRequest: async (pull) => {
62
81
  await request("POST", `/repos/${repo}/pulls`, pull);
package/dist/handler.js CHANGED
@@ -2,13 +2,21 @@ import { branchFor, ensureWorktree, hasUnpushedCommits, pushBranch } from "./git
2
2
  import { agentPrefix } from "./poller.js";
3
3
  export async function handleTrigger(trigger, { github, agent, repoRoot, log = () => { }, }) {
4
4
  const base = await github.getDefaultBranch();
5
+ const branch = branchFor(trigger.issueNumber);
5
6
  const worktree = await ensureWorktree(repoRoot, trigger.issueNumber, base);
6
- const answer = await agent.run(trigger, { cwd: worktree, progress: log });
7
+ let mergeRequested = false;
8
+ const requestMerge = async () => {
9
+ if (!(await github.hasWriteAccess(trigger.author))) {
10
+ return `@${trigger.author} does not have write access, so the pull request will not be merged.`;
11
+ }
12
+ mergeRequested = true;
13
+ return "The pull request will be merged and the issue closed after you finish.";
14
+ };
15
+ const answer = await agent.run(trigger, { cwd: worktree, progress: log, requestMerge });
7
16
  if (await hasUnpushedCommits(worktree, base)) {
8
- const branch = branchFor(trigger.issueNumber);
9
17
  await pushBranch(worktree);
10
18
  log(`pushed ${branch}`);
11
- if (!(await github.hasOpenPullRequest(branch))) {
19
+ if ((await github.findOpenPullRequest(branch)) === undefined) {
12
20
  const issue = await github.getIssue(trigger.issueNumber);
13
21
  await github.createPullRequest({
14
22
  head: branch,
@@ -19,5 +27,23 @@ export async function handleTrigger(trigger, { github, agent, repoRoot, log = ()
19
27
  log("opened a pull request");
20
28
  }
21
29
  }
22
- await github.postComment(trigger.issueNumber, `${agentPrefix} ${answer}`);
30
+ let note = "";
31
+ if (mergeRequested) {
32
+ const pullNumber = await github.findOpenPullRequest(branch);
33
+ if (pullNumber === undefined) {
34
+ note = "\n\nThere is no open pull request for this issue, so nothing was merged.";
35
+ }
36
+ else {
37
+ try {
38
+ await github.mergePullRequest(pullNumber, await github.getMergeMethod());
39
+ log(`merged pull request #${pullNumber}`);
40
+ await github.closeIssue(trigger.issueNumber);
41
+ log("closed the issue");
42
+ }
43
+ catch (error) {
44
+ note = `\n\nMerging failed: ${error instanceof Error ? error.message : error}`;
45
+ }
46
+ }
47
+ }
48
+ await github.postComment(trigger.issueNumber, `${agentPrefix} ${answer}${note}`);
23
49
  }
package/dist/pi-agent.js CHANGED
@@ -1,12 +1,20 @@
1
1
  import { join } from "node:path";
2
- import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
2
+ import { createAgentSession, defineTool, SessionManager } from "@earendil-works/pi-coding-agent";
3
+ import { Type } from "typebox";
3
4
  import { describeToolCall } from "./progress.js";
4
5
  import { buildPrompt } from "./prompt.js";
5
6
  export function createPiAgent({ github, repo, sessionsDir }) {
6
7
  return {
7
- async run(trigger, { cwd, progress }) {
8
+ async run(trigger, { cwd, progress, requestMerge }) {
8
9
  const sessionManager = SessionManager.continueRecent(cwd, join(sessionsDir, `issue-${trigger.issueNumber}`));
9
- const { session } = await createAgentSession({ cwd, sessionManager });
10
+ const mergeTool = defineTool({
11
+ name: "merge_pull_request",
12
+ label: "Merge pull request",
13
+ description: "Merge this issue's pull request into the default branch and close the issue, once you have finished. Call it only when the latest comment confirms the implementation is good.",
14
+ parameters: Type.Object({}),
15
+ execute: async () => ({ content: [{ type: "text", text: await requestMerge() }], details: {} }),
16
+ });
17
+ const { session } = await createAgentSession({ cwd, sessionManager, customTools: [mergeTool] });
10
18
  const unsubscribe = session.subscribe((event) => {
11
19
  if (event.type === "tool_execution_start" && !event.parentToolCallId)
12
20
  progress(describeToolCall(event.toolName, event.args));
package/dist/prompt.js CHANGED
@@ -8,6 +8,7 @@ export function buildPrompt({ repo, issue, trigger, firstInSession, }) {
8
8
  `Your working directory is a git worktree on branch ${branchFor(issue.number)}.`,
9
9
  "If the request needs code changes, make them and commit them. Do not push and do not open pull requests; that happens automatically after you finish.",
10
10
  "Your final message is posted as a comment on the issue, so write it for the person who asked.",
11
+ "When a comment confirms the implementation is good, call merge_pull_request.",
11
12
  ...(trigger.kind === "comment" ? [`Issue description:\n\n${issue.body ?? ""}`] : []),
12
13
  message,
13
14
  ].join("\n\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ravshansbox/pi-github",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Watch GitHub issues and answer them with pi agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -20,7 +20,8 @@
20
20
  "prepare": "tsc"
21
21
  },
22
22
  "dependencies": {
23
- "@earendil-works/pi-coding-agent": "^1.4.0"
23
+ "@earendil-works/pi-coding-agent": "^1.4.0",
24
+ "typebox": "^1.3.27"
24
25
  },
25
26
  "devDependencies": {
26
27
  "@types/node": "^26.6.6",