@oh-my-pi/pi-coding-agent 14.5.9 → 14.5.11

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 (37) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/package.json +7 -15
  3. package/scripts/build-binary.ts +1 -1
  4. package/src/cli/update-cli.ts +25 -1
  5. package/src/config/model-registry.ts +21 -19
  6. package/src/config/settings-schema.ts +11 -16
  7. package/src/discovery/claude-plugins.ts +28 -3
  8. package/src/edit/modes/atom.ts +50 -19
  9. package/src/edit/modes/hashline.ts +171 -110
  10. package/src/export/html/template.generated.ts +1 -1
  11. package/src/export/html/template.js +14 -2
  12. package/src/extensibility/extensions/runner.ts +34 -1
  13. package/src/extensibility/extensions/types.ts +8 -0
  14. package/src/internal-urls/docs-index.generated.ts +54 -54
  15. package/src/lsp/client.ts +27 -35
  16. package/src/memories/index.ts +5 -0
  17. package/src/modes/components/settings-defs.ts +1 -1
  18. package/src/modes/controllers/selector-controller.ts +2 -2
  19. package/src/modes/controllers/todo-command-controller.ts +22 -74
  20. package/src/modes/interactive-mode.ts +36 -9
  21. package/src/modes/theme/theme.ts +10 -1
  22. package/src/modes/types.ts +1 -3
  23. package/src/modes/utils/ui-helpers.ts +19 -6
  24. package/src/prompts/system/auto-continue.md +1 -0
  25. package/src/prompts/system/eager-todo.md +1 -1
  26. package/src/prompts/tools/github.md +3 -3
  27. package/src/prompts/tools/todo-write.md +19 -19
  28. package/src/sdk.ts +13 -2
  29. package/src/session/agent-session.ts +196 -96
  30. package/src/session/session-manager.ts +19 -2
  31. package/src/tools/bash.ts +9 -4
  32. package/src/tools/gh.ts +267 -119
  33. package/src/tools/todo-write.ts +157 -195
  34. package/src/utils/git.ts +61 -2
  35. package/src/web/search/providers/searxng.ts +71 -13
  36. package/examples/custom-tools/todo/index.ts +0 -211
  37. package/examples/extensions/todo.ts +0 -295
