Capability: git

Implement git hosting — the credentials a sandbox clones with, repository pickers, pushes as the member, and pull request status next to a task.

git is where a project's repositories live. It is resolved per repository: a project can mix two forges, and each repository's owner (acme in acme/api) decides which installation answers.

Only cloneCredential is required. A forge that can clone and do nothing else is a complete git app; every other operation unlocks one more thing in the dashboard.

OperationRequiredUnlocksActs as
cloneCredentialYesSandboxes and tasks clone private repositoriesThe organization
listReposThe repository picker in the project formThe organization
listBranchesChoosing a branch, knowing the default oneThe organization
userCredentialPushes from a sandbox attributed to the member who owns itThe member — needs a grant
registerUserSshKeyRegistering a member's SSH key on their own accountThe member — needs a grant
pullRequestForBranchThe pull request card next to a task's diffThe organization

Repositories are always named owner/name.

A sandbox clones as the organization, so it gets its code even if the member who started it never connected anything. Everything after that — fetch, push — happens as the member who owns the sandbox: git asks a credential helper in the sandbox, which asks Spunto, which calls your userCredential with that member's userId. A push that carries someone's name has to come from their own authorization; an installation credential cannot stand in for it.

That is why userCredential and registerUserSshKey belong in your manifest's userGrantOperations, and fail with grant_required when the member has not authorized your app.

A short-lived credential a sandbox clones with. Called once per repository owner when a sandbox is set up — the credential is reused for every repository of that owner in the project.

// input
{ "repo": "acme/api", "repos": ["acme/api", "acme/web"] }
 
// result
{ "scheme": "https-token", "username": "x-access-token", "token": "…", "expiresInSeconds": 3600 }
  • repos lists every repository of the project under the same owner. Narrow the token to them, read-only, if your forge can: it ends up in the sandbox's setup, readable by anyone who can open that sandbox.
  • If some of them are out of the installation's reach, mint for the reachable ones rather than failing all of them; fail with not_found only if none is.
  • scheme is https-token — git authenticates with username and token over HTTPS.

// input
{}
 
// result
{ "repos": [{ "fullName": "acme/api", "name": "api", "private": true, "defaultBranch": "main" }] }

Every repository the organization's installation can reach. If it has several installations and one is unreachable, return the others.

// input
{ "repo": "acme/api" }
 
// result
{ "branches": ["main", "feat/login"], "defaultBranch": "main" }

// input — userId in the envelope is the member to act as
{ "repo": "acme/api" }
 
// result
{ "username": "x-oauth-token", "password": "…", "expiresAt": "2026-10-28T15:00:00Z" }

Handed to git at the moment it asks, never written to disk. expiresAt may be null if the credential does not expire. Refresh the member's authorization yourself if your forge's tokens expire; if it cannot be refreshed, fail with grant_required so they are asked to authorize again.

// input
{ "publicKey": "ssh-ed25519 AAAA…", "title": "Spunto" }
 
// result
{ "keyId": 81723 }

Registers the key on the member's own account. Calling it again should replace the key you registered before, not add one more.

What a reviewer glances at before opening the pull request — shown next to a task's diff.

// input
{ "repo": "acme/api", "branch": "task/fix-login-a1b2c3" }
 
// result
{
  "pullRequest": {
    "label": "#262",
    "url": "https://git.acme.dev/acme/api/pulls/262",
    "title": "Fix the login redirect",
    "state": "open",
    "branch": "task/fix-login-a1b2c3",
    "baseBranch": "main",
    "checks": "passing",
    "review": "pending",
    "comments": 3,
    "updatedAt": "2026-10-28T14:02:11Z"
  }
}

pullRequest is null when nothing is open for the branch. state is one of draft, open, merged, closed; checks one of pending, passing, failing; review one of approved, changes-requested, pending. Every field after state may be null, and null means your forge does not say — a forge without CI reports "checks": null, which the dashboard draws differently from checks that haven't run.

It reports, it decides nothing

What this returns is displayed, never acted on. A task's state comes from the agent session Spunto started and from the member's Accept or Drop — not from a merge on your forge. A forge able to close a task could close work nobody reviewed.