Skip to content

gh Adapter Contract

gh Adapter Contract — The gh Surface BoB Depends On

Section titled “gh Adapter Contract — The gh Surface BoB Depends On”

What a second forge backend behind bin/gh would have to reproduce (#1753). (Back to reference index.)

BoB talks to its forge through gh. A backend for another forge (#1746) does not have to be all of gh: it has to be the part of gh BoB uses. This page names that part, and the engine that reads it from the tree so it stays true.

scripts/forge/gh-inventory.js reads every tracked file and lists each gh call it finds, by command:

Terminal window
node scripts/forge/gh-inventory.js scan # one line per command: executable and prompt-text counts
node scripts/forge/gh-inventory.js scan --json # gh-inventory/1: totals, per-command flags / --json fields / files, every site

Two kinds of call site are counted apart, and the split is the point:

Kind Where Why it matters
executable .sh / .js / .ts / workflow YAML, and extensionless scripts with a shell or node shebang Code that runs gh. It could be ported by editing a function.
prompt-text .md under commands/, skills/, agents/, registry/, runbooks/, docs/, templates/, and the root guides Instructions an agent follows and then runs through Bash. They can only be ported by keeping gh working, which is why a gh-shaped shim beats a rewrite.

Not read: tests/ (they stub gh), www/ (published pages), journal/, story/, wiki/, evals/, CHANGELOG.md and app test files.

Two forms are recognised:

  • shell — gh issue list --json number,title on a line. In code, comment lines are skipped. <N> placeholders are words, and the command ends at the first pipe, ;, &&, redirect or closing backtick.
  • argv — a JS/TS array literal whose first element is a gh command and the second its subcommand: gh(['issue', 'list', …]), execFileSync('gh', ['api', …]), const args = ['pr', 'merge', …]. A command that takes no subcommand (status, browse) only counts with gh in sight, so git’s ['status', '--porcelain'] is not read as gh status.

A subcommand outside gh’s own command tree is prose (“the gh issue family”), not a call. An argv array assembled piece by piece is not seen; the shim’s runtime record (Forge Health → The shim) sees every call that actually runs, and is the cross-check for those.

A call is keyed <command> <subcommand> — issue list, pr merge. For gh api the key is the normalised endpoint: every interpolation (${x}, $X, <N>, {owner}) and every numeric path segment becomes {}, and the repo is always one {} whether it was written {owner}/{repo}, ${slug} or a literal owner and name — so api repos/{}/issues/{} is one endpoint. api graphql is its own key.

A --json field list held in a variable cannot be read statically; the command is marked json_dynamic.

scripts/forge/gh-contract.json (gh-contract/1) records one entry per command the tree calls:

Field Meaning
behavior What the command does for BoB — the part of gh’s behaviour a backend must reproduce.
output The output shape callers read: a JSON array or object of the --json fields, a URL whose last segment is the new number, a REST body, or nothing but the exit code.
portable true — a backend must reproduce it. false — known-unsupported: no forge-neutral equivalent.
reason For an unsupported command, why.
flags, json The flags and --json fields BoB calls it with. json_dynamic: true when a field list is held in a variable.

The behavior, output, portable and reason text is written by hand. The flags and fields are what the scan finds, recorded by record.

  • Transparency. Everything outside the contract passes through unchanged: stdin, stdout, stderr and the exit code (Forge Health → The shim).
  • An honest failure. A read that cannot be answered exits non-zero; it is never an empty list. The cache, the queue and the callers rely on telling “nothing matched” from “the lookup failed”.
Terminal window
make check-gh-contract # the gate, part of make check
node scripts/forge/gh-inventory.js check --json # gh-contract-check/1
node scripts/forge/gh-inventory.js record # fold new usage into the contract, re-render this page
node scripts/forge/gh-inventory.js write # re-render this page's block only

check fails, naming the call sites, when:

Problem Meaning Fix
unrecorded-command The tree calls a gh command the contract does not list. record, then write its behavior, output and portable.
unrecorded-flag / unrecorded-json A known command is called with a new flag or --json field, or a field list in a variable. record — and know a backend must now support it.
unused-command The contract lists a command nothing calls. record drops it.
undocumented An entry has no behavior, output or portable, or is unsupported with no reason. Write the text.
doc-drift / doc-missing The generated block below is not what the contract renders. write.

New coupling to the forge therefore cannot land silently: it lands with a contract edit in the same diff, where a reviewer sees it. Exit codes: 0 clean, 1 drift, 2 the tree or contract could not be read.

The cap behind this contract (#1746) measures “zero GraphQL in executable code”. Today there are four executable callers of gh api graphql, and each is kept, and listed as known-unsupported, not ported:

Caller What one query reads
scripts/brief/reconcile.js The state and labels of up to 60 issues across repos, for the brief reconciler (#3100).
scripts/brief/brief.js The newest releases of every repo in the portfolio, for the briefing (#2243).
scripts/forge/batch.js A repo’s issues 40 at a time, for the stale-todo sweep (#3219).
scripts/release/notes.js 40 issue titles, bodies and labels at a time, for the release notes.

Each was batched on purpose: one query answers what dozens of REST calls (or gh issue views) would, and that is what keeps these readers inside the quota. A port would multiply quota spend by the batch size for no gain on GitHub, so a backend refuses them instead; every caller already treats a failed read as unknown, never as an empty answer. Each call site carries a one-line comment naming this contract.

Nothing passes the metric silently. The generated block names every executable file that calls an unsupported command, so a fifth GraphQL caller changes the block, and make check-gh-contract fails with doc-drift until the block is re-rendered in the same diff — where a reviewer sees the new row.

85 commands: 51 a backend must reproduce, 34 known-unsupported. GraphQL in executable code: 7 call site(s) in 4 file(s), listed as unsupported below.

Command Behavior Output Flags --json fields
api repos/{} Read one repository’s metadata (default branch, visibility, archived). the REST response body as JSON (or the –jq projection of it) --jq —
api repos/{}/collaborators List the repo’s collaborators. the REST response body as JSON (or the –jq projection of it) — —
api repos/{}/collaborators/{} Add or remove one collaborator (-X PUT / DELETE). the REST response body as JSON (or the –jq projection of it) -X —
api repos/{}/compare/{}...{} Compare two refs: ahead/behind counts and the commits between. the REST response body as JSON (or the –jq projection of it) — —
api repos/{}/contents/{} Read one file’s content at a ref. the file’s content (raw with -H Accept: raw, else the contents object) -H —
api repos/{}/git/refs/heads/master Move master to a SHA (-X PATCH -f sha=…). the REST response body as JSON (or the –jq projection of it) -X -f —
api repos/{}/git/refs/heads/{} Create, move or delete a branch ref — merge-on-green’s fast-forward (-X PATCH -f sha=… -F force=false). the REST response body as JSON (or the –jq projection of it) -F -X -f —
api repos/{}/issues List or create issues through REST (–paginate; -X POST -f title=…). the REST response body as JSON (or the –jq projection of it) --jq --paginate -X -f —
api repos/{}/issues/comments/{} Read, edit or delete one issue comment (-X PATCH –input). the REST response body as JSON (or the –jq projection of it) --input --jq -X —
api repos/{}/issues/{} Read one issue, or patch its state, body or labels (-X PATCH –input). the REST response body as JSON (or the –jq projection of it) --input --jq -X —
api repos/{}/issues/{}/comments List or add an issue’s comments (–paginate; -X POST -f body=…). the REST response body as JSON (or the –jq projection of it) --jq --paginate -X -f —
api repos/{}/labels List the repo’s labels (–paginate). the REST response body as JSON (or the –jq projection of it) --jq --paginate —
api repos/{}/pulls/{}/merge Merge one pull request (-X PUT). the REST response body as JSON (or the –jq projection of it) -X —
api user Read the signed-in account (its login). the REST response body as JSON (or the –jq projection of it) -q —
api user/keys List the account’s SSH keys. the REST response body as JSON (or the –jq projection of it) --jq —
auth login Sign the machine in to the forge. nothing callers parse; the exit code is the answer --git-protocol --hostname --web —
auth status Is this machine signed in? Exit 0 = yes. nothing callers parse; the exit code is the answer — —
auth token Print the current token, for a caller that talks HTTP itself. the token on stdout — —
issue close Close an issue, optionally with a comment and a reason (completed / not planned). nothing callers parse; the exit code is the answer --comment --reason --repo -R —
issue comment Add a comment to an issue. the comment URL on stdout (not parsed) --body --body-file --repo -R —
issue create File an issue with a title, body, labels and assignee. the URL of the new object on stdout — callers read the number from its last path segment --assignee --body --body-file --label --repo --title -R —
issue edit Change an issue’s body, title or labels (–add-label / –remove-label). the issue URL on stdout (not parsed) --add-label --body --body-file --remove-label --repo —
issue list List issues by state, label, assignee or a search string. –search takes GitHub query syntax, which a backend must translate or refuse. a JSON array of objects carrying exactly the requested –json fields (or the –jq projection of it) --assignee --jq --json --label --limit --repo --search --state -R -l -q -s assignees body closedAt comments createdAt labels number state stateReason title updatedAt url + dynamic
issue reopen Reopen a closed issue. nothing callers parse; the exit code is the answer --comment --repo —
issue view Read one issue: body, labels, state, comments. one JSON object carrying the requested –json fields (or the –jq projection of it); without –json, human text callers do not parse --comments --jq --json --repo -q assignees body closedAt comments createdAt labels number state stateReason title url + dynamic
label create Create a label with a colour and description; –force updates one that exists. nothing callers parse; the exit code is the answer --color --description --force -R —
label delete Delete a label. nothing callers parse; the exit code is the answer --repo --yes —
label edit Rename a label or change its colour or description. nothing callers parse; the exit code is the answer --name --repo —
label list List the repo’s labels. a JSON array of objects carrying exactly the requested –json fields (or the –jq projection of it) --jq --json --limit --search description name
pr close Close a pull request, optionally deleting its branch. nothing callers parse; the exit code is the answer --comment --delete-branch —
pr comment Add a comment to a pull request. the comment URL on stdout (not parsed) --body --repo —
pr create Open a pull request from a head branch to a base. the URL of the new object on stdout — callers read the number from its last path segment --base --body --body-file --fill --head --label --title —
pr diff Print a pull request’s diff. a unified diff on stdout — —
pr edit Change a pull request’s body or labels. nothing callers parse; the exit code is the answer --add-label --body —
pr list List pull requests by state, base, head, author or label. statusCheckRollup is GitHub’s checks model; a backend must synthesise it from its own commit statuses. a JSON array of objects carrying exactly the requested –json fields (or the –jq projection of it) --author --base --head --jq --json --label --limit --search --state -R -q body createdAt headRefName isDraft labels mergedAt number state statusCheckRollup title url
pr merge Merge a pull request (–squash, –delete-branch). –admin bypasses branch protection, which a backend may not offer. nothing callers parse; the exit code is the answer --delete-branch --squash —
pr view Read one pull request: body, head branch, commits, check rollup. one JSON object carrying the requested –json fields (or the –jq projection of it); without –json, human text callers do not parse --jq --json --repo -q body commits headRefName number statusCheckRollup url + dynamic
release create Publish a release for a tag, with notes from a file or generated. the URL of the new object on stdout — callers read the number from its last path segment --generate-notes --notes --notes-file --title --verify-tag —
release download Download a release’s assets. files written to the working directory — —
release list List releases. a JSON array of objects carrying exactly the requested –json fields (or the –jq projection of it) --json --limit --repo isDraft name publishedAt tagName
release view Read one release (tag, URL). one JSON object carrying the requested –json fields (or the –jq projection of it); without –json, human text callers do not parse --jq --json tagName url
repo archive Archive a repository. nothing callers parse; the exit code is the answer --yes —
repo clone Clone a repository by owner/name. a git checkout on disk — —
repo create Create a repository, optionally from a local source directory. the repository URL on stdout --private --source —
repo edit Change repository settings (docs show --help). nothing callers parse; the exit code is the answer --help —
repo list List an owner’s repositories. a JSON array of objects carrying exactly the requested –json fields (or the –jq projection of it) --json --limit isArchived isPrivate nameWithOwner pushedAt
repo unarchive Unarchive a repository. nothing callers parse; the exit code is the answer — —
repo view Read the current repository: name, owner, default branch, visibility, archived. one JSON object carrying the requested –json fields (or the –jq projection of it); without –json, human text callers do not parse --jq --json -q defaultBranchRef isArchived isPrivate name nameWithOwner + dynamic
ssh-key add Register a public SSH key on the account. nothing callers parse; the exit code is the answer --title —
ssh-key delete Remove an SSH key from the account. nothing callers parse; the exit code is the answer — —
ssh-key list List the account’s SSH keys. human text — —

No portable equivalent: a backend refuses these, and the callers named here are what a port would have to change.

Command Behavior Why it is unsupported Executable call sites Prompt-text call sites
api graphql One GraphQL query that reads many objects at once: the state and labels of up to 60 cross-repo issues for the brief reconciler (scripts/brief/reconcile.js, #3100), the newest releases of every repo for the briefing (scripts/brief/brief.js, #2243), a repo’s issues in chunks for the stale-todo sweep (scripts/forge/batch.js, #3219) and 40 issue titles at a time for the release notes (scripts/release/notes.js). GitHub’s GraphQL schema. Each of these reads is batched on purpose: one query answers what dozens of REST calls would, which is what keeps the briefing, the reconciler and the stale-todo sweep inside the quota (#3100, #3219). A port to REST or gh issue view would multiply quota spend by the batch size, so they stay GraphQL and a backend refuses them; every caller already treats a failed read as unknown, never as an empty answer. A new executable caller changes this row, so it cannot land silently. scripts/brief/brief.js scripts/brief/reconcile.js scripts/forge/batch.js scripts/release/notes.js docs/reference/bob-asks.md docs/reference/briefing.md docs/reference/forge-health.md
api rate_limit Read the token’s remaining quota per resource (the shim’s budget, #3190). GitHub’s quota model; the shim’s pacing (scripts/forge/budget.js) is built on it. A backend reports its own limits or none. scripts/bootstrap-labels.sh scripts/forge/budget.js scripts/forge/health.js docs/reference/forge-health.md
api repos/{}/actions/permissions Read the repo’s Actions permission settings. GitHub Actions: runners, runs, workflows, secrets and variables have no forge-neutral model — a backend with its own CI needs its own mapping, and BoB’s CI locus (docs/reference/ci-locus.md) is GitHub-only today — registry/runbooks/bob-attack-surface-review.md
api repos/{}/actions/runners List the repo’s self-hosted runners and their status. GitHub Actions: runners, runs, workflows, secrets and variables have no forge-neutral model — a backend with its own CI needs its own mapping, and BoB’s CI locus (docs/reference/ci-locus.md) is GitHub-only today scripts/dashboard/server.js scripts/fleet/ci-locus.js scripts/fleet/ci-rollout.js scripts/fleet/runner.sh —
api repos/{}/actions/runners/registration-token Mint a runner registration token. GitHub Actions: runners, runs, workflows, secrets and variables have no forge-neutral model — a backend with its own CI needs its own mapping, and BoB’s CI locus (docs/reference/ci-locus.md) is GitHub-only today scripts/fleet/runner.sh —
api repos/{}/actions/runners/remove-token Mint a runner removal token. GitHub Actions: runners, runs, workflows, secrets and variables have no forge-neutral model — a backend with its own CI needs its own mapping, and BoB’s CI locus (docs/reference/ci-locus.md) is GitHub-only today scripts/fleet/runner.sh —
api repos/{}/actions/runs List workflow runs. GitHub Actions: runners, runs, workflows, secrets and variables have no forge-neutral model — a backend with its own CI needs its own mapping, and BoB’s CI locus (docs/reference/ci-locus.md) is GitHub-only today scripts/fleet/ci-locus.js —
api repos/{}/actions/runs/{}/jobs List the jobs of one workflow run. GitHub Actions: runners, runs, workflows, secrets and variables have no forge-neutral model — a backend with its own CI needs its own mapping, and BoB’s CI locus (docs/reference/ci-locus.md) is GitHub-only today scripts/dashboard/server.js scripts/fleet/ci-locus.js runbooks/self-hosted-runner.md
api repos/{}/actions/variables/{} Read one Actions variable (CI_RUNNER_LABEL). GitHub Actions: runners, runs, workflows, secrets and variables have no forge-neutral model — a backend with its own CI needs its own mapping, and BoB’s CI locus (docs/reference/ci-locus.md) is GitHub-only today scripts/fleet/ci-locus.js scripts/fleet/ci-rollout.js scripts/fleet/runner.sh —
api repos/{}/actions/workflows List the repo’s workflows. GitHub Actions: runners, runs, workflows, secrets and variables have no forge-neutral model — a backend with its own CI needs its own mapping, and BoB’s CI locus (docs/reference/ci-locus.md) is GitHub-only today scripts/fleet/ci-rollout.js —
api repos/{}/branches/master/protection Read master’s branch protection. GitHub’s branch-protection model; a backend’s protected-branch settings do not share its shape. — registry/runbooks/bob-attack-surface-review.md
api repos/{}/branches/{}/protection Read or replace a branch’s protection (-X PUT –input). GitHub’s branch-protection model; a backend’s protected-branch settings do not share its shape. scripts/fleet/branch-protection.js —
api repos/{}/branches/{}/protection/required_status_checks Read a branch’s required status checks (merge-on-green’s base-side read). GitHub’s branch-protection model; merge-on-green falls back to .github/required-checks.json when it cannot read this. scripts/promotion/merge-on-green.sh —
api repos/{}/commits/{}/check-runs List the check runs reported on one commit. GitHub Checks: check runs come from GitHub Apps and Actions and have no forge-neutral model — a backend with its own CI needs its own mapping — docs/how-to/promotion-pipeline-setup.md
api repos/{}/hooks List the repo’s webhooks. GitHub’s webhook model and event names; a backend’s hooks carry different events and payloads. — skills/todo-writing/references/platform-recipes.md
api repos/{}/pages Read the repo’s GitHub Pages site. GitHub Pages; BoB publishes its sites through Cloudflare, and this read is a docs example. — skills/todo-writing/references/platform-recipes.md
api repos/{}/rulesets List the repo’s rulesets. GitHub rulesets have no forge-neutral equivalent. — skills/todo-writing/references/platform-recipes.md
api user/installations List the GitHub App installations the token can see. GitHub Apps. — registry/runbooks/bob-attack-surface-review.md registry/runbooks/drill-farm-compromise.md
api {} A call whose endpoint is held in a variable (a docs example of gh api <path>). The endpoint cannot be read from the tree, so the contract cannot promise it. — docs/reference/forge-health.md
api {}/{}/settings/billing/actions Read an owner’s Actions minutes against the included quota (#1695). GitHub billing for Actions minutes. scripts/fleet/actions-budget.js —
api {}/{}/settings/billing/usage Read an owner’s billing usage (#1695). GitHub billing. scripts/fleet/actions-budget.js —
auth refresh Add OAuth scopes to the token (-h host -s user,…). GitHub OAuth scopes; a backend’s token permissions are named differently. scripts/fleet/actions-budget.js commands/what-next.md docs/reference/actions-budget.md
run list List workflow runs for a branch, commit or workflow. GitHub Actions: runners, runs, workflows, secrets and variables have no forge-neutral model — a backend with its own CI needs its own mapping, and BoB’s CI locus (docs/reference/ci-locus.md) is GitHub-only today .github/workflows/auto-release.yml scripts/asks/done-check.js scripts/brief/completion.js scripts/dashboard/project-detail.js scripts/dashboard/series.js scripts/dashboard/server.js scripts/fleet/ci-health.js scripts/fleet/ci-locus.js scripts/fleet/ci-rollout.js scripts/release/release.sh scripts/rituals/evidence.js docs/explanation/bob-viz.md docs/explanation/warp-drive-internals.md docs/how-to/promotion-pipeline-setup.md docs/how-to/versioning.md docs/reference/ci-health.md docs/reference/project-detail.md docs/reference/series.md runbooks/local-delivery.md runbooks/self-hosted-runner.md
run rerun Re-run a workflow run (–failed). GitHub Actions: runners, runs, workflows, secrets and variables have no forge-neutral model — a backend with its own CI needs its own mapping, and BoB’s CI locus (docs/reference/ci-locus.md) is GitHub-only today scripts/asks/done-check.js scripts/fleet/ci-locus.js docs/reference/ci-locus.md docs/reference/todo-format-contract.md runbooks/self-hosted-runner.md
run view Read one workflow run, its jobs or its log. GitHub Actions: runners, runs, workflows, secrets and variables have no forge-neutral model — a backend with its own CI needs its own mapping, and BoB’s CI locus (docs/reference/ci-locus.md) is GitHub-only today scripts/asks/done-check.js scripts/fleet/ci-locus.js docs/reference/ci-locus.md registry/runbooks/github-pat-rotation.md runbooks/self-hosted-runner.md
run watch Wait for a workflow run to finish (–exit-status). GitHub Actions: runners, runs, workflows, secrets and variables have no forge-neutral model — a backend with its own CI needs its own mapping, and BoB’s CI locus (docs/reference/ci-locus.md) is GitHub-only today — registry/runbooks/cloudflare-api-token-rotation.md registry/runbooks/github-pat-rotation.md
search issues Search issues across an owner or repos with GitHub query syntax (the briefing’s sources). GitHub’s search API and query syntax (is:, label:, repo:, owner-wide) — a backend has no equivalent; callers would have to move to per-repo issue/pr list filters scripts/brief/brief.js scripts/dashboard/briefings.js scripts/dashboard/notices.js scripts/dashboard/reports.js scripts/dashboard/series.js scripts/forge/cache.js scripts/warp-drive/preflight-todo-cleanup.js docs/explanation/bob-viz.md docs/reference/briefing.md docs/reference/notices.md docs/reference/reports.md docs/reference/series.md
search prs Search pull requests across an owner (the briefing’s merged and open PRs). GitHub’s search API and query syntax (is:, label:, repo:, owner-wide) — a backend has no equivalent; callers would have to move to per-repo issue/pr list filters scripts/brief/brief.js docs/reference/briefing.md
secret list List a repo’s or org’s Actions secrets by name (never values). GitHub Actions: runners, runs, workflows, secrets and variables have no forge-neutral model — a backend with its own CI needs its own mapping, and BoB’s CI locus (docs/reference/ci-locus.md) is GitHub-only today scripts/asks/done-check.js scripts/docs-site/init.sh scripts/security/cred-check.js scripts/security/secrets.js docs/reference/bob-secrets.md docs/reference/secrets-inventory.md registry/runbooks/bob-attack-surface-review.md registry/runbooks/bridge-secret-rotation.md registry/runbooks/cloudflare-api-token-rotation.md registry/runbooks/drill-lost-laptop.md registry/runbooks/github-pat-rotation.md registry/skills/secrets-management/SKILL.md registry/skills/tailscale-remote-ops/SKILL.md skills/todo-writing/assets/todo-template.md skills/todo-writing/references/platform-recipes.md
secret set Set an Actions secret. GitHub Actions: runners, runs, workflows, secrets and variables have no forge-neutral model — a backend with its own CI needs its own mapping, and BoB’s CI locus (docs/reference/ci-locus.md) is GitHub-only today scripts/docs-site/init.sh scripts/fleet/distribute-secret.sh scripts/security/cred-check.js scripts/security/secrets.js docs/how-to/promotion-pipeline-setup.md docs/how-to/secrets-check.md docs/reference/bob-secrets.md docs/reference/branch-protection.md docs/reference/secrets-inventory.md registry/runbooks/bridge-secret-rotation.md registry/runbooks/cloudflare-api-token-rotation.md registry/runbooks/github-pat-rotation.md registry/skills/docs-site/SKILL.md registry/skills/secrets-management/SKILL.md skills/todo-writing/references/platform-recipes.md
variable delete Delete an Actions variable. GitHub Actions: runners, runs, workflows, secrets and variables have no forge-neutral model — a backend with its own CI needs its own mapping, and BoB’s CI locus (docs/reference/ci-locus.md) is GitHub-only today scripts/fleet/runner.sh —
variable list List the repo’s Actions variables. GitHub Actions: runners, runs, workflows, secrets and variables have no forge-neutral model — a backend with its own CI needs its own mapping, and BoB’s CI locus (docs/reference/ci-locus.md) is GitHub-only today — registry/runbooks/bob-attack-surface-review.md skills/todo-writing/references/platform-recipes.md
variable set Set an Actions variable (CI_RUNNER_LABEL, #1728). GitHub Actions: runners, runs, workflows, secrets and variables have no forge-neutral model — a backend with its own CI needs its own mapping, and BoB’s CI locus (docs/reference/ci-locus.md) is GitHub-only today scripts/fleet/runner.sh docs/reference/branch-protection.md skills/todo-writing/references/platform-recipes.md
workflow run Dispatch a workflow. GitHub Actions: runners, runs, workflows, secrets and variables have no forge-neutral model — a backend with its own CI needs its own mapping, and BoB’s CI locus (docs/reference/ci-locus.md) is GitHub-only today — docs/how-to/skill-authoring.md registry/runbooks/cloudflare-api-token-rotation.md registry/runbooks/github-pat-rotation.md
  • Forge Health — the shim every call goes through, and the cache, queue and budget behind it.