@oh-my-pi/pi-coding-agent 14.2.1 → 14.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.
Files changed (137) hide show
  1. package/CHANGELOG.md +143 -1
  2. package/package.json +19 -19
  3. package/src/autoresearch/prompt.md +1 -1
  4. package/src/cli/args.ts +10 -1
  5. package/src/cli/shell-cli.ts +15 -3
  6. package/src/commit/agentic/prompts/analyze-file.md +1 -1
  7. package/src/config/model-registry.ts +67 -15
  8. package/src/config/prompt-templates.ts +5 -5
  9. package/src/config/settings-schema.ts +63 -4
  10. package/src/cursor.ts +3 -8
  11. package/src/debug/system-info.ts +6 -2
  12. package/src/discovery/claude.ts +58 -36
  13. package/src/discovery/helpers.ts +3 -3
  14. package/src/discovery/opencode.ts +20 -2
  15. package/src/edit/diff.ts +50 -47
  16. package/src/edit/index.ts +87 -57
  17. package/src/edit/line-hash.ts +735 -19
  18. package/src/edit/modes/apply-patch.ts +0 -9
  19. package/src/edit/modes/atom.ts +658 -0
  20. package/src/edit/modes/chunk.ts +144 -78
  21. package/src/edit/modes/hashline.ts +223 -146
  22. package/src/edit/modes/patch.ts +5 -9
  23. package/src/edit/modes/replace.ts +6 -11
  24. package/src/edit/renderer.ts +112 -143
  25. package/src/edit/streaming.ts +385 -0
  26. package/src/exec/bash-executor.ts +58 -5
  27. package/src/export/html/template.generated.ts +1 -1
  28. package/src/export/html/template.js +4 -12
  29. package/src/extensibility/custom-tools/types.ts +2 -0
  30. package/src/extensibility/custom-tools/wrapper.ts +2 -1
  31. package/src/internal-urls/docs-index.generated.ts +7 -7
  32. package/src/internal-urls/pi-protocol.ts +0 -2
  33. package/src/lsp/client.ts +8 -1
  34. package/src/lsp/defaults.json +2 -1
  35. package/src/lsp/index.ts +1 -1
  36. package/src/mcp/render.ts +1 -8
  37. package/src/modes/acp/acp-agent.ts +76 -2
  38. package/src/modes/components/assistant-message.ts +5 -34
  39. package/src/modes/components/diff.ts +23 -14
  40. package/src/modes/components/footer.ts +21 -16
  41. package/src/modes/components/hook-editor.ts +1 -1
  42. package/src/modes/components/settings-defs.ts +6 -1
  43. package/src/modes/components/todo-reminder.ts +1 -8
  44. package/src/modes/components/tool-execution.ts +112 -105
  45. package/src/modes/controllers/input-controller.ts +1 -1
  46. package/src/modes/controllers/selector-controller.ts +1 -1
  47. package/src/modes/interactive-mode.ts +0 -2
  48. package/src/modes/print-mode.ts +8 -0
  49. package/src/modes/theme/mermaid-cache.ts +13 -52
  50. package/src/modes/theme/theme.ts +2 -2
  51. package/src/prompts/agents/librarian.md +1 -1
  52. package/src/prompts/agents/reviewer.md +4 -4
  53. package/src/prompts/ci-green-request.md +1 -1
  54. package/src/prompts/review-request.md +1 -1
  55. package/src/prompts/system/subagent-system-prompt.md +3 -3
  56. package/src/prompts/system/subagent-yield-reminder.md +11 -0
  57. package/src/prompts/system/system-prompt.md +4 -1
  58. package/src/prompts/tools/ask.md +3 -2
  59. package/src/prompts/tools/ast-edit.md +15 -19
  60. package/src/prompts/tools/ast-grep.md +18 -24
  61. package/src/prompts/tools/atom.md +96 -0
  62. package/src/prompts/tools/browser.md +1 -0
  63. package/src/prompts/tools/chunk-edit.md +58 -179
  64. package/src/prompts/tools/debug.md +4 -5
  65. package/src/prompts/tools/exit-plan-mode.md +4 -5
  66. package/src/prompts/tools/find.md +4 -8
  67. package/src/prompts/tools/github.md +18 -0
  68. package/src/prompts/tools/grep.md +8 -8
  69. package/src/prompts/tools/hashline.md +22 -89
  70. package/src/prompts/tools/{gemini-image.md → image-gen.md} +1 -1
  71. package/src/prompts/tools/inspect-image.md +6 -6
  72. package/src/prompts/tools/lsp.md +6 -0
  73. package/src/prompts/tools/patch.md +12 -19
  74. package/src/prompts/tools/python.md +3 -2
  75. package/src/prompts/tools/read-chunk.md +46 -8
  76. package/src/prompts/tools/read.md +9 -6
  77. package/src/prompts/tools/ssh.md +8 -17
  78. package/src/prompts/tools/todo-write.md +54 -41
  79. package/src/sdk.ts +22 -14
  80. package/src/session/agent-session.ts +61 -22
  81. package/src/session/session-manager.ts +228 -57
  82. package/src/session/streaming-output.ts +11 -0
  83. package/src/system-prompt.ts +7 -2
  84. package/src/task/executor.ts +44 -48
  85. package/src/task/render.ts +11 -13
  86. package/src/tools/ask.ts +7 -7
  87. package/src/tools/ast-edit.ts +45 -41
  88. package/src/tools/ast-grep.ts +77 -85
  89. package/src/tools/bash.ts +21 -9
  90. package/src/tools/browser.ts +32 -30
  91. package/src/tools/calculator.ts +4 -4
  92. package/src/tools/cancel-job.ts +1 -1
  93. package/src/tools/checkpoint.ts +2 -2
  94. package/src/tools/debug.ts +41 -37
  95. package/src/tools/exit-plan-mode.ts +1 -1
  96. package/src/tools/find.ts +4 -4
  97. package/src/tools/gh-renderer.ts +12 -4
  98. package/src/tools/gh.ts +514 -712
  99. package/src/tools/grep.ts +115 -130
  100. package/src/tools/{gemini-image.ts → image-gen.ts} +459 -60
  101. package/src/tools/index.ts +14 -32
  102. package/src/tools/inspect-image.ts +3 -3
  103. package/src/tools/json-tree.ts +114 -114
  104. package/src/tools/match-line-format.ts +9 -8
  105. package/src/tools/notebook.ts +8 -7
  106. package/src/tools/poll-tool.ts +2 -1
  107. package/src/tools/python.ts +9 -23
  108. package/src/tools/read.ts +32 -21
  109. package/src/tools/render-mermaid.ts +1 -1
  110. package/src/tools/render-utils.ts +18 -0
  111. package/src/tools/renderers.ts +2 -2
  112. package/src/tools/report-tool-issue.ts +3 -2
  113. package/src/tools/resolve.ts +1 -1
  114. package/src/tools/review.ts +12 -10
  115. package/src/tools/search-tool-bm25.ts +2 -4
  116. package/src/tools/sqlite-reader.ts +116 -3
  117. package/src/tools/ssh.ts +4 -4
  118. package/src/tools/todo-write.ts +172 -147
  119. package/src/tools/vim.ts +14 -15
  120. package/src/tools/write.ts +4 -4
  121. package/src/tools/{submit-result.ts → yield.ts} +11 -13
  122. package/src/utils/edit-mode.ts +2 -1
  123. package/src/utils/file-display-mode.ts +10 -5
  124. package/src/utils/git.ts +9 -5
  125. package/src/utils/shell-snapshot.ts +2 -3
  126. package/src/vim/render.ts +4 -4
  127. package/src/web/search/providers/codex.ts +129 -6
  128. package/src/prompts/system/subagent-submit-reminder.md +0 -11
  129. package/src/prompts/tools/gh-issue-view.md +0 -11
  130. package/src/prompts/tools/gh-pr-checkout.md +0 -12
  131. package/src/prompts/tools/gh-pr-diff.md +0 -12
  132. package/src/prompts/tools/gh-pr-push.md +0 -11
  133. package/src/prompts/tools/gh-pr-view.md +0 -11
  134. package/src/prompts/tools/gh-repo-view.md +0 -11
  135. package/src/prompts/tools/gh-run-watch.md +0 -12
  136. package/src/prompts/tools/gh-search-issues.md +0 -11
  137. package/src/prompts/tools/gh-search-prs.md +0 -11
package/src/tools/gh.ts CHANGED
@@ -1,17 +1,10 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
4
+ import { StringEnum } from "@oh-my-pi/pi-ai";
4
5
  import { abortableSleep, isEnoent, prompt, untilAborted } from "@oh-my-pi/pi-utils";
5
6
  import { type Static, Type } from "@sinclair/typebox";
6
- import ghIssueViewDescription from "../prompts/tools/gh-issue-view.md" with { type: "text" };
7
- import ghPrCheckoutDescription from "../prompts/tools/gh-pr-checkout.md" with { type: "text" };
8
- import ghPrDiffDescription from "../prompts/tools/gh-pr-diff.md" with { type: "text" };
9
- import ghPrPushDescription from "../prompts/tools/gh-pr-push.md" with { type: "text" };
10
- import ghPrViewDescription from "../prompts/tools/gh-pr-view.md" with { type: "text" };
11
- import ghRepoViewDescription from "../prompts/tools/gh-repo-view.md" with { type: "text" };
12
- import ghRunWatchDescription from "../prompts/tools/gh-run-watch.md" with { type: "text" };
13
- import ghSearchIssuesDescription from "../prompts/tools/gh-search-issues.md" with { type: "text" };
14
- import ghSearchPrsDescription from "../prompts/tools/gh-search-prs.md" with { type: "text" };
7
+ import githubDescription from "../prompts/tools/github.md" with { type: "text" };
15
8
  import * as git from "../utils/git";
