@oh-my-pi/pi-coding-agent 14.3.0 → 14.4.1

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 (120) hide show
  1. package/CHANGELOG.md +98 -1
  2. package/package.json +7 -7
  3. package/src/autoresearch/prompt.md +1 -1
  4. package/src/commit/agentic/prompts/analyze-file.md +1 -1
  5. package/src/config/model-registry.ts +67 -15
  6. package/src/config/prompt-templates.ts +5 -5
  7. package/src/config/settings-schema.ts +4 -4
  8. package/src/cursor.ts +3 -8
  9. package/src/discovery/helpers.ts +3 -3
  10. package/src/edit/diff.ts +50 -47
  11. package/src/edit/index.ts +86 -57
  12. package/src/edit/line-hash.ts +743 -24
  13. package/src/edit/modes/apply-patch.ts +0 -9
  14. package/src/edit/modes/atom.ts +893 -0
  15. package/src/edit/modes/chunk.ts +14 -24
  16. package/src/edit/modes/hashline.ts +193 -146
  17. package/src/edit/modes/patch.ts +5 -9
  18. package/src/edit/modes/replace.ts +6 -11
  19. package/src/edit/renderer.ts +14 -10
  20. package/src/edit/streaming.ts +50 -16
  21. package/src/exec/bash-executor.ts +2 -4
  22. package/src/export/html/template.generated.ts +1 -1
  23. package/src/export/html/template.js +4 -12
  24. package/src/extensibility/custom-tools/types.ts +2 -0
  25. package/src/extensibility/custom-tools/wrapper.ts +2 -1
  26. package/src/internal-urls/docs-index.generated.ts +2 -2
  27. package/src/lsp/defaults.json +142 -652
  28. package/src/lsp/index.ts +1 -1
  29. package/src/mcp/render.ts +1 -8
  30. package/src/modes/components/assistant-message.ts +4 -0
  31. package/src/modes/components/diff.ts +23 -14
  32. package/src/modes/components/footer.ts +21 -16
  33. package/src/modes/components/session-selector.ts +3 -3
  34. package/src/modes/components/settings-defs.ts +6 -1
  35. package/src/modes/components/todo-reminder.ts +1 -8
  36. package/src/modes/components/tool-execution.ts +1 -4
  37. package/src/modes/controllers/selector-controller.ts +1 -1
  38. package/src/modes/print-mode.ts +8 -0
  39. package/src/prompts/agents/librarian.md +1 -1
  40. package/src/prompts/agents/reviewer.md +4 -4
  41. package/src/prompts/ci-green-request.md +1 -1
  42. package/src/prompts/review-request.md +1 -1
  43. package/src/prompts/system/subagent-system-prompt.md +3 -3
  44. package/src/prompts/system/subagent-yield-reminder.md +11 -0
  45. package/src/prompts/system/system-prompt.md +3 -0
  46. package/src/prompts/tools/ask.md +3 -2
  47. package/src/prompts/tools/ast-edit.md +16 -20
  48. package/src/prompts/tools/ast-grep.md +19 -24
  49. package/src/prompts/tools/atom.md +87 -0
  50. package/src/prompts/tools/chunk-edit.md +37 -161
  51. package/src/prompts/tools/debug.md +4 -5
  52. package/src/prompts/tools/exit-plan-mode.md +4 -5
  53. package/src/prompts/tools/find.md +4 -8
  54. package/src/prompts/tools/github.md +18 -0
  55. package/src/prompts/tools/grep.md +4 -5
  56. package/src/prompts/tools/hashline.md +22 -89
  57. package/src/prompts/tools/{gemini-image.md → image-gen.md} +1 -1
  58. package/src/prompts/tools/inspect-image.md +6 -6
  59. package/src/prompts/tools/lsp.md +1 -1
  60. package/src/prompts/tools/patch.md +12 -19
  61. package/src/prompts/tools/python.md +3 -2
  62. package/src/prompts/tools/read-chunk.md +2 -3
  63. package/src/prompts/tools/read.md +2 -2
  64. package/src/prompts/tools/ssh.md +8 -17
  65. package/src/prompts/tools/todo-write.md +54 -41
  66. package/src/sdk.ts +14 -9
  67. package/src/session/agent-session.ts +25 -2
  68. package/src/session/session-manager.ts +4 -1
  69. package/src/task/executor.ts +43 -48
  70. package/src/task/render.ts +11 -13
  71. package/src/tools/ask.ts +7 -7
  72. package/src/tools/ast-edit.ts +45 -41
  73. package/src/tools/ast-grep.ts +77 -85
  74. package/src/tools/bash.ts +8 -9
  75. package/src/tools/browser.ts +32 -30
  76. package/src/tools/calculator.ts +4 -4
  77. package/src/tools/cancel-job.ts +1 -1
  78. package/src/tools/checkpoint.ts +2 -2
  79. package/src/tools/debug.ts +41 -37
  80. package/src/tools/exit-plan-mode.ts +1 -1
  81. package/src/tools/find.ts +4 -4
  82. package/src/tools/gh-renderer.ts +12 -4
  83. package/src/tools/gh.ts +509 -697
  84. package/src/tools/grep.ts +116 -131
  85. package/src/tools/{gemini-image.ts → image-gen.ts} +459 -60
  86. package/src/tools/index.ts +14 -32
  87. package/src/tools/inspect-image.ts +3 -3
  88. package/src/tools/json-tree.ts +114 -114
  89. package/src/tools/match-line-format.ts +8 -7
  90. package/src/tools/notebook.ts +8 -7
  91. package/src/tools/poll-tool.ts +2 -1
  92. package/src/tools/python.ts +9 -23
  93. package/src/tools/read.ts +32 -25
  94. package/src/tools/render-mermaid.ts +1 -1
  95. package/src/tools/render-utils.ts +18 -0
  96. package/src/tools/renderers.ts +2 -2
  97. package/src/tools/report-tool-issue.ts +3 -2
  98. package/src/tools/resolve.ts +1 -1
  99. package/src/tools/review.ts +12 -10
  100. package/src/tools/search-tool-bm25.ts +2 -4
  101. package/src/tools/ssh.ts +4 -4
  102. package/src/tools/todo-write.ts +172 -147
  103. package/src/tools/vim.ts +14 -15
  104. package/src/tools/write.ts +4 -4
  105. package/src/tools/{submit-result.ts → yield.ts} +11 -13
  106. package/src/utils/edit-mode.ts +2 -1
  107. package/src/utils/file-display-mode.ts +10 -5
  108. package/src/utils/git.ts +9 -5
  109. package/src/utils/shell-snapshot.ts +2 -3
  110. package/src/vim/render.ts +4 -4
  111. package/src/prompts/system/subagent-submit-reminder.md +0 -11
  112. package/src/prompts/tools/gh-issue-view.md +0 -11
  113. package/src/prompts/tools/gh-pr-checkout.md +0 -12
  114. package/src/prompts/tools/gh-pr-diff.md +0 -12
  115. package/src/prompts/tools/gh-pr-push.md +0 -12
  116. package/src/prompts/tools/gh-pr-view.md +0 -11
  117. package/src/prompts/tools/gh-repo-view.md +0 -11
  118. package/src/prompts/tools/gh-run-watch.md +0 -12
  119. package/src/prompts/tools/gh-search-issues.md +0 -11
  120. 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.",
