@oh-my-pi/pi-coding-agent 13.18.0 → 14.0.2

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 (235) hide show
  1. package/CHANGELOG.md +316 -1
  2. package/package.json +86 -24
  3. package/scripts/format-prompts.ts +2 -2
  4. package/src/autoresearch/apply-contract-to-state.ts +24 -0
  5. package/src/autoresearch/contract.ts +0 -44
  6. package/src/autoresearch/dashboard.ts +1 -2
  7. package/src/autoresearch/git.ts +116 -30
  8. package/src/autoresearch/helpers.ts +49 -0
  9. package/src/autoresearch/index.ts +28 -187
  10. package/src/autoresearch/prompt.md +26 -9
  11. package/src/autoresearch/state.ts +0 -6
  12. package/src/autoresearch/tools/init-experiment.ts +202 -117
  13. package/src/autoresearch/tools/log-experiment.ts +123 -178
  14. package/src/autoresearch/tools/run-experiment.ts +48 -10
  15. package/src/autoresearch/types.ts +2 -2
  16. package/src/capability/index.ts +4 -2
  17. package/src/cli/file-processor.ts +3 -3
  18. package/src/cli/grep-cli.ts +8 -8
  19. package/src/cli/grievances-cli.ts +78 -0
  20. package/src/cli/read-cli.ts +67 -0
  21. package/src/cli/setup-cli.ts +4 -4
  22. package/src/cli/update-cli.ts +3 -3
  23. package/src/cli.ts +2 -0
  24. package/src/commands/grep.ts +6 -1
  25. package/src/commands/grievances.ts +20 -0
  26. package/src/commands/read.ts +33 -0
  27. package/src/commit/agentic/agent.ts +5 -8
  28. package/src/commit/agentic/index.ts +22 -26
  29. package/src/commit/agentic/tools/analyze-file.ts +3 -3
  30. package/src/commit/agentic/tools/git-file-diff.ts +3 -6
  31. package/src/commit/agentic/tools/git-hunk.ts +3 -3
  32. package/src/commit/agentic/tools/git-overview.ts +6 -9
  33. package/src/commit/agentic/tools/index.ts +6 -8
  34. package/src/commit/agentic/tools/propose-commit.ts +4 -7
  35. package/src/commit/agentic/tools/recent-commits.ts +3 -3
  36. package/src/commit/agentic/tools/split-commit.ts +4 -4
  37. package/src/commit/agentic/validation.ts +1 -1
  38. package/src/commit/analysis/conventional.ts +4 -4
  39. package/src/commit/analysis/summary.ts +3 -3
  40. package/src/commit/changelog/generate.ts +4 -4
  41. package/src/commit/changelog/index.ts +5 -9
  42. package/src/commit/map-reduce/map-phase.ts +4 -4
  43. package/src/commit/map-reduce/reduce-phase.ts +4 -4
  44. package/src/commit/pipeline.ts +13 -16
  45. package/src/config/keybindings.ts +7 -6
  46. package/src/config/prompt-templates.ts +44 -226
  47. package/src/config/resolve-config-value.ts +4 -2
  48. package/src/config/settings-schema.ts +98 -2
  49. package/src/config/settings.ts +25 -26
  50. package/src/dap/client.ts +674 -0
  51. package/src/dap/config.ts +150 -0
  52. package/src/dap/defaults.json +211 -0
  53. package/src/dap/index.ts +4 -0
  54. package/src/dap/session.ts +1255 -0
  55. package/src/dap/types.ts +600 -0
  56. package/src/debug/log-viewer.ts +3 -2
  57. package/src/discovery/builtin.ts +1 -2
  58. package/src/discovery/codex.ts +2 -2
  59. package/src/discovery/github.ts +2 -1
  60. package/src/discovery/helpers.ts +2 -2
  61. package/src/discovery/opencode.ts +2 -2
  62. package/src/edit/diff.ts +818 -0
  63. package/src/edit/index.ts +309 -0
  64. package/src/edit/line-hash.ts +67 -0
  65. package/src/edit/modes/chunk.ts +454 -0
  66. package/src/{patch → edit/modes}/hashline.ts +741 -361
  67. package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
  68. package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
  69. package/src/{patch → edit}/normalize.ts +97 -76
  70. package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
  71. package/src/exec/bash-executor.ts +4 -2
  72. package/src/exec/idle-timeout-watchdog.ts +126 -0
  73. package/src/exec/non-interactive-env.ts +5 -0
  74. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +6 -18
  75. package/src/extensibility/custom-commands/bundled/review/index.ts +45 -43
  76. package/src/extensibility/custom-commands/loader.ts +1 -2
  77. package/src/extensibility/custom-tools/loader.ts +34 -11
  78. package/src/extensibility/custom-tools/types.ts +1 -1
  79. package/src/extensibility/extensions/loader.ts +9 -4
  80. package/src/extensibility/extensions/runner.ts +24 -1
  81. package/src/extensibility/extensions/types.ts +4 -2
  82. package/src/extensibility/hooks/loader.ts +5 -6
  83. package/src/extensibility/hooks/types.ts +2 -2
  84. package/src/extensibility/plugins/doctor.ts +2 -1
  85. package/src/extensibility/plugins/marketplace/fetcher.ts +2 -57
  86. package/src/extensibility/plugins/marketplace/source-resolver.ts +4 -4
  87. package/src/extensibility/slash-commands.ts +3 -7
  88. package/src/index.ts +3 -1
  89. package/src/internal-urls/docs-index.generated.ts +11 -11
  90. package/src/ipy/executor.ts +58 -17
  91. package/src/ipy/gateway-coordinator.ts +6 -4
  92. package/src/ipy/kernel.ts +45 -22
  93. package/src/ipy/runtime.ts +2 -2
  94. package/src/lsp/client.ts +7 -4
  95. package/src/lsp/clients/lsp-linter-client.ts +4 -4
  96. package/src/lsp/config.ts +2 -2
  97. package/src/lsp/defaults.json +688 -154
  98. package/src/lsp/index.ts +234 -45
  99. package/src/lsp/lspmux.ts +2 -2
  100. package/src/lsp/startup-events.ts +13 -0
  101. package/src/lsp/types.ts +12 -1
  102. package/src/lsp/utils.ts +8 -1
  103. package/src/main.ts +125 -47
  104. package/src/memories/index.ts +4 -5
  105. package/src/modes/acp/acp-agent.ts +563 -163
  106. package/src/modes/acp/acp-event-mapper.ts +9 -1
  107. package/src/modes/acp/acp-mode.ts +4 -2
  108. package/src/modes/components/agent-dashboard.ts +3 -4
  109. package/src/modes/components/diff.ts +6 -7
  110. package/src/modes/components/footer.ts +9 -29
  111. package/src/modes/components/hook-editor.ts +3 -3
  112. package/src/modes/components/hook-selector.ts +6 -1
  113. package/src/modes/components/read-tool-group.ts +6 -12
  114. package/src/modes/components/session-observer-overlay.ts +472 -0
  115. package/src/modes/components/settings-defs.ts +24 -0
  116. package/src/modes/components/status-line.ts +15 -61
  117. package/src/modes/components/tool-execution.ts +1 -1
  118. package/src/modes/components/welcome.ts +1 -1
  119. package/src/modes/controllers/btw-controller.ts +2 -2
  120. package/src/modes/controllers/command-controller.ts +4 -2
  121. package/src/modes/controllers/event-controller.ts +59 -2
  122. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  123. package/src/modes/controllers/input-controller.ts +15 -8
  124. package/src/modes/controllers/selector-controller.ts +26 -0
  125. package/src/modes/index.ts +20 -2
  126. package/src/modes/interactive-mode.ts +278 -69
  127. package/src/modes/rpc/host-tools.ts +186 -0
  128. package/src/modes/rpc/rpc-client.ts +178 -13
  129. package/src/modes/rpc/rpc-mode.ts +73 -3
  130. package/src/modes/rpc/rpc-types.ts +53 -1
  131. package/src/modes/session-observer-registry.ts +146 -0
  132. package/src/modes/shared.ts +0 -42
  133. package/src/modes/theme/theme.ts +80 -8
  134. package/src/modes/types.ts +4 -2
  135. package/src/modes/utils/keybinding-matchers.ts +9 -0
  136. package/src/prompts/system/custom-system-prompt.md +5 -0
  137. package/src/prompts/system/system-prompt.md +8 -1
  138. package/src/prompts/tools/chunk-edit.md +219 -0
  139. package/src/prompts/tools/debug.md +43 -0
  140. package/src/prompts/tools/grep.md +3 -0
  141. package/src/prompts/tools/lsp.md +5 -5
  142. package/src/prompts/tools/read-chunk.md +17 -0
  143. package/src/prompts/tools/read.md +19 -5
  144. package/src/sdk.ts +216 -165
  145. package/src/secrets/index.ts +1 -1
  146. package/src/secrets/obfuscator.ts +25 -17
  147. package/src/session/agent-session.ts +381 -286
  148. package/src/session/agent-storage.ts +12 -12
  149. package/src/session/compaction/branch-summarization.ts +3 -3
  150. package/src/session/compaction/compaction.ts +5 -6
  151. package/src/session/compaction/utils.ts +3 -3
  152. package/src/session/history-storage.ts +62 -19
  153. package/src/session/messages.ts +3 -3
  154. package/src/session/session-dump-format.ts +203 -0
  155. package/src/session/session-manager.ts +15 -5
  156. package/src/session/session-storage.ts +4 -2
  157. package/src/session/streaming-output.ts +1 -1
  158. package/src/session/tool-choice-queue.ts +213 -0
  159. package/src/slash-commands/builtin-registry.ts +56 -8
  160. package/src/ssh/connection-manager.ts +2 -2
  161. package/src/ssh/sshfs-mount.ts +5 -5
  162. package/src/stt/downloader.ts +4 -4
  163. package/src/stt/recorder.ts +4 -4
  164. package/src/stt/transcriber.ts +2 -2
  165. package/src/system-prompt.ts +25 -13
  166. package/src/task/agents.ts +5 -6
  167. package/src/task/commands.ts +2 -5
  168. package/src/task/executor.ts +32 -4
  169. package/src/task/index.ts +91 -82
  170. package/src/task/template.ts +2 -2
  171. package/src/task/types.ts +25 -0
  172. package/src/task/worktree.ts +131 -149
  173. package/src/tools/ask.ts +2 -3
  174. package/src/tools/ast-edit.ts +7 -7
  175. package/src/tools/ast-grep.ts +7 -7
  176. package/src/tools/auto-generated-guard.ts +36 -41
  177. package/src/tools/await-tool.ts +2 -2
  178. package/src/tools/bash.ts +5 -23
  179. package/src/tools/browser.ts +4 -5
  180. package/src/tools/calculator.ts +2 -3
  181. package/src/tools/cancel-job.ts +2 -2
  182. package/src/tools/checkpoint.ts +3 -3
  183. package/src/tools/debug.ts +1007 -0
  184. package/src/tools/exit-plan-mode.ts +3 -3
  185. package/src/tools/fetch.ts +67 -3
  186. package/src/tools/find.ts +4 -5
  187. package/src/tools/fs-cache-invalidation.ts +5 -0
  188. package/src/tools/gemini-image.ts +13 -5
  189. package/src/tools/gh.ts +130 -308
  190. package/src/tools/grep.ts +57 -9
  191. package/src/tools/index.ts +44 -22
  192. package/src/tools/inspect-image.ts +4 -4
  193. package/src/tools/output-meta.ts +1 -1
  194. package/src/tools/python.ts +19 -6
  195. package/src/tools/read.ts +211 -146
  196. package/src/tools/render-mermaid.ts +2 -3
  197. package/src/tools/render-utils.ts +20 -6
  198. package/src/tools/renderers.ts +3 -1
  199. package/src/tools/report-tool-issue.ts +80 -0
  200. package/src/tools/resolve.ts +70 -39
  201. package/src/tools/search-tool-bm25.ts +2 -2
  202. package/src/tools/ssh.ts +2 -2
  203. package/src/tools/todo-write.ts +2 -2
  204. package/src/tools/tool-timeouts.ts +1 -0
  205. package/src/tools/write.ts +5 -6
  206. package/src/tui/tree-list.ts +3 -1
  207. package/src/utils/clipboard.ts +80 -0
  208. package/src/utils/commit-message-generator.ts +2 -3
  209. package/src/utils/edit-mode.ts +49 -0
  210. package/src/utils/external-editor.ts +11 -5
  211. package/src/utils/file-display-mode.ts +6 -5
  212. package/src/utils/file-mentions.ts +8 -7
  213. package/src/utils/git.ts +1400 -0
  214. package/src/utils/image-loading.ts +98 -0
  215. package/src/utils/title-generator.ts +2 -3
  216. package/src/utils/tools-manager.ts +6 -6
  217. package/src/web/scrapers/choosealicense.ts +1 -1
  218. package/src/web/search/index.ts +3 -3
  219. package/src/web/search/render.ts +6 -4
  220. package/src/autoresearch/command-initialize.md +0 -34
  221. package/src/commit/git/errors.ts +0 -9
  222. package/src/commit/git/index.ts +0 -210
  223. package/src/commit/git/operations.ts +0 -54
  224. package/src/patch/diff.ts +0 -433
  225. package/src/patch/index.ts +0 -888
  226. package/src/patch/parser.ts +0 -532
  227. package/src/patch/types.ts +0 -292
  228. package/src/prompts/agents/oracle.md +0 -77
  229. package/src/tools/gh-cli.ts +0 -125
  230. package/src/tools/pending-action.ts +0 -49
  231. package/src/utils/child-process.ts +0 -88
  232. package/src/utils/frontmatter.ts +0 -117
  233. package/src/utils/image-input.ts +0 -274
  234. package/src/utils/mime.ts +0 -53
  235. package/src/utils/prompt-format.ts +0 -170