@@ -566,6 +566,14 @@ export function buildSessionContext(
566
566
  let hasPersistedMCPToolSelection = false;
567
567
  let mode = "none";
568
568
  let modeData: Record<string, unknown> | undefined;
569
+ // Track whether an explicit `model_change` with role="default" has been
570
+ // seen on this path. Once a user (or the agent itself) records an
571
+ // explicit default, later assistant-message inference must NOT overwrite
572
+ // it: temporary fallbacks (retry fallback, context promotion) and
573
+ // server-side model downgrades both produce assistant messages tagged
574
+ // with the wrong model id, which previously clobbered the user's pick on
575
+ // resume (issue #849).
576
+ let hasExplicitDefaultModel = false;
569
577
 
570
578
  for (const entry of path) {
571
579
  if (entry.type === "thinking_level_change") {
@@ -575,12 +583,21 @@ export function buildSessionContext(
575
583
  if (entry.model) {
576
584
  const role = entry.role ?? "default";
577
585
  models[role] = entry.model;
586
+ if (role === "default") {
587
+ hasExplicitDefaultModel = true;
588
+ }
578
589
  }
579
590
  } else if (entry.type === "service_tier_change") {
580
591
  serviceTier = entry.serviceTier ?? undefined;
581
592
  } else if (entry.type === "message" && entry.message.role === "assistant") {
582
- // Infer default model from assistant messages
583
- models.default = `${entry.message.provider}/${entry.message.model}`;
593
+ // Legacy fallback: infer default model from assistant messages only
594
+ // when no explicit `model_change` (role=default) entry has been
595
+ // recorded yet. Newer sessions always record an explicit default
596
+ // model_change at the start of the conversation, so this branch is
597
+ // only used to keep pre-model_change sessions working.
598
+ if (!hasExplicitDefaultModel) {
599
+ models.default = `${entry.message.provider}/${entry.message.model}`;
600
+ }
584
601
  } else if (entry.type === "compaction") {
585
602
  compaction = entry;
586
603
  } else if (entry.type === "ttsr_injection") {
package/src/tools/bash.ts CHANGED
@@ -508,12 +508,17 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
508
508
  const headLines = head;
509
509
  const tailLines = tail;
510
510
 
511
- // Check interception if enabled and available tools are known
511
+ // Check both the original command and the cwd-normalized command so
512
+ // leading `cd ... &&` wrappers do not hide either shell-navigation rules
513
+ // or the dedicated-tool command that follows the directory change.
512
514
  if (this.session.settings.get("bashInterceptor.enabled")) {
513
515
  const rules = this.session.settings.getBashInterceptorRules();
514
- const interception = checkBashInterception(command, ctx?.toolNames ?? [], rules);
515
- if (interception.block) {
516
- throw new ToolError(interception.message ?? "Command blocked");
516
+ const commandsToCheck = rawCommand === command ? [command] : [rawCommand, command];
517
+ for (const commandToCheck of commandsToCheck) {
518
+ const interception = checkBashInterception(commandToCheck, ctx?.toolNames ?? [], rules);
519
+ if (interception.block) {
520
+ throw new ToolError(interception.message ?? "Command blocked");
521
+ }
517
522
  }
518
523
  }
519
524
 
package/src/tools/gh.ts CHANGED
@@ -2,7 +2,7 @@ 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
4
  import { StringEnum } from "@oh-my-pi/pi-ai";
5
- import { abortableSleep, isEnoent, prompt, untilAborted } from "@oh-my-pi/pi-utils";
5
+ import { abortableSleep, getWorktreesDir, isEnoent, prompt, untilAborted } from "@oh-my-pi/pi-utils";
6
6
  import { type Static, Type } from "@sinclair/typebox";
7
7
  import githubDescription from "../prompts/tools/github.md" with { type: "text" };
8
8
  import * as git from "../utils/git";
@@ -151,7 +151,7 @@ const githubSchema = Type.Object({
151
151
  ),
152
152
  branch: Type.Optional(
153
153
  Type.String({
154
- description: "branch (repo_view, pr_checkout local branch, pr_push local branch, run_watch)",
154
+ description: "branch (repo_view, pr_push local branch, run_watch)",
155
155
  examples: ["main", "develop"],
156
156
  }),
157
157
  ),
@@ -162,10 +162,18 @@ const githubSchema = Type.Object({
162
162
  }),
163
163
  ),
164
164
  pr: Type.Optional(
165
- Type.String({
166
- description: "pr number, url, or branch (pr_view, pr_diff, pr_checkout)",
167
- examples: ["123", "feature-branch"],
168
- }),
165
+ Type.Union(
166
+ [
167
+ Type.String({ examples: ["123", "feature-branch"] }),
168
+ Type.Array(Type.String(), {
169
+ examples: [["123", "456"]],
170
+ }),
171
+ ],
172
+ {
173
+ description:
174
+ "pr number, url, or branch (pr_view, pr_diff, pr_checkout); pass an array to batch-process multiple pull requests in one call",
175
+ },
176
+ ),
169
177
  ),
170
178
  comments: Type.Optional(Type.Boolean({ description: "include comments (issue_view, pr_view)", default: true })),
171
179
  nameOnly: Type.Optional(Type.Boolean({ description: "return file names only (pr_diff)" })),
@@ -174,7 +182,6 @@ const githubSchema = Type.Object({
174
182
  description: "file globs to exclude (pr_diff)",
175
183
  }),
176
184
  ),
177
- worktree: Type.Optional(Type.String({ description: "worktree path (pr_checkout)" })),
178
185
  force: Type.Optional(Type.Boolean({ description: "reset existing local branch (pr_checkout)" })),
179
186
  forceWithLease: Type.Optional(Type.Boolean({ description: "force-with-lease push (pr_push)" })),
180
187
  query: Type.Optional(
@@ -205,6 +212,17 @@ export interface GhToolDetails {
205
212
  conclusion?: string;
206
213
  failedJobs?: string[];
207
214
  watch?: GhRunWatchViewDetails;
215
+ checkouts?: GhPrCheckoutSummary[];
216
+ }
217
+
218
+ export interface GhPrCheckoutSummary {
219
+ prNumber?: number;
220
+ url?: string;
221
+ branch: string;
222
+ worktreePath: string;
223
+ remote: string;
224
+ remoteBranch: string;
225
+ reused: boolean;
208
226
  }
209
227
 
210
228
  export interface GhRunWatchJobDetails {
@@ -482,6 +500,17 @@ function normalizeOptionalString(value: string | null | undefined): string | und
482
500
  return normalized ? normalized : undefined;
483
501
  }
484
502
 
503
+ function normalizePrIdentifierList(value: string | string[] | undefined): string[] {
504
+ if (value === undefined) return [];
505
+ const raw = typeof value === "string" ? [value] : value;
506
+ const cleaned: string[] = [];
507
+ for (const entry of raw) {
508
+ const trimmed = entry?.trim();
509
+ if (trimmed) cleaned.push(trimmed);
510
+ }
511
+ return cleaned;
512
+ }
513
+
485
514
  function requireNonEmpty(value: string | null | undefined, label: string): string {
486
515
  const normalized = normalizeOptionalString(value);
487
516
  if (!normalized) {
@@ -543,6 +572,19 @@ function sanitizeRemoteName(value: string): string {
543
572
  return sanitized.length > 0 ? `fork-${sanitized}` : "fork";
544
573
  }
545
574
 
575
+ /**
576
+ * Encode an absolute repository path into a single filesystem-safe segment.
577
+ * Mirrors the legacy session-dir encoding used elsewhere in the project: drop
578
+ * the leading separator, then collapse `/`, `\\`, and `:` to `-`. The result
579
+ * is not strictly injective for pathological inputs (e.g. `/a/b` vs `/a-b`)
580
+ * but matches the rest of the codebase and stays human-readable.
581
+ */
582
+ function encodeRepoPathForFilesystem(repoPath: string): string {
583
+ const resolved = path.resolve(repoPath);
584
+ const encoded = resolved.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-");
585
+ return encoded || "root";
586
+ }
587
+
546
588
  function toLocalBranchRef(value: string): string {
547
589
  return `refs/heads/${value}`;
548
590
  }
@@ -1908,24 +1950,40 @@ async function executePrView(
1908
1950
  params: GithubInput,
1909
1951
  signal: AbortSignal | undefined,
1910
1952
  ): Promise<AgentToolResult<GhToolDetails>> {
1911
- const pr = normalizeOptionalString(params.pr);
1912
1953
  const repo = normalizeOptionalString(params.repo);
1913
1954
  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(","));
1955
+ const prList = normalizePrIdentifierList(params.pr);
1956
+ const prRefs: (string | undefined)[] = prList.length > 0 ? prList : [undefined];
1957
+
1958
+ const views = await Promise.all(
1959
+ prRefs.map(async prRef => {
1960
+ const args = ["pr", "view"];
1961
+ if (prRef) args.push(prRef);
1962
+ appendRepoFlag(args, repo, prRef);
1963
+ args.push("--json", (includeComments ? GH_PR_FIELDS : GH_PR_FIELDS_NO_COMMENTS).join(","));
1964
+
1965
+ const data = await git.github.json<GhPrViewData>(session.cwd, args, signal, {
1966
+ repoProvided: Boolean(repo),
1967
+ });
1968
+ const resolvedRepo = repo ?? parsePullRequestUrl(data.url).repo;
1969
+ if (includeComments && resolvedRepo && typeof data.number === "number") {
1970
+ data.reviewComments = await fetchPrReviewComments(session.cwd, resolvedRepo, data.number, signal);
1971
+ }
1972
+ return { prRef, data };
1973
+ }),
1974
+ );
1920
1975
 
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);
1976
+ if (views.length === 1) {
1977
+ const [view] = views;
1978
+ return buildTextResult(
1979
+ formatPrView(view.data, { pr: view.prRef, repo, comments: includeComments }),
1980
+ view.data.url,
1981
+ );
1927
1982
  }
1928
- return buildTextResult(formatPrView(data, { pr, repo, comments: includeComments }), data.url);
1983
+
1984
+ const sections = views.map(view => formatPrView(view.data, { pr: view.prRef, repo, comments: includeComments }));
1985
+ const text = [`# ${views.length} Pull Requests`, "", ...joinSections(sections)].join("\n").trim();
1986
+ return buildTextResult(text);
1929
1987
  }
1930
1988
 
1931
1989
  async function executePrDiff(
@@ -1933,29 +1991,51 @@ async function executePrDiff(
1933
1991
  params: GithubInput,
1934
1992
  signal: AbortSignal | undefined,
1935
1993
  ): Promise<AgentToolResult<GhToolDetails>> {
1936
- const pr = normalizeOptionalString(params.pr);
1937
1994
  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);
1995
+ const prList = normalizePrIdentifierList(params.pr);
1996
+ const prRefs: (string | undefined)[] = prList.length > 0 ? prList : [undefined];
1997
+
1998
+ const diffs = await Promise.all(
1999
+ prRefs.map(async prRef => {
2000
+ const args = ["pr", "diff"];
2001
+ if (prRef) args.push(prRef);
2002
+ appendRepoFlag(args, repo, prRef);
2003
+ args.push("--color", "never");
2004
+ if (params.nameOnly) args.push("--name-only");
2005
+ for (const pattern of params.exclude ?? []) {
2006
+ args.push("--exclude", requireNonEmpty(pattern, "exclude pattern"));
2007
+ }
2008
+ const output = await git.github.text(session.cwd, args, signal, {
2009
+ repoProvided: Boolean(repo),
2010
+ trimOutput: false,
2011
+ });
2012
+ return { prRef, output };
2013
+ }),
2014
+ );
2015
+
2016
+ const singleTitle = params.nameOnly ? "# Pull Request Files" : "# Pull Request Diff";
2017
+ const emptyBody = params.nameOnly ? "No changed files." : "No diff output.";
2018
+
2019
+ if (diffs.length === 1) {
2020
+ const [diff] = diffs;
2021
+ const body = diff.output.length > 0 ? diff.output : emptyBody;
2022
+ return buildTextResult(`${singleTitle}\n\n${body}`);
1950
2023
  }
1951
2024
 
1952
- const output = await git.github.text(session.cwd, args, signal, {
1953
- repoProvided: Boolean(repo),
1954
- trimOutput: false,
2025
+ const header = params.nameOnly
2026
+ ? `# ${diffs.length} Pull Request File Lists`
2027
+ : `# ${diffs.length} Pull Request Diffs`;
2028
+ const sections = diffs.map(diff => {
2029
+ const label = diff.prRef ? `PR ${diff.prRef}` : "PR (current branch)";
2030
+ const body = diff.output.length > 0 ? diff.output : emptyBody;
2031
+ return `## ${label}\n\n${body}`;
1955
2032
  });
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}`);
2033
+ const text = [header, "", ...joinSections(sections)].join("\n").trim();
2034
+ return buildTextResult(text);
2035
+ }
2036
+
2037
+ function joinSections(sections: string[]): string[] {
2038
+ return sections.flatMap((section, idx) => (idx === 0 ? [section] : ["", "---", "", section]));
1959
2039
  }
1960
2040
 
1961
2041
  async function executePrCheckout(
@@ -1963,16 +2043,68 @@ async function executePrCheckout(
1963
2043
  params: GithubInput,
1964
2044
  signal: AbortSignal | undefined,
1965
2045
  ): Promise<AgentToolResult<GhToolDetails>> {
1966
- const pr = normalizeOptionalString(params.pr);
1967
2046
  const repo = normalizeOptionalString(params.repo);
1968
- const requestedBranch = normalizeOptionalString(params.branch);
1969
- const requestedWorktree = normalizeOptionalString(params.worktree);
1970
2047
  const force = params.force ?? false;
1971
- const args = ["pr", "view"];
1972
- if (pr) {
1973
- args.push(pr);
2048
+ const prList = normalizePrIdentifierList(params.pr);
2049
+ const prRefs = prList.length > 0 ? prList : [undefined];
2050
+ const isMulti = prRefs.length > 1;
2051
+
2052
+ const outcomes = await Promise.all(
2053
+ prRefs.map(prRef => checkoutPullRequest(session, signal, { prRef, repo, force })),
2054
+ );
2055
+
2056
+ if (!isMulti) {
2057
+ const [outcome] = outcomes;
2058
+ return buildTextResult(formatPrCheckoutResult(outcome), outcome.data.url, {
2059
+ repo: repo ?? outcome.data.headRepository?.nameWithOwner,
2060
+ branch: outcome.localBranch,
2061
+ worktreePath: outcome.worktreePath,
2062
+ remote: outcome.remoteName,
2063
+ remoteBranch: outcome.headRefName,
2064
+ checkouts: [outcomeToSummary(outcome)],
2065
+ });
1974
2066
  }
1975
- appendRepoFlag(args, repo, pr);
2067
+
2068
+ const sections = outcomes.map(formatPrCheckoutResult);
2069
+ const reusedCount = outcomes.reduce((acc, o) => acc + (o.reused ? 1 : 0), 0);
2070
+ const newCount = outcomes.length - reusedCount;
2071
+ const headerParts: string[] = [];
2072
+ if (newCount > 0) headerParts.push(`${newCount} checked out`);
2073
+ if (reusedCount > 0) headerParts.push(`${reusedCount} reused`);
2074
+ const header = `# ${outcomes.length} Pull Request Worktrees (${headerParts.join(", ")})`;
2075
+ const text = [header, "", ...joinSections(sections)].join("\n").trim();
2076
+
2077
+ return buildTextResult(text, undefined, {
2078
+ repo,
2079
+ checkouts: outcomes.map(outcomeToSummary),
2080
+ });
2081
+ }
2082
+
2083
+ interface PrCheckoutOptions {
2084
+ prRef: string | undefined;
2085
+ repo: string | undefined;
2086
+ force: boolean;
2087
+ }
2088
+
2089
+ interface PrCheckoutOutcome {
2090
+ data: GhPrViewData;
2091
+ localBranch: string;
2092
+ worktreePath: string;
2093
+ remoteName: string;
2094
+ remoteUrl: string;
2095
+ headRefName: string;
2096
+ reused: boolean;
2097
+ }
2098
+
2099
+ async function checkoutPullRequest(
2100
+ session: ToolSession,
2101
+ signal: AbortSignal | undefined,
2102
+ options: PrCheckoutOptions,
2103
+ ): Promise<PrCheckoutOutcome> {
2104
+ const { prRef, repo, force } = options;
2105
+ const args = ["pr", "view"];
2106
+ if (prRef) args.push(prRef);
2107
+ appendRepoFlag(args, repo, prRef);
1976
2108
  args.push("--json", GH_PR_CHECKOUT_FIELDS.join(","));
1977
2109
 
1978
2110
  const data = await git.github.json<GhPrViewData>(session.cwd, args, signal, {
@@ -1987,89 +2119,105 @@ async function executePrCheckout(
1987
2119
  const headRefOid = requireNonEmpty(data.headRefOid, "head commit");
1988
2120
  const repoRoot = await requireGitRepoRoot(session.cwd, signal);
1989
2121
  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(
2122
+ const localBranch = `pr-${prNumber}`;
2123
+ const worktreePath = path.join(getWorktreesDir(), encodeRepoPathForFilesystem(primaryRepoRoot), localBranch);
2124
+
2125
+ // Every git mutation against `repoRoot` from here on must run under the
2126
+ // per-repo lock. Worktrees of the same primary repo share `.git/config`,
2127
+ // `commit-graph` chain, `packed-refs`, and worktree metadata files — git
2128
+ // uses O_EXCL lock files for each, with no waiter. Concurrent in-process
2129
+ // callers (e.g. parallel `pr_checkout` calls) would otherwise lose lock
2130
+ // races and surface "could not lock config file" / "Another git process
2131
+ // seems to be running" errors. The gh API call above stays outside the
2132
+ // lock so multiple checkouts can fetch PR metadata in parallel.
2133
+ return git.withRepoLock(
1999
2134
  repoRoot,
2000
- remote.name,
2001
- `refs/heads/${headRefName}`,
2002
- `refs/remotes/${remote.name}/${headRefName}`,
2003
- signal,
2004
- );
2135
+ async () => {
2136
+ const existingWorktrees = await git.worktree.list(repoRoot, signal);
2137
+ const existingWorktree = existingWorktrees.find(entry => entry.branch === toLocalBranchRef(localBranch));
2138
+
2139
+ const remote = await ensurePrRemote(repoRoot, data, signal);
2140
+ await git.fetch(
2141
+ repoRoot,
2142
+ remote.name,
2143
+ `refs/heads/${headRefName}`,
2144
+ `refs/remotes/${remote.name}/${headRefName}`,
2145
+ signal,
2146
+ );
2005
2147
 
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
- );
2148
+ if (!existingWorktree) {
2149
+ const localBranchRef = toLocalBranchRef(localBranch);
2150
+ const localBranchExists = await git.ref.exists(repoRoot, localBranchRef, signal);
2151
+ if (localBranchExists) {
2152
+ const existingOid = await git.ref.resolve(repoRoot, localBranchRef, signal);
2153
+ if (existingOid !== headRefOid) {
2154
+ if (!force) {
2155
+ throw new ToolError(
2156
+ `local branch ${localBranch} already exists at ${formatShortSha(existingOid ?? undefined) ?? existingOid ?? "unknown commit"}; pass force=true to reset it`,
2157
+ );
2158
+ }
2159
+
2160
+ await git.branch.force(repoRoot, localBranch, `refs/remotes/${remote.name}/${headRefName}`, signal);
2161
+ }
2162
+ } else {
2163
+ await git.branch.create(repoRoot, localBranch, `refs/remotes/${remote.name}/${headRefName}`, signal);
2016
2164
  }
2017
-
2018
- await git.branch.force(repoRoot, localBranch, `refs/remotes/${remote.name}/${headRefName}`, signal);
2019
2165
  }
2020
- } else {
2021
- await git.branch.create(repoRoot, localBranch, `refs/remotes/${remote.name}/${headRefName}`, signal);
2022
- }
2023
- }
2024
2166
 
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
- );
2167
+ await git.config.setBranch(repoRoot, localBranch, "remote", remote.name, signal);
2168
+ await git.config.setBranch(repoRoot, localBranch, "merge", `refs/heads/${headRefName}`, signal);
2169
+ await git.config.setBranch(repoRoot, localBranch, "pushRemote", remote.name, signal);
2170
+ await git.config.setBranch(repoRoot, localBranch, "ompPrHeadRef", headRefName, signal);
2171
+ await git.config.setBranch(repoRoot, localBranch, "ompPrUrl", data.url ?? "", signal);
2172
+ await git.config.setBranch(
2173
+ repoRoot,
2174
+ localBranch,
2175
+ "ompPrIsCrossRepository",
2176
+ String(Boolean(data.isCrossRepository)),
2177
+ signal,
2178
+ );
2179
+ await git.config.setBranch(
2180
+ repoRoot,
2181
+ localBranch,
2182
+ "ompPrMaintainerCanModify",
2183
+ String(Boolean(data.maintainerCanModify)),
2184
+ signal,
2185
+ );
2044
2186
 
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);
2187
+ const finalWorktreePath = existingWorktree?.path ?? worktreePath;
2188
+ if (!existingWorktree) {
2189
+ await ensureGitWorktreePathAvailable(finalWorktreePath, existingWorktrees);
2190
+ await fs.mkdir(path.dirname(finalWorktreePath), { recursive: true });
2191
+ await git.worktree.add(repoRoot, finalWorktreePath, localBranch, { signal });
2192
+ }
2193
+ const resolvedWorktreePath = await fs.realpath(finalWorktreePath);
2052
2194
 
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,
2195
+ return {
2196
+ data,
2197
+ localBranch,
2198
+ worktreePath: resolvedWorktreePath,
2199
+ remoteName: remote.name,
2200
+ remoteUrl: remote.url,
2201
+ headRefName,
2202
+ reused: Boolean(existingWorktree),
2203
+ };
2069
2204
  },
2205
+ signal,
2070
2206
  );
2071
2207
  }
2072
2208
 
2209
+ function outcomeToSummary(outcome: PrCheckoutOutcome): GhPrCheckoutSummary {
2210
+ return {
2211
+ prNumber: typeof outcome.data.number === "number" ? outcome.data.number : undefined,
2212
+ url: outcome.data.url ?? undefined,
2213
+ branch: outcome.localBranch,
2214
+ worktreePath: outcome.worktreePath,
2215
+ remote: outcome.remoteName,
2216
+ remoteBranch: outcome.headRefName,
2217
+ reused: outcome.reused,
2218
+ };
2219
+ }
2220
+
2073
2221
  async function executePrPush(
2074
2222
  session: ToolSession,
2075
2223
  params: GithubInput,