github
Dispatch GitHub CLI operations for repositories, issues, pull requests, search, and Actions run watching.
Source
- Entry:
packages/coding-agent/src/tools/gh.ts - Model-facing prompt:
packages/coding-agent/src/prompts/tools/github.md - Key collaborators:
packages/coding-agent/src/tools/gh-format.ts— shorten commit SHAs for summaries.packages/coding-agent/src/tools/gh-renderer.ts— TUI rendering, especiallyrun_watchlive/result views.packages/coding-agent/src/utils/git.ts—gh/gitprocess wrappers, repo locking, branch config writes.packages/utils/src/dirs.ts— base directory for dedicated PR worktrees.packages/coding-agent/src/sdk.ts— session artifact allocation hook.packages/coding-agent/src/session/artifacts.ts— artifact filename format<id>.<toolType>.log.
Inputs
| 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. 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. |
forceWithLease |
boolean |
No | Used only by pr_push; passed through to git push. |
title |
string |
No | Used only by pr_create. Required unless fill is true. |
body |
string |
No | Used only by pr_create. Mutually exclusive with fill. Empty/omitted body becomes --body "" to suppress the interactive editor. Non-empty body is written to a temp file and passed as --body-file. |
base |
string |
No | Used only by pr_create; passed as --base. |
head |
string |
No | Used only by pr_create; passed as --head. |
draft |
boolean |
No | Used only by pr_create. Defaults to false. |
fill |
boolean |
No | Used only by pr_create. Defaults to false. Mutually exclusive with title and body. |
reviewer |
string[] |
No | Used only by pr_create; each entry becomes --reviewer. |
assignee |
string[] |
No | Used only by pr_create; each entry becomes --assignee. |
label |
string[] |
No | Used only by pr_create; each entry becomes --label. |
query |
string |
No | Used by all search_* ops. Required by local validation only for search_code; the other search ops compose it with optional date/repo/type qualifiers and send the result to GitHub. |
since |
string |
No | Lower date bound for search_issues, search_prs, search_commits, and search_repos. Accepts relative durations (3d, 12h, 2w, 2mo, 1y), YYYY-MM-DD, or an ISO datetime. Rejected for search_code. |
until |
string |
No | Upper date bound for search_issues, search_prs, search_commits, and search_repos. Same formats as since. Rejected for search_code. |
dateField |
"created" | "updated" |
No | Date qualifier field for issue/PR/repo search. Defaults to created; repo search maps updated to GitHub's pushed: qualifier. Ignored for commit search, which always uses committer-date:. |
limit |
number |
No | Used by all search_* ops. Defaults to 10, floored, clamped to 50, and must be > 0. |
run |
string |
No | Used only by run_watch. Must be a numeric run ID or full GitHub Actions run URL. |
tail |
number |
No | Used only by run_watch. Defaults to 15, floored, clamped to 200, and must be > 0. |
Outputs
The tool returns a single text result built by buildTextResult() in packages/coding-agent/src/tools/gh.ts.
content: one text block. Multi-item ops join sections with blank lines and---separators.sourceUrl: set for single repo/PR/run results when a canonical URL is known.details: optional structured metadata used by the TUI renderer.- Common fields:
artifactId,repo,branch,worktreePath,remote,remoteBranch,headSha,runId,runIds,status,conclusion,failedJobs. pr_checkoutaddscheckouts: GhPrCheckoutSummary[].run_watchaddswatch: GhRunWatchViewDetails, which drives the custom live/result renderer inpackages/coding-agent/src/tools/gh-renderer.ts.
- Common fields:
- Artifact trailer: when
artifactIdis present, the text body gets an appended line likeFull failed-job logs: artifact://<id>.run_watchallocates artifacts withsession.allocateOutputArtifact("github"); persistent sessions therefore save failed-log bodies as<artifact-dir>/<id>.github.log.
run_watch is the only streaming op. It emits onUpdate snapshots while polling, then returns one final text result.
Flow
GithubTool.createIf()exposes the tool only whengit.github.available()findsghonPATH.GithubTool.execute()wraps dispatch inuntilAborted()and switches onparams.op.- Each op normalizes optional strings, arrays, booleans, and numeric caps locally in
packages/coding-agent/src/tools/gh.ts. - CLI execution goes through
git.github.run/json/text()inpackages/coding-agent/src/utils/git.ts:- spawns
gh ...withBun.spawn(); - trims stdout/stderr unless
trimOutput: false; - maps common auth/repo-context failures into tool-facing
ToolErrormessages; json()rejects empty or invalid JSON.
- spawns
- Read-style ops (
repo_view,search_*) fetch JSON and format Markdown-like text summaries. Single-issue and single-PR views were moved out of the tool and now resolve through theissue:///pr://internal URL schemes, which share the same SQLite cache. - PR diffs moved out of the tool.
pr://<N>/difflists changed files,pr://<N>/diff/<i>slices a single file, andpr://<N>/diff/allreturns the full unified diff — seedocs/tools/read.md. All three variants share onegh pr diffinvocation through thepr-diffcache row. pr_checkoutresolves PR metadata first, then entersgit.withRepoLock()before any git mutation so parallel checkout calls for the same primary repo do not race on shared.gitstate.pr_pushreads PR head metadata back from git branch config, derives a refspec, then pushes withgit.push().pr_createshells out once, then best-effort re-reads the created PR for a richer summary.run_watchchooses either run mode (runsupplied) or commit mode (runomitted), polls GitHub Actions APIs every 3 seconds, emits streaming updates, and may save a full failed-log artifact before returning.- Final text goes through
toolResult().text(...); ifsession.allocateOutputArtifact()returns a slot, failed-log text is persisted withBun.write().
Modes / Variants
repo_view
| Aspect | Value |
|---|---|
| Required fields | op |
| Optional fields | repo, branch |
gh command |
gh repo view [<repo>] [--branch <branch>] --json <GH_REPO_FIELDS> |
| Batching | None |
| Output | # <owner/repo> header, description, URL, default branch, requested branch, visibility, permission, primary language, stars, forks, archive/fork flags, updated timestamp, homepage, topics. sourceUrl = data.url. |
If repo is omitted, gh repository resolution is used.
Single-issue and single-PR reads live in the issue://<N> / pr://<N> URL schemes (see docs/tools/read.md). They share ~/.omp/cache/github-cache.db (override via OMP_GITHUB_CACHE_DB) and the github.cache.softTtlSec / github.cache.hardTtlSec / github.cache.enabled settings. The cache retains rendered Markdown plus the raw JSON payload returned by gh, including private bodies, comments, reviews, and review comments when comments are enabled; rows are scoped by the local GitHub credential fingerprint. Root and repo-scoped reads (issue://, pr://owner/repo) issue a live gh issue list / gh pr list for browsing; query params state, limit, author, label pass through to gh (issue:// accepts state=open|closed|all; pr:// also accepts merged). PR diffs ride the same cache under pr://<N>/diff[/…]: the listing, full diff, and per-file slices all share one pr-diff row keyed by repo and PR number.
pr_create
| Aspect | Value |
|---|---|
| Required fields | op plus either fill=true or title |
| Optional fields | repo, title, body, base, head, draft, fill, reviewer[], assignee[], label[] |
gh command |
gh pr create ... with flags assembled from provided fields |
| Batching | None |
| Output | # Created Pull Request ... summary with URL, state, draft flag, base/head, author, created time, labels, optional body. sourceUrl is the created PR URL. |
Branches:
fill && (title || body !== undefined)throws.- Non-empty
bodyis written under a temp dirgh-pr-body-*inos.tmpdir(), passed as--body-file, then removed infinally. - After creation, the tool parses the returned URL and best-effort runs
gh pr view <number> --repo <repo> --json <GH_PR_FIELDS_NO_COMMENTS>; failures there are swallowed.
pr_checkout
| Aspect | Value |
|---|---|
| Required fields | op |
| Optional fields | repo, pr, force |
gh command |
For each requested PR: gh pr view [<pr>] [--repo <repo>] --json <GH_PR_CHECKOUT_FIELDS>; cross-repo PRs may also call gh repo view <headRepository> --json <GH_REPO_CLONE_FIELDS>. |
| Batching | Yes. pr may be string[]; each PR is resolved in parallel, but git mutations are serialized per primary repo by git.withRepoLock(). |
| Output | Single PR: checkout/worktree summary plus details.repo, details.branch, details.worktreePath, details.remote, details.remoteBranch, details.checkouts. Batched: # <n> Pull Request Worktrees (...) plus one section per PR and aggregated details.checkouts. |
Worktree and metadata behavior:
- Local branch name is always
pr-<number>. - Worktree path is
path.join(getWorktreesDir(), encodeRepoPathForFilesystem(primaryRepoRoot), localBranch), wheregetWorktreesDir()is~/.omp/wt; effective path is~/.omp/wt/<encoded-primary-repo-root>/pr-<number>. - Existing worktree detection is by branch ref
refs/heads/pr-<number>fromgit.worktree.list(). - New worktree creation calls
git.worktree.add(repoRoot, finalWorktreePath, localBranch, { signal })after verifying the path is neither already registered nor already present on disk. - For same-repo PRs, remote is
origin. For cross-repo PRs, the tool resolves a clone URL for the head repo, reuses an existing remote with the same URL when possible, or createsfork-<owner>/fork-<owner>-<n>. - The branch push metadata is persisted with
git configunder the repository's shared.git/configas:branch.pr-<number>.remotebranch.pr-<number>.mergebranch.pr-<number>.pushRemotebranch.pr-<number>.ompPrHeadRefbranch.pr-<number>.ompPrUrlbranch.pr-<number>.ompPrIsCrossRepositorybranch.pr-<number>.ompPrMaintainerCanModify
- If
refs/heads/pr-<number>already exists at a different commit, checkout fails unlessforce=true, in which casegit branch --forceresets it to the fetched PR head. - If a matching worktree already exists, the tool reuses it and reports
reused: true.
pr_push
| Aspect | Value |
|---|---|
| Required fields | op |
| Optional fields | branch, forceWithLease |
gh command |
None. This path uses git, not gh. |
| Batching | None |
| Output | # Pushed Pull Request Branch summary with local branch, remote, remote branch, remote URL, PR URL, and force-with-lease flag. sourceUrl = prUrl when known. |
Push target resolution reads the branch.<name>.ompPrHeadRef, pushRemote/remote, ompPrUrl, ompPrMaintainerCanModify, and ompPrIsCrossRepository git-config keys written by pr_checkout. If the current checked-out branch matches the target branch, the source ref is HEAD; otherwise it pushes refs/heads/<branch>. The refspec is HEAD:refs/heads/<headRef> or refs/heads/<branch>:refs/heads/<headRef>.
search_issues
| Aspect | Value |
|---|---|
| Required fields | op |
| Optional fields | repo, query, limit, since, until, dateField |
gh command |
gh api -X GET /search/issues -f q="<query> [date qualifier] [repo:<repo>] is:issue" -F per_page=<limit> |
| 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 composed 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 |
|---|---|
| Required fields | op |
| Optional fields | repo, query, limit, since, until, dateField |
gh command |
gh api -X GET /search/issues -f q="<query> [date qualifier] [repo:<repo>] is:pr" -F per_page=<limit> |
| 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 |
|---|---|
| Required fields | op, query |
| Optional fields | repo, limit |
gh command |
gh api -X GET /search/code -f q="<query> [repo:<repo>]" -F per_page=<limit> -H "Accept: application/vnd.github.text-match+json" |
| 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. since and until are explicitly rejected for this op because GitHub code search has no supported date qualifier.
search_commits
| Aspect | Value |
|---|---|
| Required fields | op |
| Optional fields | repo, query, limit, since, until, dateField (accepted but ignored; commit searches use committer-date) |
gh command |
gh api -X GET /search/commits -f q="<query> [committer-date qualifier] [repo:<repo>]" -F per_page=<limit> |
| 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 |
|---|---|
| Required fields | op |
| Optional fields | query, limit, since, until, dateField |
gh command |
gh api -X GET /search/repositories -f q="<query> [date qualifier]" -F per_page=<limit> |
| Batching | None |
| Output | # GitHub repositories search, result count, then one bullet per repo with first description line, language, stars, forks, open issues, visibility, archive/fork flags, updated time, URL. |
repo is intentionally not used for this op. If query, since, and until are all omitted, the tool sends an empty GitHub repository-search query and the GitHub API may reject it.
run_watch
| Aspect | Value |
|---|---|
| Required fields | op |
| Optional fields | repo, branch, run, tail |
gh command |
Repo resolution: gh repo view --json nameWithOwner -q .nameWithOwner when repo and run URL repo are both absent. Single-run mode uses gh api --method GET /repos/<repo>/actions/runs/<runId> and gh api --method GET /repos/<repo>/actions/runs/<runId>/jobs. Commit mode uses gh api --method GET /repos/<repo>/branches/<branch>, gh api --method GET /repos/<repo>/actions/runs, gh api --method GET /repos/<repo>/actions/runs/<runId>/jobs, and gh api /repos/<repo>/actions/jobs/<jobId>/logs for failed jobs. |
| Batching | Implicit batching only in commit mode: all workflow runs for one commit are tracked together. |
| Output | Streaming watch snapshots via onUpdate, then a final text report. On failure, appends Full failed-job logs: artifact://<id> and sets details.artifactId. |
Watch flow:
runparsing accepts either a decimal run ID or a full run URL. URL repo must match explicitrepowhen both are given.- Poll interval is fixed at 3 seconds (
RUN_WATCH_INTERVAL_DEFAULT). - Failure grace period is fixed at 5 seconds (
RUN_WATCH_GRACE_DEFAULT). When any failed job appears before completion, the tool emits a note, waits once, re-fetches state, then collects logs so concurrent failures are included. - Failed-job logs are fetched with
gh api /repos/<repo>/actions/jobs/<jobId>/logsviagit.github.run(), notjson(). Non-zero exit leavesavailable: falseinstead of failing the whole watch. - Inline result includes only the last
taillines per failed job. The saved artifact contains full logs (mode: "full"). - In commit mode, success is intentionally double-checked: once all known runs are successful, the tool waits one more poll interval and succeeds only if the set of run IDs is unchanged. This avoids returning before late workflow runs appear for the same commit.
details.watchdrives a specialized renderer inpackages/coding-agent/src/tools/gh-renderer.ts; non-watch results fall back to generic text rendering.
Side Effects
- Filesystem
pr_createmay create a temp dir underos.tmpdir()namedgh-pr-body-*, writebody.md, then remove the dir infinally.pr_checkoutmay create directories under~/.omp/wt/<encoded-primary-repo-root>/and add git worktrees there.run_watchmay write a session artifact with full failed-job logs.
- Network
- Every op shells out to
gh, which then talks to GitHub APIs exceptpr_push. pr_pushuses git network transport to the configured remote.
- Every op shells out to
- Subprocesses / native bindings
- All
ghcalls useBun.spawn(["gh", ...args]). pr_checkoutandpr_pushalso invoke git helpers frompackages/coding-agent/src/utils/git.ts.
- All
- Session state (transcript, memory, jobs, checkpoints, registries)
run_watchconsumessession.allocateOutputArtifact()when failed-job logs are persisted.- Returned
detailsobjects carry run/checkouts metadata for the renderer/UI.
- User-visible prompts / interactive UI
ghinteractive editor fallback is suppressed forpr_createby forcing either--body-fileor--body "".gh-rendererprovides compact headers for all ops and a custom live watch view forrun_watch.
- Background work / cancellation
run_watchloops until success/failure and usesscheduler.wait()between polls.GithubTool.execute()is wrapped inuntilAborted();git.github.run()forwards the abort signal intoBun.spawn().
Limits & Caps
- Search result default:
10(SEARCH_LIMIT_DEFAULTinpackages/coding-agent/src/tools/gh.ts). - Search result max:
50(SEARCH_LIMIT_MAX). - PR file preview inside the
pr://view: first50files only (FILE_PREVIEW_LIMITingh.ts). - Run-watch poll interval:
3s(RUN_WATCH_INTERVAL_DEFAULT). - Run-watch failure grace period:
5s(RUN_WATCH_GRACE_DEFAULT). - Run-watch failed-log tail default:
15lines (RUN_WATCH_TAIL_DEFAULT). - Run-watch failed-log tail max:
200lines (RUN_WATCH_TAIL_MAX). - PR review comments page size:
100(REVIEW_COMMENTS_PAGE_SIZE). - Actions jobs page size:
100(RUN_JOBS_PAGE_SIZE). - Search and tail numeric inputs are floored with
Math.floor(), clamped to the max, and rejected when non-finite or<= 0. pr_checkoutbatch fan-out is unbounded in tool code; all requested PRs are launched withPromise.all().
Errors
- Tool creation is skipped entirely when
ghis not installed. git.github.run()throwsToolError("GitHub CLI (gh) is not installed...")ifghis missing at execution time.git.github.text/json()map common failures to model-facing messages:- not authenticated →
GitHub CLI is not authenticated. Run \gh auth login`.` - missing repo context without explicit
repo→GitHub repository context is unavailable. Pass \repo` explicitly or run the tool inside a GitHub checkout.` - otherwise stderr/stdout text, or fallback
GitHub CLI command failed: gh ...
- not authenticated →
json()also throws on empty stdout or invalid JSON.- Local validation errors throw
ToolError, including:- missing required per-op fields (
queryforsearch_code,title unless fill=true) - invalid numeric
limit/tail - invalid
since/untildate bound - invalid
runformat fillcombined withtitleorbody- missing git repo / branch / HEAD context for checkout, push, or watch
pr_pushon a branch withoutompPrHeadRefmetadata- conflicting existing worktree path or branch without
force
- missing required per-op fields (
run_watchtreats failed-job log fetches specially: missing log content does not fail the watch; it marks that logavailable: falseand printsLog tail unavailable./Full log unavailable..pr_createswallows only the post-create best-effortgh pr viewrefresh; the create step itself still fails normally.
Notes
appendRepoFlag()intentionally skips--repowhen the identifier argument is already a full GitHub URL; that letsghderive repo/number from the URL.normalizePrIdentifierList()acceptsreviewer,assignee, andlabelarrays too; the helper name is broader than its callers.pr_pushdepends onpr_checkouthaving run first for that local branch; there is no alternate metadata source.pr_checkoutstores push metadata in branch config, not in the worktree directory. Reusing the samepr-<number>branch reuses those config keys.- Worktree write serialization is keyed by the primary repo root, not the current worktree path, because git worktrees share
.git/config,packed-refs, commit-graph, and worktree metadata files. search_reposis the only search op that never forwardsrepo; repository scoping must be expressed in the query itself.run_watchsuccess on commit mode means “all observed runs succeeded and no additional runs appeared one poll later”, not merely “latest poll looked green”.- The TUI renderer collapses failed log previews unless the result view is expanded; the underlying text result still contains the same tailed lines plus any artifact reference.