package/src/tools/gh.ts CHANGED
@@ -1,10 +1,8 @@
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 { abortableSleep, isEnoent, untilAborted } from "@oh-my-pi/pi-utils";
4
+ import { abortableSleep, isEnoent, prompt, untilAborted } from "@oh-my-pi/pi-utils";
5
5
  import { type Static, Type } from "@sinclair/typebox";
6
- import { $ } from "bun";
7
- import { renderPromptTemplate } from "../config/prompt-templates";
8
6
  import ghIssueViewDescription from "../prompts/tools/gh-issue-view.md" with { type: "text" };
9
7
  import ghPrCheckoutDescription from "../prompts/tools/gh-pr-checkout.md" with { type: "text" };
10
8
  import ghPrDiffDescription from "../prompts/tools/gh-pr-diff.md" with { type: "text" };
@@ -14,8 +12,8 @@ import ghRepoViewDescription from "../prompts/tools/gh-repo-view.md" with { type
14
12
  import ghRunWatchDescription from "../prompts/tools/gh-run-watch.md" with { type: "text" };
15
13
  import ghSearchIssuesDescription from "../prompts/tools/gh-search-issues.md" with { type: "text" };
16
14
  import ghSearchPrsDescription from "../prompts/tools/gh-search-prs.md" with { type: "text" };
15
+ import * as git from "../utils/git";
17
16
  import type { ToolSession } from ".";
18
- import { isGhAvailable, runGhCommand, runGhJson, runGhText } from "./gh-cli";
19
17
  import type { OutputMeta } from "./output-meta";
20
18
  import { ToolError, throwIfAborted } from "./tool-errors";
21
19
  import { toolResult } from "./tool-result";
@@ -401,19 +399,6 @@ interface GhPrViewData extends GhIssueViewData {
401
399
  reviewDecision?: string;
402
400
  }
403
401
 
404
- interface GitCommandResult {
405
- exitCode: number;
406
- stdout: string;
407
- stderr: string;
408
- }
409
-
410
- interface GitWorktreeEntry {
411
- path: string;
412
- head?: string;
413
- branch?: string;
414
- detached: boolean;
415
- }
416
-
417
402
  interface GhPrReviewCommit {
418
403
  oid?: string | null;
419
404
  }
@@ -641,142 +626,45 @@ function stripHeadsRef(value: string | undefined): string | undefined {
641
626
  return value.startsWith("refs/heads/") ? value.slice("refs/heads/".length) : value;
642
627
  }
643
628
 
644
- function formatGitFailure(args: string[], result: GitCommandResult): string {
645
- const output = normalizeOptionalString(result.stderr) ?? normalizeOptionalString(result.stdout);
646
- if (output) {
647
- return output;
629
+ async function requireGitRepoRoot(cwd: string, signal?: AbortSignal): Promise<string> {
630
+ const repoRoot = await git.repo.root(cwd, signal);
631
+ if (!repoRoot) {
632
+ throw new ToolError("Current git repository is unavailable.");
648
633
  }
649
634
 
650
- return `git ${args.join(" ")} failed with exit code ${result.exitCode}`;
651
- }
652
-
653
- async function runGitCommand(cwd: string, args: string[], signal?: AbortSignal): Promise<GitCommandResult> {
654
- return untilAborted(signal, async () => {
655
- throwIfAborted(signal);
656
- const child = Bun.spawn(["git", ...args], {
657
- cwd,
658
- stdin: "ignore",
659
- stdout: "pipe",
660
- stderr: "pipe",
661
- windowsHide: true,
662
- signal,
663
- });
664
- throwIfAborted(signal);
665
-
666
- if (!child.stdout || !child.stderr) {
667
- throw new ToolError("Failed to capture git command output.");
668
- }
669
-
670
- const [stdout, stderr, exitCode] = await Promise.all([
671
- new Response(child.stdout).text(),
672
- new Response(child.stderr).text(),
673
- child.exited,
674
- ]);
675
- throwIfAborted(signal);
676
-
677
- return {
678
- exitCode: exitCode ?? 0,
679
- stdout: normalizeBlock(stdout),
680
- stderr: normalizeBlock(stderr),
681
- };
682
- });
635
+ return repoRoot;
683
636
  }
684
637
 
685
- async function runGitTextChecked(cwd: string, args: string[], signal?: AbortSignal): Promise<string> {
686
- const result = await runGitChecked(cwd, args, signal);
687
-
688
- const text = normalizeOptionalString(result.stdout);
689
- if (!text) {
690
- throw new ToolError(`git ${args.join(" ")} returned empty output.`);
638
+ async function requirePrimaryGitRepoRoot(cwd: string, signal?: AbortSignal): Promise<string> {
639
+ const primaryRepoRoot = await git.repo.primaryRoot(cwd, signal);
640
+ if (!primaryRepoRoot) {
641
+ throw new ToolError("Current git repository is unavailable.");
691
642
  }
692
643
 
693
- return text;
644
+ return primaryRepoRoot;
694
645
  }
695
646
 
696
- async function runGitChecked(cwd: string, args: string[], signal?: AbortSignal): Promise<GitCommandResult> {
697
- const result = await runGitCommand(cwd, args, signal);
698
- if (result.exitCode !== 0) {
699
- throw new ToolError(formatGitFailure(args, result));
647
+ async function requireCurrentGitBranch(cwd: string, signal?: AbortSignal): Promise<string> {
648
+ const branch = await git.branch.current(cwd, signal);
649
+ if (!branch) {
650
+ throw new ToolError("Current git branch is unavailable. Pass `branch` or `run` explicitly.");
700
651
  }
701
652
 
702
- return result;
653
+ return branch;
703
654
  }
704
655
 
705
- async function tryRunGitText(cwd: string, args: string[], signal?: AbortSignal): Promise<string | undefined> {
706
- const result = await runGitCommand(cwd, args, signal);
707
- if (result.exitCode !== 0) {
708
- return undefined;
656
+ async function requireCurrentGitHead(cwd: string, signal?: AbortSignal): Promise<string> {
657
+ const headSha = await git.head.sha(cwd, signal);
658
+ if (!headSha) {
659
+ throw new ToolError("Current git HEAD is unavailable. Pass `run` explicitly.");
709
660
  }
710
661
 
711
- return normalizeOptionalString(result.stdout);
712
- }
713
-
714
- async function resolveGitRepoRoot(cwd: string, signal?: AbortSignal): Promise<string> {
715
- return runGitTextChecked(cwd, ["rev-parse", "--show-toplevel"], signal);
716
- }
717
-
718
- async function resolvePrimaryGitRepoRoot(repoRoot: string, signal?: AbortSignal): Promise<string> {
719
- const commonDir = await runGitTextChecked(
720
- repoRoot,
721
- ["rev-parse", "--path-format=absolute", "--git-common-dir"],
722
- signal,
723
- );
724
- if (path.basename(commonDir) === ".git") {
725
- return path.dirname(commonDir);
726
- }
727
-
728
- return repoRoot;
729
- }
730
-
731
- function parseGitWorktreeList(text: string): GitWorktreeEntry[] {
732
- const trimmed = text.trim();
733
- if (!trimmed) {
734
- return [];
735
- }
736
-
737
- return trimmed
738
- .split(/\n\s*\n/)
739
- .map(block => block.trim())
740
- .filter(Boolean)
741
- .map(block => {
742
- const entry: GitWorktreeEntry = {
743
- path: "",
744
- detached: false,
745
- };
746
- for (const line of block.split("\n")) {
747
- if (line.startsWith("worktree ")) {
748
- entry.path = line.slice("worktree ".length);
749
- continue;
750
- }
751
- if (line.startsWith("HEAD ")) {
752
- entry.head = line.slice("HEAD ".length);
753
- continue;
754
- }
755
- if (line.startsWith("branch ")) {
756
- entry.branch = line.slice("branch ".length);
757
- continue;
758
- }
759
- if (line === "detached") {
760
- entry.detached = true;
761
- }
762
- }
763
- return entry;
764
- });
765
- }
766
-
767
- async function listGitWorktrees(repoRoot: string, signal?: AbortSignal): Promise<GitWorktreeEntry[]> {
768
- const output = await runGitTextChecked(repoRoot, ["worktree", "list", "--porcelain"], signal);
769
- return parseGitWorktreeList(output);
770
- }
771
-
772
- async function gitRefExists(repoRoot: string, ref: string, signal?: AbortSignal): Promise<boolean> {
773
- const result = await runGitCommand(repoRoot, ["show-ref", "--verify", "--quiet", ref], signal);
774
- return result.exitCode === 0;
662
+ return headSha;
775
663
  }
776
664
 
777
665
  async function ensureGitWorktreePathAvailable(
778
666
  worktreePath: string,
779
- existingWorktrees: GitWorktreeEntry[],
667
+ existingWorktrees: git.GitWorktreeEntry[],
780
668
  ): Promise<void> {
781
669
  const normalizedTarget = path.resolve(worktreePath);
782
670
  const conflictingWorktree = existingWorktrees.find(entry => path.resolve(entry.path) === normalizedTarget);
@@ -804,15 +692,10 @@ function selectPrCloneUrl(originUrl: string | undefined, repo: Pick<GhRepoViewDa
804
692
  }
805
693
 
806
694
  async function getRemoteUrls(repoRoot: string, signal?: AbortSignal): Promise<Map<string, string>> {
807
- const remoteList = await tryRunGitText(repoRoot, ["remote"], signal);
808
- const remotes =
809
- remoteList
810
- ?.split("\n")
811
- .map(value => value.trim())
812
- .filter(Boolean) ?? [];
695
+ const remotes = await git.remote.list(repoRoot, signal);
813
696
  const urls = new Map<string, string>();
814
697
  for (const remoteName of remotes) {
815
- const remoteUrl = await tryRunGitText(repoRoot, ["remote", "get-url", remoteName], signal);
698
+ const remoteUrl = await git.remote.url(repoRoot, remoteName, signal);
816
699
  if (remoteUrl) {
817
700
  urls.set(remoteName, remoteUrl);
818
701
  }
@@ -826,7 +709,7 @@ async function ensurePrRemote(
826
709
  signal?: AbortSignal,
827
710
  ): Promise<{ name: string; url: string }> {
828
711
  if (!data.isCrossRepository) {
829
- const originUrl = normalizeOptionalString(await tryRunGitText(repoRoot, ["remote", "get-url", "origin"], signal));
712
+ const originUrl = await git.remote.url(repoRoot, "origin", signal);
830
713
  if (!originUrl) {
831
714
  throw new ToolError("origin remote is unavailable for this repository.");
832
715
  }
@@ -838,13 +721,13 @@ async function ensurePrRemote(
838
721
  }
839
722
 
840
723
  const headRepository = requireNonEmpty(data.headRepository?.nameWithOwner, "head repository");
841
- const repoSummary = await runGhJson<GhRepoViewData>(
724
+ const repoSummary = await git.github.json<GhRepoViewData>(
842
725
  repoRoot,
843
726
  ["repo", "view", headRepository, "--json", GH_REPO_CLONE_FIELDS.join(",")],
844
727
  signal,
845
728
  { repoProvided: true },
846
729
  );
847
- const originUrl = await tryRunGitText(repoRoot, ["remote", "get-url", "origin"], signal);
730
+ const originUrl = await git.remote.url(repoRoot, "origin", signal);
848
731
  const remoteUrl = selectPrCloneUrl(originUrl, repoSummary);
849
732
  if (!remoteUrl) {
850
733
  throw new ToolError(`Could not determine a clone URL for ${headRepository}.`);
@@ -867,10 +750,7 @@ async function ensurePrRemote(
867
750
  suffix += 1;
868
751
  }
869
752
 
870
- const result = await runGitCommand(repoRoot, ["remote", "add", remoteName, remoteUrl], signal);
871
- if (result.exitCode !== 0) {
872
- throw new ToolError(formatGitFailure(["remote", "add", remoteName, remoteUrl], result));
873
- }
753
+ await git.remote.add(repoRoot, remoteName, remoteUrl, signal);
874
754
 
875
755
  return {
876
756
  name: remoteName,
@@ -878,28 +758,6 @@ async function ensurePrRemote(
878
758
  };
879
759
  }
880
760
 
881
- async function setBranchConfig(
882
- repoRoot: string,
883
- localBranch: string,
884
- key: string,
885
- value: string,
886
- signal?: AbortSignal,
887
- ): Promise<void> {
888
- const result = await runGitCommand(repoRoot, ["config", `branch.${localBranch}.${key}`, value], signal);
889
- if (result.exitCode !== 0) {
890
- throw new ToolError(formatGitFailure(["config", `branch.${localBranch}.${key}`, value], result));
891
- }
892
- }
893
-
894
- async function getBranchConfig(
895
- repoRoot: string,
896
- localBranch: string,
897
- key: string,
898
- signal?: AbortSignal,
899
- ): Promise<string | undefined> {
900
- return tryRunGitText(repoRoot, ["config", "--get", `branch.${localBranch}.${key}`], signal);
901
- }
902
-
903
761
  async function resolvePrBranchPushTarget(
904
762
  repoRoot: string,
905
763
  localBranch: string,
@@ -912,13 +770,18 @@ async function resolvePrBranchPushTarget(
912
770
  maintainerCanModify?: boolean;
913
771
  isCrossRepository: boolean;
914
772
  }> {
915
- const pushRemote = await getBranchConfig(repoRoot, localBranch, "pushRemote", signal);
916
- const remote = await getBranchConfig(repoRoot, localBranch, "remote", signal);
917
- const mergeRef = await getBranchConfig(repoRoot, localBranch, "merge", signal);
918
- const headRef = await getBranchConfig(repoRoot, localBranch, "ompPrHeadRef", signal);
919
- const prUrl = await getBranchConfig(repoRoot, localBranch, "ompPrUrl", signal);
920
- const maintainerCanModifyValue = await getBranchConfig(repoRoot, localBranch, "ompPrMaintainerCanModify", signal);
921
- const isCrossRepositoryValue = await getBranchConfig(repoRoot, localBranch, "ompPrIsCrossRepository", signal);
773
+ const pushRemote = await git.config.getBranch(repoRoot, localBranch, "pushRemote", signal);
774
+ const remote = await git.config.getBranch(repoRoot, localBranch, "remote", signal);
775
+ const mergeRef = await git.config.getBranch(repoRoot, localBranch, "merge", signal);
776
+ const headRef = await git.config.getBranch(repoRoot, localBranch, "ompPrHeadRef", signal);
777
+ const prUrl = await git.config.getBranch(repoRoot, localBranch, "ompPrUrl", signal);
778
+ const maintainerCanModifyValue = await git.config.getBranch(
779
+ repoRoot,
780
+ localBranch,
781
+ "ompPrMaintainerCanModify",
782
+ signal,
783
+ );
784
+ const isCrossRepositoryValue = await git.config.getBranch(repoRoot, localBranch, "ompPrIsCrossRepository", signal);
922
785
 
923
786
  const remoteName = pushRemote ?? remote;
924
787
  if (!remoteName) {
@@ -933,7 +796,7 @@ async function resolvePrBranchPushTarget(
933
796
  return {
934
797
  remoteName,
935
798
  remoteBranch,
936
- remoteUrl: await tryRunGitText(repoRoot, ["remote", "get-url", remoteName], signal),
799
+ remoteUrl: await git.remote.url(repoRoot, remoteName, signal),
937
800
  prUrl,
938
801
  maintainerCanModify:
939
802
  maintainerCanModifyValue === undefined
@@ -1085,6 +948,10 @@ function getRunCollectionOutcome(runs: GhRunSnapshot[]): "success" | "failure" |
1085
948
 
1086
949
  let pending = false;
1087
950
  for (const run of runs) {
951
+ if (run.jobs.some(isFailedJob)) {
952
+ return "failure";
953
+ }
954
+
1088
955
  const outcome = getRunSnapshotOutcome(run);
1089
956
  if (outcome === "failure") {
1090
957
  return "failure";
@@ -1483,44 +1350,6 @@ function buildCommitRunWatchDetails(
1483
1350
  };
1484
1351
  }
1485
1352
 
1486
- async function resolveCurrentGitBranch(cwd: string, signal?: AbortSignal): Promise<string> {
1487
- return untilAborted(signal, async () => {
1488
- throwIfAborted(signal);
1489
- const result = await $`git symbolic-ref --short HEAD`.cwd(cwd).quiet().nothrow();
1490
- throwIfAborted(signal);
1491
-
1492
- if (result.exitCode !== 0) {
1493
- throw new ToolError("Current git branch is unavailable. Pass `branch` or `run` explicitly.");
1494
- }
1495
-
1496
- const branch = normalizeOptionalString(result.text());
1497
- if (!branch) {
1498
- throw new ToolError("Current git branch is unavailable. Pass `branch` or `run` explicitly.");
1499
- }
1500
-
1501
- return branch;
1502
- });
1503
- }
1504
-
1505
- async function resolveCurrentGitHead(cwd: string, signal?: AbortSignal): Promise<string> {
1506
- return untilAborted(signal, async () => {
1507
- throwIfAborted(signal);
1508
- const result = await $`git rev-parse HEAD`.cwd(cwd).quiet().nothrow();
1509
- throwIfAborted(signal);
1510
-
1511
- if (result.exitCode !== 0) {
1512
- throw new ToolError("Current git HEAD is unavailable. Pass `run` explicitly.");
1513
- }
1514
-
1515
- const headSha = normalizeOptionalString(result.text());
1516
- if (!headSha) {
1517
- throw new ToolError("Current git HEAD is unavailable. Pass `run` explicitly.");
1518
- }
1519
-
1520
- return headSha;
1521
- });
1522
- }
1523
-
1524
1353
  async function resolveGitHubRepo(
1525
1354
  cwd: string,
1526
1355
  repo: string | undefined,
@@ -1539,7 +1368,11 @@ async function resolveGitHubRepo(
1539
1368
  return runRepo;
1540
1369
  }
1541
1370
 
1542
- const resolved = await runGhText(cwd, ["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"], signal);
1371
+ const resolved = await git.github.text(
1372
+ cwd,
1373
+ ["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"],
1374
+ signal,
1375
+ );
1543
1376
  return requireNonEmpty(resolved, "repo");
1544
1377
  }
1545
1378
 
@@ -1549,7 +1382,7 @@ async function resolveGitHubBranchHead(
1549
1382
  branch: string,
1550
1383
  signal?: AbortSignal,
1551
1384
  ): Promise<string> {
1552
- const response = await runGhJson<GhBranchApiResponse>(
1385
+ const response = await git.github.json<GhBranchApiResponse>(
1553
1386
  cwd,
1554
1387
  ["api", "--method", "GET", `/repos/${repo}/branches/${encodeURIComponent(branch)}`],
1555
1388
  signal,
@@ -1565,7 +1398,7 @@ async function fetchRunsForCommit(
1565
1398
  branch: string | undefined,
1566
1399
  signal?: AbortSignal,
1567
1400
  ): Promise<GhRunSnapshot[]> {
1568
- const response = await runGhJson<GhActionsRunListResponse>(
1401
+ const response = await git.github.json<GhActionsRunListResponse>(
1569
1402
  cwd,
1570
1403
  [
1571
1404
  "api",
@@ -1602,7 +1435,7 @@ async function fetchRunJobs(
1602
1435
  let page = 1;
1603
1436
 
1604
1437
  while (true) {
1605
- const response = await runGhJson<GhActionsJobsResponse>(
1438
+ const response = await git.github.json<GhActionsJobsResponse>(
1606
1439
  cwd,
1607
1440
  [
1608
1441
  "api",
@@ -1646,7 +1479,7 @@ async function fetchPrReviewComments(
1646
1479
  let page = 1;
1647
1480
 
1648
1481
  while (true) {
1649
- const response = await runGhJson<GhPrReviewCommentApi[]>(
1482
+ const response = await git.github.json<GhPrReviewCommentApi[]>(
1650
1483
  cwd,
1651
1484
  [
1652
1485
  "api",
@@ -1684,9 +1517,14 @@ async function fetchRunSnapshot(
1684
1517
  signal?: AbortSignal,
1685
1518
  ): Promise<GhRunSnapshot> {
1686
1519
  const [run, jobs] = await Promise.all([
1687
- runGhJson<GhActionsRunApi>(cwd, ["api", "--method", "GET", `/repos/${repo}/actions/runs/${runId}`], signal, {
1688
- repoProvided: true,
1689
- }),
1520
+ git.github.json<GhActionsRunApi>(
1521
+ cwd,
1522
+ ["api", "--method", "GET", `/repos/${repo}/actions/runs/${runId}`],
1523
+ signal,
1524
+ {
1525
+ repoProvided: true,
1526
+ },
1527
+ ),
1690
1528
  fetchRunJobs(cwd, repo, runId, signal),
1691
1529
  ]);
1692
1530
 
@@ -1712,7 +1550,7 @@ async function fetchFailedJobLogs(
1712
1550
  ): Promise<GhFailedJobLog[]> {
1713
1551
  return Promise.all(
1714
1552
  failedJobs.map(async entry => {
1715
- const result = await runGhCommand(cwd, ["api", `/repos/${repo}/actions/jobs/${entry.job.id}/logs`], signal);
1553
+ const result = await git.github.run(cwd, ["api", `/repos/${repo}/actions/jobs/${entry.job.id}/logs`], signal);
1716
1554
  const fullLog = result.exitCode === 0 ? normalizeBlock(result.stdout) : undefined;
1717
1555
  const logTail = fullLog ? tailLogLines(fullLog, tail) : undefined;
1718
1556
  return {
@@ -2063,14 +1901,14 @@ function buildTextResult(
2063
1901
  export class GhRepoViewTool implements AgentTool<typeof ghRepoViewSchema, GhToolDetails> {
2064
1902
  readonly name = "gh_repo_view";
2065
1903
  readonly label = "GitHub Repo";
2066
- readonly description = renderPromptTemplate(ghRepoViewDescription);
1904
+ readonly description = prompt.render(ghRepoViewDescription);
2067
1905
  readonly parameters = ghRepoViewSchema;
2068
1906
  readonly strict = true;
2069
1907
 
2070
1908
  constructor(private readonly session: ToolSession) {}
2071
1909
 
2072
1910
  static createIf(session: ToolSession): GhRepoViewTool | null {
2073
- if (!isGhAvailable()) return null;
1911
+ if (!git.github.available()) return null;
2074
1912
  return new GhRepoViewTool(session);
2075
1913
  }
2076
1914
 
@@ -2093,7 +1931,9 @@ export class GhRepoViewTool implements AgentTool<typeof ghRepoViewSchema, GhTool
2093
1931
  }
2094
1932
  args.push("--json", GH_REPO_FIELDS.join(","));
2095
1933
 
2096
- const data = await runGhJson<GhRepoViewData>(this.session.cwd, args, signal, { repoProvided: Boolean(repo) });
1934
+ const data = await git.github.json<GhRepoViewData>(this.session.cwd, args, signal, {
1935
+ repoProvided: Boolean(repo),
1936
+ });
2097
1937
  return buildTextResult(formatRepoView(data, { repo, branch }), data.url);
2098
1938
  });
2099
1939
  }
@@ -2102,14 +1942,14 @@ export class GhRepoViewTool implements AgentTool<typeof ghRepoViewSchema, GhTool
2102
1942
  export class GhIssueViewTool implements AgentTool<typeof ghIssueViewSchema, GhToolDetails> {
2103
1943
  readonly name = "gh_issue_view";
2104
1944
  readonly label = "GitHub Issue";
2105
- readonly description = renderPromptTemplate(ghIssueViewDescription);
1945
+ readonly description = prompt.render(ghIssueViewDescription);
2106
1946
  readonly parameters = ghIssueViewSchema;
2107
1947
  readonly strict = true;
2108
1948
 
2109
1949
  constructor(private readonly session: ToolSession) {}
2110
1950
 
2111
1951
  static createIf(session: ToolSession): GhIssueViewTool | null {
2112
- if (!isGhAvailable()) return null;
1952
+ if (!git.github.available()) return null;
2113
1953
  return new GhIssueViewTool(session);
2114
1954
  }
2115
1955
 
@@ -2128,7 +1968,9 @@ export class GhIssueViewTool implements AgentTool<typeof ghIssueViewSchema, GhTo
2128
1968
  appendRepoFlag(args, repo, issue);
2129
1969
  args.push("--json", (includeComments ? GH_ISSUE_FIELDS : GH_ISSUE_FIELDS_NO_COMMENTS).join(","));
2130
1970
 
2131
- const data = await runGhJson<GhIssueViewData>(this.session.cwd, args, signal, { repoProvided: Boolean(repo) });
1971
+ const data = await git.github.json<GhIssueViewData>(this.session.cwd, args, signal, {
1972
+ repoProvided: Boolean(repo),
1973
+ });
2132
1974
  return buildTextResult(formatIssueView(data, { issue, repo, comments: includeComments }), data.url);
2133
1975
  });
2134
1976
  }
@@ -2137,14 +1979,14 @@ export class GhIssueViewTool implements AgentTool<typeof ghIssueViewSchema, GhTo
2137
1979
  export class GhPrViewTool implements AgentTool<typeof ghPrViewSchema, GhToolDetails> {
2138
1980
  readonly name = "gh_pr_view";
2139
1981
  readonly label = "GitHub PR";
2140
- readonly description = renderPromptTemplate(ghPrViewDescription);
1982
+ readonly description = prompt.render(ghPrViewDescription);
2141
1983
  readonly parameters = ghPrViewSchema;
2142
1984
  readonly strict = true;
2143
1985
 
2144
1986
  constructor(private readonly session: ToolSession) {}
2145
1987
 
2146
1988
  static createIf(session: ToolSession): GhPrViewTool | null {
2147
- if (!isGhAvailable()) return null;
1989
+ if (!git.github.available()) return null;
2148
1990
  return new GhPrViewTool(session);
2149
1991
  }
2150
1992
 
@@ -2166,7 +2008,9 @@ export class GhPrViewTool implements AgentTool<typeof ghPrViewSchema, GhToolDeta
2166
2008
  appendRepoFlag(args, repo, pr);
2167
2009
  args.push("--json", (includeComments ? GH_PR_FIELDS : GH_PR_FIELDS_NO_COMMENTS).join(","));
2168
2010
 
2169
- const data = await runGhJson<GhPrViewData>(this.session.cwd, args, signal, { repoProvided: Boolean(repo) });
2011
+ const data = await git.github.json<GhPrViewData>(this.session.cwd, args, signal, {
2012
+ repoProvided: Boolean(repo),
2013
+ });
2170
2014
  const resolvedRepo = repo ?? parsePullRequestUrl(data.url).repo;
2171
2015
  if (includeComments && resolvedRepo && typeof data.number === "number") {
2172
2016
  data.reviewComments = await fetchPrReviewComments(this.session.cwd, resolvedRepo, data.number, signal);
@@ -2179,14 +2023,14 @@ export class GhPrViewTool implements AgentTool<typeof ghPrViewSchema, GhToolDeta
2179
2023
  export class GhPrDiffTool implements AgentTool<typeof ghPrDiffSchema, GhToolDetails> {
2180
2024
  readonly name = "gh_pr_diff";
2181
2025
  readonly label = "GitHub PR Diff";
2182
- readonly description = renderPromptTemplate(ghPrDiffDescription);
2026
+ readonly description = prompt.render(ghPrDiffDescription);
2183
2027
  readonly parameters = ghPrDiffSchema;
2184
2028
  readonly strict = true;
2185
2029
 
2186
2030
  constructor(private readonly session: ToolSession) {}
2187
2031
 
2188
2032
  static createIf(session: ToolSession): GhPrDiffTool | null {
2189
- if (!isGhAvailable()) return null;
2033
+ if (!git.github.available()) return null;
2190
2034
  return new GhPrDiffTool(session);
2191
2035
  }
2192
2036
 
@@ -2214,7 +2058,7 @@ export class GhPrDiffTool implements AgentTool<typeof ghPrDiffSchema, GhToolDeta
2214
2058
  args.push("--exclude", normalizedPattern);
2215
2059
  }
2216
2060
 
2217
- const output = await runGhText(this.session.cwd, args, signal, {
2061
+ const output = await git.github.text(this.session.cwd, args, signal, {
2218
2062
  repoProvided: Boolean(repo),
2219
2063
  trimOutput: false,
2220
2064
  });
@@ -2228,14 +2072,14 @@ export class GhPrDiffTool implements AgentTool<typeof ghPrDiffSchema, GhToolDeta
2228
2072
  export class GhPrCheckoutTool implements AgentTool<typeof ghPrCheckoutSchema, GhToolDetails> {
2229
2073
  readonly name = "gh_pr_checkout";
2230
2074
  readonly label = "GitHub PR Checkout";
2231
- readonly description = renderPromptTemplate(ghPrCheckoutDescription);
2075
+ readonly description = prompt.render(ghPrCheckoutDescription);
2232
2076
  readonly parameters = ghPrCheckoutSchema;
2233
2077
  readonly strict = true;
2234
2078
 
2235
2079
  constructor(private readonly session: ToolSession) {}
2236
2080
 
2237
2081
  static createIf(session: ToolSession): GhPrCheckoutTool | null {
2238
- if (!isGhAvailable()) return null;
2082
+ if (!git.github.available()) return null;
2239
2083
  return new GhPrCheckoutTool(session);
2240
2084
  }
2241
2085
 
@@ -2259,7 +2103,7 @@ export class GhPrCheckoutTool implements AgentTool<typeof ghPrCheckoutSchema, Gh
2259
2103
  appendRepoFlag(args, repo, pr);
2260
2104
  args.push("--json", GH_PR_CHECKOUT_FIELDS.join(","));
2261
2105
 
2262
- const data = await runGhJson<GhPrViewData>(this.session.cwd, args, signal, {
2106
+ const data = await git.github.json<GhPrViewData>(this.session.cwd, args, signal, {
2263
2107
  repoProvided: Boolean(repo),
2264
2108
  });
2265
2109
  const prNumber = data.number;
@@ -2269,68 +2113,56 @@ export class GhPrCheckoutTool implements AgentTool<typeof ghPrCheckoutSchema, Gh
2269
2113
 
2270
2114
  const headRefName = requireNonEmpty(data.headRefName, "head branch");
2271
2115
  const headRefOid = requireNonEmpty(data.headRefOid, "head commit");
2272
- const repoRoot = await resolveGitRepoRoot(this.session.cwd, signal);
2273
- const primaryRepoRoot = await resolvePrimaryGitRepoRoot(repoRoot, signal);
2116
+ const repoRoot = await requireGitRepoRoot(this.session.cwd, signal);
2117
+ const primaryRepoRoot = await requirePrimaryGitRepoRoot(repoRoot, signal);
2274
2118
  const localBranch = requestedBranch ?? `pr-${prNumber}`;
2275
2119
  const worktreePath = requestedWorktree
2276
2120
  ? path.resolve(this.session.cwd, requestedWorktree)
2277
2121
  : path.join(primaryRepoRoot, ".worktrees", localBranch);
2278
- const existingWorktrees = await listGitWorktrees(repoRoot, signal);
2122
+ const existingWorktrees = await git.worktree.list(repoRoot, signal);
2279
2123
  const existingWorktree = existingWorktrees.find(entry => entry.branch === toLocalBranchRef(localBranch));
2280
2124
 
2281
2125
  const remote = await ensurePrRemote(repoRoot, data, signal);
2282
- await runGitChecked(
2126
+ await git.fetch(
2283
2127
  repoRoot,
2284
- ["fetch", remote.name, `+refs/heads/${headRefName}:refs/remotes/${remote.name}/${headRefName}`],
2128
+ remote.name,
2129
+ `refs/heads/${headRefName}`,
2130
+ `refs/remotes/${remote.name}/${headRefName}`,
2285
2131
  signal,
2286
2132
  );
2287
2133
 
2288
2134
  if (!existingWorktree) {
2289
2135
  const localBranchRef = toLocalBranchRef(localBranch);
2290
- const localBranchExists = await gitRefExists(repoRoot, localBranchRef, signal);
2136
+ const localBranchExists = await git.ref.exists(repoRoot, localBranchRef, signal);
2291
2137
  if (localBranchExists) {
2292
- const existingOid = await runGitTextChecked(repoRoot, ["rev-parse", localBranchRef], signal);
2138
+ const existingOid = await git.ref.resolve(repoRoot, localBranchRef, signal);
2293
2139
  if (existingOid !== headRefOid) {
2294
2140
  if (!force) {
2295
2141
  throw new ToolError(
2296
- `local branch ${localBranch} already exists at ${formatShortSha(existingOid) ?? existingOid}; pass force=true to reset it`,
2142
+ `local branch ${localBranch} already exists at ${formatShortSha(existingOid ?? undefined) ?? existingOid ?? "unknown commit"}; pass force=true to reset it`,
2297
2143
  );
2298
2144
  }
2299
2145
 
2300
- const resetResult = await runGitCommand(
2301
- repoRoot,
2302
- ["branch", "--force", localBranch, `refs/remotes/${remote.name}/${headRefName}`],
2303
- signal,
2304
- );
2305
- if (resetResult.exitCode !== 0) {
2306
- throw new ToolError(formatGitFailure(["branch", "--force", localBranch], resetResult));
2307
- }
2146
+ await git.branch.force(repoRoot, localBranch, `refs/remotes/${remote.name}/${headRefName}`, signal);
2308
2147
  }
2309
2148
  } else {
2310
- const createResult = await runGitCommand(
2311
- repoRoot,
2312
- ["branch", localBranch, `refs/remotes/${remote.name}/${headRefName}`],
2313
- signal,
2314
- );
2315
- if (createResult.exitCode !== 0) {
2316
- throw new ToolError(formatGitFailure(["branch", localBranch], createResult));
2317
- }
2149
+ await git.branch.create(repoRoot, localBranch, `refs/remotes/${remote.name}/${headRefName}`, signal);
2318
2150
  }
2319
2151
  }
2320
2152
 
2321
- await setBranchConfig(repoRoot, localBranch, "remote", remote.name, signal);
2322
- await setBranchConfig(repoRoot, localBranch, "merge", `refs/heads/${headRefName}`, signal);
2323
- await setBranchConfig(repoRoot, localBranch, "pushRemote", remote.name, signal);
2324
- await setBranchConfig(repoRoot, localBranch, "ompPrHeadRef", headRefName, signal);
2325
- await setBranchConfig(repoRoot, localBranch, "ompPrUrl", data.url ?? "", signal);
2326
- await setBranchConfig(
2153
+ await git.config.setBranch(repoRoot, localBranch, "remote", remote.name, signal);
2154
+ await git.config.setBranch(repoRoot, localBranch, "merge", `refs/heads/${headRefName}`, signal);
2155
+ await git.config.setBranch(repoRoot, localBranch, "pushRemote", remote.name, signal);
2156
+ await git.config.setBranch(repoRoot, localBranch, "ompPrHeadRef", headRefName, signal);
2157
+ await git.config.setBranch(repoRoot, localBranch, "ompPrUrl", data.url ?? "", signal);
2158
+ await git.config.setBranch(
2327
2159
  repoRoot,
2328
2160
  localBranch,
2329
2161
  "ompPrIsCrossRepository",
2330
2162
  String(Boolean(data.isCrossRepository)),
2331
2163
  signal,
2332
2164
  );
2333
- await setBranchConfig(
2165
+ await git.config.setBranch(
2334
2166
  repoRoot,
2335
2167
  localBranch,
2336
2168
  "ompPrMaintainerCanModify",
@@ -2342,21 +2174,15 @@ export class GhPrCheckoutTool implements AgentTool<typeof ghPrCheckoutSchema, Gh
2342
2174
  if (!existingWorktree) {
2343
2175
  await ensureGitWorktreePathAvailable(finalWorktreePath, existingWorktrees);
2344
2176
  await fs.mkdir(path.dirname(finalWorktreePath), { recursive: true });
2345
- const addResult = await runGitCommand(
2346
- repoRoot,
2347
- ["worktree", "add", finalWorktreePath, localBranch],
2348
- signal,
2349
- );
2350
- if (addResult.exitCode !== 0) {
2351
- throw new ToolError(formatGitFailure(["worktree", "add", finalWorktreePath, localBranch], addResult));
2352
- }
2177
+ await git.worktree.add(repoRoot, finalWorktreePath, localBranch, { signal });
2353
2178
  }
2179
+ const resolvedWorktreePath = await fs.realpath(finalWorktreePath);
2354
2180
 
2355
2181
  return buildTextResult(
2356
2182
  formatPrCheckoutResult({
2357
2183
  data,
2358
2184
  localBranch,
2359
- worktreePath: finalWorktreePath,
2185
+ worktreePath: resolvedWorktreePath,
2360
2186
  remoteName: remote.name,
2361
2187
  remoteUrl: remote.url,
2362
2188
  reused: Boolean(existingWorktree),
@@ -2365,7 +2191,7 @@ export class GhPrCheckoutTool implements AgentTool<typeof ghPrCheckoutSchema, Gh
2365
2191
  {
2366
2192
  repo: repo ?? data.headRepository?.nameWithOwner,
2367
2193
  branch: localBranch,
2368
- worktreePath: finalWorktreePath,
2194
+ worktreePath: resolvedWorktreePath,
2369
2195
  remote: remote.name,
2370
2196
  remoteBranch: headRefName,
2371
2197
  },
@@ -2377,14 +2203,14 @@ export class GhPrCheckoutTool implements AgentTool<typeof ghPrCheckoutSchema, Gh
2377
2203
  export class GhPrPushTool implements AgentTool<typeof ghPrPushSchema, GhToolDetails> {
2378
2204
  readonly name = "gh_pr_push";
2379
2205
  readonly label = "GitHub PR Push";
2380
- readonly description = renderPromptTemplate(ghPrPushDescription);
2206
+ readonly description = prompt.render(ghPrPushDescription);
2381
2207
  readonly parameters = ghPrPushSchema;
2382
2208
  readonly strict = true;
2383
2209
 
2384
2210
  constructor(private readonly session: ToolSession) {}
2385
2211
 
2386
2212
  static createIf(session: ToolSession): GhPrPushTool | null {
2387
- if (!isGhAvailable()) return null;
2213
+ if (!git.github.available()) return null;
2388
2214
  return new GhPrPushTool(session);
2389
2215
  }
2390
2216
 
@@ -2396,28 +2222,24 @@ export class GhPrPushTool implements AgentTool<typeof ghPrPushSchema, GhToolDeta
2396
2222
  _context?: AgentToolContext,
2397
2223
  ): Promise<AgentToolResult<GhToolDetails>> {
2398
2224
  return untilAborted(signal, async () => {
2399
- const repoRoot = await resolveGitRepoRoot(this.session.cwd, signal);
2225
+ const repoRoot = await requireGitRepoRoot(this.session.cwd, signal);
2400
2226
  const localBranch =
2401
- normalizeOptionalString(params.branch) ?? (await resolveCurrentGitBranch(repoRoot, signal));
2402
- const refExists = await gitRefExists(repoRoot, toLocalBranchRef(localBranch), signal);
2227
+ normalizeOptionalString(params.branch) ?? (await requireCurrentGitBranch(repoRoot, signal));
2228
+ const refExists = await git.ref.exists(repoRoot, toLocalBranchRef(localBranch), signal);
2403
2229
  if (!refExists) {
2404
2230
  throw new ToolError(`local branch ${localBranch} does not exist`);
2405
2231
  }
2406
2232
 
2407
2233
  const target = await resolvePrBranchPushTarget(repoRoot, localBranch, signal);
2408
- const currentBranch = await tryRunGitText(repoRoot, ["branch", "--show-current"], signal);
2234
+ const currentBranch = await git.branch.current(repoRoot, signal);
2409
2235
  const sourceRef = currentBranch === localBranch ? "HEAD" : toLocalBranchRef(localBranch);
2410
2236
  const refspec = `${sourceRef}:refs/heads/${target.remoteBranch}`;
2411
- const pushArgs = ["push"];
2412
- if (params.forceWithLease) {
2413
- pushArgs.push("--force-with-lease");
2414
- }
2415
- pushArgs.push(target.remoteName, refspec);
2416
-
2417
- const pushResult = await runGitCommand(repoRoot, pushArgs, signal);
2418
- if (pushResult.exitCode !== 0) {
2419
- throw new ToolError(formatGitFailure(pushArgs, pushResult));
2420
- }
2237
+ await git.push(repoRoot, {
2238
+ forceWithLease: params.forceWithLease,
2239
+ refspec,
2240
+ remote: target.remoteName,
2241
+ signal,
2242
+ });
2421
2243
 
2422
2244
  return buildTextResult(
2423
2245
  formatPrPushResult({
@@ -2442,14 +2264,14 @@ export class GhPrPushTool implements AgentTool<typeof ghPrPushSchema, GhToolDeta
2442
2264
  export class GhSearchIssuesTool implements AgentTool<typeof ghSearchIssuesSchema, GhToolDetails> {
2443
2265
  readonly name = "gh_search_issues";
2444
2266
  readonly label = "GitHub Issue Search";
2445
- readonly description = renderPromptTemplate(ghSearchIssuesDescription);
2267
+ readonly description = prompt.render(ghSearchIssuesDescription);
2446
2268
  readonly parameters = ghSearchIssuesSchema;
2447
2269
  readonly strict = true;
2448
2270
 
2449
2271
  constructor(private readonly session: ToolSession) {}
2450
2272
 
2451
2273
  static createIf(session: ToolSession): GhSearchIssuesTool | null {
2452
- if (!isGhAvailable()) return null;
2274
+ if (!git.github.available()) return null;
2453
2275
  return new GhSearchIssuesTool(session);
2454
2276
  }
2455
2277
 
@@ -2466,7 +2288,7 @@ export class GhSearchIssuesTool implements AgentTool<typeof ghSearchIssuesSchema
2466
2288
  const limit = resolveSearchLimit(params.limit);
2467
2289
  const args = buildGhSearchArgs("issues", query, limit, repo);
2468
2290
 
2469
- const items = await runGhJson<GhSearchResult[]>(this.session.cwd, args, signal, {
2291
+ const items = await git.github.json<GhSearchResult[]>(this.session.cwd, args, signal, {
2470
2292
  repoProvided: Boolean(repo),
2471
2293
  });
2472
2294
  return buildTextResult(formatSearchResults("issues", query, repo, items));
@@ -2477,14 +2299,14 @@ export class GhSearchIssuesTool implements AgentTool<typeof ghSearchIssuesSchema
2477
2299
  export class GhSearchPrsTool implements AgentTool<typeof ghSearchPrsSchema, GhToolDetails> {
2478
2300
  readonly name = "gh_search_prs";
2479
2301
  readonly label = "GitHub PR Search";
2480
- readonly description = renderPromptTemplate(ghSearchPrsDescription);
2302
+ readonly description = prompt.render(ghSearchPrsDescription);
2481
2303
  readonly parameters = ghSearchPrsSchema;
2482
2304
  readonly strict = true;
2483
2305
 
2484
2306
  constructor(private readonly session: ToolSession) {}
2485
2307
 
2486
2308
  static createIf(session: ToolSession): GhSearchPrsTool | null {
2487
- if (!isGhAvailable()) return null;
2309
+ if (!git.github.available()) return null;
2488
2310
  return new GhSearchPrsTool(session);
2489
2311
  }
2490
2312
 
@@ -2501,7 +2323,7 @@ export class GhSearchPrsTool implements AgentTool<typeof ghSearchPrsSchema, GhTo
2501
2323
  const limit = resolveSearchLimit(params.limit);
2502
2324
  const args = buildGhSearchArgs("prs", query, limit, repo);
2503
2325
 
2504
- const items = await runGhJson<GhSearchResult[]>(this.session.cwd, args, signal, {
2326
+ const items = await git.github.json<GhSearchResult[]>(this.session.cwd, args, signal, {
2505
2327
  repoProvided: Boolean(repo),
2506
2328
  });
2507
2329
  return buildTextResult(formatSearchResults("pull requests", query, repo, items));
@@ -2512,14 +2334,14 @@ export class GhSearchPrsTool implements AgentTool<typeof ghSearchPrsSchema, GhTo
2512
2334
  export class GhRunWatchTool implements AgentTool<typeof ghRunWatchSchema, GhToolDetails> {
2513
2335
  readonly name = "gh_run_watch";
2514
2336
  readonly label = "GitHub Run Watch";
2515
- readonly description = renderPromptTemplate(ghRunWatchDescription);
2337
+ readonly description = prompt.render(ghRunWatchDescription);
2516
2338
  readonly parameters = ghRunWatchSchema;
2517
2339
  readonly strict = true;
2518
2340
 
2519
2341
  constructor(private readonly session: ToolSession) {}
2520
2342
 
2521
2343
  static createIf(session: ToolSession): GhRunWatchTool | null {
2522
- if (!isGhAvailable()) return null;
2344
+ if (!git.github.available()) return null;
2523
2345
  return new GhRunWatchTool(session);
2524
2346
  }
2525
2347
 
@@ -2613,10 +2435,10 @@ export class GhRunWatchTool implements AgentTool<typeof ghRunWatchSchema, GhTool
2613
2435
  }
2614
2436
  }
2615
2437
 
2616
- const branch = branchInput ?? (await resolveCurrentGitBranch(this.session.cwd, signal));
2438
+ const branch = branchInput ?? (await requireCurrentGitBranch(this.session.cwd, signal));
2617
2439
  const headSha = branchInput
2618
2440
  ? await resolveGitHubBranchHead(this.session.cwd, repo, branch, signal)
2619
- : await resolveCurrentGitHead(this.session.cwd, signal);
2441
+ : await requireCurrentGitHead(this.session.cwd, signal);
2620
2442
  let pollCount = 0;
2621
2443
  let settledSuccessSignature: string | undefined;
2622
2444