Skip to content

GitHub tools: repo and PR reads, Actions triage + issue writes from any agent ​

Every agent with a tool loop can read the org's repositories and read/write their issues through Switchboard's own GitHub App credential — over the REST API from the bot process, with no workspace — so a plain mention answers "open an issue on the switchboard app" and "how does our resident system work?" instead of bouncing the user to a directive, and the research agent never again reports our private repo as "inaccessible" after a public-web 404. A pasted GitHub Actions URL is triaged the same way: "why did this run fail?" is answered from the run, its jobs and the failed job's log, with no shell.

  • Code: src/core/dispatch/executionGithubContext.ts (same-run native source proof and current capability), src/core/dispatch/reviewHistoryRestore.ts (recorded history proof and separate freshness checks), src/core/dispatch/githubReadContext.ts (per-call requester-scoped GitHub source receipts), src/core/references/sourceResultContext.ts (stored result byte verification), src/core/references/githubAccessDiagnostic.ts (closed execution read failure facts), src/execution/githubReadSource.ts (closed native task snapshots), src/core/resumeLaunch.ts (original snapshot recovery), src/execution/githubApi.ts (the GithubApi seam; RestGithubApi — REST + the one GraphQL deleteIssue mutation, read-scoped token for reads and write-scoped for writes; InMemoryGithubApi), src/tools/github.ts (the thirteen github_* tools, GithubCapability, GITHUB_READ_TOOLS / GITHUB_ISSUE_WRITE_TOOLS), src/tools/runnableTool.ts (ToolContext.github), src/tools/toolsets.ts (TOOLSETS incl. the assistant set), src/agents/registry.ts (general on assistant, prompts), src/core/dispatch/repositoryBriefs.ts (authorized connected repository metadata and source briefs), src/core/dispatch/contextAccess.ts (saved-context revalidation), src/core/dispatch/runLoop.ts (verified request capability), src/core/dispatcher.ts (operator and reply rechecks), src/core/dispatch/run.ts (githubCapabilityFor — the per-run capability with the requesting user's canUseRepo gate; injected into every tool context, ship children included), src/execution/githubApp.ts (issues:read, pull_requests:read and actions:read in the read-scoped permission set).
  • Tests: src/core/dispatch/executionGithubContext.test.ts, src/core/dispatch/reviewHistoryRestore.test.ts, src/core/dispatch/githubReadContext.test.ts, src/core/dispatch/run.test.ts, src/core/dispatch/contextAccess.test.ts, src/core/references/sourceResultContext.test.ts, src/core/dispatch/repositoryBriefs.test.ts, src/execution/githubApi.test.ts, src/tools/github.test.ts, src/agents/registry.test.ts, src/core/dispatcher.test.ts (the self-description … and the github_* tools block).

Behavior ​

github_pull_get accepts includeChecks: true. It reads the latest GitHub check runs at the exact returned head through the read credential, including checks from external CI providers. Each row carries its head, name, status, conclusion, check URL and details URL. Checks are read on the first history page only; later pages retain the original history page size without rereading changing CI. The complete read allows at most 300 rows across three pages. Malformed, partial, duplicate, changed-count, wrong-head or over-limit evidence returns an error. An empty result does not establish passing CI. Legacy commit statuses and required-check rules are outside this read. These results supply context only; they grant no publication or execution authority.

  1. The seam, two implementations (invariant 2/5): GithubApi covers listRepos (the installation's repositories), isPublicRepo (current public visibility for one exact repository), readFile / listTree / searchCode (repo reads at any ref), listIssues / getIssue (issues, never pull requests; getIssue includes up to 30 comments), getPullRequest (the PR title, body, state and head/base refs for context, never publication authority), createIssue / updateIssue / commentIssue / deleteIssue, and the Actions reads getActionsRun / getActionsJob / getActionsJobLog (item 9). RestGithubApi speaks api.github.com from the bot process — never gh, never a clone — and mints the App token per call class: reads on the read-scoped token, writes on the write-scoped one, so a read tool cannot write even under prompt injection. The read-scoped permission set gains issues: read for this (execution.md item 5). Every non-2xx is a GithubApiError carrying the HTTP status and GitHub's own message; a missing credential is a 401 before any request. Bodies over GitHub's 65536-char limit are clipped with a visible note; files are read as decoded UTF-8 (binary → a note, by default >200k chars → clipped unless a caller sets a larger ReadFileOptions.maxChars, >1 MB → re-read via the raw media type as a stream that is cancelled once the caller's clip is in hand, so a multi-MB blob costs the clip's memory, not its own). listIssues reads 100-row pages (at most 3) until limit issues are collected or the list ends — GitHub interleaves pull requests in that endpoint and they are dropped, so one page can hold fewer issues than rows. deleteIssue resolves the node id over REST and runs the GraphQL deleteIssue mutation; a FORBIDDEN / "not authorized" answer is a 403, a PR number is refused as not-an-issue. Known limit: GitHub grants issue deletion only to a repository admin's USER credential — an App installation gets "Viewer not authorized to delete" even with issues: write — so with the production credential deleteIssue always fails closed; the seam keeps the method because a GH_TOKEN of an admin (the non-App fallback) can delete. InMemoryGithubApi (repos with files + issues + pull requests + seeded Actions runs; unknown repo → 404 like GitHub; issues listed newest-updated first on a deterministic clock, as GitHub's sort=updated&direction=desc does) is the second implementation and the test double.

  2. Thirteen tools, reads vs writes. Reads — github_repos, github_file, github_tree, github_search_code, github_issue_list, github_issue_get, github_pull_get, github_actions_run, github_actions_job_log — are sideEffectFree (the runner may run several from one turn concurrently). Writes — github_issue_create, github_issue_update (title/body/state/labels/assignees; closing is state: closed), github_issue_comment, github_issue_delete (permanent; the description tells the model to use it only on an explicit delete request, and that the App credential cannot delete — a 403 from GitHub becomes "issue deletion is not available to Switchboard's GitHub App credential … Nothing was changed. Offer to close it instead …") — run strictly in order. Every tool answers a string, never throws: no capability → "GitHub tools are not available in this context."; a malformed repo/number/missing field → a one-line correction naming the expectation; a 404 is worded as what it usually is ("the repo is outside the Switchboard GitHub App installation (github_repos lists the reachable ones), or the path/ref/number does not exist"); 401/403/timeouts each their own line. Successful writes report exactly what happened with the number and URL (Opened owner/name#41: title\n<url>) so the answer can quote it.

  3. Writes and main-agent reads use the resolved requester. The dispatcher builds GithubCapability.canWrite from config.canUseRepo(actor, repo). A refused issue write stops before the API call. The main agent reads public installation repositories allowed by the resolved actor. It also reads private repositories in a verified one-person Slack DM when that same requester has a stored GitHub login and id, and GitHub currently returns that id with read, write, or admin permission on the installed repository. Legacy login-only bindings, missing proof, error, shared channel, relay, and stale permission all refuse private reads. The same capability gates the catalog, every tool read, stored-result reuse, and reply publication; code search stays within one approved repo. Other agents retain their installation-scoped read access.

  4. Enablement by toolset: the reads join every toolset with a tool loop — full (coding), readonly (review), web (research), explore (agent-explore.md item 2), assistant, conductor, and orchestrator (orchestration-plane.md item 12); the issue writes join assistant and full only — the read-only review agent, the research agent and the explore agent never mutate GitHub. none stays empty.

  5. general: the assistant (agent-general.md): toolset assistant = the GitHub tools + web_fetch + update_status + authorized recall + own-session notes — no shell, no file writes, no web_search, no verdict/PR submission — with a 60-minute budget for longer multi-source lookups and the turn guard derived from it. Its prompt names the tools, tells it to resolve a loosely-named repo with github_repos (or the thread) rather than asking, to read a repo before answering about it, to report exactly what a tool did and never claim an unperformed action, and to redirect code changes / PR reviews / web research to agent:coding / agent:review / agent:research. It still declares no resources — the tools are REST in the bot process, so a general ask never provisions a workspace (agent-general item 4).

  6. research reads repos too (web-tools.md item 5): its toolset gains the reads (not the writes) and its prompt names them, tells it to read a github.com URL of ours with github_file/github_tree (web_fetch cannot see private repos), and forbids concluding a repo is inaccessible from a public-web 404.

  7. Requires the App installation to cover the repo (execution.md item 5): a repo outside repository_selection: "selected" 404s on every call — the tool wording says so and points at github_repos. The App needs Contents (read for the read tools), Issues (read + write), Actions (read, for item 9), Metadata; the installation grants issues: write and contents: write over the repos it covers.

  8. Every call is measured (tracing.md item 23). The github_* tools see the REST client as a view under their own call's span, so each request is a github.rest span naming its route word (never the repo, path or query) and each token mint a github.token_mint span; the run's timeline counts them inside the tool call, and the log carries them.

  9. Actions triage: two more reads. github_actions_run takes the URL a person pastes (github.com/<owner>/<repo>/actions/runs/<id>, with or without /job/<id> or /attempts/<n>) or repo + a run id, and renders the run (workflow, run number and attempt, conclusion and status, event, branch, short sha, title, start and span, workflow file) and every job — what went wrong first, then what is still running, then what passed — with its conclusion, duration and id and, for a job that did not succeed, its non-success steps and its URL; it ends by naming the job to read next. github_actions_job_log takes a job URL or repo + a job id and renders the job's steps with their conclusions and durations, every ##[error] line (30 at most), and the last lines lines of the log (default 200, at most 2000) or, with match, only the lines containing that text. The log is remote text: ANSI is stripped and secrets are redacted before anything reads it, and GitHub's per-line timestamp is dropped. Behind them GithubApi gains getActionsRun (the run plus its jobs, 100-row pages, at most 3), getActionsJob and getActionsJobLog. The log endpoint answers a 302 to a signed URL on a blob host; the client follows it by hand without the App credential (the URL is its own credential) and reads the body as a tail window of ACTIONS_LOG_MAX_CHARS (2 M) characters — a multi-MB log costs the cap's memory and keeps its end, where a failed job's failure is; complete: false and a note in the answer say the head was dropped. A run URL handed to the log tool, an id without a repo, a repo that contradicts the URL and a non-numeric id each get a one-line correction; a 404 is worded as items 2 and 7 word it. The read-scoped token already carries actions: read. Known limits: the latest attempt only (an /attempts/<n> URL reads the run's current attempt); the combined job log, never the per-step files of the run's log archive; a live job's log is what GitHub has so far, and the answer says so.

  10. Repository briefs reuse connected-repository reads. The operator's brief loader uses the same GithubApi installation listing, tree and file reads. Installation listing follows every page; it never silently drops repositories after a fixed page count. It lists only repositories the resolved requester may use, and checks that permission again before each brief read. Every authorized connected repository contributes its description and default branch; a bounded selection, prioritizing established thread repositories and request relevance, also contributes README, repository guidance and package identity. Selection limits enrichment, never repository eligibility: the model can read any authorized catalog entry. Source paths, blob SHAs, truncation and observation time travel with each brief. Missing files or an unavailable individual repository preserve the other context; an unavailable catalog is explicit. Repository text is quoted, redacted untrusted context and cannot grant permission or become an instruction. Briefs refresh from the existing read seam each request; concurrent reads within that request share their result.

Same-run specialist reads use version 2 of the existing context envelope and source-result receipt. Their references bind the original acknowledged run admission, tool input, call and exact result bytes. Only the current live owner can validate those references against its delivered canonical session and fresh native read capability. Message, main-agent and other-run readers receive no scope. Each original repository must remain installed or have current explicit public visibility for the same repository name through the read-scoped metadata API. Private, internal, unknown and mismatched metadata never proves public access. The original read family must still succeed after that check. When public visibility replaces installation membership, its current result after the standard delivery cap must match the original result hash. A different public result cannot authorize retained private bytes; changed public results remain unproved. Global code search also proves current contents access to every originally exposed repository. Unknown tool families remain unproved. This preserves installation-scoped private specialist policy; it adds no requester grant.

Validation criteria ​

GitHub tools return failures with the shared error: prefix and declare failsInText, so both harnesses and the run-owned recovery guard observe failed calls. API errors, invalid inputs and access refusals never count as successful reads or writes. Empty successful results remain successful.

CriterionProof
PR readers discover complete latest check runs and external CI links at the returned head without a run ID. Invalid, unavailable, partial, wrong-head, duplicate, changed-count and over-limit reads do not establish CI; an empty list is explicit.[unit] src/execution/githubApi.test.ts::RestGithubApi — pull request reads::reads every check page at the exact commit with the read credential, ::refuses … check evidence instead of presenting an empty or complete CI result; src/tools/github.test.ts::github_* reads::github_pull_get exposes checks and CI links for its exact head without a supplied run ID, ::github_pull_get reports unavailable checks without presenting passing CI or completing history, ::delivers later history pages without rereading changing CI checks
Every GitHub read and issue-write tool reports an API failure to the relay and stops repeated failures before another model call.[unit] src/core/harness/toolRecovery.test.ts::run-owned tool recovery::reports…
Failed current-access revalidation or source persistence is a failed tool result; successful reads remain successful even when source text starts with an error prefix.[unit] src/core/harness/toolRecovery.test.ts::run-owned tool recovery::reports a refused GitHub…
History reads expose full findings, case tables and author comments for the bound PR; large results require every ordered page from an unchanged head and source before closure submission. Source receipt failures cannot enable submission.[unit] src/tools/github.test.ts::github_* reads::loads full prior findings and author comments only for the bound PR and submits verified closures, ::delivers large history in order before enabling submission and invalidates changed or failed refreshes; src/core/dispatch/githubReadContext.test.ts::durable requester-scoped GitHub reads::enables review closure only after delivering the history source receipt
History redaction validates native Bot markers before sanitizing text leaves, keeps finding IDs, closure IDs/dispositions and complete case tables parseable, and redacts ordinary prose and human context separately. Reconstructed markers and serialized history are not redacted again.[unit] src/tools/github.test.ts::github_* reads::delivers parseable typed history…, ::delivers parseable %s closure history…, ::redacts Cookie prose and human context without consuming the authoritative final Bot marker
PR history metadata, page progress and complete snapshots use the same authoritative head policy as dispatch: a positively read open same-repository branch commit wins; failed or malformed ref reads fall back to the PR object; fork and frozen closed heads never consult a base-repository branch. A branch move between pages restarts delivery.[unit] src/execution/githubApi.test.ts::RestGithubApi — pull request reads::getPullRequest uses the authoritative head…; src/tools/github.test.ts::github_* reads::binds history paging and verdicts to the authoritative branch tip while PR metadata lags
PR feedback reads page all reviews, conversation comments, inline/file review comments and replies with the read token. Native comment, review, author and reply IDs, creation/edit times, commit IDs and nullable diff locations remain source evidence. Incomplete, malformed or over-limit history fails explicitly.[unit] src/execution/githubApi.test.ts::pull request feedback::*
Inline bot comments and human replies enter the existing redacted history document. Their bodies cannot supply native review-marker authority. An edit changes the history fingerprint without a head change and requires every page again.[unit] src/tools/github.test.ts::github_* reads::delivers inline bot comments and human replies as redacted evidence without native verdict authority; ::requires fresh ordered pages after an inline reply changes, even without a head change
An inline comment or reply with native user: null retains its evidence and comment/reply IDs with null author, author ID and author type. It does not block complete history or a bound verdict. Missing or malformed non-null authors remain invalid.[unit] src/execution/githubApi.test.ts::pull request feedback::retains an inline comment with a null native author…; src/tools/github.test.ts::github_* reads::reads unattributed native inline evidence without blocking a bound verdict
Recovery retains original inline evidence but withholds fresh history credit after a reply changes at the same head. A new complete read can restore credit.[unit] src/core/dispatch/reviewHistoryRestore.test.ts::review history restoration from canonical results::retains recorded inline evidence but refuses fresh credit after a reply changes at the same head
Installation repository listing includes pages beyond 300 repositories and stops at the advertised total.[unit] src/execution/githubApi.test.ts::RestGithubApi — reads use the read token::listRepos includes every connected repository beyond three pages
Repository brief loading exposes only authorized connected repositories, prioritizes bounded enrichment without excluding other catalog entries, preserves source provenance and freshness, isolates missing files and failed repositories, rechecks access on reads, and renders source text as untrusted data.[unit] src/core/dispatch/repositoryBriefs.test.ts::connected repository briefs::*
Main orchestrator GitHub reads are requester-scoped and safe to publish in Slack: list hides restricted and unproved private repos, file/tree/issue/pull request/Actions reads deny before resource calls, search requires a repo and drops other-repo hits, absent access proof denies; unknown repository visibility is private. Repository name comparison ignores case, including code-search hits.[unit] src/tools/github.test.ts::github_* reads::the orchestrator refuses every repository outside the requester-scoped catalog before the API, ::accepts the requester's repository grant with mixed-case spelling, ::keeps authorized code-search hits for a mixed-case requested repository; src/core/dispatch/run.test.ts::githubCapabilityFor — the github_* tools' capability for one run::limits main-agent reads to public installation repos the resolved requester may use, ::admits a private repo only for its bound requester in a verified direct DM with current GitHub read permission; src/execution/githubApi.test.ts::RestGithubApi — reads use the read token::listRepos pages the installation's repositories, ::reads current user permission on the App read token and treats missing or malformed proof as absent; src/tools/github.test.ts::github_* reads::lets the orchestrator read a private repository in its verified requester catalog
The main answer uses a GitHub result only while the requester still holds every exposed repository. A denied or unknown repository withholds the Slack answer; legacy saved GitHub results without a verified receipt require a fresh question; proved results retain their repository access dependencies for later reuse.[unit] src/tools/github.test.ts::github_* reads::records each repository exposed by an orchestrator read for the final requester check, ::records the scoped search repository and never exposes a foreign hit; src/core/dispatch/mainAudience.test.ts::main conversation source audience::refuses saved GitHub reads after a requester grant may have changed, ::refuses a resumed GitHub result before the model or answer sees a revoked repo, ::withholds a current GitHub answer after a repo grant is revoked or its repository is unknown; src/core/dispatcher.test.ts::dispatch::rechecks a GitHub read at publication and withholds its answer after requester repo access is revoked, ::withholds a GitHub answer when a listed repository has no usable identity, ::withholds a github_repos answer when its listed repo is revoked before publication
A GitHub error body is redacted before it is sliced into the thrown message, in every client that slices one (RestGithubApi, pulls, comments, the App token mint, the issue tracker) — resident-repos item 62's GitHub half[unit] src/execution/githubApi.test.ts::*::a failure body is redacted before it is sliced into the error (resident-repos item 62's GitHub half); src/execution/githubPulls.test.ts::*::redacts a credential in the failure body before slicing it into the error (item 62); src/execution/githubApp.test.ts::*::redacts a credential in the mint failure body before slicing it into the error (item 62); src/execution/githubIssues.test.ts::*::redacts a credential in the failure body before slicing it into the error (item 62)
RestGithubApi reads: bearer = read token, accept headers (raw for big files, text-match for search), decoded/clipped/binary/raw content (the raw stream is cancelled at the clip), directory-vs-file 400s naming the other tool, URL-encoded paths, search scoping + limit clamp, repo paging, issue list drops PRs + query params and pages until limit issues (3-page cap), getIssue comments only when >0 and PR refused, non-2xx → GithubApiError with status + GitHub's message, no credential → 401 with no request[unit] src/execution/githubApi.test.ts::RestGithubApi — reads use the read token::*
Pull request metadata is a read-scoped REST call to /repos/<owner>/<repo>/pulls/<number>; the relayed github_pull_get tool renders title, body, exact head/base refs with the authoritative head policy above, and the merged state and time when GitHub reports state: closed plus merged_at, distinguishing a merged PR from one closed without merge. These facts are context, not publication authority; an unknown PR 404s; the description turn uses this tool[unit] src/execution/githubApi.test.ts::RestGithubApi — pull request reads::getPullRequest reads title, body and exact refs with the read token; src/execution/githubApi.test.ts::RestGithubApi — pull request reads::getPullRequest preserves merge time so a merged PR is not reported as merely closed; src/execution/githubApi.test.ts::InMemoryGithubApi::reads a seeded pull request and 404s an unknown number; src/tools/github.test.ts::github_* reads::github_pull_get renders PR metadata and body, with number and 404 errors; src/tools/github.test.ts::github_* reads::github_pull_get distinguishes a merged PR from a closed unmerged PR; src/core/descriptionTurn.test.ts::descriptionFollowUp — the user turn the model is given
RestGithubApi writes: bearer = write token; create omits empty label/assignee lists and clips a 70k body; update PATCHes only the given fields; comment returns the URL; delete = REST node id (read) → GraphQL mutation (write), FORBIDDEN → 403, PR → 400[unit] src/execution/githubApi.test.ts::RestGithubApi — writes use the write token::*
InMemoryGithubApi mirrors the contract (files/trees/search, sequential numbers, patch, comment count, delete recorded, unknown repo 404, newest-updated-first listing)[unit] src/execution/githubApi.test.ts::InMemoryGithubApi::*
Tools: unavailable without the capability (all thirteen, no throw); repos/file/tree/search/issue_list/issue_get renderings; 404 wording; slug/number/required-field corrections; reads sideEffectFree, writes not[unit] src/tools/github.test.ts::github_* reads::*
Writes: create → number + URL, update patches (state=closed) and refuses an empty patch, comment URL, delete permanent; the per-repo gate refuses all four BEFORE the API and reads still work; outside-installation 404 wording on a write[unit] src/tools/github.test.ts::github_issue_* writes::*
Toolsets: GitHub reads in full/readonly/web/assistant/explore/conductor/orchestrator; GitHub issue writes only in assistant + full; assistant = web_fetch + update_status + GitHub + recall + own-session notes; web adds submit_research; conductor adds the five run tools and session tools without shell, file tools, submissions or issue writes; explore = shell, file reads, web, skills, GitHub reads and session tools without GitHub writes or source submissions; none empty. Session notes are writer-fenced run state, not repository or GitHub writes[unit] src/tools/github.test.ts::toolset wiring::*, src/tools/web.test.ts::toolset + agent wiring::*
general: assistant toolset, 60 min; prompt names the tools, forbids claiming unperformed actions, redirects coding/review/research; research prompt names the read tools and forbids the public-404 conclusion[unit] src/agents/registry.test.ts::general*, ::research's prompt*
Dispatcher: a plain mention's run offers the github_* tools (no bash), an issue is created on the injected API, the reply carries the tool's number + URL; a restricted repo refuses an ungranted user before the API and admits a listed one[unit] src/core/dispatcher.test.ts::self-description in the system prompt … and the github_* tools::a plain mention opens an issue*, ::the issue write is gated*
Read-scoped token requests issues: read alongside contents/pull_requests/actions/checks/metadata[unit] src/execution/githubApp.test.ts::mints a READ-scoped token*
RestGithubApi live against the App credential: listRepos (the installation's repositories), listTree/readFile on one of them (a known file, its byte count), searchCode, listIssues/getIssue, then create → close → comment on a labelled smoke issue; deleteIssue → "Viewer not authorized to delete" (the App cannot delete; delete the smoke issue by hand)[agent] A scratch script driving RestGithubApi with the App credentials from a dev checkout; re-run after any change to the REST paths.
Live: @switchboard open an issue on the switchboard app with the title "foo" and the body "bar" (no directive) → the reply quotes <owner>/<repo>#<n> + URL and the issue exists; @switchboard close it in the thread → closed; @switchboard delete issue #<n> on <owner>/<repo> → the honest refusal (App cannot delete) with the offer to close[agent] Run in a channel the bot is in after the bot deploy; verify on GitHub.
Live: @switchboard agent:research find and explain how Switchboard's resident system works and how to configure priority repositories → the answer describes residents/onboarding/the cap from docs/reference/specs/resident-repos.md (cited by path), never "repo is private, inaccessible"[agent] Same channel.
8: a tool's requests are github.rest spans under its call with the route word, the mint a github.token_mint child[unit] src/execution/githubApi.test.ts::RestGithubApi.withSpan::*, src/core/harness/pi/relay.test.ts::runRelayedTool — a native tool run in the bot for pi::runs the tool with the run's context under the call's span — a tracing executor, the span on what it publishes — and answers pi's content
9: RestGithubApi Actions reads on the read token: the run and its jobs (100-row pages until short, at most 3) mapped field by field; one job; the log's 302 followed to the signed URL with NO authorization header; a log past the cap keeps its LAST maxChars and says complete: false; a direct 200 needs no second request; a missing run/job is a 404; a 302 without a location is a 502[unit] src/execution/githubApi.test.ts::RestGithubApi — Actions reads use the read token::*
9: InMemoryGithubApi serves seeded runs, jobs and logs (tail-capped), 404s an unknown run, job or repo[unit] src/execution/githubApi.test.ts::InMemoryGithubApi — Actions::*
9: github_actions_run renders the run header, jobs failed-first with durations, failed/skipped steps, URLs for non-success jobs and the next-step line; takes a run or job URL (/attempts/<n> included) or repo + id; refuses a contradicting repo, a non-numeric id, an id without a repo; words a 404. github_actions_job_log renders steps, ##[error] lines and the tail with timestamps and ANSI stripped and a token redacted; lines clamps to 2000; match filters and says none; takes a job URL, refuses a run URL, a bad id, an id without a repo; words a 404; a live job says its log is partial. Both sideEffectFree and in GITHUB_READ_TOOLS[unit] src/tools/github.test.ts::github_actions_* reads::*
9: general and research prompts name github_actions_run (and general github_actions_job_log)[unit] src/agents/registry.test.ts::general's prompt names its GitHub tools*, ::research's prompt names the GitHub read tools*
9 live: @switchboard why did <a failed Actions run URL of ours> fail? (no directive) → the reply names the failed job and step and quotes its ##[error] line, from github_actions_run then github_actions_job_log on the run's timeline[agent] Run in a channel the bot is in after the bot deploy; read the run page's tool calls.
All visible repository metadata carries bounded access dependencies; oversized catalogs name their omission and preserve lazy named reads.[unit] src/core/dispatch/repositoryBriefs.test.ts::connected repository briefs::declares every exposed metadata repository and explicitly bounds a large catalog
An authorized GitHub read records its exact producer, call, tool, repositories and returned bytes before exposure. Its receipt survives session replay and child handoff; altered bytes, an unrelated call and a different repository never inherits its proof.[unit] src/core/dispatch/githubReadContext.test.ts::durable requester-scoped GitHub reads::*, src/core/references/sourceResultContext.test.ts::public GitHub result receipts::*, src/core/runLedger/writeThrough.test.ts::open — claim and seed::writes admitted GitHub result receipts with their exact mirrored bytes and preserves them in a later seed…
A production App installation token can verify a bound requester's current permission on a selected private repository, and a revoked requester loses access before a later answer.[gap] The REST shape, identity match and revocation are unit-tested; no installed App credential request or deployed private-DM answer was exercised in this source change.
Native bound history reads attach optional controller metadata to the existing source result receipt: exact target/head, page order, layout and history fingerprint, plus payload offset/length/hash inside the returned bytes. Capping removes history credit; metadata-absent receipts remain readable but require a new history read before restoration. A controller-only layout override never exceeds the current safe result budget.[unit] src/core/dispatch/reviewHistoryRestore.test.ts::review history restoration from canonical results::binds structured history…, ::withholds history credit…, ::preserves a recorded custom page size…, ::restarts partial history…, ::does not restore completion from…
Same-run source references use a fail-closed versioned envelope; downgrade, replacement evidence and changed result bytes cannot preserve known context. Both canonical stores retain exact covered v1 and v2 results.[unit] src/core/references/contextDependencies.test.ts::whole-context dependencies::keeps same-run native references…; src/core/runLedger/writeThrough.test.ts::open — claim and seed::writes admitted GitHub result receipts…; deploy/cloudflare-memory/sessionUnification.test.ts::unified sessions — durable owner and retention lifecycle::hash-verifies repository results…
Empty successful global searches cannot prove retained private access after installation removal or contents revocation. Unknown global issue-search families remain unproved.[unit] src/core/dispatch/executionGithubContext.test.ts::current native execution read authority::*
Public sources outside the installation catalog pass only with explicit current public metadata for the same name and a successful original read. A public source cannot admit a missing private dependency. Metadata or read failures remain closed diagnostic refusals. A public reread with different delivered bytes cannot authorize a formerly private result.[unit] src/execution/githubApi.test.ts::RestGithubApi — reads use the read token::requires explicit current public metadata for the same repository on the read token; src/core/dispatch/executionGithubContext.test.ts::current native execution read authority::rechecks an uninstalled public source…, ::keeps a private dependency denied after proving a public source, ::denies retained private bytes when a now-public source returns different content, ::retains public visibility probe failures…; src/core/dispatch/contextAccess.test.ts::message-bound context access::checks source access, receipts, session and bytes…
Current execution read refusal identifies the failed stage, tool and read operation, a hash of the original call, and closed credential, native HTTP, local API, timeout, transport, decode or unknown failure facts. Invalid request IDs and all source bytes stay out of diagnostics; successful reads retain their authority checks.[unit] src/core/dispatch/executionGithubContext.test.ts::current native execution read authority::retains …; ::allows current reads and distinguishes a tool that never reaches the API; src/core/dispatch/contextAccess.test.ts::message-bound context access::checks source access, receipts, session and bytes…
Native GitHub probes retain the closed route and classify transport errors even without a parent trace. Synthetic API validation statuses are not reported as observed HTTP responses.[unit] src/core/dispatch/executionGithubContext.test.ts::current native execution read authority::identifies a native …; ::retains …; ::identifies signed job-log …; ::attributes a concurrent Actions failure to its original read operation; ::retains observed response facts for …; ::preserves …; ::retains signed log body …

Native identities in read metadata ​

The existing GithubApi.listRepos and getPullRequest results retain native GitHub IDs when the source supplies them. A PR object ID stays separate from its repository-local number. Head and base repository IDs stay separate, including a fork. An explicit null head repository stays null; it cannot borrow the base repository ID. Repository names remain display/source metadata and do not define native identity.

Older or seeded metadata without IDs remains readable, with those fields absent. A supplied malformed, non-positive or unsafe numeric ID is refused; it is never coerced or replaced with a name, PR number or generated value. Both REST and in-memory implementations preserve this contract. Existing token permissions, request paths and user-facing tool formatting stay unchanged. These IDs are scoped to the GitHub client/source; they do not prove current access, freshness, source custody or a valid task match.

Repository and PR read metadata also retain the credential's non-secret source: host, App ID and installation ID. The existing mint cache captures this source with the token; later configuration changes cannot relabel an older token. Each response retains its own source through concurrent reads, pagination and traced client views. Static or injected opaque tokens have no installation source. Invalid configured IDs leave source identity unknown; malformed supplied numeric source metadata is refused. A branch tip can override the PR head only when both reads have the same known source, or both sources remain unknown under the existing legacy policy. The source describes the read credential, not the installation membership of a fork or a grant of current access.

This extends read metadata only. Canonical source/context matching, native-ID backfill and access checks still need the shared intake integration. A future native matcher must hold missing identity instead of guessing from a name. No source producer, inbox delivery or ownership change is enabled.

CriterionProof
Existing read calls retain distinct PR/head/base IDs and stable repository identity through a name change; credentials and paths stay read-scoped[unit] src/execution/githubApi.test.ts::GitHub native read identities::retains distinct PR and fork/base repository IDs through the existing read route; ::keeps repository identity when its name changes; ::keeps native IDs when the existing branch-tip policy selects a newer same-repository commit
Repository and PR metadata retain the host/App/installation of the exact read credential. Cached credentials retain their original source after configuration changes; opaque credentials remain unknown. Concurrent calls and listing pages keep their own source. A branch tip from a different source cannot override PR metadata.[unit] src/execution/githubApp.test.ts::resolveGithubCredential::keeps the installation that minted a cached credential and exposes it on the corresponding API read; ::leaves a noncanonical configured App ID unknown instead of coercing source identity; src/execution/githubApi.test.ts::GitHub native read identities::binds concurrent reads to their own credential and keeps opaque tokens unknown; ::does not combine a PR with a branch tip read by another installation; ::retains the credential source of each repository page and the in-memory metadata
Deleted forks keep null identity; legacy metadata stays readable without invented IDs. The in-memory port preserves native metadata and legacy absence.[unit] src/execution/githubApi.test.ts::GitHub native read identities::preserves a deleted fork as null and does not guess its repository from the base; ::keeps old metadata readable without inventing missing IDs; ::retains native IDs in the in-memory implementation and keeps legacy IDs absent
Native IDs do not replace the PR number, refs or body in the existing read tool[unit] src/tools/github.test.ts::github_* reads::github_pull_get renders PR metadata and body, with number and 404 errors
Supplied malformed IDs are refused before they can become identity evidence[unit] src/execution/githubApi.test.ts::GitHub native read identities::refuses a supplied malformed native PR ID…; ::refuses malformed repository IDs in each read position
Malformed credential source metadata is refused before an HTTP read or in-memory result[unit] src/execution/githubApi.test.ts::GitHub native read identities::refuses malformed supplied credential identity before reading source data
Concurrent cold mints return each token with its own source and expiry. A completed concurrent mint cannot make a PR read lose known source and reject its current same-source branch tip.[unit] src/execution/githubApp.test.ts::resolveGithubCredential::keeps overlapping mint results together through the real API resolver and selects the current branch tip
Native-ID matching and current source/access checks bind the orchestrator context before internal handling[gap] This cut returns metadata only; no new matcher or source producer is enabled.

Saved native task source context ​

A complete PR read can add githubSource to the existing RepoContext and LiveRunMeta. The snapshot keeps the read credential's host/App/installation, the native base repository and PR IDs, the PR number, separate nullable fork identity, and observed head/base refs and SHA. Explicit, accepted and inherited PR resolution use the same projection. Partial metadata and opaque credential identity stay absent. No name supplies a missing native ID.

The existing reservation and execution claim save the snapshot with the run. Promotion preserves a carried row's original snapshot. The existing JSON stores retain it through reclaim; resume restores it only for the same PR number. The private main conversation still omits repository metadata. These are source observations, not execution targets, source-access grants or instructions. Ref strings remain source data and do not authorize a checkout. A future matcher must validate the canonical row, original orchestrator link and current source/access before routing to the orchestrator.

This cut does not index or match source events, route to workers, backfill legacy rows, persist private main context, or retain the snapshot in finished public run summaries. Those paths remain unqualified. A missing or invalid source snapshot gives future matching no native proof.

CriterionProof
Closed native snapshots retain source, PR/base/fork identities and an explicit null fork; malformed, incomplete and authority-bearing records are held[unit] src/execution/githubReadSource.test.ts::GitHub task source context::*
Explicit, accepted and inherited PR resolution retain the native snapshot without binding a fork ref on the base repo[unit] src/core/repoContext.test.ts::resolveRepoContext: read-scoped PR lookup::keeps native source identity for explicit, accepted and inherited PR targets
Reservation and promotion persist native source metadata while the private main reservation omits it[unit] src/core/dispatch/provision.test.ts::reserveRun — the ledger reservation before the attach::keeps repository facts on a non-private main run's reservation; ::stores only a matching requester DM provenance on the reserved row; src/core/dispatch/run.test.ts::claimRun — the ledger claim once the prompt exists::promotes a same-ID segment with its raw original baseline, prior push and PR base before writable execution
Saved source metadata survives both existing storage implementations and generation reclaim[unit] src/core/runLedger/inMemory.test.ts::native GitHub source recovery::retains the saved source snapshot when a new generation reclaims the run; [worker] deploy/cloudflare-memory/runLedger.test.ts::run ledger — claim and admission (item 29)::retains the native GitHub source snapshot through the existing SQLite claim and live read
Resume restores the saved snapshot for its original PR and does not relabel it after a target change[unit] src/core/resumeLaunch.test.ts::*::restores a native source snapshot without borrowing a changed PR number
Canonical source matching, private context, legacy migration and current access are qualified before source delivery[gap] No matcher or source delivery caller is enabled in this cut.