feat(coding-agent/tools): defaulted GitHub search repo scope to the current checkout
- Added a `resolveSearchRepoScope` helper that uses an explicit `repo` when provided, skips defaulting when a query already contains a repo/org/user/owner scope qualifier, and otherwise resolves the current checkout via `resolveDefaultRepoMemoized`. - Updated `search_issues`, `search_prs`, `search_code`, and `search_commits` to use the resolver before composing API queries, defaulting `repo` when omitted but silently falling back to an unscoped search on resolution failure. - Documented the new search-repo defaulting rules in tool prompts, user docs, and the package changelog.
This commit is contained in:
@@ -18,7 +18,7 @@
|
||||
| Field | Type | Required | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `op` | `"repo_view" \| "pr_create" \| "pr_checkout" \| "pr_push" \| "search_issues" \| "search_prs" \| "search_code" \| "search_commits" \| "search_repos" \| "run_watch"` | Yes | Dispatch selector. `GithubTool.execute()` switches only on this field. |
|
||||
| `repo` | `string` | No | `owner/repo` override. Ignored when the identifier argument is already a full GitHub URL. Required in practice when `gh` cannot infer repo context from the current checkout. |
|
||||
| `repo` | `string` | No | `owner/repo` override. Ignored when the identifier argument is already a full GitHub URL. For `search_issues`/`search_prs`/`search_code`/`search_commits`, defaults to the current checkout's `owner/repo` when omitted (skipped when the query already contains a `repo:`/`org:`/`user:`/`owner:` qualifier or when current-repo resolution fails). Required in practice when `gh` cannot infer repo context from the current checkout. |
|
||||
| `branch` | `string` | No | Used by `repo_view`, `pr_push`, and `run_watch`. `run_watch` falls back to current git branch when `run` is omitted; `pr_push` falls back to current branch. |
|
||||
| `pr` | `string \| string[]` | No | Used by `pr_checkout`. Each item may be a PR number, branch name, or GitHub PR URL. Array form enables batching. Omitted means current branch PR. |
|
||||
| `force` | `boolean` | No | Used only by `pr_checkout`. Defaults to `false`; allows resetting an existing `pr-<number>` local branch to the PR head commit. |
|
||||
@@ -148,6 +148,8 @@ Push target resolution reads the `branch.<name>.ompPrHeadRef`, `pushRemote`/`rem
|
||||
| Batching | None |
|
||||
| Output | `# GitHub issues search`, echoed query, optional repo, result count, then one bullet per issue with repo/state/author/labels/timestamps/URL. |
|
||||
|
||||
`repo` defaults to the current checkout's `owner/repo` via `resolveSearchRepoScope()` when omitted. The default is suppressed when the query already contains a leading `repo:`/`org:`/`user:`/`owner:` qualifier or when `gh repo view` fails to resolve the current checkout (e.g. outside a github remote).
|
||||
|
||||
### `search_prs`
|
||||
|
||||
| Aspect | Value |
|
||||
@@ -158,6 +160,8 @@ Push target resolution reads the `branch.<name>.ompPrHeadRef`, `pushRemote`/`rem
|
||||
| Batching | None |
|
||||
| Output | Same shape as `search_issues`, labeled as pull requests. |
|
||||
|
||||
`repo` defaults to the current checkout's `owner/repo` as in `search_issues`.
|
||||
|
||||
### `search_code`
|
||||
|
||||
| Aspect | Value |
|
||||
@@ -168,6 +172,8 @@ Push target resolution reads the `branch.<name>.ompPrHeadRef`, `pushRemote`/`rem
|
||||
| Batching | None |
|
||||
| Output | `# GitHub code search`, result count, then one bullet per match with path, repo, short commit SHA, URL, and first normalized text-match fragment line when present. |
|
||||
|
||||
`repo` defaults to the current checkout's `owner/repo` as in `search_issues`.
|
||||
|
||||
### `search_commits`
|
||||
|
||||
| Aspect | Value |
|
||||
@@ -178,6 +184,8 @@ Push target resolution reads the `branch.<name>.ompPrHeadRef`, `pushRemote`/`rem
|
||||
| Batching | None |
|
||||
| Output | `# GitHub commits search`, result count, then one bullet per commit: short SHA + first commit-message line, repo, author, date, URL. |
|
||||
|
||||
`repo` defaults to the current checkout's `owner/repo` as in `search_issues`.
|
||||
|
||||
### `search_repos`
|
||||
|
||||
| Aspect | Value |
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed the `github` tool's search ops (`search_issues`, `search_prs`, `search_code`, `search_commits`) to default the `repo` scope to the current checkout's `owner/repo` when `repo` is omitted. The auto-scope is skipped when the query already carries an explicit `repo:`/`org:`/`user:`/`owner:` qualifier or when `gh repo view` cannot resolve a github remote (in which case the search proceeds across all of GitHub as before). `search_repos` is unchanged — repository-scoping there must live in the query.
|
||||
|
||||
- Changed bash command preprocessing to strip trailing `| head` and `| tail` pipelines (including `|&`) from each top-level segment in command chains separated by `;`, `&&`, `||`, or `&`
|
||||
- Changed bash fixup notices to state that stderr is already merged into stdout and to reflect that fixes were applied for multiple stripped segments when several transforms fire
|
||||
- Changed shell-minimizer per-line truncation marker from a bare `…` to `…[+N]`, where `N` is the count of dropped Unicode scalars. The bracketed tally disambiguates minimizer-driven cuts from genuine `…` characters in the source (paths, JSON, stack traces, etc.) and gives the agent an exact count so it can decide whether the missing tail is recoverable inline or warrants reading the `[raw output: artifact://<id>]` footer the bash wrapper already emits when the minimizer rewrites output. Affects pipeline Stage 5 (`truncate_lines_at` in `defs/*.toml`) and the internal callers in `filters/git.rs`, `filters/listing.rs`, and `filters/lint.rs`. ([#1046](https://github.com/can1357/oh-my-pi/issues/1046))
|
||||
|
||||
@@ -6,10 +6,10 @@ Pick the operation via `op`. Each op uses a subset of the parameters:
|
||||
- `pr_create` — Create a pull request. Either provide `title` (and optional `body`) or set `fill: true` to auto-fill from commits. Optional `base` (target, defaults to repo default), `head` (source, defaults to current branch), `draft`, `repo`, `reviewer[]`, `assignee[]`, `label[]`. Returns the new PR URL plus a summary.
|
||||
- `pr_checkout` — Check one or more pull requests out into dedicated git worktrees. Optional `pr` (number, URL, branch, or array of any of those — pass an array to batch-check-out multiple PRs in one call), `repo`, `force` (reset existing local branch).
|
||||
- `pr_push` — Push a checked-out PR branch back to its source branch. Requires the branch to have been checked out via `op: pr_checkout` (carries push metadata). Optional `branch`; defaults to the current checked-out git branch. Optional `forceWithLease`.
|
||||
- `search_issues` — Search issues using normal GitHub issue search syntax. Optional `query` (required unless `since`/`until` is set), `repo`, `limit`, `since`, `until`, `dateField`.
|
||||
- `search_prs` — Search pull requests using normal GitHub PR search syntax. Optional `query` (required unless `since`/`until` is set), `repo`, `limit`, `since`, `until`, `dateField`.
|
||||
- `search_code` — Search code with GitHub code search syntax. Required `query`. Optional `repo`, `limit`. Returns matching paths with surrounding fragments. Date filtering (`since`/`until`) is **not** supported by GitHub code search.
|
||||
- `search_commits` — Search commits across GitHub. Optional `query` (required unless `since`/`until` is set), `repo`, `limit`, `since`, `until`. `dateField` is ignored — always uses `committer-date`.
|
||||
- `search_issues` — Search issues using normal GitHub issue search syntax. Optional `query` (required unless `since`/`until` is set), `repo`, `limit`, `since`, `until`, `dateField`. Defaults `repo` to the current checkout's `owner/repo` when omitted; pass an explicit `repo:`/`org:`/`user:` qualifier in `query` to search outside it.
|
||||
- `search_prs` — Search pull requests using normal GitHub PR search syntax. Optional `query` (required unless `since`/`until` is set), `repo`, `limit`, `since`, `until`, `dateField`. Defaults `repo` to the current checkout's `owner/repo` when omitted; pass an explicit `repo:`/`org:`/`user:` qualifier in `query` to search outside it.
|
||||
- `search_code` — Search code with GitHub code search syntax. Required `query`. Optional `repo`, `limit`. Returns matching paths with surrounding fragments. Defaults `repo` to the current checkout's `owner/repo` when omitted; pass an explicit `repo:`/`org:`/`user:` qualifier in `query` to search outside it. Date filtering (`since`/`until`) is **not** supported by GitHub code search.
|
||||
- `search_commits` — Search commits across GitHub. Optional `query` (required unless `since`/`until` is set), `repo`, `limit`, `since`, `until`. `dateField` is ignored — always uses `committer-date`. Defaults `repo` to the current checkout's `owner/repo` when omitted; pass an explicit `repo:`/`org:`/`user:` qualifier in `query` to search outside it.
|
||||
- `search_repos` — Search repositories across GitHub. Optional `query` (required unless `since`/`until` is set), `limit`, `since`, `until`, `dateField` (use query qualifiers like `org:`, `language:` instead of `repo`).
|
||||
- Date filter format for `since` / `until`: relative duration `<n><unit>` (`m`/`h`/`d`/`w`/`mo`/`y`, e.g. `3d`, `12h`, `2w`), an ISO date `YYYY-MM-DD`, or an ISO datetime. Translated to a single GitHub-search qualifier (`created:≥…`, `created:≤…`, or `created:since..until`). `dateField: "updated"` maps to `updated:` for issues/prs and `pushed:` for repos. When you only want a date filter and no keywords, omit `query` entirely.
|
||||
- `run_watch` — Watch a GitHub Actions workflow run. Optional `run` (id or URL). Omitting `run` watches all workflow runs for the current HEAD commit; `branch` falls back to the current branch. Optional `tail` (log lines per failed job). Streams snapshots, fast-fails on the first detected job failure (with a brief grace period to capture concurrent failures), then fetches tailed logs for the failed jobs. The full failed-job logs are saved as a session artifact for on-demand reads.
|
||||
|
||||
@@ -1774,6 +1774,39 @@ export async function resolveDefaultRepoMemoized(cwd: string, signal?: AbortSign
|
||||
return untilAborted(signal, pending);
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches search-query qualifiers that already scope to a repository, org, or
|
||||
* user. When present, callers should avoid layering a default `repo:<current>`
|
||||
* on top — the user has already expressed an explicit scope.
|
||||
*
|
||||
* Only the leading `repo:`/`org:`/`user:`/`owner:` token is treated as a
|
||||
* scope marker; arbitrary substrings (e.g. inside quoted text) are ignored.
|
||||
*/
|
||||
const REPO_SCOPE_QUALIFIER_PATTERN = /(?:^|\s)-?(?:repo|org|user|owner):\S/i;
|
||||
|
||||
/**
|
||||
* Resolve the effective `repo:` scope for a search op. Returns the explicit
|
||||
* `repo` when set, `undefined` when the query already carries a scoping
|
||||
* qualifier, and otherwise the current checkout's `owner/repo` via
|
||||
* `resolveDefaultRepoMemoized`. Resolution failures (no git/gh context, no
|
||||
* configured remote) silently fall back to `undefined` so the search proceeds
|
||||
* across all of GitHub instead of throwing.
|
||||
*/
|
||||
async function resolveSearchRepoScope(
|
||||
cwd: string,
|
||||
repo: string | undefined,
|
||||
query: string | undefined,
|
||||
signal: AbortSignal | undefined,
|
||||
): Promise<string | undefined> {
|
||||
if (repo) return repo;
|
||||
if (query && REPO_SCOPE_QUALIFIER_PATTERN.test(query)) return undefined;
|
||||
try {
|
||||
return await resolveDefaultRepoMemoized(cwd, signal);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveGitHubBranchHead(
|
||||
cwd: string,
|
||||
repo: string,
|
||||
@@ -3267,11 +3300,11 @@ async function executeSearchIssues(
|
||||
params: GithubInput,
|
||||
signal: AbortSignal | undefined,
|
||||
): Promise<AgentToolResult<GhToolDetails>> {
|
||||
const repo = normalizeOptionalString(params.repo);
|
||||
const limit = resolveSearchLimit(params.limit);
|
||||
const dateField = resolveSearchDateField("issues", params.dateField);
|
||||
const dateQualifier = buildSearchDateQualifier(dateField, params.since, params.until);
|
||||
const displayQuery = composeSearchQuery([params.query, dateQualifier]);
|
||||
const repo = await resolveSearchRepoScope(session.cwd, normalizeOptionalString(params.repo), displayQuery, signal);
|
||||
const apiQuery = composeSearchQuery([displayQuery, repo ? `repo:${repo}` : undefined, "is:issue"]);
|
||||
const args = buildGhApiSearchArgs("issues", apiQuery, limit);
|
||||
|
||||
@@ -3285,11 +3318,11 @@ async function executeSearchPrs(
|
||||
params: GithubInput,
|
||||
signal: AbortSignal | undefined,
|
||||
): Promise<AgentToolResult<GhToolDetails>> {
|
||||
const repo = normalizeOptionalString(params.repo);
|
||||
const limit = resolveSearchLimit(params.limit);
|
||||
const dateField = resolveSearchDateField("prs", params.dateField);
|
||||
const dateQualifier = buildSearchDateQualifier(dateField, params.since, params.until);
|
||||
const displayQuery = composeSearchQuery([params.query, dateQualifier]);
|
||||
const repo = await resolveSearchRepoScope(session.cwd, normalizeOptionalString(params.repo), displayQuery, signal);
|
||||
const apiQuery = composeSearchQuery([displayQuery, repo ? `repo:${repo}` : undefined, "is:pr"]);
|
||||
const args = buildGhApiSearchArgs("issues", apiQuery, limit);
|
||||
|
||||
@@ -3307,8 +3340,8 @@ async function executeSearchCode(
|
||||
if (params.since !== undefined || params.until !== undefined) {
|
||||
throw new ToolError("search_code does not support since/until; GitHub code search has no date qualifier.");
|
||||
}
|
||||
const repo = normalizeOptionalString(params.repo);
|
||||
const limit = resolveSearchLimit(params.limit);
|
||||
const repo = await resolveSearchRepoScope(session.cwd, normalizeOptionalString(params.repo), query, signal);
|
||||
const apiQuery = composeSearchQuery([query, repo ? `repo:${repo}` : undefined]);
|
||||
const args = buildGhApiSearchArgs("code", apiQuery, limit, ["Accept: application/vnd.github.text-match+json"]);
|
||||
|
||||
@@ -3322,11 +3355,11 @@ async function executeSearchCommits(
|
||||
params: GithubInput,
|
||||
signal: AbortSignal | undefined,
|
||||
): Promise<AgentToolResult<GhToolDetails>> {
|
||||
const repo = normalizeOptionalString(params.repo);
|
||||
const limit = resolveSearchLimit(params.limit);
|
||||
const dateField = resolveSearchDateField("commits", params.dateField);
|
||||
const dateQualifier = buildSearchDateQualifier(dateField, params.since, params.until);
|
||||
const displayQuery = composeSearchQuery([params.query, dateQualifier]);
|
||||
const repo = await resolveSearchRepoScope(session.cwd, normalizeOptionalString(params.repo), displayQuery, signal);
|
||||
const apiQuery = composeSearchQuery([displayQuery, repo ? `repo:${repo}` : undefined]);
|
||||
const args = buildGhApiSearchArgs("commits", apiQuery, limit);
|
||||
|
||||
|
||||
@@ -604,6 +604,80 @@ describe("github tool", () => {
|
||||
expect(reposArgs.some(arg => typeof arg === "string" && arg.includes("repo:ignored/value"))).toBe(false);
|
||||
});
|
||||
|
||||
it("search_prs: defaults `repo:` to the current checkout when `repo` is omitted", async () => {
|
||||
const textSpy = vi.spyOn(git.github, "text").mockResolvedValue("acme/widgets\n");
|
||||
const jsonSpy = vi.spyOn(git.github, "json").mockResolvedValue({ items: [] });
|
||||
const tool = new GithubTool(createSession("/tmp/gh-default-prs"));
|
||||
await tool.execute("search-prs", {
|
||||
op: "search_prs",
|
||||
query: "is:open",
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
// `gh repo view --json nameWithOwner` runs against the session cwd to fetch the
|
||||
// default scope; the resolved owner/repo gets layered onto the API query.
|
||||
expect(textSpy).toHaveBeenCalled();
|
||||
const repoViewArgs = textSpy.mock.calls[0]?.[1] ?? [];
|
||||
expect(repoViewArgs.slice(0, 2)).toEqual(["repo", "view"]);
|
||||
expect(repoViewArgs).toContain("nameWithOwner");
|
||||
|
||||
const apiArgs = jsonSpy.mock.calls[0]?.[1] ?? [];
|
||||
expect(apiArgs).toContain("q=is:open repo:acme/widgets is:pr");
|
||||
});
|
||||
|
||||
it("search_issues: skips the current-repo default when the query already carries a scope qualifier", async () => {
|
||||
const textSpy = vi.spyOn(git.github, "text").mockResolvedValue("acme/widgets\n");
|
||||
const jsonSpy = vi.spyOn(git.github, "json").mockResolvedValue({ items: [] });
|
||||
const tool = new GithubTool(createSession("/tmp/gh-default-skip-qualifier"));
|
||||
await tool.execute("search-issues", {
|
||||
op: "search_issues",
|
||||
query: "is:open org:torvalds",
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
// Explicit `org:` qualifier suppresses the auto-resolved `repo:` injection.
|
||||
expect(textSpy).not.toHaveBeenCalled();
|
||||
const apiArgs = jsonSpy.mock.calls[0]?.[1] ?? [];
|
||||
expect(apiArgs).toContain("q=is:open org:torvalds is:issue");
|
||||
expect(apiArgs.some(a => typeof a === "string" && a.startsWith("q=") && a.includes("repo:acme/widgets"))).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("search_code: falls back to global search when `gh repo view` cannot resolve the current checkout", async () => {
|
||||
const textSpy = vi.spyOn(git.github, "text").mockRejectedValue(new Error("not a git repository"));
|
||||
const jsonSpy = vi.spyOn(git.github, "json").mockResolvedValue({ items: [] });
|
||||
const tool = new GithubTool(createSession("/tmp/gh-default-no-remote"));
|
||||
await tool.execute("search-code", {
|
||||
op: "search_code",
|
||||
query: "findThing",
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
expect(textSpy).toHaveBeenCalled();
|
||||
const apiArgs = jsonSpy.mock.calls[0]?.[1] ?? [];
|
||||
// No `repo:` should be injected — resolution failed, so the search proceeds globally.
|
||||
expect(apiArgs).toContain("q=findThing");
|
||||
expect(apiArgs.some(a => typeof a === "string" && a.startsWith("q=") && a.includes("repo:"))).toBe(false);
|
||||
});
|
||||
|
||||
it("search_commits: honors an explicit `repo` override over the current-checkout default", async () => {
|
||||
const textSpy = vi.spyOn(git.github, "text").mockResolvedValue("acme/widgets\n");
|
||||
const jsonSpy = vi.spyOn(git.github, "json").mockResolvedValue({ items: [] });
|
||||
const tool = new GithubTool(createSession("/tmp/gh-default-explicit-override"));
|
||||
await tool.execute("search-commits", {
|
||||
op: "search_commits",
|
||||
query: "fix",
|
||||
repo: "other/project",
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
// Explicit `repo` short-circuits resolution — no `gh repo view` invocation.
|
||||
expect(textSpy).not.toHaveBeenCalled();
|
||||
const apiArgs = jsonSpy.mock.calls[0]?.[1] ?? [];
|
||||
expect(apiArgs).toContain("q=fix repo:other/project");
|
||||
});
|
||||
|
||||
it("checks out a pull request into a worktree and configures contributor push metadata", async () => {
|
||||
const fixture = await createPrFixture();
|
||||
const tempHome = await setupTempHome();
|
||||
|
||||
Reference in New Issue
Block a user