Merge PR #5424: fix(github): add dedicated repository file reads (@roboomp)
# Conflicts: # packages/coding-agent/src/prompts/tools/bash.md # packages/coding-agent/src/prompts/tools/read.md
This commit is contained in:
@@ -776,6 +776,7 @@
|
||||
- Fixed `/handoff` and auto-handoff skipping extension lifecycle hooks by emitting cancellable `session_before_switch` hooks and a `session_switch` with `reason: "handoff"` after the replacement session is ready ([#4434](https://github.com/can1357/oh-my-pi/issues/4434)).
|
||||
- Fixed TTSR stream interrupts so only the tool call whose stream matched a rule receives the rule-named abort result; sibling tool-call placeholders now use a neutral abort reason ([#2783](https://github.com/can1357/oh-my-pi/issues/2783)).
|
||||
- Fixed auto-compaction dead-ending in a warning loop ("Compaction freed too little context to make progress") when the single most-recent turn is itself over budget so `prepareCompaction` has nothing to summarize (`findCutPoint` never cuts inside a tool result). This `!preparation` short-circuit never ran the artifact-backed `shake` elide rescue that #3786 added to the post-maintenance guard, so snapcompact/context-full maintenance paused with no attempt to shrink the oversized tail. The dead-end now runs the same elide pass, re-prepares on the shrunken branch, and falls through to a normal compaction when the tail became summarizable — only pausing (single warning) when nothing is elide-eligible. ([#4786](https://github.com/can1357/oh-my-pi/issues/4786))
|
||||
- Fixed GitHub-hosted repository file reads falling back to `curl` by adding a dedicated `github` file-read operation and explicit tool-routing guidance ([#4805](https://github.com/can1357/oh-my-pi/issues/4805)).
|
||||
|
||||
## [16.3.11] - 2026-07-06
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
Runs commands in a persistent shell session.
|
||||
|
||||
Use ONLY for: single binary call or short pipeline that COMPUTES a fact (`wc -l`, `sort | uniq -c`, `comm`, `diff`).
|
||||
GitHub repository file? MUST use `github` `file_read` when available; otherwise `read`. NEVER use `curl`/`wget`.
|
||||
{{#if hasLaunch}}Services, watchers, debuggers, REPLs → `hub` (`op:"start"`).{{/if}}
|
||||
{{#if hasEval}}Inline scripts, heredocs, shell control flow, `$(…)`, multi-stage pipelines, `&&`-chains, quote/JSON escaping → `eval` cells.{{else}}Inline scripts, heredocs, shell control flow, `$(…)`, multi-stage pipelines, `&&`-chains → purpose-built tool or checked-in script.{{/if}}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
Op-based `gh` wrapper: repos, PRs, search, checkout, push, Actions watch. Read an issue/PR via `issue://<N>`/`pr://<N>`. PR diffs: `pr://<N>/diff` (file listing), `pr://<N>/diff/<i>` (file slice, 1-indexed), `pr://<N>/diff/all` (full diff).
|
||||
Op-based `gh` wrapper: repos, repository files, PRs, search, checkout, push, Actions watch. Read an issue/PR via `issue://<N>`/`pr://<N>`. PR diffs: `pr://<N>/diff` (file listing), `pr://<N>/diff/<i>` (file slice, 1-indexed), `pr://<N>/diff/all` (full diff).
|
||||
|
||||
<instruction>
|
||||
Pick op via `op`. Beyond the field descriptions, per op:
|
||||
- `repo_view` — omit `repo` to view the current checkout.
|
||||
- `file_read` — reads `path` from `repo`; omit `repo` for the current checkout and `branch` for its default branch.
|
||||
- `pr_create` — `head` defaults to the current branch.
|
||||
- `pr_checkout` — checks PR(s) out into dedicated git worktrees, not your working tree; pass an array of `pr` to batch multiple in one call.
|
||||
- `pr_push` — requires the branch to have been checked out first via `op: pr_checkout`.
|
||||
@@ -15,3 +16,7 @@ Pick op via `op`. Beyond the field descriptions, per op:
|
||||
<output>
|
||||
Concise summary per op. `run_watch` failures save full logs to a session artifact.
|
||||
</output>
|
||||
|
||||
<critical>
|
||||
GitHub-hosted repository file? MUST use `file_read`; NEVER `curl`/`wget`.
|
||||
</critical>
|
||||
|
||||
@@ -3,6 +3,7 @@ Read files, directories, archives, SQLite, images, documents, internal resources
|
||||
<instruction>
|
||||
- SHOULD parallelize independent reads.
|
||||
- SHOULD use `read` (not browser) for web content; browser only when `read` can't deliver.
|
||||
- GitHub repository file? MUST use `github` `file_read` when available; otherwise `read`. NEVER use `curl`/`wget`.
|
||||
</instruction>
|
||||
|
||||
## Selectors — append `:<sel>` to `path` (e.g. `src/foo.ts:50-200`, `src/foo.ts:raw`, `db.sqlite:users:42`)
|
||||
|
||||
@@ -249,6 +249,7 @@ const RUN_FAILURE_CONCLUSIONS = new Set(["failure", "timed_out", "cancelled", "a
|
||||
const JOB_FAILURE_CONCLUSIONS = new Set(["failure", "timed_out", "cancelled", "action_required"]);
|
||||
const GITHUB_READONLY_OPS: ReadonlySet<string> = new Set([
|
||||
"repo_view",
|
||||
"file_read",
|
||||
"search_issues",
|
||||
"search_prs",
|
||||
"search_code",
|
||||
@@ -259,10 +260,11 @@ const GITHUB_READONLY_OPS: ReadonlySet<string> = new Set([
|
||||
|
||||
const githubSchema = type({
|
||||
op: type(
|
||||
"'repo_view' | 'pr_create' | 'pr_checkout' | 'pr_push' | 'search_issues' | 'search_prs' | 'search_code' | 'search_commits' | 'search_repos' | 'run_watch'",
|
||||
"'repo_view' | 'file_read' | 'pr_create' | 'pr_checkout' | 'pr_push' | 'search_issues' | 'search_prs' | 'search_code' | 'search_commits' | 'search_repos' | 'run_watch'",
|
||||
).describe("github operation"),
|
||||
"repo?": type("string").describe("owner/repo"),
|
||||
"branch?": type("string").describe("branch"),
|
||||
"path?": type("string").describe("repository-relative file path"),
|
||||
"pr?": type("string | string[]").describe("pr number, url, or branch"),
|
||||
"force?": type("boolean").describe("reset existing local branch"),
|
||||
"forceWithLease?": type("boolean").describe("force-with-lease push"),
|
||||
@@ -2455,7 +2457,7 @@ export class GithubTool implements AgentTool<typeof githubSchema, GhToolDetails>
|
||||
const op = typeof rawOp === "string" ? rawOp : "";
|
||||
return GITHUB_READONLY_OPS.has(op) ? "read" : "exec";
|
||||
};
|
||||
readonly summary = "Interact with GitHub issues, pull requests, and repositories";
|
||||
readonly summary = "Interact with GitHub repositories, files, pull requests, and Actions";
|
||||
readonly loadMode = "discoverable";
|
||||
readonly label = "GitHub";
|
||||
readonly description = prompt.render(githubDescription);
|
||||
@@ -2480,6 +2482,8 @@ export class GithubTool implements AgentTool<typeof githubSchema, GhToolDetails>
|
||||
switch (params.op) {
|
||||
case "repo_view":
|
||||
return executeRepoView(this.session, params, signal);
|
||||
case "file_read":
|
||||
return executeFileRead(this.session, params, signal);
|
||||
case "pr_create":
|
||||
return executePrCreate(this.session, params, signal);
|
||||
case "pr_checkout":
|
||||
@@ -2525,6 +2529,40 @@ async function executeRepoView(
|
||||
return buildTextResult(formatRepoView(data, { repo, branch }), data.url);
|
||||
}
|
||||
|
||||
async function executeFileRead(
|
||||
session: ToolSession,
|
||||
params: GithubInput,
|
||||
signal: AbortSignal | undefined,
|
||||
): Promise<AgentToolResult<GhToolDetails>> {
|
||||
const repo = await resolveGitHubRepo(session.cwd, normalizeOptionalString(params.repo), undefined, signal);
|
||||
const filePath = requireNonEmpty(normalizeOptionalString(params.path), "path");
|
||||
if (filePath.startsWith("/")) {
|
||||
throw new ToolError("path must be repository-relative");
|
||||
}
|
||||
const branch = normalizeOptionalString(params.branch);
|
||||
const endpointPath = filePath
|
||||
.split("/")
|
||||
.map(segment => encodeURIComponent(segment))
|
||||
.join("/");
|
||||
const args = [
|
||||
"api",
|
||||
`/repos/${repo}/contents/${endpointPath}`,
|
||||
"--method",
|
||||
"GET",
|
||||
"-H",
|
||||
"Accept: application/vnd.github.raw+json",
|
||||
];
|
||||
if (branch) {
|
||||
args.push("-f", `ref=${branch}`);
|
||||
}
|
||||
const text = await git.github.text(session.cwd, args, signal, {
|
||||
repoProvided: true,
|
||||
trimOutput: false,
|
||||
});
|
||||
const sourceUrl = `https://github.com/${repo}/blob/${encodeURIComponent(branch ?? "HEAD")}/${endpointPath}`;
|
||||
return buildTextResult(text, sourceUrl, { repo, branch });
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Cached issue/PR view fetchers
|
||||
//
|
||||
|
||||
@@ -438,6 +438,35 @@ describe("github tool", () => {
|
||||
expect(text).toContain("Topics: cli, github");
|
||||
});
|
||||
|
||||
it("reads repository files through the GitHub contents API", async () => {
|
||||
const textSpy = vi.spyOn(git.github, "text").mockResolvedValue('{"version":"16.3.11"}\n');
|
||||
const tool = new GithubTool(createSession());
|
||||
const result = await tool.execute("file-read", {
|
||||
op: "file_read",
|
||||
repo: "can1357/oh-my-pi",
|
||||
branch: "main",
|
||||
path: "packages/coding-agent/package.json",
|
||||
});
|
||||
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
|
||||
|
||||
expect(text).toBe('{"version":"16.3.11"}\n');
|
||||
expect(textSpy).toHaveBeenCalledWith(
|
||||
"/tmp/test",
|
||||
[
|
||||
"api",
|
||||
"/repos/can1357/oh-my-pi/contents/packages/coding-agent/package.json",
|
||||
"--method",
|
||||
"GET",
|
||||
"-H",
|
||||
"Accept: application/vnd.github.raw+json",
|
||||
"-f",
|
||||
"ref=main",
|
||||
],
|
||||
undefined,
|
||||
{ repoProvided: true, trimOutput: false },
|
||||
);
|
||||
});
|
||||
|
||||
it("creates a pull request via gh and renders the resulting summary", async () => {
|
||||
const textCalls: string[][] = [];
|
||||
const textSpy = vi.spyOn(git.github, "text").mockImplementation(async (_cwd, args) => {
|
||||
|
||||
Reference in New Issue
Block a user