193
- }),
194
- ),
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.",
160
+ description: "issue number or url (issue_view)",
161
+ examples: ["123", "https://github.com/owner/repo/issues/123"],
205
162
  }),
206
163
  ),
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;
@@ -757,7 +693,7 @@ async function resolvePrBranchPushTarget(
757
693
  }> {
758
694
  const headRef = await git.config.getBranch(repoRoot, localBranch, "ompPrHeadRef", signal);
759
695
  if (!headRef) {
760
- throw new ToolError(`branch ${localBranch} has no PR push metadata; check it out via gh_pr_checkout first`);
696
+ throw new ToolError(`branch ${localBranch} has no PR push metadata; check it out via op: pr_checkout first`);
761
697
  }
762
698
 
763
699
  const pushRemote = await git.config.getBranch(repoRoot, localBranch, "pushRemote", signal);
@@ -1638,7 +1574,7 @@ function formatReviewCommentsSection(comments: GhPrReviewComment[] | undefined):
1638
1574
  return lines;
1639
1575
  }
1640
1576
 
1641
- function formatRepoView(data: GhRepoViewData, input: GhRepoViewInput): string {
1577
+ function formatRepoView(data: GhRepoViewData, input: { repo?: string; branch?: string }): string {
1642
1578
  const lines: string[] = [];
1643
1579
  const name = data.nameWithOwner ?? input.repo ?? "GitHub Repository";
1644
1580
  lines.push(`# ${name}`);
@@ -1665,7 +1601,7 @@ function formatRepoView(data: GhRepoViewData, input: GhRepoViewInput): string {
1665
1601
  return lines.join("\n").trim();
1666
1602
  }
1667
1603
 
1668
- function formatIssueView(data: GhIssueViewData, input: GhIssueViewInput): string {
1604
+ function formatIssueView(data: GhIssueViewData, input: { issue: string; repo?: string; comments?: boolean }): string {
1669
1605
  const lines: string[] = [];
1670
1606
  const issueNumber = data.number ?? input.issue;
1671
1607
  lines.push(`# Issue #${issueNumber}: ${data.title ?? "Untitled"}`);
@@ -1711,7 +1647,7 @@ function formatPrFiles(files: GhPrFile[] | undefined): string[] {
1711
1647
  return lines;
1712
1648
  }
1713
1649
 
1714
- function formatPrView(data: GhPrViewData, input: GhPrViewInput): string {
1650
+ function formatPrView(data: GhPrViewData, input: { pr?: string; repo?: string; comments?: boolean }): string {
1715
1651
  const lines: string[] = [];
1716
1652
  const prIdentifier = data.number ?? input.pr ?? "current";
1717
1653
  lines.push(`# Pull Request #${prIdentifier}: ${data.title ?? "Untitled"}`);
@@ -1881,644 +1817,520 @@ function buildTextResult(
1881
1817
  return builder.done();
1882
1818
  }
1883
1819
 
1884
- export class GhRepoViewTool implements AgentTool<typeof ghRepoViewSchema, GhToolDetails> {
1885
- readonly name = "gh_repo_view";
1886
- readonly label = "GitHub Repo";
1887
- readonly description = prompt.render(ghRepoViewDescription);
1888
- 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;
1889
1825
  readonly strict = true;
1890
1826
 
1891
1827
  constructor(private readonly session: ToolSession) {}
1892
1828
 
1893
- static createIf(session: ToolSession): GhRepoViewTool | null {
1829
+ static createIf(session: ToolSession): GithubTool | null {
1894
1830
  if (!git.github.available()) return null;
1895
- return new GhRepoViewTool(session);
1831
+ return new GithubTool(session);
1896
1832
  }
1897
1833
 
1898
1834
  async execute(
1899
1835
  _toolCallId: string,
1900
- params: GhRepoViewInput,
1836
+ params: GithubInput,
1901
1837
  signal?: AbortSignal,
1902
- _onUpdate?: AgentToolUpdateCallback<GhToolDetails>,
1838
+ onUpdate?: AgentToolUpdateCallback<GhToolDetails>,
1903
1839
  _context?: AgentToolContext,
1904
1840
  ): Promise<AgentToolResult<GhToolDetails>> {
1905
1841
  return untilAborted(signal, async () => {
1906
- const repo = normalizeOptionalString(params.repo);
1907
- const branch = normalizeOptionalString(params.branch);
1908
- const args = ["repo", "view"];
1909
- if (repo) {
1910
- args.push(repo);
1911
- }
1912
- if (branch) {
1913
- 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);
1914
1861
  }
1915
- args.push("--json", GH_REPO_FIELDS.join(","));
1916
-
1917
- const data = await git.github.json<GhRepoViewData>(this.session.cwd, args, signal, {
1918
- repoProvided: Boolean(repo),
1919
- });
1920
- return buildTextResult(formatRepoView(data, { repo, branch }), data.url);
1921
- });
1922
- }
1923
- }
1924
-
1925
- export class GhIssueViewTool implements AgentTool<typeof ghIssueViewSchema, GhToolDetails> {
1926
- readonly name = "gh_issue_view";
1927
- readonly label = "GitHub Issue";
1928
- readonly description = prompt.render(ghIssueViewDescription);
1929
- readonly parameters = ghIssueViewSchema;
1930
- readonly strict = true;
1931
-
1932
- constructor(private readonly session: ToolSession) {}
1933
-
1934
- static createIf(session: ToolSession): GhIssueViewTool | null {
1935
- if (!git.github.available()) return null;
1936
- return new GhIssueViewTool(session);
1937
- }
1938
-
1939
- async execute(
1940
- _toolCallId: string,
1941
- params: GhIssueViewInput,
1942
- signal?: AbortSignal,
1943
- _onUpdate?: AgentToolUpdateCallback<GhToolDetails>,
1944
- _context?: AgentToolContext,
1945
- ): Promise<AgentToolResult<GhToolDetails>> {
1946
- return untilAborted(signal, async () => {
1947
- const issue = requireNonEmpty(params.issue, "issue");
1948
- const repo = normalizeOptionalString(params.repo);
1949
- const includeComments = params.comments ?? true;
1950
- const args = ["issue", "view", issue];
1951
- appendRepoFlag(args, repo, issue);
1952
- args.push("--json", (includeComments ? GH_ISSUE_FIELDS : GH_ISSUE_FIELDS_NO_COMMENTS).join(","));
1953
-
1954
- const data = await git.github.json<GhIssueViewData>(this.session.cwd, args, signal, {
1955
- repoProvided: Boolean(repo),
1956
- });
1957
- return buildTextResult(formatIssueView(data, { issue, repo, comments: includeComments }), data.url);
1958
1862
  });
1959
1863
  }
1960
1864
  }
1961
1865
 
1962
- export class GhPrViewTool implements AgentTool<typeof ghPrViewSchema, GhToolDetails> {
1963
- readonly name = "gh_pr_view";
1964
- readonly label = "GitHub PR";
1965
- readonly description = prompt.render(ghPrViewDescription);
1966
- readonly parameters = ghPrViewSchema;
1967
- readonly strict = true;
1968
-
1969
- constructor(private readonly session: ToolSession) {}
1970
-
1971
- static createIf(session: ToolSession): GhPrViewTool | null {
1972
- if (!git.github.available()) return null;
1973
- return new GhPrViewTool(session);
1974
- }
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
+ );
1975
2005
 
1976
- async execute(
1977
- _toolCallId: string,
1978
- params: GhPrViewInput,
1979
- signal?: AbortSignal,
1980
- _onUpdate?: AgentToolUpdateCallback<GhToolDetails>,
1981
- _context?: AgentToolContext,
1982
- ): Promise<AgentToolResult<GhToolDetails>> {
1983
- return untilAborted(signal, async () => {
1984
- const pr = normalizeOptionalString(params.pr);
1985
- const repo = normalizeOptionalString(params.repo);
1986
- const includeComments = params.comments ?? true;
1987
- const args = ["pr", "view"];
1988
- if (pr) {
1989
- args.push(pr);
1990
- }
1991
- appendRepoFlag(args, repo, pr);
1992
- 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
+ }
1993
2017
 
1994
- const data = await git.github.json<GhPrViewData>(this.session.cwd, args, signal, {
1995
- repoProvided: Boolean(repo),
1996
- });
1997
- const resolvedRepo = repo ?? parsePullRequestUrl(data.url).repo;
1998
- if (includeComments && resolvedRepo && typeof data.number === "number") {
1999
- data.reviewComments = await fetchPrReviewComments(this.session.cwd, resolvedRepo, data.number, signal);
2018
+ await git.branch.force(repoRoot, localBranch, `refs/remotes/${remote.name}/${headRefName}`, signal);
2000
2019
  }
2001
- return buildTextResult(formatPrView(data, { pr, repo, comments: includeComments }), data.url);
2002
- });
2003
- }
2004
- }
2005
-
2006
- export class GhPrDiffTool implements AgentTool<typeof ghPrDiffSchema, GhToolDetails> {
2007
- readonly name = "gh_pr_diff";
2008
- readonly label = "GitHub PR Diff";
2009
- readonly description = prompt.render(ghPrDiffDescription);
2010
- readonly parameters = ghPrDiffSchema;
2011
- readonly strict = true;
2012
-
2013
- constructor(private readonly session: ToolSession) {}
2014
-
2015
- static createIf(session: ToolSession): GhPrDiffTool | null {
2016
- if (!git.github.available()) return null;
2017
- return new GhPrDiffTool(session);
2020
+ } else {
2021
+ await git.branch.create(repoRoot, localBranch, `refs/remotes/${remote.name}/${headRefName}`, signal);
2022
+ }
2018
2023
  }
2019
2024
 
2020
- async execute(
2021
- _toolCallId: string,
2022
- params: GhPrDiffInput,
2023
- signal?: AbortSignal,
2024
- _onUpdate?: AgentToolUpdateCallback<GhToolDetails>,
2025
- _context?: AgentToolContext,
2026
- ): Promise<AgentToolResult<GhToolDetails>> {
2027
- return untilAborted(signal, async () => {
2028
- const pr = normalizeOptionalString(params.pr);
2029
- const repo = normalizeOptionalString(params.repo);
2030
- const args = ["pr", "diff"];
2031
- if (pr) {
2032
- args.push(pr);
2033
- }
2034
- appendRepoFlag(args, repo, pr);
2035
- args.push("--color", "never");
2036
- if (params.nameOnly) {
2037
- args.push("--name-only");
2038
- }
2039
- for (const pattern of params.exclude ?? []) {
2040
- const normalizedPattern = requireNonEmpty(pattern, "exclude pattern");
2041
- args.push("--exclude", normalizedPattern);
2042
- }
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
+ );
2043
2044
 
2044
- const output = await git.github.text(this.session.cwd, args, signal, {
2045
- repoProvided: Boolean(repo),
2046
- trimOutput: false,
2047
- });
2048
- const title = params.nameOnly ? "# Pull Request Files" : "# Pull Request Diff";
2049
- const body = output.length > 0 ? output : params.nameOnly ? "No changed files." : "No diff output.";
2050
- return buildTextResult(`${title}\n\n${body}`);
2051
- });
2052
- }
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
+ );
2053
2071
  }
2054
2072
 
2055
- export class GhPrCheckoutTool implements AgentTool<typeof ghPrCheckoutSchema, GhToolDetails> {
2056
- readonly name = "gh_pr_checkout";
2057
- readonly label = "GitHub PR Checkout";
2058
- readonly description = prompt.render(ghPrCheckoutDescription);
2059
- readonly parameters = ghPrCheckoutSchema;
2060
- readonly strict = true;
2061
-
2062
- constructor(private readonly session: ToolSession) {}
2063
-
2064
- static createIf(session: ToolSession): GhPrCheckoutTool | null {
2065
- if (!git.github.available()) return null;
2066
- return new GhPrCheckoutTool(session);
2067
- }
2068
-
2069
- async execute(
2070
- _toolCallId: string,
2071
- params: GhPrCheckoutInput,
2072
- signal?: AbortSignal,
2073
- _onUpdate?: AgentToolUpdateCallback<GhToolDetails>,
2074
- _context?: AgentToolContext,
2075
- ): Promise<AgentToolResult<GhToolDetails>> {
2076
- return untilAborted(signal, async () => {
2077
- const pr = normalizeOptionalString(params.pr);
2078
- const repo = normalizeOptionalString(params.repo);
2079
- const requestedBranch = normalizeOptionalString(params.branch);
2080
- const requestedWorktree = normalizeOptionalString(params.worktree);
2081
- const force = params.force ?? false;
2082
- const args = ["pr", "view"];
2083
- if (pr) {
2084
- args.push(pr);
2085
- }
2086
- appendRepoFlag(args, repo, pr);
2087
- 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
+ }
2088
2113
 
2089
- const data = await git.github.json<GhPrViewData>(this.session.cwd, args, signal, {
2090
- 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,
2091
2175
  });
2092
- const prNumber = data.number;
2093
- if (typeof prNumber !== "number") {
2094
- throw new ToolError("GitHub CLI did not return a pull request number.");
2095
- }
2096
2176
 
2097
- const headRefName = requireNonEmpty(data.headRefName, "head branch");
2098
- const headRefOid = requireNonEmpty(data.headRefOid, "head commit");
2099
- const repoRoot = await requireGitRepoRoot(this.session.cwd, signal);
2100
- const primaryRepoRoot = await requirePrimaryGitRepoRoot(repoRoot, signal);
2101
- const localBranch = requestedBranch ?? `pr-${prNumber}`;
2102
- const worktreePath = requestedWorktree
2103
- ? path.resolve(this.session.cwd, requestedWorktree)
2104
- : path.join(primaryRepoRoot, ".worktrees", localBranch);
2105
- const existingWorktrees = await git.worktree.list(repoRoot, signal);
2106
- const existingWorktree = existingWorktrees.find(entry => entry.branch === toLocalBranchRef(localBranch));
2107
-
2108
- const remote = await ensurePrRemote(repoRoot, data, signal);
2109
- await git.fetch(
2110
- repoRoot,
2111
- remote.name,
2112
- `refs/heads/${headRefName}`,
2113
- `refs/remotes/${remote.name}/${headRefName}`,
2114
- signal,
2115
- );
2177
+ const failedJobs = run.jobs.filter(isFailedJob);
2178
+ const runCompleted = run.status === "completed";
2116
2179
 
2117
- if (!existingWorktree) {
2118
- const localBranchRef = toLocalBranchRef(localBranch);
2119
- const localBranchExists = await git.ref.exists(repoRoot, localBranchRef, signal);
2120
- if (localBranchExists) {
2121
- const existingOid = await git.ref.resolve(repoRoot, localBranchRef, signal);
2122
- if (existingOid !== headRefOid) {
2123
- if (!force) {
2124
- throw new ToolError(
2125
- `local branch ${localBranch} already exists at ${formatShortSha(existingOid ?? undefined) ?? existingOid ?? "unknown commit"}; pass force=true to reset it`,
2126
- );
2127
- }
2128
-
2129
- await git.branch.force(repoRoot, localBranch, `refs/remotes/${remote.name}/${headRefName}`, signal);
2130
- }
2131
- } else {
2132
- 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);
2133
2198
  }
2134
- }
2135
-
2136
- await git.config.setBranch(repoRoot, localBranch, "remote", remote.name, signal);
2137
- await git.config.setBranch(repoRoot, localBranch, "merge", `refs/heads/${headRefName}`, signal);
2138
- await git.config.setBranch(repoRoot, localBranch, "pushRemote", remote.name, signal);
2139
- await git.config.setBranch(repoRoot, localBranch, "ompPrHeadRef", headRefName, signal);
2140
- await git.config.setBranch(repoRoot, localBranch, "ompPrUrl", data.url ?? "", signal);
2141
- await git.config.setBranch(
2142
- repoRoot,
2143
- localBranch,
2144
- "ompPrIsCrossRepository",
2145
- String(Boolean(data.isCrossRepository)),
2146
- signal,
2147
- );
2148
- await git.config.setBranch(
2149
- repoRoot,
2150
- localBranch,
2151
- "ompPrMaintainerCanModify",
2152
- String(Boolean(data.maintainerCanModify)),
2153
- signal,
2154
- );
2155
2199
 
2156
- const finalWorktreePath = existingWorktree?.path ?? worktreePath;
2157
- if (!existingWorktree) {
2158
- await ensureGitWorktreePathAvailable(finalWorktreePath, existingWorktrees);
2159
- await fs.mkdir(path.dirname(finalWorktreePath), { recursive: true });
2160
- 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
+ );
2161
2222
  }
2162
- const resolvedWorktreePath = await fs.realpath(finalWorktreePath);
2163
-
2164
- return buildTextResult(
2165
- formatPrCheckoutResult({
2166
- data,
2167
- localBranch,
2168
- worktreePath: resolvedWorktreePath,
2169
- remoteName: remote.name,
2170
- remoteUrl: remote.url,
2171
- reused: Boolean(existingWorktree),
2172
- }),
2173
- data.url,
2174
- {
2175
- repo: repo ?? data.headRepository?.nameWithOwner,
2176
- branch: localBranch,
2177
- worktreePath: resolvedWorktreePath,
2178
- remote: remote.name,
2179
- remoteBranch: headRefName,
2180
- },
2181
- );
2182
- });
2183
- }
2184
- }
2185
2223
 
