@oh-my-pi/pi-coding-agent 14.5.8 → 14.5.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/package.json +7 -15
- package/scripts/build-binary.ts +1 -1
- package/src/cli/update-cli.ts +25 -1
- package/src/config/model-registry.ts +21 -19
- package/src/config/settings-schema.ts +14 -19
- package/src/discovery/claude-plugins.ts +28 -3
- package/src/edit/modes/atom.lark +7 -5
- package/src/edit/modes/atom.ts +510 -73
- package/src/edit/modes/hashline.ts +172 -91
- package/src/extensibility/extensions/runner.ts +34 -1
- package/src/extensibility/extensions/types.ts +8 -0
- package/src/lsp/client.ts +27 -35
- package/src/lsp/index.ts +2 -4
- package/src/lsp/render.ts +0 -3
- package/src/lsp/types.ts +1 -4
- package/src/lsp/utils.ts +18 -14
- package/src/memories/index.ts +5 -0
- package/src/modes/components/settings-defs.ts +1 -1
- package/src/modes/controllers/command-controller.ts +17 -0
- package/src/modes/controllers/input-controller.ts +7 -1
- package/src/modes/controllers/selector-controller.ts +2 -2
- package/src/modes/interactive-mode.ts +57 -26
- package/src/modes/theme/theme.ts +10 -1
- package/src/modes/types.ts +5 -3
- package/src/modes/utils/context-usage.ts +294 -0
- package/src/modes/utils/ui-helpers.ts +19 -6
- package/src/prompts/system/auto-continue.md +1 -0
- package/src/prompts/tools/atom.md +99 -44
- package/src/prompts/tools/exit-plan-mode.md +5 -39
- package/src/prompts/tools/github.md +3 -3
- package/src/prompts/tools/lsp.md +2 -3
- package/src/prompts/tools/{run-command.md → recipe.md} +1 -1
- package/src/prompts/tools/task.md +34 -147
- package/src/prompts/tools/todo-write.md +22 -64
- package/src/sdk.ts +13 -2
- package/src/session/agent-session.ts +175 -79
- package/src/session/compaction/compaction.ts +35 -22
- package/src/session/session-dump-format.ts +1 -0
- package/src/session/session-manager.ts +19 -2
- package/src/slash-commands/builtin-registry.ts +12 -5
- package/src/tools/bash.ts +9 -4
- package/src/tools/debug.ts +57 -70
- package/src/tools/gh.ts +267 -119
- package/src/tools/index.ts +7 -7
- package/src/tools/{run-command → recipe}/index.ts +19 -19
- package/src/tools/recipe/render.ts +19 -0
- package/src/tools/{run-command → recipe}/runner.ts +28 -7
- package/src/tools/{run-command → recipe}/runners/pkg.ts +23 -53
- package/src/tools/renderers.ts +2 -2
- package/src/utils/git.ts +61 -2
- package/src/web/search/providers/searxng.ts +71 -13
- package/src/tools/run-command/render.ts +0 -18
- /package/src/tools/{run-command → recipe}/runners/cargo.ts +0 -0
- /package/src/tools/{run-command → recipe}/runners/index.ts +0 -0
- /package/src/tools/{run-command → recipe}/runners/just.ts +0 -0
- /package/src/tools/{run-command → recipe}/runners/make.ts +0 -0
- /package/src/tools/{run-command → recipe}/runners/task.ts +0 -0
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,
|
|
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.
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
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
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
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
|
-
|
|
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
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
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
|
|
1953
|
-
|
|
1954
|
-
|
|
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
|
|
1957
|
-
|
|
1958
|
-
|
|
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
|
|
1972
|
-
|
|
1973
|
-
|
|
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
|
-
|
|
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 =
|
|
1991
|
-
const worktreePath =
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
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
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
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
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
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
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
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
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
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
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
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,
|
package/src/tools/index.ts
CHANGED
|
@@ -37,11 +37,11 @@ import { NotebookTool } from "./notebook";
|
|
|
37
37
|
import { wrapToolWithMetaNotice } from "./output-meta";
|
|
38
38
|
import { PythonTool } from "./python";
|
|
39
39
|
import { ReadTool } from "./read";
|
|
40
|
+
import { RecipeTool } from "./recipe";
|
|
40
41
|
import { RenderMermaidTool } from "./render-mermaid";
|
|
41
42
|
import { createReportToolIssueTool, isAutoQaEnabled } from "./report-tool-issue";
|
|
42
43
|
import { ResolveTool } from "./resolve";
|
|
43
44
|
import { reportFindingTool } from "./review";
|
|
44
|
-
import { RunCommandTool } from "./run-command";
|
|
45
45
|
import { SearchTool } from "./search";
|
|
46
46
|
import { SearchToolBm25Tool } from "./search-tool-bm25";
|
|
47
47
|
import { loadSshTool } from "./ssh";
|
|
@@ -76,11 +76,11 @@ export * from "./job";
|
|
|
76
76
|
export * from "./notebook";
|
|
77
77
|
export * from "./python";
|
|
78
78
|
export * from "./read";
|
|
79
|
+
export * from "./recipe";
|
|
79
80
|
export * from "./render-mermaid";
|
|
80
81
|
export * from "./report-tool-issue";
|
|
81
82
|
export * from "./resolve";
|
|
82
83
|
export * from "./review";
|
|
83
|
-
export * from "./run-command";
|
|
84
84
|
export * from "./search";
|
|
85
85
|
export * from "./search-tool-bm25";
|
|
86
86
|
export * from "./ssh";
|
|
@@ -226,7 +226,7 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
226
226
|
rewind: RewindTool.createIf,
|
|
227
227
|
task: TaskTool.create,
|
|
228
228
|
job: JobTool.createIf,
|
|
229
|
-
|
|
229
|
+
recipe: RecipeTool.createIf,
|
|
230
230
|
irc: IrcTool.createIf,
|
|
231
231
|
todo_write: s => new TodoWriteTool(s),
|
|
232
232
|
web_search: s => new WebSearchTool(s),
|
|
@@ -375,10 +375,10 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
375
375
|
}
|
|
376
376
|
if (
|
|
377
377
|
requestedTools.includes("bash") &&
|
|
378
|
-
!requestedTools.includes("
|
|
379
|
-
session.settings.get("
|
|
378
|
+
!requestedTools.includes("recipe") &&
|
|
379
|
+
session.settings.get("recipe.enabled")
|
|
380
380
|
) {
|
|
381
|
-
requestedTools.push("
|
|
381
|
+
requestedTools.push("recipe");
|
|
382
382
|
}
|
|
383
383
|
}
|
|
384
384
|
const allTools: Record<string, ToolFactory> = { ...BUILTIN_TOOLS, ...HIDDEN_TOOLS };
|
|
@@ -402,7 +402,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
402
402
|
if (name === "browser") return session.settings.get("browser.enabled");
|
|
403
403
|
if (name === "checkpoint" || name === "rewind") return session.settings.get("checkpoint.enabled");
|
|
404
404
|
if (name === "irc") return session.settings.get("irc.enabled");
|
|
405
|
-
if (name === "
|
|
405
|
+
if (name === "recipe") return session.settings.get("recipe.enabled");
|
|
406
406
|
if (name === "task") {
|
|
407
407
|
const maxDepth = session.settings.get("task.maxRecursionDepth") ?? 2;
|
|
408
408
|
const currentDepth = session.taskDepth ?? 0;
|
|
@@ -4,43 +4,43 @@ import { prompt } from "@oh-my-pi/pi-utils";
|
|
|
4
4
|
import { type Static, Type } from "@sinclair/typebox";
|
|
5
5
|
import type { RenderResultOptions } from "../../extensibility/custom-tools/types";
|
|
6
6
|
import type { Theme } from "../../modes/theme/theme";
|
|
7
|
-
import
|
|
7
|
+
import recipeDescription from "../../prompts/tools/recipe.md" with { type: "text" };
|
|
8
8
|
import type { ToolSession } from "..";
|
|
9
9
|
import { type BashRenderContext, BashTool, type BashToolDetails } from "../bash";
|
|
10
|
-
import {
|
|
10
|
+
import { createRecipeToolRenderer, type RecipeRenderArgs } from "./render";
|
|
11
11
|
import { buildPromptModel, type DetectedRunner, resolveCommand } from "./runner";
|
|
12
12
|
import { RUNNERS } from "./runners";
|
|
13
13
|
|
|
14
|
-
const
|
|
14
|
+
const recipeSchema = Type.Object({
|
|
15
15
|
op: Type.String({
|
|
16
16
|
description: 'task name and args, e.g. "test" or "build --release"',
|
|
17
17
|
examples: ["test", "build --release", "pkg:test --watch"],
|
|
18
18
|
}),
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
-
type
|
|
21
|
+
type RecipeParams = Static<typeof recipeSchema>;
|
|
22
22
|
|
|
23
|
-
type
|
|
23
|
+
type RecipeRenderResult = {
|
|
24
24
|
content: Array<{ type: string; text?: string }>;
|
|
25
25
|
details?: BashToolDetails;
|
|
26
26
|
isError?: boolean;
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
-
export class
|
|
30
|
-
readonly name = "
|
|
29
|
+
export class RecipeTool implements AgentTool<typeof recipeSchema, BashToolDetails, Theme> {
|
|
30
|
+
readonly name = "recipe";
|
|
31
31
|
readonly label = "Run";
|
|
32
32
|
readonly description: string;
|
|
33
|
-
readonly parameters =
|
|
33
|
+
readonly parameters = recipeSchema;
|
|
34
34
|
readonly strict = true;
|
|
35
35
|
readonly concurrency = "exclusive";
|
|
36
36
|
readonly mergeCallAndResult = true;
|
|
37
37
|
readonly inline = true;
|
|
38
|
-
readonly renderCall: (args:
|
|
38
|
+
readonly renderCall: (args: RecipeRenderArgs, options: RenderResultOptions, uiTheme: Theme) => Component;
|
|
39
39
|
readonly renderResult: (
|
|
40
|
-
result:
|
|
40
|
+
result: RecipeRenderResult,
|
|
41
41
|
options: RenderResultOptions & { renderContext?: BashRenderContext },
|
|
42
42
|
uiTheme: Theme,
|
|
43
|
-
args?:
|
|
43
|
+
args?: RecipeRenderArgs,
|
|
44
44
|
) => Component;
|
|
45
45
|
|
|
46
46
|
readonly #bash: BashTool;
|
|
@@ -49,30 +49,30 @@ export class RunCommandTool implements AgentTool<typeof runCommandSchema, BashTo
|
|
|
49
49
|
constructor(session: ToolSession, runners: DetectedRunner[]) {
|
|
50
50
|
this.#runners = runners;
|
|
51
51
|
this.#bash = new BashTool(session);
|
|
52
|
-
this.description = prompt.render(
|
|
53
|
-
const renderer =
|
|
52
|
+
this.description = prompt.render(recipeDescription, buildPromptModel(runners));
|
|
53
|
+
const renderer = createRecipeToolRenderer(runners);
|
|
54
54
|
this.renderCall = renderer.renderCall;
|
|
55
55
|
this.renderResult = renderer.renderResult;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
static async createIf(session: ToolSession): Promise<
|
|
59
|
-
if (!session.settings.get("
|
|
58
|
+
static async createIf(session: ToolSession): Promise<RecipeTool | null> {
|
|
59
|
+
if (!session.settings.get("recipe.enabled")) return null;
|
|
60
60
|
const detected = (await Promise.all(RUNNERS.map(runner => runner.detect(session.cwd)))).filter(
|
|
61
61
|
(runner): runner is DetectedRunner => runner !== null && runner.tasks.length > 0,
|
|
62
62
|
);
|
|
63
63
|
if (detected.length === 0) return null;
|
|
64
|
-
return new
|
|
64
|
+
return new RecipeTool(session, detected);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
async execute(
|
|
68
68
|
toolCallId: string,
|
|
69
|
-
{ op }:
|
|
69
|
+
{ op }: RecipeParams,
|
|
70
70
|
signal?: AbortSignal,
|
|
71
71
|
onUpdate?: AgentToolUpdateCallback<BashToolDetails>,
|
|
72
72
|
ctx?: AgentToolContext,
|
|
73
73
|
): Promise<AgentToolResult<BashToolDetails>> {
|
|
74
|
-
const command = resolveCommand(op, this.#runners);
|
|
75
|
-
return await this.#bash.execute(toolCallId, { command }, signal, onUpdate, ctx);
|
|
74
|
+
const { command, cwd } = resolveCommand(op, this.#runners);
|
|
75
|
+
return await this.#bash.execute(toolCallId, { command, cwd }, signal, onUpdate, ctx);
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createShellRenderer } from "../bash";
|
|
2
|
+
import type { DetectedRunner } from "./runner";
|
|
3
|
+
import { commandFromOp, cwdFromOp, titleFromOp } from "./runner";
|
|
4
|
+
|
|
5
|
+
export interface RecipeRenderArgs {
|
|
6
|
+
op?: string;
|
|
7
|
+
__partialJson?: string;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createRecipeToolRenderer(runners: DetectedRunner[]) {
|
|
12
|
+
return createShellRenderer<RecipeRenderArgs>({
|
|
13
|
+
resolveTitle: args => titleFromOp(args?.op, runners),
|
|
14
|
+
resolveCommand: args => commandFromOp(args?.op, runners),
|
|
15
|
+
resolveCwd: args => cwdFromOp(args?.op, runners),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const recipeToolRenderer = createRecipeToolRenderer([]);
|