Skip to content

Build master @ bc50cd5bunreleased — 31 commits past v1.282.0Last reviewed Last changed Grade fresh (aging >90d, stale >180d)Source / History

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.

The inventory ​

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

bash
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:

KindWhereWhy it matters
executable.sh / .js / .ts / workflow YAML, and extensionless scripts with a shell or node shebangCode 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 guidesInstructions 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.

Keys ​

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.

The contract ​

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

FieldMeaning
behaviorWhat the command does for BoB — the part of gh's behaviour a backend must reproduce.
outputThe 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.
portabletrue — a backend must reproduce it. false — known-unsupported: no forge-neutral equivalent.
reasonFor an unsupported command, why.
flags, jsonThe 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.

What every backend must also keep ​

  • 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".

The drift gate ​

bash
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:

ProblemMeaningFix
unrecorded-commandThe tree calls a gh command the contract does not list.record, then write its behavior, output and portable.
unrecorded-flag / unrecorded-jsonA 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-commandThe contract lists a command nothing calls.record drops it.
undocumentedAn entry has no behavior, output or portable, or is unsupported with no reason.Write the text.
doc-drift / doc-missingThe 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.

GraphQL ​

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:

CallerWhat one query reads
scripts/brief/reconcile.jsThe state and labels of up to 60 issues across repos, for the brief reconciler (#3100).
scripts/brief/brief.jsThe newest releases of every repo in the portfolio, for the briefing (#2243).
scripts/forge/batch.jsA repo's issues 40 at a time, for the stale-todo sweep (#3219).
scripts/release/notes.js40 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.

The surface ​

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.

Commands a backend must reproduce ​

CommandBehaviorOutputFlags--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/{}/collaboratorsList 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/masterMove 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/{}/issuesList 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/{}/commentsList 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/{}/labelsList the repo's labels (--paginate).the REST response body as JSON (or the --jq projection of it)--jq --paginate—
api repos/{}/pulls/{}/mergeMerge one pull request (-X PUT).the REST response body as JSON (or the --jq projection of it)-X—
api userRead the signed-in account (its login).the REST response body as JSON (or the --jq projection of it)-q—
api user/keysList the account's SSH keys.the REST response body as JSON (or the --jq projection of it)--jq—
auth loginSign the machine in to the forge.nothing callers parse; the exit code is the answer--git-protocol --hostname --web—
auth statusIs this machine signed in? Exit 0 = yes.nothing callers parse; the exit code is the answer——
auth tokenPrint the current token, for a caller that talks HTTP itself.the token on stdout——
issue closeClose 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 commentAdd a comment to an issue.the comment URL on stdout (not parsed)--body --body-file --repo -R—
issue createFile 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 editChange 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 listList 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 -sassignees body closedAt comments createdAt labels number state stateReason title updatedAt url + dynamic
issue reopenReopen a closed issue.nothing callers parse; the exit code is the answer--comment --repo—
issue viewRead 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 -qassignees body closedAt comments createdAt labels number state stateReason title url + dynamic
label createCreate 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 deleteDelete a label.nothing callers parse; the exit code is the answer--repo --yes—
label editRename a label or change its colour or description.nothing callers parse; the exit code is the answer--name --repo—
label listList the repo's labels.a JSON array of objects carrying exactly the requested --json fields (or the --jq projection of it)--jq --json --limit --searchdescription name
pr closeClose a pull request, optionally deleting its branch.nothing callers parse; the exit code is the answer--comment --delete-branch—
pr commentAdd a comment to a pull request.the comment URL on stdout (not parsed)--body --repo—
pr createOpen 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 diffPrint a pull request's diff.a unified diff on stdout——
pr editChange a pull request's body or labels.nothing callers parse; the exit code is the answer--add-label --body—
pr listList 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 -qbody createdAt headRefName isDraft labels mergedAt number state statusCheckRollup title url
pr mergeMerge 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 viewRead 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 -qbody commits headRefName number statusCheckRollup url + dynamic
release createPublish 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 downloadDownload a release's assets.files written to the working directory——
release listList releases.a JSON array of objects carrying exactly the requested --json fields (or the --jq projection of it)--json --limit --repoisDraft name publishedAt tagName
release viewRead 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 --jsontagName url
repo archiveArchive a repository.nothing callers parse; the exit code is the answer--yes—
repo cloneClone a repository by owner/name.a git checkout on disk——
repo createCreate a repository, optionally from a local source directory.the repository URL on stdout--private --source—
repo editChange repository settings (docs show --help).nothing callers parse; the exit code is the answer--help—
repo listList an owner's repositories.a JSON array of objects carrying exactly the requested --json fields (or the --jq projection of it)--json --limitisArchived isPrivate nameWithOwner pushedAt
repo unarchiveUnarchive a repository.nothing callers parse; the exit code is the answer——
repo viewRead 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 -qdefaultBranchRef isArchived isPrivate name nameWithOwner + dynamic
ssh-key addRegister a public SSH key on the account.nothing callers parse; the exit code is the answer--title—
ssh-key deleteRemove an SSH key from the account.nothing callers parse; the exit code is the answer——
ssh-key listList the account's SSH keys.human text——

Known-unsupported ​

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

CommandBehaviorWhy it is unsupportedExecutable call sitesPrompt-text call sites
api graphqlOne 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.jsdocs/reference/bob-asks.md docs/reference/briefing.md docs/reference/forge-health.md
api rate_limitRead 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.jsdocs/reference/forge-health.md
api repos/{}/actions/permissionsRead 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/runnersList 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 todayscripts/dashboard/server.js scripts/fleet/ci-locus.js scripts/fleet/ci-rollout.js scripts/fleet/runner.sh—
api repos/{}/actions/runners/registration-tokenMint 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 todayscripts/fleet/runner.sh—
api repos/{}/actions/runners/remove-tokenMint 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 todayscripts/fleet/runner.sh—
api repos/{}/actions/runsList 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 todayscripts/fleet/ci-locus.js—
api repos/{}/actions/runs/{}/jobsList 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 todayscripts/dashboard/server.js scripts/fleet/ci-locus.jsrunbooks/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 todayscripts/fleet/ci-locus.js scripts/fleet/ci-rollout.js scripts/fleet/runner.sh—
api repos/{}/actions/workflowsList 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 todayscripts/fleet/ci-rollout.js—
api repos/{}/branches/master/protectionRead 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/{}/protectionRead 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_checksRead 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-runsList 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/{}/hooksList 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/{}/pagesRead 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/{}/rulesetsList the repo's rulesets.GitHub rulesets have no forge-neutral equivalent.—skills/todo-writing/references/platform-recipes.md
api user/installationsList 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/actionsRead an owner's Actions minutes against the included quota (#1695).GitHub billing for Actions minutes.scripts/fleet/actions-budget.js—
api {}/{}/settings/billing/usageRead an owner's billing usage (#1695).GitHub billing.scripts/fleet/actions-budget.js—
auth refreshAdd OAuth scopes to the token (-h host -s user,…).GitHub OAuth scopes; a backend's token permissions are named differently.scripts/fleet/actions-budget.jscommands/what-next.md docs/reference/actions-budget.md
run listList 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.jsdocs/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 rerunRe-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 todayscripts/asks/done-check.js scripts/fleet/ci-locus.jsdocs/reference/ci-locus.md docs/reference/todo-format-contract.md runbooks/self-hosted-runner.md
run viewRead 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 todayscripts/asks/done-check.js scripts/fleet/ci-locus.jsdocs/reference/ci-locus.md registry/runbooks/github-pat-rotation.md runbooks/self-hosted-runner.md
run watchWait 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 issuesSearch 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 filtersscripts/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.jsdocs/explanation/bob-viz.md docs/reference/briefing.md docs/reference/notices.md docs/reference/reports.md docs/reference/series.md
search prsSearch 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 filtersscripts/brief/brief.jsdocs/reference/briefing.md
secret listList 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 todayscripts/asks/done-check.js scripts/docs-site/init.sh scripts/security/cred-check.js scripts/security/secrets.jsdocs/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 setSet 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 todayscripts/docs-site/init.sh scripts/fleet/distribute-secret.sh scripts/security/cred-check.js scripts/security/secrets.jsdocs/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 deleteDelete 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 todayscripts/fleet/runner.sh—
variable listList 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 setSet 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 todayscripts/fleet/runner.shdocs/reference/branch-protection.md skills/todo-writing/references/platform-recipes.md
workflow runDispatch 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.