2186
- export class GhPrPushTool implements AgentTool<typeof ghPrPushSchema, GhToolDetails> {
2187
- readonly name = "gh_pr_push";
2188
- readonly label = "GitHub PR Push";
2189
- readonly description = prompt.render(ghPrPushDescription);
2190
- readonly parameters = ghPrPushSchema;
2191
- readonly strict = true;
2192
-
2193
- constructor(private readonly session: ToolSession) {}
2194
-
2195
- static createIf(session: ToolSession): GhPrPushTool | null {
2196
- if (!git.github.available()) return null;
2197
- return new GhPrPushTool(session);
2198
- }
2199
-
2200
- async execute(
2201
- _toolCallId: string,
2202
- params: GhPrPushInput,
2203
- signal?: AbortSignal,
2204
- _onUpdate?: AgentToolUpdateCallback<GhToolDetails>,
2205
- _context?: AgentToolContext,
2206
- ): Promise<AgentToolResult<GhToolDetails>> {
2207
- return untilAborted(signal, async () => {
2208
- const repoRoot = await requireGitRepoRoot(this.session.cwd, signal);
2209
- const localBranch =
2210
- normalizeOptionalString(params.branch) ?? (await requireCurrentGitBranch(repoRoot, signal));
2211
- const refExists = await git.ref.exists(repoRoot, toLocalBranchRef(localBranch), signal);
2212
- if (!refExists) {
2213
- 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);
2214
2229
  }
2215
2230
 
2216
- const target = await resolvePrBranchPushTarget(repoRoot, localBranch, signal);
2217
- const currentBranch = await git.branch.current(repoRoot, signal);
2218
- const sourceRef = currentBranch === localBranch ? "HEAD" : toLocalBranchRef(localBranch);
2219
- const refspec = `${sourceRef}:refs/heads/${target.remoteBranch}`;
2220
- await git.push(repoRoot, {
2221
- forceWithLease: params.forceWithLease,
2222
- refspec,
2223
- remote: target.remoteName,
2224
- signal,
2225
- });
2226
-
2227
- return buildTextResult(
2228
- formatPrPushResult({
2229
- localBranch,
2230
- remoteName: target.remoteName,
2231
- remoteBranch: target.remoteBranch,
2232
- remoteUrl: target.remoteUrl,
2233
- prUrl: target.prUrl,
2234
- forceWithLease: params.forceWithLease ?? false,
2235
- }),
2236
- target.prUrl,
2237
- {
2238
- branch: localBranch,
2239
- remote: target.remoteName,
2240
- remoteBranch: target.remoteBranch,
2241
- },
2242
- );
2243
- });
2231
+ await abortableSleep(intervalSeconds * 1000, signal);
2232
+ }
2244
2233
  }