16
9
  import type { ToolSession } from ".";
17
10
  import { formatShortSha } from "./gh-format";
@@ -135,124 +128,67 @@ const RUN_SUCCESS_CONCLUSIONS = new Set(["success", "neutral", "skipped"]);
135
128
  const RUN_FAILURE_CONCLUSIONS = new Set(["failure", "timed_out", "cancelled", "action_required", "startup_failure"]);
136
129
  const JOB_FAILURE_CONCLUSIONS = new Set(["failure", "timed_out", "cancelled", "action_required"]);
137
130
 
138
- const ghRepoViewSchema = Type.Object({
139
- repo: Type.Optional(
140
- Type.String({
141
- description: "Repository in OWNER/REPO format. Defaults to the current GitHub repository context.",
142
- }),
131
+ const githubSchema = Type.Object({
132
+ op: StringEnum(
133
+ [
134
+ "repo_view",
135
+ "issue_view",
136
+ "pr_view",
137
+ "pr_diff",
138
+ "pr_checkout",
139
+ "pr_push",
140
+ "search_issues",
141
+ "search_prs",
142
+ "run_watch",
143
+ ],
144
+ { description: "github operation" },
143
145
  ),
144
- branch: Type.Optional(Type.String({ description: "Branch name to inspect instead of the default branch." })),
145
- });
146
-
147
- const ghIssueViewSchema = Type.Object({
148
- issue: Type.String({ description: "Issue number or full GitHub issue URL." }),
149
146
  repo: Type.Optional(
150
- Type.String({ description: "Repository in OWNER/REPO format. Omit when passing a full issue URL." }),
151
- ),
152
- comments: Type.Optional(Type.Boolean({ description: "Include issue comments.", default: true })),
153
- });
154
-
155
- const ghPrViewSchema = Type.Object({
156
- pr: Type.Optional(
157
147
  Type.String({
158
- description:
159
- "Pull request number, full GitHub pull request URL, or branch name. Defaults to the current branch PR.",
148
+ description: "owner/repo (any op)",
149
+ examples: ["facebook/react"],
160
150
  }),
161
151
  ),
162
- repo: Type.Optional(
163
- Type.String({ description: "Repository in OWNER/REPO format. Omit when passing a full pull request URL." }),
164
- ),
165
- comments: Type.Optional(Type.Boolean({ description: "Include pull request comments.", default: true })),
166
- });
167
-
168
- const ghPrDiffSchema = Type.Object({
169
- pr: Type.Optional(
152
+ branch: Type.Optional(
170
153
  Type.String({
171
- description:
172
- "Pull request number, full GitHub pull request URL, or branch name. Defaults to the current branch PR.",
173
- }),
174
- ),
175
- repo: Type.Optional(
176
- Type.String({ description: "Repository in OWNER/REPO format. Omit when passing a full pull request URL." }),
177
- ),
178
- nameOnly: Type.Optional(
179
- Type.Boolean({ description: "Return only changed file names instead of unified diff output." }),
180
- ),
181
- exclude: Type.Optional(
182
- Type.Array(Type.String({ description: "Glob pattern for files to exclude from the diff." }), {
183
- description: "File globs to exclude from the diff output.",
154
+ description: "branch (repo_view, pr_checkout local branch, pr_push local branch, run_watch)",
155
+ examples: ["main", "develop"],
184
156
  }),
185
157
  ),
186
- });
187
-
188
- const ghPrCheckoutSchema = Type.Object({
189
- pr: Type.Optional(
158
+ issue: Type.Optional(
190
159
  Type.String({
191
- description:
192
- "Pull request number, full GitHub pull request URL, or branch name. Defaults to the current branch PR.",
160
+ description: "issue number or url (issue_view)",
161
+ examples: ["123", "https://github.com/owner/repo/issues/123"],
193
162
  }),
194
163
  ),
195
- repo: Type.Optional(
196
- Type.String({ description: "Repository in OWNER/REPO format. Omit when passing a full pull request URL." }),
197
- ),
198
- branch: Type.Optional(Type.String({ description: "Local branch name to create or reuse (default: pr-<number>)." })),
199
- worktree: Type.Optional(
200
- Type.String({ description: "Worktree path to create. Defaults to <repo>/.worktrees/<branch>." }),
201
- ),
202
- force: Type.Optional(
203
- Type.Boolean({
204
- description: "Reset an existing local branch to the PR head when it is not already checked out elsewhere.",
205
- }),
206
- ),
207
- });
208
-
209
- const ghPrPushSchema = Type.Object({
210
- branch: Type.Optional(
164
+ pr: Type.Optional(
211
165
  Type.String({
212
- description: "Local branch name to push. Defaults to the current checked-out git branch.",
166
+ description: "pr number, url, or branch (pr_view, pr_diff, pr_checkout)",
167
+ examples: ["123", "feature-branch"],
213
168
  }),
214
169
  ),
215
- forceWithLease: Type.Optional(Type.Boolean({ description: "Use --force-with-lease when pushing the PR branch." })),
216
- });
217
-
218
- const ghSearchIssuesSchema = Type.Object({
219
- query: Type.String({ description: "GitHub issue search query. Supports GitHub search syntax." }),
220
- repo: Type.Optional(Type.String({ description: "Repository in OWNER/REPO format to scope the search." })),
221
- limit: Type.Optional(Type.Number({ description: "Maximum results to return (max: 50).", default: 10 })),
222
- });
223
-
224
- const ghSearchPrsSchema = Type.Object({
225
- query: Type.String({ description: "GitHub pull request search query. Supports GitHub search syntax." }),
226
- repo: Type.Optional(Type.String({ description: "Repository in OWNER/REPO format to scope the search." })),
227
- limit: Type.Optional(Type.Number({ description: "Maximum results to return (max: 50).", default: 10 })),
228
- });
229
-
230
- const ghRunWatchSchema = Type.Object({
231
- run: Type.Optional(
232
- Type.String({
233
- description:
234
- "GitHub Actions run ID or full run URL. Omitting this watches the workflow runs for the current HEAD commit on the selected branch.",
170
+ comments: Type.Optional(Type.Boolean({ description: "include comments (issue_view, pr_view)", default: true })),
171
+ nameOnly: Type.Optional(Type.Boolean({ description: "return file names only (pr_diff)" })),
172
+ exclude: Type.Optional(
173
+ Type.Array(Type.String({ description: "glob to exclude" }), {
174
+ description: "file globs to exclude (pr_diff)",
235
175
  }),
236
176
  ),
237
- branch: Type.Optional(
177
+ worktree: Type.Optional(Type.String({ description: "worktree path (pr_checkout)" })),
178
+ force: Type.Optional(Type.Boolean({ description: "reset existing local branch (pr_checkout)" })),
179
+ forceWithLease: Type.Optional(Type.Boolean({ description: "force-with-lease push (pr_push)" })),
180
+ query: Type.Optional(
238
181
  Type.String({
239
- description: "Branch to inspect when omitting `run`. Defaults to the current checked-out git branch.",
182
+ description: "search query (search_issues, search_prs)",
183
+ examples: ["is:open label:bug"],
240
184
  }),
241
185
  ),
242
- tail: Type.Optional(
243
- Type.Number({ description: "Number of log lines to include per failed job (max: 200).", default: 15 }),
244
- ),
186
+ limit: Type.Optional(Type.Number({ description: "max results (search_issues, search_prs)", default: 10 })),
187
+ run: Type.Optional(Type.String({ description: "actions run id or url (run_watch)", examples: ["123456"] })),
188
+ tail: Type.Optional(Type.Number({ description: "log lines per failed job (run_watch)", default: 15 })),
245
189
  });
246
190
 
247
- type GhRepoViewInput = Static<typeof ghRepoViewSchema>;
248
- type GhIssueViewInput = Static<typeof ghIssueViewSchema>;
249
- type GhPrViewInput = Static<typeof ghPrViewSchema>;
250
- type GhPrDiffInput = Static<typeof ghPrDiffSchema>;
251
- type GhPrCheckoutInput = Static<typeof ghPrCheckoutSchema>;
252
- type GhPrPushInput = Static<typeof ghPrPushSchema>;
253
- type GhSearchIssuesInput = Static<typeof ghSearchIssuesSchema>;
254
- type GhSearchPrsInput = Static<typeof ghSearchPrsSchema>;
255
- type GhRunWatchInput = Static<typeof ghRunWatchSchema>;
191
+ type GithubInput = Static<typeof githubSchema>;
256
192
 
257
193
  export interface GhToolDetails {
258
194
  meta?: OutputMeta;
@@ -611,14 +547,6 @@ function toLocalBranchRef(value: string): string {
611
547
  return `refs/heads/${value}`;
612
548
  }
613
549
 
614
- function stripHeadsRef(value: string | undefined): string | undefined {
615
- if (!value) {
616
- return undefined;
617
- }
618
-
619
- return value.startsWith("refs/heads/") ? value.slice("refs/heads/".length) : value;
620
- }
621
-
622
550
  async function requireGitRepoRoot(cwd: string, signal?: AbortSignal): Promise<string> {
623
551
  const repoRoot = await git.repo.root(cwd, signal);
624
552
  if (!repoRoot) {
@@ -763,10 +691,13 @@ async function resolvePrBranchPushTarget(
763
691
  maintainerCanModify?: boolean;
764
692
  isCrossRepository: boolean;
765
693
  }> {
694
+ const headRef = await git.config.getBranch(repoRoot, localBranch, "ompPrHeadRef", signal);
695
+ if (!headRef) {
696
+ throw new ToolError(`branch ${localBranch} has no PR push metadata; check it out via op: pr_checkout first`);
697
+ }
698
+
766
699
  const pushRemote = await git.config.getBranch(repoRoot, localBranch, "pushRemote", signal);
767
700
  const remote = await git.config.getBranch(repoRoot, localBranch, "remote", signal);
768
- const mergeRef = await git.config.getBranch(repoRoot, localBranch, "merge", signal);
769
- const headRef = await git.config.getBranch(repoRoot, localBranch, "ompPrHeadRef", signal);
770
701
  const prUrl = await git.config.getBranch(repoRoot, localBranch, "ompPrUrl", signal);
771
702
  const maintainerCanModifyValue = await git.config.getBranch(
772
703
  repoRoot,
@@ -781,14 +712,9 @@ async function resolvePrBranchPushTarget(
781
712
  throw new ToolError(`branch ${localBranch} has no configured push remote`);
782
713
  }
783
714
 
784
- const remoteBranch = headRef ?? stripHeadsRef(mergeRef);
785
- if (!remoteBranch) {
786
- throw new ToolError(`branch ${localBranch} has no tracked PR head ref`);
787
- }
788
-
789
715
  return {
790
716
  remoteName,
791
- remoteBranch,
717
+ remoteBranch: headRef,
792
718
  remoteUrl: await git.remote.url(repoRoot, remoteName, signal),
793
719
  prUrl,
794
720
  maintainerCanModify:
@@ -1648,7 +1574,7 @@ function formatReviewCommentsSection(comments: GhPrReviewComment[] | undefined):
1648
1574
  return lines;
1649
1575
  }
1650
1576
 
1651
- function formatRepoView(data: GhRepoViewData, input: GhRepoViewInput): string {
1577
+ function formatRepoView(data: GhRepoViewData, input: { repo?: string; branch?: string }): string {
1652
1578
  const lines: string[] = [];
1653
1579
  const name = data.nameWithOwner ?? input.repo ?? "GitHub Repository";
1654
1580
  lines.push(`# ${name}`);
@@ -1675,7 +1601,7 @@ function formatRepoView(data: GhRepoViewData, input: GhRepoViewInput): string {
1675
1601
  return lines.join("\n").trim();
1676
1602
  }
1677
1603
 
1678
- function formatIssueView(data: GhIssueViewData, input: GhIssueViewInput): string {
1604
+ function formatIssueView(data: GhIssueViewData, input: { issue: string; repo?: string; comments?: boolean }): string {
1679
1605
  const lines: string[] = [];
1680
1606
  const issueNumber = data.number ?? input.issue;
1681
1607
  lines.push(`# Issue #${issueNumber}: ${data.title ?? "Untitled"}`);
@@ -1721,7 +1647,7 @@ function formatPrFiles(files: GhPrFile[] | undefined): string[] {
1721
1647
  return lines;
1722
1648
  }
1723
1649
 
1724
- function formatPrView(data: GhPrViewData, input: GhPrViewInput): string {
1650
+ function formatPrView(data: GhPrViewData, input: { pr?: string; repo?: string; comments?: boolean }): string {
1725
1651
  const lines: string[] = [];
1726
1652
  const prIdentifier = data.number ?? input.pr ?? "current";
1727
1653
  lines.push(`# Pull Request #${prIdentifier}: ${data.title ?? "Untitled"}`);
@@ -1891,644 +1817,520 @@ function buildTextResult(
1891
1817
  return builder.done();
1892
1818
  }
1893
1819
 
1894
- export class GhRepoViewTool implements AgentTool<typeof ghRepoViewSchema, GhToolDetails> {
1895
- readonly name = "gh_repo_view";
1896
- readonly label = "GitHub Repo";
1897
- readonly description = prompt.render(ghRepoViewDescription);
1898
- readonly parameters = ghRepoViewSchema;
1820
+ export class GithubTool implements AgentTool<typeof githubSchema, GhToolDetails> {
1821
+ readonly name = "github";
1822
+ readonly label = "GitHub";
1823
+ readonly description = prompt.render(githubDescription);
1824
+ readonly parameters = githubSchema;
1899
1825
  readonly strict = true;
1900
1826
 
1901
1827
  constructor(private readonly session: ToolSession) {}
1902
1828
 
1903
- static createIf(session: ToolSession): GhRepoViewTool | null {
1829
+ static createIf(session: ToolSession): GithubTool | null {
1904
1830
  if (!git.github.available()) return null;
1905
- return new GhRepoViewTool(session);
1831
+ return new GithubTool(session);
1906
1832
  }
1907
1833
 
1908
1834
  async execute(
1909
1835
  _toolCallId: string,
1910
- params: GhRepoViewInput,
1836
+ params: GithubInput,
1911
1837
  signal?: AbortSignal,
1912
- _onUpdate?: AgentToolUpdateCallback<GhToolDetails>,
1838
+ onUpdate?: AgentToolUpdateCallback<GhToolDetails>,
1913
1839
  _context?: AgentToolContext,
1914
1840
  ): Promise<AgentToolResult<GhToolDetails>> {
1915
1841
  return untilAborted(signal, async () => {
1916
- const repo = normalizeOptionalString(params.repo);
1917
- const branch = normalizeOptionalString(params.branch);
1918
- const args = ["repo", "view"];
1919
- if (repo) {
1920
- args.push(repo);
1921
- }
1922
- if (branch) {
1923
- args.push("--branch", branch);
1842
+ switch (params.op) {
1843
+ case "repo_view":
1844
+ return executeRepoView(this.session, params, signal);
1845
+ case "issue_view":
1846
+ return executeIssueView(this.session, params, signal);
1847
+ case "pr_view":
1848
+ return executePrView(this.session, params, signal);
1849
+ case "pr_diff":
1850
+ return executePrDiff(this.session, params, signal);
1851
+ case "pr_checkout":
1852
+ return executePrCheckout(this.session, params, signal);
1853
+ case "pr_push":
1854
+ return executePrPush(this.session, params, signal);
1855
+ case "search_issues":
1856
+ return executeSearchIssues(this.session, params, signal);
1857
+ case "search_prs":
1858
+ return executeSearchPrs(this.session, params, signal);
1859
+ case "run_watch":
1860
+ return executeRunWatch(this.session, this.name, params, signal, onUpdate);
1924
1861
  }
1925
- args.push("--json", GH_REPO_FIELDS.join(","));
1926
-
1927
- const data = await git.github.json<GhRepoViewData>(this.session.cwd, args, signal, {
1928
- repoProvided: Boolean(repo),
1929
- });
1930
- return buildTextResult(formatRepoView(data, { repo, branch }), data.url);
1931
1862
  });
1932
1863
  }
1933
1864
  }
1934
1865
 
1935
- export class GhIssueViewTool implements AgentTool<typeof ghIssueViewSchema, GhToolDetails> {
1936
- readonly name = "gh_issue_view";
1937
- readonly label = "GitHub Issue";
1938
- readonly description = prompt.render(ghIssueViewDescription);
1939
- readonly parameters = ghIssueViewSchema;
1940
- readonly strict = true;
1941
-
1942
- constructor(private readonly session: ToolSession) {}
1943
-
1944
- static createIf(session: ToolSession): GhIssueViewTool | null {
1945
- if (!git.github.available()) return null;
1946
- return new GhIssueViewTool(session);
1947
- }
1948
-
1949
- async execute(
1950
- _toolCallId: string,
1951
- params: GhIssueViewInput,
1952
- signal?: AbortSignal,
1953
- _onUpdate?: AgentToolUpdateCallback<GhToolDetails>,
1954
- _context?: AgentToolContext,
1955
- ): Promise<AgentToolResult<GhToolDetails>> {
1956
- return untilAborted(signal, async () => {
1957
- const issue = requireNonEmpty(params.issue, "issue");
1958
- const repo = normalizeOptionalString(params.repo);
1959
- const includeComments = params.comments ?? true;
1960
- const args = ["issue", "view", issue];
1961
- appendRepoFlag(args, repo, issue);
1962
- args.push("--json", (includeComments ? GH_ISSUE_FIELDS : GH_ISSUE_FIELDS_NO_COMMENTS).join(","));
1963
-
1964
- const data = await git.github.json<GhIssueViewData>(this.session.cwd, args, signal, {
1965
- repoProvided: Boolean(repo),
1966
- });
1967
- return buildTextResult(formatIssueView(data, { issue, repo, comments: includeComments }), data.url);
1968
- });
1969
- }
1970
- }
1971
-
1972
- export class GhPrViewTool implements AgentTool<typeof ghPrViewSchema, GhToolDetails> {
1973
- readonly name = "gh_pr_view";
1974
- readonly label = "GitHub PR";
1975
- readonly description = prompt.render(ghPrViewDescription);
1976
- readonly parameters = ghPrViewSchema;
1977
- readonly strict = true;
1978
-
1979
- constructor(private readonly session: ToolSession) {}
1980
-
1981
- static createIf(session: ToolSession): GhPrViewTool | null {
1982
- if (!git.github.available()) return null;
1983
- return new GhPrViewTool(session);
1984
- }
1866
+ async function executeRepoView(
1867
+ session: ToolSession,
1868
+ params: GithubInput,
1869
+ signal: AbortSignal | undefined,
1870
+ ): Promise<AgentToolResult<GhToolDetails>> {
1871
+ const repo = normalizeOptionalString(params.repo);
1872
+ const branch = normalizeOptionalString(params.branch);
1873
+ const args = ["repo", "view"];
1874
+ if (repo) {
1875
+ args.push(repo);
1876
+ }
1877
+ if (branch) {
1878
+ args.push("--branch", branch);
1879
+ }
1880
+ args.push("--json", GH_REPO_FIELDS.join(","));
1881
+
1882
+ const data = await git.github.json<GhRepoViewData>(session.cwd, args, signal, {
1883
+ repoProvided: Boolean(repo),
1884
+ });
1885
+ return buildTextResult(formatRepoView(data, { repo, branch }), data.url);
1886
+ }
1887
+
1888
+ async function executeIssueView(
1889
+ session: ToolSession,
1890
+ params: GithubInput,
1891
+ signal: AbortSignal | undefined,
1892
+ ): Promise<AgentToolResult<GhToolDetails>> {
1893
+ const issue = requireNonEmpty(params.issue, "issue");
1894
+ const repo = normalizeOptionalString(params.repo);
1895
+ const includeComments = params.comments ?? true;
1896
+ const args = ["issue", "view", issue];
1897
+ appendRepoFlag(args, repo, issue);
1898
+ args.push("--json", (includeComments ? GH_ISSUE_FIELDS : GH_ISSUE_FIELDS_NO_COMMENTS).join(","));
1899
+
1900
+ const data = await git.github.json<GhIssueViewData>(session.cwd, args, signal, {
1901
+ repoProvided: Boolean(repo),
1902
+ });
1903
+ return buildTextResult(formatIssueView(data, { issue, repo, comments: includeComments }), data.url);
1904
+ }
1905
+
1906
+ async function executePrView(
1907
+ session: ToolSession,
1908
+ params: GithubInput,
1909
+ signal: AbortSignal | undefined,
1910
+ ): Promise<AgentToolResult<GhToolDetails>> {
1911
+ const pr = normalizeOptionalString(params.pr);
1912
+ const repo = normalizeOptionalString(params.repo);
1913
+ const includeComments = params.comments ?? true;
1914
+ const args = ["pr", "view"];
1915
+ if (pr) {
1916
+ args.push(pr);
1917
+ }
1918
+ appendRepoFlag(args, repo, pr);
1919
+ args.push("--json", (includeComments ? GH_PR_FIELDS : GH_PR_FIELDS_NO_COMMENTS).join(","));
1920
+
1921
+ const data = await git.github.json<GhPrViewData>(session.cwd, args, signal, {
1922
+ repoProvided: Boolean(repo),
1923
+ });
1924
+ const resolvedRepo = repo ?? parsePullRequestUrl(data.url).repo;
1925
+ if (includeComments && resolvedRepo && typeof data.number === "number") {
1926
+ data.reviewComments = await fetchPrReviewComments(session.cwd, resolvedRepo, data.number, signal);
1927
+ }
1928
+ return buildTextResult(formatPrView(data, { pr, repo, comments: includeComments }), data.url);
1929
+ }
1930
+
1931
+ async function executePrDiff(
1932
+ session: ToolSession,
1933
+ params: GithubInput,
1934
+ signal: AbortSignal | undefined,
1935
+ ): Promise<AgentToolResult<GhToolDetails>> {
1936
+ const pr = normalizeOptionalString(params.pr);
1937
+ const repo = normalizeOptionalString(params.repo);
1938
+ const args = ["pr", "diff"];
1939
+ if (pr) {
1940
+ args.push(pr);
1941
+ }
1942
+ appendRepoFlag(args, repo, pr);
1943
+ args.push("--color", "never");
1944
+ if (params.nameOnly) {
1945
+ args.push("--name-only");
1946
+ }
1947
+ for (const pattern of params.exclude ?? []) {
1948
+ const normalizedPattern = requireNonEmpty(pattern, "exclude pattern");
1949
+ args.push("--exclude", normalizedPattern);
1950
+ }
1951
+
1952
+ const output = await git.github.text(session.cwd, args, signal, {
1953
+ repoProvided: Boolean(repo),
1954
+ trimOutput: false,
1955
+ });
1956
+ const title = params.nameOnly ? "# Pull Request Files" : "# Pull Request Diff";
1957
+ const body = output.length > 0 ? output : params.nameOnly ? "No changed files." : "No diff output.";
1958
+ return buildTextResult(`${title}\n\n${body}`);
1959
+ }
1960
+
1961
+ async function executePrCheckout(
1962
+ session: ToolSession,
1963
+ params: GithubInput,
1964
+ signal: AbortSignal | undefined,
1965
+ ): Promise<AgentToolResult<GhToolDetails>> {
1966
+ const pr = normalizeOptionalString(params.pr);
1967
+ const repo = normalizeOptionalString(params.repo);
1968
+ const requestedBranch = normalizeOptionalString(params.branch);
1969
+ const requestedWorktree = normalizeOptionalString(params.worktree);
1970
+ const force = params.force ?? false;
1971
+ const args = ["pr", "view"];
1972
+ if (pr) {
1973
+ args.push(pr);
1974
+ }
1975
+ appendRepoFlag(args, repo, pr);
1976
+ args.push("--json", GH_PR_CHECKOUT_FIELDS.join(","));
1977
+
1978
+ const data = await git.github.json<GhPrViewData>(session.cwd, args, signal, {
1979
+ repoProvided: Boolean(repo),
1980
+ });
1981
+ const prNumber = data.number;
1982
+ if (typeof prNumber !== "number") {
1983
+ throw new ToolError("GitHub CLI did not return a pull request number.");
1984
+ }
1985
+
1986
+ const headRefName = requireNonEmpty(data.headRefName, "head branch");
1987
+ const headRefOid = requireNonEmpty(data.headRefOid, "head commit");
1988
+ const repoRoot = await requireGitRepoRoot(session.cwd, signal);
1989
+ const primaryRepoRoot = await requirePrimaryGitRepoRoot(repoRoot, signal);
1990
+ const localBranch = requestedBranch ?? `pr-${prNumber}`;
1991
+ const worktreePath = requestedWorktree
1992
+ ? path.resolve(session.cwd, requestedWorktree)
1993
+ : path.join(primaryRepoRoot, ".worktrees", localBranch);
1994
+ const existingWorktrees = await git.worktree.list(repoRoot, signal);
1995
+ const existingWorktree = existingWorktrees.find(entry => entry.branch === toLocalBranchRef(localBranch));
1996
+
1997
+ const remote = await ensurePrRemote(repoRoot, data, signal);
1998
+ await git.fetch(
1999
+ repoRoot,
2000
+ remote.name,
2001
+ `refs/heads/${headRefName}`,
2002
+ `refs/remotes/${remote.name}/${headRefName}`,
2003
+ signal,
2004
+ );
1985
2005
 
1986
- async execute(
1987
- _toolCallId: string,
1988
- params: GhPrViewInput,
1989
- signal?: AbortSignal,
1990
- _onUpdate?: AgentToolUpdateCallback<GhToolDetails>,
1991
- _context?: AgentToolContext,
1992
- ): Promise<AgentToolResult<GhToolDetails>> {
1993
- return untilAborted(signal, async () => {
1994
- const pr = normalizeOptionalString(params.pr);
1995
- const repo = normalizeOptionalString(params.repo);
1996
- const includeComments = params.comments ?? true;
1997
- const args = ["pr", "view"];
1998
- if (pr) {
1999
- args.push(pr);
2000
- }
2001
- appendRepoFlag(args, repo, pr);
2002
- args.push("--json", (includeComments ? GH_PR_FIELDS : GH_PR_FIELDS_NO_COMMENTS).join(","));
2006
+ if (!existingWorktree) {
2007
+ const localBranchRef = toLocalBranchRef(localBranch);
2008
+ const localBranchExists = await git.ref.exists(repoRoot, localBranchRef, signal);
2009
+ if (localBranchExists) {
2010
+ const existingOid = await git.ref.resolve(repoRoot, localBranchRef, signal);
2011
+ if (existingOid !== headRefOid) {
2012
+ if (!force) {
2013
+ throw new ToolError(
2014
+ `local branch ${localBranch} already exists at ${formatShortSha(existingOid ?? undefined) ?? existingOid ?? "unknown commit"}; pass force=true to reset it`,
2015
+ );
2016
+ }
2003
2017
 
2004
- const data = await git.github.json<GhPrViewData>(this.session.cwd, args, signal, {
2005
- repoProvided: Boolean(repo),
2006
- });
2007
- const resolvedRepo = repo ?? parsePullRequestUrl(data.url).repo;
2008
- if (includeComments && resolvedRepo && typeof data.number === "number") {
2009
- data.reviewComments = await fetchPrReviewComments(this.session.cwd, resolvedRepo, data.number, signal);
2018
+ await git.branch.force(repoRoot, localBranch, `refs/remotes/${remote.name}/${headRefName}`, signal);
2010
2019
  }
2011
- return buildTextResult(formatPrView(data, { pr, repo, comments: includeComments }), data.url);
2012
- });
2013
- }
2014
- }
2015
-
2016
- export class GhPrDiffTool implements AgentTool<typeof ghPrDiffSchema, GhToolDetails> {
2017
- readonly name = "gh_pr_diff";
2018
- readonly label = "GitHub PR Diff";
2019
- readonly description = prompt.render(ghPrDiffDescription);
2020
- readonly parameters = ghPrDiffSchema;
2021
- readonly strict = true;
2022
-
2023
- constructor(private readonly session: ToolSession) {}
2024
-
2025
- static createIf(session: ToolSession): GhPrDiffTool | null {
2026
- if (!git.github.available()) return null;
2027
- return new GhPrDiffTool(session);
2020
+ } else {
2021
+ await git.branch.create(repoRoot, localBranch, `refs/remotes/${remote.name}/${headRefName}`, signal);
2022
+ }
2028
2023
  }
2029
2024
 
2030
- async execute(
2031
- _toolCallId: string,
2032
- params: GhPrDiffInput,
2033
- signal?: AbortSignal,
2034
- _onUpdate?: AgentToolUpdateCallback<GhToolDetails>,
2035
- _context?: AgentToolContext,
2036
- ): Promise<AgentToolResult<GhToolDetails>> {
2037
- return untilAborted(signal, async () => {
2038
- const pr = normalizeOptionalString(params.pr);
2039
- const repo = normalizeOptionalString(params.repo);
2040
- const args = ["pr", "diff"];
2041
- if (pr) {
2042
- args.push(pr);
2043
- }
2044
- appendRepoFlag(args, repo, pr);
2045
- args.push("--color", "never");
2046
- if (params.nameOnly) {
2047
- args.push("--name-only");
2048
- }
2049
- for (const pattern of params.exclude ?? []) {
2050
- const normalizedPattern = requireNonEmpty(pattern, "exclude pattern");
2051
- args.push("--exclude", normalizedPattern);
2052
- }
2025
+ await git.config.setBranch(repoRoot, localBranch, "remote", remote.name, signal);
2026
+ await git.config.setBranch(repoRoot, localBranch, "merge", `refs/heads/${headRefName}`, signal);
2027
+ await git.config.setBranch(repoRoot, localBranch, "pushRemote", remote.name, signal);
2028
+ await git.config.setBranch(repoRoot, localBranch, "ompPrHeadRef", headRefName, signal);
2029
+ await git.config.setBranch(repoRoot, localBranch, "ompPrUrl", data.url ?? "", signal);
2030
+ await git.config.setBranch(
2031
+ repoRoot,
2032
+ localBranch,
2033
+ "ompPrIsCrossRepository",
2034
+ String(Boolean(data.isCrossRepository)),
2035
+ signal,
2036
+ );
2037
+ await git.config.setBranch(
2038
+ repoRoot,
2039
+ localBranch,
2040
+ "ompPrMaintainerCanModify",
2041
+ String(Boolean(data.maintainerCanModify)),
2042
+ signal,
2043
+ );
2053
2044
 
2054
- const output = await git.github.text(this.session.cwd, args, signal, {
2055
- repoProvided: Boolean(repo),
2056
- trimOutput: false,
2057
- });
2058
- const title = params.nameOnly ? "# Pull Request Files" : "# Pull Request Diff";
2059
- const body = output.length > 0 ? output : params.nameOnly ? "No changed files." : "No diff output.";
2060
- return buildTextResult(`${title}\n\n${body}`);
2061
- });
2062
- }
2045
+ const finalWorktreePath = existingWorktree?.path ?? worktreePath;
2046
+ if (!existingWorktree) {
2047
+ await ensureGitWorktreePathAvailable(finalWorktreePath, existingWorktrees);
2048
+ await fs.mkdir(path.dirname(finalWorktreePath), { recursive: true });
2049
+ await git.worktree.add(repoRoot, finalWorktreePath, localBranch, { signal });
2050
+ }
2051
+ const resolvedWorktreePath = await fs.realpath(finalWorktreePath);
2052
+
2053
+ return buildTextResult(
2054
+ formatPrCheckoutResult({
2055
+ data,
2056
+ localBranch,
2057
+ worktreePath: resolvedWorktreePath,
2058
+ remoteName: remote.name,
2059
+ remoteUrl: remote.url,
2060
+ reused: Boolean(existingWorktree),
2061
+ }),
2062
+ data.url,
2063
+ {
2064
+ repo: repo ?? data.headRepository?.nameWithOwner,
2065
+ branch: localBranch,
2066
+ worktreePath: resolvedWorktreePath,
2067
+ remote: remote.name,
2068
+ remoteBranch: headRefName,
2069
+ },
2070
+ );
2063
2071
  }
2064
2072
 
2065
- export class GhPrCheckoutTool implements AgentTool<typeof ghPrCheckoutSchema, GhToolDetails> {
2066
- readonly name = "gh_pr_checkout";
2067
- readonly label = "GitHub PR Checkout";
2068
- readonly description = prompt.render(ghPrCheckoutDescription);
2069
- readonly parameters = ghPrCheckoutSchema;
2070
- readonly strict = true;
2071
-
2072
- constructor(private readonly session: ToolSession) {}
2073
-
2074
- static createIf(session: ToolSession): GhPrCheckoutTool | null {
2075
- if (!git.github.available()) return null;
2076
- return new GhPrCheckoutTool(session);
2077
- }
2078
-
2079
- async execute(
2080
- _toolCallId: string,
2081
- params: GhPrCheckoutInput,
2082
- signal?: AbortSignal,
2083
- _onUpdate?: AgentToolUpdateCallback<GhToolDetails>,
2084
- _context?: AgentToolContext,
2085
- ): Promise<AgentToolResult<GhToolDetails>> {
2086
- return untilAborted(signal, async () => {
2087
- const pr = normalizeOptionalString(params.pr);
2088
- const repo = normalizeOptionalString(params.repo);
2089
- const requestedBranch = normalizeOptionalString(params.branch);
2090
- const requestedWorktree = normalizeOptionalString(params.worktree);
2091
- const force = params.force ?? false;
2092
- const args = ["pr", "view"];
2093
- if (pr) {
2094
- args.push(pr);
2095
- }
2096
- appendRepoFlag(args, repo, pr);
2097
- args.push("--json", GH_PR_CHECKOUT_FIELDS.join(","));
2073
+ async function executePrPush(
2074
+ session: ToolSession,
2075
+ params: GithubInput,
2076
+ signal: AbortSignal | undefined,
2077
+ ): Promise<AgentToolResult<GhToolDetails>> {
2078
+ const repoRoot = await requireGitRepoRoot(session.cwd, signal);
2079
+ const localBranch = normalizeOptionalString(params.branch) ?? (await requireCurrentGitBranch(repoRoot, signal));
2080
+ const refExists = await git.ref.exists(repoRoot, toLocalBranchRef(localBranch), signal);
2081
+ if (!refExists) {
2082
+ throw new ToolError(`local branch ${localBranch} does not exist`);
2083
+ }
2084
+
2085
+ const target = await resolvePrBranchPushTarget(repoRoot, localBranch, signal);
2086
+ const currentBranch = await git.branch.current(repoRoot, signal);
2087
+ const sourceRef = currentBranch === localBranch ? "HEAD" : toLocalBranchRef(localBranch);
2088
+ const refspec = `${sourceRef}:refs/heads/${target.remoteBranch}`;
2089
+ await git.push(repoRoot, {
2090
+ forceWithLease: params.forceWithLease,
2091
+ refspec,
2092
+ remote: target.remoteName,
2093
+ signal,
2094
+ });
2095
+
2096
+ return buildTextResult(
2097
+ formatPrPushResult({
2098
+ localBranch,
2099
+ remoteName: target.remoteName,
2100
+ remoteBranch: target.remoteBranch,
2101
+ remoteUrl: target.remoteUrl,
2102
+ prUrl: target.prUrl,
2103
+ forceWithLease: params.forceWithLease ?? false,
2104
+ }),
2105
+ target.prUrl,
2106
+ {
2107
+ branch: localBranch,
2108
+ remote: target.remoteName,
2109
+ remoteBranch: target.remoteBranch,
2110
+ },
2111
+ );
2112
+ }
2098
2113
 
2099
- const data = await git.github.json<GhPrViewData>(this.session.cwd, args, signal, {
2100
- repoProvided: Boolean(repo),
2114
+ async function executeSearchIssues(
2115
+ session: ToolSession,
2116
+ params: GithubInput,
2117
+ signal: AbortSignal | undefined,
2118
+ ): Promise<AgentToolResult<GhToolDetails>> {
2119
+ const query = requireNonEmpty(params.query, "query");
2120
+ const repo = normalizeOptionalString(params.repo);
2121
+ const limit = resolveSearchLimit(params.limit);
2122
+ const args = buildGhSearchArgs("issues", query, limit, repo);
2123
+
2124
+ const items = await git.github.json<GhSearchResult[]>(session.cwd, args, signal, {
2125
+ repoProvided: Boolean(repo),
2126
+ });
2127
+ return buildTextResult(formatSearchResults("issues", query, repo, items));
2128
+ }
2129
+
2130
+ async function executeSearchPrs(
2131
+ session: ToolSession,
2132
+ params: GithubInput,
2133
+ signal: AbortSignal | undefined,
2134
+ ): Promise<AgentToolResult<GhToolDetails>> {
2135
+ const query = requireNonEmpty(params.query, "query");
2136
+ const repo = normalizeOptionalString(params.repo);
2137
+ const limit = resolveSearchLimit(params.limit);
2138
+ const args = buildGhSearchArgs("prs", query, limit, repo);
2139
+
2140
+ const items = await git.github.json<GhSearchResult[]>(session.cwd, args, signal, {
2141
+ repoProvided: Boolean(repo),
2142
+ });
2143
+ return buildTextResult(formatSearchResults("pull requests", query, repo, items));
2144
+ }
2145
+
2146
+ async function executeRunWatch(
2147
+ session: ToolSession,
2148
+ toolName: string,
2149
+ params: GithubInput,
2150
+ signal: AbortSignal | undefined,
2151
+ onUpdate: AgentToolUpdateCallback<GhToolDetails> | undefined,
2152
+ ): Promise<AgentToolResult<GhToolDetails>> {
2153
+ const branchInput = normalizeOptionalString(params.branch);
2154
+ const runReference = parseRunReference(params.run);
2155
+ const repo = await resolveGitHubRepo(session.cwd, undefined, runReference.repo, signal);
2156
+ const intervalSeconds = RUN_WATCH_INTERVAL_DEFAULT;
2157
+ const graceSeconds = RUN_WATCH_GRACE_DEFAULT;
2158
+ const tail = resolveTailLimit(params.tail);
2159
+ if (runReference.runId !== undefined) {
2160
+ const runId = runReference.runId;
2161
+ let pollCount = 0;
2162
+
2163
+ while (true) {
2164
+ throwIfAborted(signal);
2165
+ pollCount += 1;
2166
+
2167
+ let run = await fetchRunSnapshot(session.cwd, repo, runId, signal);
2168
+ const details = buildRunWatchDetails(repo, run, {
2169
+ state: "watching",
2170
+ pollCount,
2171
+ });
2172
+ onUpdate?.({
2173
+ content: [{ type: "text", text: formatRunWatchSnapshot(repo, run, pollCount) }],
2174
+ details,
2101
2175
  });
2102
- const prNumber = data.number;
2103
- if (typeof prNumber !== "number") {
2104
- throw new ToolError("GitHub CLI did not return a pull request number.");
2105
- }
2106
2176
 
2107
- const headRefName = requireNonEmpty(data.headRefName, "head branch");
2108
- const headRefOid = requireNonEmpty(data.headRefOid, "head commit");
2109
- const repoRoot = await requireGitRepoRoot(this.session.cwd, signal);
2110
- const primaryRepoRoot = await requirePrimaryGitRepoRoot(repoRoot, signal);
2111
- const localBranch = requestedBranch ?? `pr-${prNumber}`;
2112
- const worktreePath = requestedWorktree
2113
- ? path.resolve(this.session.cwd, requestedWorktree)
2114
- : path.join(primaryRepoRoot, ".worktrees", localBranch);
2115
- const existingWorktrees = await git.worktree.list(repoRoot, signal);
2116
- const existingWorktree = existingWorktrees.find(entry => entry.branch === toLocalBranchRef(localBranch));
2117
-
2118
- const remote = await ensurePrRemote(repoRoot, data, signal);
2119
- await git.fetch(
2120
- repoRoot,
2121
- remote.name,
2122
- `refs/heads/${headRefName}`,
2123
- `refs/remotes/${remote.name}/${headRefName}`,
2124
- signal,
2125
- );
2177
+ const failedJobs = run.jobs.filter(isFailedJob);
2178
+ const runCompleted = run.status === "completed";
2126
2179
 
2127
- if (!existingWorktree) {
2128
- const localBranchRef = toLocalBranchRef(localBranch);
2129
- const localBranchExists = await git.ref.exists(repoRoot, localBranchRef, signal);
2130
- if (localBranchExists) {
2131
- const existingOid = await git.ref.resolve(repoRoot, localBranchRef, signal);
2132
- if (existingOid !== headRefOid) {
2133
- if (!force) {
2134
- throw new ToolError(
2135
- `local branch ${localBranch} already exists at ${formatShortSha(existingOid ?? undefined) ?? existingOid ?? "unknown commit"}; pass force=true to reset it`,
2136
- );
2137
- }
2138
-
2139
- await git.branch.force(repoRoot, localBranch, `refs/remotes/${remote.name}/${headRefName}`, signal);
2140
- }
2141
- } else {
2142
- await git.branch.create(repoRoot, localBranch, `refs/remotes/${remote.name}/${headRefName}`, signal);
2180
+ if (failedJobs.length > 0) {
2181
+ if (!runCompleted && graceSeconds > 0) {
2182
+ const note = `Failure detected. Waiting ${graceSeconds}s to capture concurrent failures before fetching logs.`;
2183
+ onUpdate?.({
2184
+ content: [
2185
+ {
2186
+ type: "text",
2187
+ text: formatRunWatchSnapshot(repo, run, pollCount, note),
2188
+ },
2189
+ ],
2190
+ details: buildRunWatchDetails(repo, run, {
2191
+ state: "watching",
2192
+ pollCount,
2193
+ note,
2194
+ }),
2195
+ });
2196
+ await abortableSleep(graceSeconds * 1000, signal);
2197
+ run = await fetchRunSnapshot(session.cwd, repo, runId, signal);
2143
2198
  }
2144
- }
2145
2199
 
2146
- await git.config.setBranch(repoRoot, localBranch, "remote", remote.name, signal);
2147
- await git.config.setBranch(repoRoot, localBranch, "merge", `refs/heads/${headRefName}`, signal);
2148
- await git.config.setBranch(repoRoot, localBranch, "pushRemote", remote.name, signal);
2149
- await git.config.setBranch(repoRoot, localBranch, "ompPrHeadRef", headRefName, signal);
2150
- await git.config.setBranch(repoRoot, localBranch, "ompPrUrl", data.url ?? "", signal);
2151
- await git.config.setBranch(
2152
- repoRoot,
2153
- localBranch,
2154
- "ompPrIsCrossRepository",
2155
- String(Boolean(data.isCrossRepository)),
2156
- signal,
2157
- );
2158
- await git.config.setBranch(
2159
- repoRoot,
2160
- localBranch,
2161
- "ompPrMaintainerCanModify",
2162
- String(Boolean(data.maintainerCanModify)),
2163
- signal,
2164
- );
2165
-
2166
- const finalWorktreePath = existingWorktree?.path ?? worktreePath;
2167
- if (!existingWorktree) {
2168
- await ensureGitWorktreePathAvailable(finalWorktreePath, existingWorktrees);
2169
- await fs.mkdir(path.dirname(finalWorktreePath), { recursive: true });
2170
- await git.worktree.add(repoRoot, finalWorktreePath, localBranch, { signal });
2200
+ const failedJobLogs = await fetchFailedJobLogs(
2201
+ session.cwd,
2202
+ repo,
2203
+ run.jobs.filter(isFailedJob).map(job => ({ run, job })),
2204
+ tail,
2205
+ signal,
2206
+ );
2207
+ const finalDetails = buildRunWatchDetails(repo, run, {
2208
+ state: "completed",
2209
+ failedJobLogs,
2210
+ });
2211
+ const artifactId = await saveArtifactText(
2212
+ session,
2213
+ toolName,
2214
+ formatRunWatchResult(repo, run, failedJobLogs, tail, { mode: "full" }),
2215
+ );
2216
+ return buildTextResult(
2217
+ formatRunWatchResult(repo, run, failedJobLogs, tail),
2218
+ run.url,
2219
+ { ...finalDetails, artifactId },
2220
+ { artifactId, artifactLabel: "Full failed-job logs" },
2221
+ );
2171
2222
  }
2172
- const resolvedWorktreePath = await fs.realpath(finalWorktreePath);
2173
-
2174
- return buildTextResult(
2175
- formatPrCheckoutResult({
2176
- data,
2177
- localBranch,
2178
- worktreePath: resolvedWorktreePath,
2179
- remoteName: remote.name,
2180
- remoteUrl: remote.url,
2181
- reused: Boolean(existingWorktree),
2182
- }),
2183
- data.url,
2184
- {
2185
- repo: repo ?? data.headRepository?.nameWithOwner,
2186
- branch: localBranch,
2187
- worktreePath: resolvedWorktreePath,
2188
- remote: remote.name,
2189
- remoteBranch: headRefName,
2190
- },
2191
- );
2192
- });
2193
- }
2194
- }
2195
-
2196
- export class GhPrPushTool implements AgentTool<typeof ghPrPushSchema, GhToolDetails> {
2197
- readonly name = "gh_pr_push";
2198
- readonly label = "GitHub PR Push";
2199
- readonly description = prompt.render(ghPrPushDescription);
2200
- readonly parameters = ghPrPushSchema;
2201
- readonly strict = true;
2202
-
2203
- constructor(private readonly session: ToolSession) {}
2204
2223
 
2205
- static createIf(session: ToolSession): GhPrPushTool | null {
2206
- if (!git.github.available()) return null;
2207
- return new GhPrPushTool(session);
2208
- }
2209
-
2210
- async execute(
2211
- _toolCallId: string,
2212
- params: GhPrPushInput,
2213
- signal?: AbortSignal,
2214
- _onUpdate?: AgentToolUpdateCallback<GhToolDetails>,
2215
- _context?: AgentToolContext,
2216
- ): Promise<AgentToolResult<GhToolDetails>> {
2217
- return untilAborted(signal, async () => {
2218
- const repoRoot = await requireGitRepoRoot(this.session.cwd, signal);
2219
- const localBranch =
2220
- normalizeOptionalString(params.branch) ?? (await requireCurrentGitBranch(repoRoot, signal));
2221
- const refExists = await git.ref.exists(repoRoot, toLocalBranchRef(localBranch), signal);
2222
- if (!refExists) {
2223
- throw new ToolError(`local branch ${localBranch} does not exist`);
2224
+ if (runCompleted) {
2225
+ const finalDetails = buildRunWatchDetails(repo, run, {
2226
+ state: "completed",
2227
+ });
2228
+ return buildTextResult(formatRunWatchResult(repo, run, [], tail), run.url, finalDetails);
2224
2229
  }
2225
2230
 
2226
- const target = await resolvePrBranchPushTarget(repoRoot, localBranch, signal);
2227
- const currentBranch = await git.branch.current(repoRoot, signal);
2228
- const sourceRef = currentBranch === localBranch ? "HEAD" : toLocalBranchRef(localBranch);
2229
- const refspec = `${sourceRef}:refs/heads/${target.remoteBranch}`;
2230
- await git.push(repoRoot, {
2231
- forceWithLease: params.forceWithLease,
2232
- refspec,
2233
- remote: target.remoteName,
2234
- signal,
2235
- });
2236
-
2237
- return buildTextResult(
2238
- formatPrPushResult({
2239
- localBranch,
2240
- remoteName: target.remoteName,
2241
- remoteBranch: target.remoteBranch,
2242
- remoteUrl: target.remoteUrl,
2243
- prUrl: target.prUrl,
2244
- forceWithLease: params.forceWithLease ?? false,
2245
- }),
2246
- target.prUrl,
2247
- {
2248
- branch: localBranch,
2249
- remote: target.remoteName,
2250
- remoteBranch: target.remoteBranch,
2251
- },
2252
- );
2253
- });
2231
+ await abortableSleep(intervalSeconds * 1000, signal);
2232
+ }
2254
2233
  }
2255
- }
2256
2234
 
2257
- export class GhSearchIssuesTool implements AgentTool<typeof ghSearchIssuesSchema, GhToolDetails> {
2258
- readonly name = "gh_search_issues";
2259
- readonly label = "GitHub Issue Search";
2260
- readonly description = prompt.render(ghSearchIssuesDescription);
2261
- readonly parameters = ghSearchIssuesSchema;
2262
- readonly strict = true;
2263
-
2264
- constructor(private readonly session: ToolSession) {}
2235
+ const branch = branchInput ?? (await requireCurrentGitBranch(session.cwd, signal));
2236
+ const headSha = branchInput
2237
+ ? await resolveGitHubBranchHead(session.cwd, repo, branch, signal)
2238
+ : await requireCurrentGitHead(session.cwd, signal);
2239
+ let pollCount = 0;
2240
+ let settledSuccessSignature: string | undefined;
2265
2241
 
2266
- static createIf(session: ToolSession): GhSearchIssuesTool | null {
2267
- if (!git.github.available()) return null;
2268
- return new GhSearchIssuesTool(session);
2269
- }
2270
-
2271
- async execute(
2272
- _toolCallId: string,
2273
- params: GhSearchIssuesInput,
2274
- signal?: AbortSignal,
2275
- _onUpdate?: AgentToolUpdateCallback<GhToolDetails>,
2276
- _context?: AgentToolContext,
2277
- ): Promise<AgentToolResult<GhToolDetails>> {
2278
- return untilAborted(signal, async () => {
2279
- const query = requireNonEmpty(params.query, "query");
2280
- const repo = normalizeOptionalString(params.repo);
2281
- const limit = resolveSearchLimit(params.limit);
2282
- const args = buildGhSearchArgs("issues", query, limit, repo);
2242
+ while (true) {
2243
+ throwIfAborted(signal);
2244
+ pollCount += 1;
2283
2245
 
2284
- const items = await git.github.json<GhSearchResult[]>(this.session.cwd, args, signal, {
2285
- repoProvided: Boolean(repo),
2286
- });
2287
- return buildTextResult(formatSearchResults("issues", query, repo, items));
2246
+ let runs = await fetchRunsForCommit(session.cwd, repo, headSha, branch, signal);
2247
+ const details = buildCommitRunWatchDetails(repo, headSha, branch, runs, {
2248
+ state: "watching",
2249
+ pollCount,
2288
2250
  });
2289
- }
2290
- }
2291
-
2292
- export class GhSearchPrsTool implements AgentTool<typeof ghSearchPrsSchema, GhToolDetails> {
2293
- readonly name = "gh_search_prs";
2294
- readonly label = "GitHub PR Search";
2295
- readonly description = prompt.render(ghSearchPrsDescription);
2296
- readonly parameters = ghSearchPrsSchema;
2297
- readonly strict = true;
2298
-
2299
- constructor(private readonly session: ToolSession) {}
2300
-
2301
- static createIf(session: ToolSession): GhSearchPrsTool | null {
2302
- if (!git.github.available()) return null;
2303
- return new GhSearchPrsTool(session);
2304
- }
2305
-
2306
- async execute(
2307
- _toolCallId: string,
2308
- params: GhSearchPrsInput,
2309
- signal?: AbortSignal,
2310
- _onUpdate?: AgentToolUpdateCallback<GhToolDetails>,
2311
- _context?: AgentToolContext,
2312
- ): Promise<AgentToolResult<GhToolDetails>> {
2313
- return untilAborted(signal, async () => {
2314
- const query = requireNonEmpty(params.query, "query");
2315
- const repo = normalizeOptionalString(params.repo);
2316
- const limit = resolveSearchLimit(params.limit);
2317
- const args = buildGhSearchArgs("prs", query, limit, repo);
2318
-
2319
- const items = await git.github.json<GhSearchResult[]>(this.session.cwd, args, signal, {
2320
- repoProvided: Boolean(repo),
2321
- });
2322
- return buildTextResult(formatSearchResults("pull requests", query, repo, items));
2251
+ onUpdate?.({
2252
+ content: [{ type: "text", text: formatCommitRunWatchSnapshot(repo, headSha, branch, runs, pollCount) }],
2253
+ details,
2323
2254
  });
2324
- }
2325
- }
2326
-
2327
- export class GhRunWatchTool implements AgentTool<typeof ghRunWatchSchema, GhToolDetails> {
2328
- readonly name = "gh_run_watch";
2329
- readonly label = "GitHub Run Watch";
2330
- readonly description = prompt.render(ghRunWatchDescription);
2331
- readonly parameters = ghRunWatchSchema;
2332
- readonly strict = true;
2333
2255
 
2334
- constructor(private readonly session: ToolSession) {}
2335
-
2336
- static createIf(session: ToolSession): GhRunWatchTool | null {
2337
- if (!git.github.available()) return null;
2338
- return new GhRunWatchTool(session);
2339
- }
2340
-
2341
- async execute(
2342
- _toolCallId: string,
2343
- params: GhRunWatchInput,
2344
- signal?: AbortSignal,
2345
- onUpdate?: AgentToolUpdateCallback<GhToolDetails>,
2346
- _context?: AgentToolContext,
2347
- ): Promise<AgentToolResult<GhToolDetails>> {
2348
- return untilAborted(signal, async () => {
2349
- const branchInput = normalizeOptionalString(params.branch);
2350
- const runReference = parseRunReference(params.run);
2351
- const repo = await resolveGitHubRepo(this.session.cwd, undefined, runReference.repo, signal);
2352
- const intervalSeconds = RUN_WATCH_INTERVAL_DEFAULT;
2353
- const graceSeconds = RUN_WATCH_GRACE_DEFAULT;
2354
- const tail = resolveTailLimit(params.tail);
2355
- if (runReference.runId !== undefined) {
2356
- const runId = runReference.runId;
2357
- let pollCount = 0;
2358
-
2359
- while (true) {
2360
- throwIfAborted(signal);
2361
- pollCount += 1;
2362
-
2363
- let run = await fetchRunSnapshot(this.session.cwd, repo, runId, signal);
2364
- const details = buildRunWatchDetails(repo, run, {
2256
+ const outcome = getRunCollectionOutcome(runs);
2257
+ if (outcome === "failure") {
2258
+ if (graceSeconds > 0) {
2259
+ const note = `Failure detected. Waiting ${graceSeconds}s to capture concurrent failures before fetching logs.`;
2260
+ onUpdate?.({
2261
+ content: [
2262
+ {
2263
+ type: "text",
2264
+ text: formatCommitRunWatchSnapshot(repo, headSha, branch, runs, pollCount, note),
2265
+ },
2266
+ ],
2267
+ details: buildCommitRunWatchDetails(repo, headSha, branch, runs, {
2365
2268
  state: "watching",
2366
2269
  pollCount,
2367
- });
2368
- onUpdate?.({
2369
- content: [{ type: "text", text: formatRunWatchSnapshot(repo, run, pollCount) }],
2370
- details,
2371
- });
2372
-
2373
- const failedJobs = run.jobs.filter(isFailedJob);
2374
- const runCompleted = run.status === "completed";
2375
-
2376
- if (failedJobs.length > 0) {
2377
- if (!runCompleted && graceSeconds > 0) {
2378
- const note = `Failure detected. Waiting ${graceSeconds}s to capture concurrent failures before fetching logs.`;
2379
- onUpdate?.({
2380
- content: [
2381
- {
2382
- type: "text",
2383
- text: formatRunWatchSnapshot(repo, run, pollCount, note),
2384
- },
2385
- ],
2386
- details: buildRunWatchDetails(repo, run, {
2387
- state: "watching",
2388
- pollCount,
2389
- note,
2390
- }),
2391
- });
2392
- await abortableSleep(graceSeconds * 1000, signal);
2393
- run = await fetchRunSnapshot(this.session.cwd, repo, runId, signal);
2394
- }
2395
-
2396
- const failedJobLogs = await fetchFailedJobLogs(
2397
- this.session.cwd,
2398
- repo,
2399
- run.jobs.filter(isFailedJob).map(job => ({ run, job })),
2400
- tail,
2401
- signal,
2402
- );
2403
- const finalDetails = buildRunWatchDetails(repo, run, {
2404
- state: "completed",
2405
- failedJobLogs,
2406
- });
2407
- const artifactId = await saveArtifactText(
2408
- this.session,
2409
- this.name,
2410
- formatRunWatchResult(repo, run, failedJobLogs, tail, { mode: "full" }),
2411
- );
2412
- return buildTextResult(
2413
- formatRunWatchResult(repo, run, failedJobLogs, tail),
2414
- run.url,
2415
- { ...finalDetails, artifactId },
2416
- { artifactId, artifactLabel: "Full failed-job logs" },
2417
- );
2418
- }
2419
-
2420
- if (runCompleted) {
2421
- const finalDetails = buildRunWatchDetails(repo, run, {
2422
- state: "completed",
2423
- });
2424
- return buildTextResult(formatRunWatchResult(repo, run, [], tail), run.url, finalDetails);
2425
- }
2426
-
2427
- await abortableSleep(intervalSeconds * 1000, signal);
2428
- }
2270
+ note,
2271
+ }),
2272
+ });
2273
+ await abortableSleep(graceSeconds * 1000, signal);
2274
+ runs = await fetchRunsForCommit(session.cwd, repo, headSha, branch, signal);
2429
2275
  }
2430
2276
 
2431
- const branch = branchInput ?? (await requireCurrentGitBranch(this.session.cwd, signal));
2432
- const headSha = branchInput
2433
- ? await resolveGitHubBranchHead(this.session.cwd, repo, branch, signal)
2434
- : await requireCurrentGitHead(this.session.cwd, signal);
2435
- let pollCount = 0;
2436
- let settledSuccessSignature: string | undefined;
2277
+ const failedJobLogs = await fetchFailedJobLogs(
2278
+ session.cwd,
2279
+ repo,
2280
+ runs.flatMap(run => run.jobs.filter(isFailedJob).map(job => ({ run, job }))),
2281
+ tail,
2282
+ signal,
2283
+ );
2284
+ const finalDetails = buildCommitRunWatchDetails(repo, headSha, branch, runs, {
2285
+ state: "completed",
2286
+ failedJobLogs,
2287
+ });
2288
+ const artifactId = await saveArtifactText(
2289
+ session,
2290
+ toolName,
2291
+ formatCommitRunWatchResult(repo, headSha, branch, runs, failedJobLogs, tail, { mode: "full" }),
2292
+ );
2293
+ return buildTextResult(
2294
+ formatCommitRunWatchResult(repo, headSha, branch, runs, failedJobLogs, tail),
2295
+ undefined,
2296
+ { ...finalDetails, artifactId },
2297
+ { artifactId, artifactLabel: "Full failed-job logs" },
2298
+ );
2299
+ }
2437
2300
 
2438
- while (true) {
2439
- throwIfAborted(signal);
2440
- pollCount += 1;
2301
+ if (outcome === "success") {
2302
+ const signature = getRunCollectionSignature(runs);
2303
+ if (signature === settledSuccessSignature) {
2304
+ const finalDetails = buildCommitRunWatchDetails(repo, headSha, branch, runs, {
2305
+ state: "completed",
2306
+ });
2307
+ return buildTextResult(
2308
+ formatCommitRunWatchResult(repo, headSha, branch, runs, [], tail),
2309
+ undefined,
2310
+ finalDetails,
2311
+ );
2312
+ }
2441
2313
 
2442
- let runs = await fetchRunsForCommit(this.session.cwd, repo, headSha, branch, signal);
2443
- const details = buildCommitRunWatchDetails(repo, headSha, branch, runs, {
2314
+ settledSuccessSignature = signature;
2315
+ const note = `All known workflow runs completed successfully. Waiting ${intervalSeconds}s to ensure no additional runs appear for this commit.`;
2316
+ onUpdate?.({
2317
+ content: [
2318
+ {
2319
+ type: "text",
2320
+ text: formatCommitRunWatchSnapshot(repo, headSha, branch, runs, pollCount, note),
2321
+ },
2322
+ ],
2323
+ details: buildCommitRunWatchDetails(repo, headSha, branch, runs, {
2444
2324
  state: "watching",
2445
2325
  pollCount,
2446
- });
2447
- onUpdate?.({
2448
- content: [{ type: "text", text: formatCommitRunWatchSnapshot(repo, headSha, branch, runs, pollCount) }],
2449
- details,
2450
- });
2451
-
2452
- const outcome = getRunCollectionOutcome(runs);
2453
- if (outcome === "failure") {
2454
- if (graceSeconds > 0) {
2455
- const note = `Failure detected. Waiting ${graceSeconds}s to capture concurrent failures before fetching logs.`;
2456
- onUpdate?.({
2457
- content: [
2458
- {
2459
- type: "text",
2460
- text: formatCommitRunWatchSnapshot(repo, headSha, branch, runs, pollCount, note),
2461
- },
2462
- ],
2463
- details: buildCommitRunWatchDetails(repo, headSha, branch, runs, {
2464
- state: "watching",
2465
- pollCount,
2466
- note,
2467
- }),
2468
- });
2469
- await abortableSleep(graceSeconds * 1000, signal);
2470
- runs = await fetchRunsForCommit(this.session.cwd, repo, headSha, branch, signal);
2471
- }
2472
-
2473
- const failedJobLogs = await fetchFailedJobLogs(
2474
- this.session.cwd,
2475
- repo,
2476
- runs.flatMap(run => run.jobs.filter(isFailedJob).map(job => ({ run, job }))),
2477
- tail,
2478
- signal,
2479
- );
2480
- const finalDetails = buildCommitRunWatchDetails(repo, headSha, branch, runs, {
2481
- state: "completed",
2482
- failedJobLogs,
2483
- });
2484
- const artifactId = await saveArtifactText(
2485
- this.session,
2486
- this.name,
2487
- formatCommitRunWatchResult(repo, headSha, branch, runs, failedJobLogs, tail, { mode: "full" }),
2488
- );
2489
- return buildTextResult(
2490
- formatCommitRunWatchResult(repo, headSha, branch, runs, failedJobLogs, tail),
2491
- undefined,
2492
- { ...finalDetails, artifactId },
2493
- { artifactId, artifactLabel: "Full failed-job logs" },
2494
- );
2495
- }
2496
-
2497
- if (outcome === "success") {
2498
- const signature = getRunCollectionSignature(runs);
2499
- if (signature === settledSuccessSignature) {
2500
- const finalDetails = buildCommitRunWatchDetails(repo, headSha, branch, runs, {
2501
- state: "completed",
2502
- });
2503
- return buildTextResult(
2504
- formatCommitRunWatchResult(repo, headSha, branch, runs, [], tail),
2505
- undefined,
2506
- finalDetails,
2507
- );
2508
- }
2509
-
2510
- settledSuccessSignature = signature;
2511
- const note = `All known workflow runs completed successfully. Waiting ${intervalSeconds}s to ensure no additional runs appear for this commit.`;
2512
- onUpdate?.({
2513
- content: [
2514
- {
2515
- type: "text",
2516
- text: formatCommitRunWatchSnapshot(repo, headSha, branch, runs, pollCount, note),
2517
- },
2518
- ],
2519
- details: buildCommitRunWatchDetails(repo, headSha, branch, runs, {
2520
- state: "watching",
2521
- pollCount,
2522
- note,
2523
- }),
2524
- });
2525
- await abortableSleep(intervalSeconds * 1000, signal);
2526
- continue;
2527
- }
2326
+ note,
2327
+ }),
2328
+ });
2329
+ await abortableSleep(intervalSeconds * 1000, signal);
2330
+ continue;
2331
+ }
2528
2332
 
2529
- settledSuccessSignature = undefined;
2530
- await abortableSleep(intervalSeconds * 1000, signal);
2531
- }
2532
- });
2333
+ settledSuccessSignature = undefined;
2334
+ await abortableSleep(intervalSeconds * 1000, signal);
2533
2335
  }
2534
2336
  }