2245
- }
2246
2234
 
2247
- export class GhSearchIssuesTool implements AgentTool<typeof ghSearchIssuesSchema, GhToolDetails> {
2248
- readonly name = "gh_search_issues";
2249
- readonly label = "GitHub Issue Search";
2250
- readonly description = prompt.render(ghSearchIssuesDescription);
2251
- readonly parameters = ghSearchIssuesSchema;
2252
- readonly strict = true;
2253
-
2254
- 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;
2255
2241
 
2256
- static createIf(session: ToolSession): GhSearchIssuesTool | null {
2257
- if (!git.github.available()) return null;
2258
- return new GhSearchIssuesTool(session);
2259
- }
2260
-
2261
- async execute(
2262
- _toolCallId: string,
2263
- params: GhSearchIssuesInput,
2264
- signal?: AbortSignal,
2265
- _onUpdate?: AgentToolUpdateCallback<GhToolDetails>,
2266
- _context?: AgentToolContext,
2267
- ): Promise<AgentToolResult<GhToolDetails>> {
2268
- return untilAborted(signal, async () => {
2269
- const query = requireNonEmpty(params.query, "query");
2270
- const repo = normalizeOptionalString(params.repo);
2271
- const limit = resolveSearchLimit(params.limit);
2272
- const args = buildGhSearchArgs("issues", query, limit, repo);
2242
+ while (true) {
2243
+ throwIfAborted(signal);
2244
+ pollCount += 1;
2273
2245
 
2274
- const items = await git.github.json<GhSearchResult[]>(this.session.cwd, args, signal, {
2275
- repoProvided: Boolean(repo),
2276
- });
2277
- 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,
2278
2250
  });
2279
- }
2280
- }
2281
-
2282
- export class GhSearchPrsTool implements AgentTool<typeof ghSearchPrsSchema, GhToolDetails> {
2283
- readonly name = "gh_search_prs";
2284
- readonly label = "GitHub PR Search";
2285
- readonly description = prompt.render(ghSearchPrsDescription);
2286
- readonly parameters = ghSearchPrsSchema;
2287
- readonly strict = true;
2288
-
2289
- constructor(private readonly session: ToolSession) {}
2290
-
2291
- static createIf(session: ToolSession): GhSearchPrsTool | null {
2292
- if (!git.github.available()) return null;
2293
- return new GhSearchPrsTool(session);
2294
- }
2295
-
2296
- async execute(
2297
- _toolCallId: string,
2298
- params: GhSearchPrsInput,
2299
- signal?: AbortSignal,
2300
- _onUpdate?: AgentToolUpdateCallback<GhToolDetails>,
2301
- _context?: AgentToolContext,
2302
- ): Promise<AgentToolResult<GhToolDetails>> {
2303
- return untilAborted(signal, async () => {
2304
- const query = requireNonEmpty(params.query, "query");
2305
- const repo = normalizeOptionalString(params.repo);
2306
- const limit = resolveSearchLimit(params.limit);
2307
- const args = buildGhSearchArgs("prs", query, limit, repo);
2308
-
2309
- const items = await git.github.json<GhSearchResult[]>(this.session.cwd, args, signal, {
2310
- repoProvided: Boolean(repo),
2311
- });
2312
- return buildTextResult(formatSearchResults("pull requests", query, repo, items));
2251
+ onUpdate?.({
2252
+ content: [{ type: "text", text: formatCommitRunWatchSnapshot(repo, headSha, branch, runs, pollCount) }],
2253
+ details,
2313
2254
  });
2314
- }
2315
- }
2316
-
2317
- export class GhRunWatchTool implements AgentTool<typeof ghRunWatchSchema, GhToolDetails> {
2318
- readonly name = "gh_run_watch";
2319
- readonly label = "GitHub Run Watch";
2320
- readonly description = prompt.render(ghRunWatchDescription);
2321
- readonly parameters = ghRunWatchSchema;
2322
- readonly strict = true;
2323
-
2324
- constructor(private readonly session: ToolSession) {}
2325
-
2326
- static createIf(session: ToolSession): GhRunWatchTool | null {
2327
- if (!git.github.available()) return null;
2328
- return new GhRunWatchTool(session);
2329
- }
2330
2255
 
2331
- async execute(
2332
- _toolCallId: string,
2333
- params: GhRunWatchInput,
2334
- signal?: AbortSignal,
2335
- onUpdate?: AgentToolUpdateCallback<GhToolDetails>,
2336
- _context?: AgentToolContext,
2337
- ): Promise<AgentToolResult<GhToolDetails>> {
2338
- return untilAborted(signal, async () => {
2339
- const branchInput = normalizeOptionalString(params.branch);
2340
- const runReference = parseRunReference(params.run);
2341
- const repo = await resolveGitHubRepo(this.session.cwd, undefined, runReference.repo, signal);
2342
- const intervalSeconds = RUN_WATCH_INTERVAL_DEFAULT;
2343
- const graceSeconds = RUN_WATCH_GRACE_DEFAULT;
2344
- const tail = resolveTailLimit(params.tail);
2345
- if (runReference.runId !== undefined) {
2346
- const runId = runReference.runId;
2347
- let pollCount = 0;
2348
-
2349
- while (true) {
2350
- throwIfAborted(signal);
2351
- pollCount += 1;
2352
-
2353
- let run = await fetchRunSnapshot(this.session.cwd, repo, runId, signal);
2354
- 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, {
2355
2268
  state: "watching",
2356
2269
  pollCount,
2357
- });
2358
- onUpdate?.({
2359
- content: [{ type: "text", text: formatRunWatchSnapshot(repo, run, pollCount) }],
2360
- details,
2361
- });
2362
-
2363
- const failedJobs = run.jobs.filter(isFailedJob);
2364
- const runCompleted = run.status === "completed";
2365
-
2366
- if (failedJobs.length > 0) {
2367
- if (!runCompleted && graceSeconds > 0) {
2368
- const note = `Failure detected. Waiting ${graceSeconds}s to capture concurrent failures before fetching logs.`;
2369
- onUpdate?.({
2370
- content: [
2371
- {
2372
- type: "text",
2373
- text: formatRunWatchSnapshot(repo, run, pollCount, note),
2374
- },
2375
- ],
2376
- details: buildRunWatchDetails(repo, run, {
2377
- state: "watching",
2378
- pollCount,
2379
- note,
2380
- }),
2381
- });
2382
- await abortableSleep(graceSeconds * 1000, signal);
2383
- run = await fetchRunSnapshot(this.session.cwd, repo, runId, signal);
2384
- }
2385
-
2386
- const failedJobLogs = await fetchFailedJobLogs(
2387
- this.session.cwd,
2388
- repo,
2389
- run.jobs.filter(isFailedJob).map(job => ({ run, job })),
2390
- tail,
2391
- signal,
2392
- );
2393
- const finalDetails = buildRunWatchDetails(repo, run, {
2394
- state: "completed",
2395
- failedJobLogs,
2396
- });
2397
- const artifactId = await saveArtifactText(
2398
- this.session,
2399
- this.name,
2400
- formatRunWatchResult(repo, run, failedJobLogs, tail, { mode: "full" }),
2401
- );
2402
- return buildTextResult(
2403
- formatRunWatchResult(repo, run, failedJobLogs, tail),
2404
- run.url,
2405
- { ...finalDetails, artifactId },
2406
- { artifactId, artifactLabel: "Full failed-job logs" },
2407
- );
2408
- }
2409
-
2410
- if (runCompleted) {
2411
- const finalDetails = buildRunWatchDetails(repo, run, {
2412
- state: "completed",
2413
- });
2414
- return buildTextResult(formatRunWatchResult(repo, run, [], tail), run.url, finalDetails);
2415
- }
2416
-
2417
- await abortableSleep(intervalSeconds * 1000, signal);
2418
- }
2270
+ note,
2271
+ }),
2272
+ });
2273
+ await abortableSleep(graceSeconds * 1000, signal);
2274
+ runs = await fetchRunsForCommit(session.cwd, repo, headSha, branch, signal);
2419
2275
  }
2420
2276
 
2421
- const branch = branchInput ?? (await requireCurrentGitBranch(this.session.cwd, signal));
2422
- const headSha = branchInput
2423
- ? await resolveGitHubBranchHead(this.session.cwd, repo, branch, signal)
2424
- : await requireCurrentGitHead(this.session.cwd, signal);
2425
- let pollCount = 0;
2426
- 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
+ }
2427
2300
 
2428
- while (true) {
2429
- throwIfAborted(signal);
2430
- 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
+ }
2431
2313
 
2432
- let runs = await fetchRunsForCommit(this.session.cwd, repo, headSha, branch, signal);
2433
- 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, {
2434
2324
  state: "watching",
2435
2325
  pollCount,
2436
- });
2437
- onUpdate?.({
2438
- content: [{ type: "text", text: formatCommitRunWatchSnapshot(repo, headSha, branch, runs, pollCount) }],
2439
- details,
2440
- });
2441
-
2442
- const outcome = getRunCollectionOutcome(runs);
2443
- if (outcome === "failure") {
2444
- if (graceSeconds > 0) {
2445
- const note = `Failure detected. Waiting ${graceSeconds}s to capture concurrent failures before fetching logs.`;
2446
- onUpdate?.({
2447
- content: [
2448
- {
2449
- type: "text",
2450
- text: formatCommitRunWatchSnapshot(repo, headSha, branch, runs, pollCount, note),
2451
- },
2452
- ],
2453
- details: buildCommitRunWatchDetails(repo, headSha, branch, runs, {
2454
- state: "watching",
2455
- pollCount,
2456
- note,
2457
- }),
2458
- });
2459
- await abortableSleep(graceSeconds * 1000, signal);
2460
- runs = await fetchRunsForCommit(this.session.cwd, repo, headSha, branch, signal);
2461
- }
2462
-
2463
- const failedJobLogs = await fetchFailedJobLogs(
2464
- this.session.cwd,
2465
- repo,
2466
- runs.flatMap(run => run.jobs.filter(isFailedJob).map(job => ({ run, job }))),
2467
- tail,
2468
- signal,
2469
- );
2470
- const finalDetails = buildCommitRunWatchDetails(repo, headSha, branch, runs, {
2471
- state: "completed",
2472
- failedJobLogs,
2473
- });
2474
- const artifactId = await saveArtifactText(
2475
- this.session,
2476
- this.name,
2477
- formatCommitRunWatchResult(repo, headSha, branch, runs, failedJobLogs, tail, { mode: "full" }),
2478
- );
2479
- return buildTextResult(
2480
- formatCommitRunWatchResult(repo, headSha, branch, runs, failedJobLogs, tail),
2481
- undefined,
2482
- { ...finalDetails, artifactId },
2483
- { artifactId, artifactLabel: "Full failed-job logs" },
2484
- );
2485
- }
2486
-
2487
- if (outcome === "success") {
2488
- const signature = getRunCollectionSignature(runs);
2489
- if (signature === settledSuccessSignature) {
2490
- const finalDetails = buildCommitRunWatchDetails(repo, headSha, branch, runs, {
2491
- state: "completed",
2492
- });
2493
- return buildTextResult(
2494
- formatCommitRunWatchResult(repo, headSha, branch, runs, [], tail),
2495
- undefined,
2496
- finalDetails,
2497
- );
2498
- }
2499
-
2500
- settledSuccessSignature = signature;
2501
- const note = `All known workflow runs completed successfully. Waiting ${intervalSeconds}s to ensure no additional runs appear for this commit.`;
2502
- onUpdate?.({
2503
- content: [
2504
- {
2505
- type: "text",
2506
- text: formatCommitRunWatchSnapshot(repo, headSha, branch, runs, pollCount, note),
2507
- },
2508
- ],
2509
- details: buildCommitRunWatchDetails(repo, headSha, branch, runs, {
2510
- state: "watching",
2511
- pollCount,
2512
- note,
2513
- }),
2514
- });
2515
- await abortableSleep(intervalSeconds * 1000, signal);
2516
- continue;
2517
- }
2326
+ note,
2327
+ }),
2328
+ });
2329
+ await abortableSleep(intervalSeconds * 1000, signal);
2330
+ continue;
2331
+ }
2518
2332
 
2519
- settledSuccessSignature = undefined;
2520
- await abortableSleep(intervalSeconds * 1000, signal);
2521
- }
2522
- });
2333
+ settledSuccessSignature = undefined;
2334
+ await abortableSleep(intervalSeconds * 1000, signal);
2523
2335
  }
2524
2336
  }