{
  "openapi": "3.1.0",
  "info": {
    "title": "Weft Platform API",
    "version": "1.0.0",
    "description": "Control-plane and Repos API for Weft: git-shaped storage served from object storage. Git smart-HTTP endpoints (clone/fetch/push at /{org}/{repo}.git) speak the standard git wire protocol and are not described here. Errors return JSON {\"error\": \"\u2026\"} with the matching status. Optimistic-concurrency conflicts return 409 with the current tip."
  },
  "servers": [
    {
      "url": "https://api.stratum.dev"
    }
  ],
  "security": [
    {
      "bearer": []
    }
  ],
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "Token of the form weft_<id>_<secret>. Scopes: org:admin, org:read, repo:read, repo:write. Repo-bound tokens are restricted to one repo."
      },
      "session": {
        "type": "apiKey",
        "in": "cookie",
        "name": "stratum_session",
        "description": "Browser session from POST /v1/auth/login. An alternative to a bearer token for the dashboard; a bearer token wins when both are presented. Routes under /v1/auth use this scheme."
      }
    },
    "schemas": {
      "RunnerPolicy": {
        "type": "object",
        "properties": {
          "hosted": {
            "type": "string",
            "enum": [
              "allowed",
              "disabled"
            ]
          },
          "self_hosted": {
            "type": "string",
            "enum": [
              "all",
              "selected",
              "disabled"
            ]
          },
          "self_hosted_repos": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Repository names, sorted. Only consulted under `selected`, but returned always so a dashboard switching back still has the list somebody chose."
          }
        }
      },
      "RunnerGroup": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "repo_access": {
            "type": "string",
            "enum": [
              "all",
              "selected"
            ]
          },
          "allow_public": {
            "type": "boolean",
            "description": "Whether public repositories may reach the machines in this group. False by default, and that default is the security property rather than a preference: a public repository can be forked, and a fork's change carries its own `run:` lines."
          },
          "is_default": {
            "type": "boolean"
          },
          "repos": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Repository names. Empty under `repo_access: all`."
          },
          "runners": {
            "type": "integer",
            "description": "How many live machines are in it."
          },
          "created_at": {
            "type": "integer",
            "format": "int64"
          },
          "updated_at": {
            "type": "integer",
            "format": "int64"
          }
        }
      },
      "Runner": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "labels": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What this machine will match. The server always adds `self-hosted`, the OS and the architecture to whatever custom labels it offered."
          },
          "os": {
            "type": "string",
            "enum": [
              "linux",
              "macos",
              "windows"
            ]
          },
          "arch": {
            "type": "string",
            "enum": [
              "x64",
              "arm64"
            ]
          },
          "version": {
            "type": "string"
          },
          "ephemeral": {
            "type": "boolean",
            "description": "Registered to run one job and then de-register."
          },
          "group": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "state": {
            "type": "string",
            "enum": [
              "online",
              "busy",
              "offline"
            ],
            "description": "Derived, never stored: `busy` if a running job points at it, else `online` if it called within the last 60 seconds, else `offline`. There is no status column for a crashed process to leave lying."
          },
          "last_seen_at": {
            "type": "integer",
            "format": "int64"
          },
          "created_at": {
            "type": "integer",
            "format": "int64"
          },
          "job": {
            "type": [
              "object",
              "null"
            ],
            "description": "What it is running right now, or null.",
            "properties": {
              "run_id": {
                "type": "string"
              },
              "job_id": {
                "type": "string",
                "description": "The workflow job's row id — what a link into the run needs."
              },
              "key": {
                "type": "string",
                "description": "The cell's human name, e.g. `test (linux)`."
              },
              "repo": {
                "type": "string",
                "description": "The repository's name — not its id. A run is addressed by `owner/repo`, so this is what a link to the build needs."
              }
            }
          }
        }
      },
      "Repo": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "org_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "native",
              "mirror"
            ]
          },
          "public": {
            "type": "boolean"
          },
          "default_branch": {
            "type": "string"
          },
          "origin_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Origin identity for a mirror \u2014 a fetchable URL for the generic provider, or \"owner/name\" for the GitHub App provider. Null for native repos."
          },
          "last_sync_at": {
            "type": [
              "integer",
              "null"
            ]
          },
          "last_synced_commit": {
            "type": [
              "string",
              "null"
            ]
          },
          "sync_error": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "integer"
          },
          "clone_url": {
            "type": "string",
            "description": "HTTPS git remote for this repo."
          },
          "ssh_clone_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "SSH git remote (ssh://git@host:port/org/repo.git), or null when the deployment has no SSH front door configured."
          },
          "origin_provider": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "github",
              "generic",
              null
            ],
            "description": "Origin integration backing a mirror; null for native repos."
          },
          "origin_installation": {
            "type": [
              "string",
              "null"
            ],
            "description": "GitHub App installation id used to mint fetch credentials for this mirror; null unless origin_provider is \"github\"."
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "homepage": {
            "type": [
              "string",
              "null"
            ],
            "description": "The project's own address on the web, shown in the repository's About panel. Always an absolute http:// or https:// URL, or null — a scheme-less \"example.com\" is refused rather than repaired, because prepending a scheme is wrong in exactly the case that matters: a relative URL resolving against this host, wearing the project's name. Empty and whitespace-only are stored as null, so \"no homepage\" has one encoding rather than two."
          },
          "fork_count": {
            "type": "integer",
            "description": "Public repositories forked directly from this one. The count a caller may see, not a stored total \u2014 a fork can be made private after the fact, so publishing the stored number would reveal how many private forks exist."
          },
          "watcher_count": {
            "type": "integer",
            "description": "People subscribed to everything that happens here. Counts only the \"all\" watch level: the default subscription is stored as no row at all, so counting the table would rise when somebody chose to be left alone. Public, and on this view rather than on .../watch \u2014 that endpoint is the caller's own setting and refuses a stranger, so a masthead reading it would have no number to draw until sign-in and then grow one."
          },
          "fork_state": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "pending",
              "ready",
              "failed",
              null
            ],
            "description": "Fork preparation state; absent on a repository that is not a fork. A fork's row exists immediately and its storage pointers a moment later, which is why forking answers 202."
          },
          "org": {
            "type": "string",
            "description": "The namespace this repository is in, by name. The row carries org_id, which is no use to a client: after forking into 'wherever I belong', the answer to 'where did it go' is a name."
          },
          "fork_parent": {
            "type": [
              "string",
              "null"
            ],
            "description": "owner/name of what this was forked from, when the viewer may see it. Null covers three different facts on purpose: not a fork, upstream deleted and this promoted, or an upstream this viewer may not see \u2014 which is why it cannot be derived client-side from an id."
          },
          "viewer_admin": {
            "type": "boolean",
            "description": "Whether the caller may administer this repository \u2014 visibility, default branch, branch protections, access. Answered by the server because the two authorities differ: patch and the protections routes accept a per-repo admin grant, while the access listing requires org-wide admin, so a client inferring one from the other hides the settings surface from somebody entitled to it. false for an anonymous reader."
          },
          "viewer_write": {
            "type": "boolean",
            "description": "Whether the caller may push here, and so whether they may open a change whose commits are already in this repository. Distinct from viewer_admin: a member writes without administering, and an outside contributor reads without either. Answered by the server because it is the scope POST /changes actually checks before refusing a change that names no source \u2014 a client guessing it either hides the form from somebody who may use it, or offers one to a stranger and refuses them after they have filled it in. false for an anonymous reader."
          },
          "write_blocked": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why nobody may write to this repository right now \u2014 the sentence a push is refused with, beginning `quota:` \u2014 or null when writes are allowed. About the repository and not the caller, so a page can say read-only to a reader too, and folded into viewer_write, which is false whenever this is set. The case is a private repository on an organization whose subscription has ended, or one with no card on file yet: it used to look exactly like any other repository until git push answered 402 with a sentence nobody had seen."
          }
        }
      },
      "CommitRequest": {
        "type": "object",
        "required": [
          "message",
          "operations"
        ],
        "properties": {
          "branch": {
            "type": "string",
            "default": "main"
          },
          "expected_parent": {
            "type": [
              "string",
              "null"
            ],
            "description": "Optimistic concurrency: the branch must currently point here (null = branch must not exist; omitted = commit on the current tip)."
          },
          "message": {
            "type": "string"
          },
          "author": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "email": {
                "type": "string"
              }
            }
          },
          "context": {
            "description": "Free-form audit blob recorded immutably with the commit."
          },
          "operations": {
            "type": "array",
            "maxItems": 10000,
            "items": {
              "oneOf": [
                {
                  "type": "object",
                  "required": [
                    "op",
                    "path",
                    "content"
                  ],
                  "properties": {
                    "op": {
                      "const": "put"
                    },
                    "path": {
                      "type": "string"
                    },
                    "content": {
                      "type": "string"
                    }
                  }
                },
                {
                  "type": "object",
                  "required": [
                    "op",
                    "path",
                    "content"
                  ],
                  "properties": {
                    "op": {
                      "const": "put_base64"
                    },
                    "path": {
                      "type": "string"
                    },
                    "content": {
                      "type": "string",
                      "contentEncoding": "base64"
                    }
                  }
                },
                {
                  "type": "object",
                  "required": [
                    "op",
                    "path"
                  ],
                  "properties": {
                    "op": {
                      "const": "delete"
                    },
                    "path": {
                      "type": "string"
                    }
                  }
                }
              ]
            }
          }
        }
      },
      "KindSummary": {
        "type": "object",
        "properties": {
          "count": {
            "type": "integer"
          },
          "bytes": {
            "type": "integer"
          },
          "ms_sum": {
            "type": "integer"
          },
          "p50_ms": {
            "type": [
              "integer",
              "null"
            ]
          },
          "p99_ms": {
            "type": [
              "integer",
              "null"
            ]
          }
        }
      },
      "RepoHit": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "org_id": {
            "type": "string"
          },
          "org": {
            "type": "string",
            "description": "Namespace name \u2014 a repo name alone is ambiguous across namespaces."
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "public": {
            "type": "boolean"
          },
          "kind": {
            "type": "string",
            "enum": [
              "native",
              "mirror"
            ]
          },
          "created_at": {
            "type": "integer",
            "format": "int64"
          }
        }
      }
    }
  },
  "paths": {
    "/v1/orgs/{org}/runner-policy": {
      "get": {
        "summary": "What kinds of runner this organisation allows (org:read)",
        "description": "Two independent switches, because they answer opposite questions. `hosted` is \"may our work run on Weft's machines\"; `self_hosted` is \"may our work run on machines we registered\". An organisation that only trusts its own hardware turns the first off; one that does not want a stranger's change near its build box turns the second off.",
        "responses": {
          "200": {
            "description": "The policy as it stands",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RunnerPolicy"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Change the runner policy (org:admin)",
        "description": "Any subset of the three keys; an absent key is left alone. `self_hosted_repos` is a list of repository names and is replaced wholesale — `[]` clears it. Audited as `runner_policy.updated`. A refusal here is a refusal at trigger time rather than a job queued forever: a `runs-on: ubuntu-latest` file under `hosted: disabled` becomes a failed run reading \"hosted runners are disabled for this organisation; use runs-on: [self-hosted, …]\".",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "hosted": {
                    "type": "string",
                    "enum": [
                      "allowed",
                      "disabled"
                    ]
                  },
                  "self_hosted": {
                    "type": "string",
                    "enum": [
                      "all",
                      "selected",
                      "disabled"
                    ]
                  },
                  "self_hosted_repos": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Repository names in this organisation. A name that is not there is a 422 rather than a silent drop: a group that quietly admits four of the five somebody listed is a build that does not run and a settings page that says it should."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The policy as it now stands",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RunnerPolicy"
                }
              }
            }
          },
          "404": {
            "description": "No such organisation, or the caller is not an admin of it"
          },
          "422": {
            "description": "A value outside the enumeration, or a repository name that is not there"
          }
        }
      }
    },
    "/v1/orgs/{org}/runner-groups": {
      "get": {
        "summary": "Runner groups in this organisation (org:read)",
        "description": "A group is the unit a repository is admitted by, and every machine is in exactly one. Every organisation has a `default` group, created lazily the first time anything needs one — so this never answers an empty list.",
        "responses": {
          "200": {
            "description": "{ groups: […] }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "groups": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/RunnerGroup"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a runner group (org:admin)",
        "description": "`repo_access` defaults to `all` and `allow_public` to false. Audited as `runner_group.created`.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "repo_access": {
                    "type": "string",
                    "enum": [
                      "all",
                      "selected"
                    ],
                    "default": "all"
                  },
                  "allow_public": {
                    "type": "boolean",
                    "default": false
                  },
                  "repos": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Repository names in this organisation. A name that is not there is a 422 rather than a silent drop: a group that quietly admits four of the five somebody listed is a build that does not run and a settings page that says it should."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RunnerGroup"
                }
              }
            }
          },
          "404": {
            "description": "No such organisation, or the caller is not an admin of it"
          },
          "409": {
            "description": "A group of that name already exists"
          },
          "422": {
            "description": "An invalid name or `repo_access`, or a repository name that is not there"
          }
        }
      }
    },
    "/v1/orgs/{org}/runner-groups/{id}": {
      "patch": {
        "summary": "Change a runner group (org:admin)",
        "description": "Any subset; an absent key is left alone, and `repos` is replaced wholesale. The default group's name is fixed (422): it is the name printed in every registration command ever handed out, and renaming it would silently point those at nothing. Audited as `runner_group.updated`.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "repo_access": {
                    "type": "string",
                    "enum": [
                      "all",
                      "selected"
                    ]
                  },
                  "allow_public": {
                    "type": "boolean"
                  },
                  "repos": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Repository names in this organisation. A name that is not there is a 422 rather than a silent drop: a group that quietly admits four of the five somebody listed is a build that does not run and a settings page that says it should."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The group as it now stands",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RunnerGroup"
                }
              }
            }
          },
          "404": {
            "description": "No such group in this organisation"
          },
          "409": {
            "description": "Another group already has that name"
          },
          "422": {
            "description": "An invalid value, or an attempt to rename the default group"
          }
        }
      },
      "delete": {
        "summary": "Delete a runner group (org:admin)",
        "description": "Its machines move to the default group rather than being removed: an operator tidying their groups has not asked to take thirty machines offline, and a runner whose group vanished would authenticate and then never be routed anything — the least debuggable failure this feature has. Any outstanding registration token for the group goes with it. The default group cannot be deleted. Audited as `runner_group.deleted`.",
        "responses": {
          "204": {
            "description": "Deleted; its machines are now in the default group"
          },
          "404": {
            "description": "No such group in this organisation"
          },
          "422": {
            "description": "This is the default group"
          }
        }
      }
    },
    "/v1/orgs/{org}/runners": {
      "get": {
        "summary": "The machines this organisation has registered (org:read)",
        "description": "Live machines only. A removed one is tombstoned rather than deleted — a job row names the machine that ran it, and a reader looking at last week's build should still be told which — but it is gone from here.",
        "responses": {
          "200": {
            "description": "{ runners: […] }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "runners": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Runner"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/orgs/{org}/runners/registration-token": {
      "post": {
        "summary": "Mint a single-use registration token (org:admin)",
        "description": "One hour, one machine. Hashed at rest like every other bearer secret here, because it travels through a shell history and a terminal scrollback on its way to the machine. `command` is the whole `weft-runner register` line spelled out — the alternative is a reader assembling it from three fields and getting `--url` wrong, which fails at registration with a network error that says nothing about which part was wrong. Audited as `runner.registration_token.created`.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "group": {
                    "type": "string",
                    "default": "default",
                    "description": "The group the machine will join."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The only time the token exists",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "token": {
                      "type": "string",
                      "description": "`weftg_…`. Not recoverable."
                    },
                    "expires_at": {
                      "type": "integer",
                      "format": "int64"
                    },
                    "group": {
                      "type": "string"
                    },
                    "command": {
                      "type": "string",
                      "description": "`weft-runner register --url … --token weftg_…`"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such organisation, or the caller is not an admin of it"
          },
          "422": {
            "description": "No group of that name"
          }
        }
      }
    },
    "/v1/orgs/{org}/runners/{id}": {
      "delete": {
        "summary": "Remove a machine (org:admin)",
        "description": "Its credential is dead from the next call it makes — which is the whole revocation mechanism, since nothing is listening on the operator's network to be told. A job running on it is failed, with the error \"runner removed while the job was running\", and deliberately not handed to the next machine in the group: an operator who removes a runner has decided the machine should stop, and a silent re-run somewhere else is the opposite of what they asked for. Audited as `runner.removed`.",
        "responses": {
          "204": {
            "description": "Removed"
          },
          "404": {
            "description": "No such machine in this organisation"
          }
        }
      }
    },
    "/v1/runners/register": {
      "post": {
        "summary": "Exchange a registration token for the machine's own credential",
        "description": "Not a route a person calls, and not a session or an API token: the bearer here is the one-hour `weftg_…` secret from the registration-token route, and it names the organisation and the group, so the machine never has to assert either. Every way the token can be wrong — malformed, unknown, already spent, expired — is the same 401, so it cannot be used to learn which organisations exist. Registering under a name that is already live replaces that machine, tombstoning the old row in the same transaction: that is how a credential is rotated, and how a reimaged machine comes back without somebody having to remove it by hand first. Audited as `runner.registered`.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "os",
                  "arch"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Letters, digits, dot, dash, underscore; at most 64. Defaults to the hostname on the runner side."
                  },
                  "labels": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Custom labels only. `self-hosted`, the OS and the architecture are added by the server, so a job's `runs-on` can rely on them being there."
                  },
                  "os": {
                    "type": "string",
                    "enum": [
                      "linux",
                      "macos",
                      "windows"
                    ]
                  },
                  "arch": {
                    "type": "string",
                    "enum": [
                      "x64",
                      "arm64"
                    ]
                  },
                  "version": {
                    "type": "string"
                  },
                  "ephemeral": {
                    "type": "boolean",
                    "default": false,
                    "description": "Run one job, then de-register. The autoscaling shape."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The only time the credential exists",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "runner_id": {
                      "type": "string"
                    },
                    "credential": {
                      "type": "string",
                      "description": "`weftr_…`. Hashed at rest and never recoverable; re-register to get another."
                    },
                    "org": {
                      "type": "string"
                    },
                    "group": {
                      "type": "string"
                    },
                    "labels": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The registration token is unknown, already used or expired"
          },
          "422": {
            "description": "A name, label, OS or architecture outside what is allowed"
          }
        }
      }
    },
    "/v1/runners/claim": {
      "post": {
        "summary": "Ask for work, and wait a while for some",
        "description": "The bearer is the machine's own `weftr_…` credential. A long poll: the server holds the request open for `STRATUM_RUNNER_CLAIM_WAIT_MS` (20 s by default), re-running its claim every 500 ms, and answers 204 if nothing came. That is the whole scaling story — asking every second is one query per machine per second forever, almost all of it answering \"nothing\"; asking every thirty is half a minute of latency on every build. Set the client's HTTP timeout longer than the server's wait, so a 204 arrives as a 204 rather than as a client-side timeout. Every call touches `last_seen_at`, which is what the runner list's `online` reading is made of. A job is routed to this machine only if its `runs-on` labels are a subset of the machine's, the machine's group admits the repository (and `allow_public` if it is public), and the organisation's policy admits self-hosted runners for it — three different people's decisions, all re-read here and not only at trigger time, because a job can sit in the queue across a settings change. On 200 the machine then uses exactly the five per-job calls a hosted runner uses, with the `token` from here.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Empty, and may be omitted."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A job was assigned",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "job_id": {
                      "type": "string"
                    },
                    "token": {
                      "type": "string",
                      "description": "The per-job, per-attempt credential for the five runner calls: `repo:read` on one repository, revoked the moment the verdict lands."
                    },
                    "runner_url": {
                      "type": "string",
                      "description": "The base URL to report on — the address this machine already reached, not the public one. They are the same behind a CDN and differ on a private network."
                    }
                  }
                }
              }
            }
          },
          "204": {
            "description": "Nothing to run after the wait. Ask again."
          },
          "401": {
            "description": "This credential is unknown, or the machine has been removed. The runner prints that it has been removed and exits: there is no way back without registering."
          },
          "409": {
            "description": "{ error: \"busy\" } — this machine already holds a running job. A runner runs one job at a time, and a second would be work nobody ever reports on."
          }
        }
      }
    },
    "/v1/users/{handle}": {
      "get": {
        "summary": "A person's public profile",
        "description": "Display name, bio, location, company, pronouns, account kind (human or agent), links, and how many public repositories their namespace holds. Open to anonymous callers \u2014 this is the page a logged-out visitor lands on. It never carries an email address and never a private repository, and `public_repos` counts public repositories for every caller alike, so it cannot be watched to learn that a private one was created. A disabled account has no profile and answers 404; its namespace stays claimed so nobody can move in behind them.",
        "security": [],
        "responses": {
          "200": {
            "description": "{ handle, name, display_name, bio, location, company, pronouns, kind, contrib_private_optin, profile_repo, created_at, links: [{ label, url }], public_repos }"
          },
          "404": {
            "description": "No such handle, or the account is disabled"
          }
        }
      },
      "patch": {
        "summary": "Edit your own profile",
        "description": "Every field is optional: an absent key leaves it alone, an explicit null clears it. `links` replaces the whole list (at most 5) and every url must be http or https \u2014 a profile link is rendered as an anchor on a page other people load, so `javascript:` and `data:` are refused. Nobody may patch anybody else's profile.",
        "responses": {
          "200": {
            "description": "The profile as it now reads"
          },
          "400": {
            "description": "A field is over its cap, holds a control character, or a link is not http(s)"
          },
          "401": {
            "description": "Not signed in"
          },
          "403": {
            "description": "This is somebody else's account"
          },
          "404": {
            "description": "No such handle"
          }
        }
      }
    },
    "/v1/users/{handle}/contributions": {
      "get": {
        "summary": "Somebody's contribution graph",
        "description": "One square per day, derived from the commits themselves rather than from activity on this platform \u2014 which is what makes a migrated history show up the moment it mirrors over. A commit counts only on one of two grounds: **email-verified**, the author address is a *proved* address on the account, or **pushed-by**, the credential that pushed it was that person's. An address nobody has proved counts for nothing, because anybody can write any string into `git config user.email`. Commit signatures are out of scope. A commit carrying a `Co-Authored-By` trailer is agent-assisted and counts for nobody.\n\nPrivate work appears only as a **number**, and only when its owner has opted in: `count` includes it, `repos` never names it, and there is no field holding it separately. `private_included` is false both for somebody opted out and for somebody with no private work \u2014 deliberately the same answer, since telling those apart would publish the existence of private work.\n\nAnonymous, and the same for every reader: a public page that says different things to different people is one nobody can quote. `from`/`to` are `YYYY-MM-DD` and default to the year ending today; at most 3653 days.",
        "security": [],
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ from, to, total, private_included, days: [{ day, date, count, repos: [{ org, name, count }] }] } \u2014 only days with something on them"
          },
          "400": {
            "description": "A date that is not YYYY-MM-DD, a range that ends before it starts, or one longer than 3653 days"
          },
          "404": {
            "description": "No such handle"
          }
        }
      }
    },
    "/v1/users/{handle}/follow": {
      "get": {
        "summary": "Follower and following counts, and your own edge",
        "description": "Anonymous readers are welcome and see `you_follow: false`. The counts are counts of a public act.",
        "security": [],
        "responses": {
          "200": {
            "description": "{ followers, following, you_follow }"
          },
          "404": {
            "description": "No such handle"
          }
        }
      },
      "put": {
        "summary": "Follow this person",
        "description": "Following twice is following once. Following yourself is refused in a sentence rather than reaching the table's CHECK, which would surface as a 500.",
        "responses": {
          "200": {
            "description": "The counts as they now read"
          },
          "400": {
            "description": "You cannot follow yourself"
          },
          "401": {
            "description": "Following is a person's act \u2014 not signed in, or a service token"
          },
          "404": {
            "description": "No such handle"
          }
        }
      },
      "delete": {
        "summary": "Unfollow this person",
        "description": "Unfollowing somebody you never followed is a request for the state you are already in, not an error.",
        "responses": {
          "200": {
            "description": "The counts as they now read"
          },
          "401": {
            "description": "Following is a person's act \u2014 not signed in, or a service token"
          },
          "404": {
            "description": "No such handle"
          }
        }
      }
    },
    "/v1/users/{handle}/followers": {
      "get": {
        "summary": "Who follows this person",
        "description": "Newest first, at most 100. Suspended accounts are absent \u2014 their public page is down, so naming them would link to a 404.",
        "security": [],
        "responses": {
          "200": {
            "description": "{ people: [{ handle, display_name }] }"
          },
          "404": {
            "description": "No such handle"
          }
        }
      }
    },
    "/v1/users/{handle}/following": {
      "get": {
        "summary": "Who this person follows",
        "description": "Newest first, at most 100. Suspended accounts are absent.",
        "security": [],
        "responses": {
          "200": {
            "description": "{ people: [{ handle, display_name }] }"
          },
          "404": {
            "description": "No such handle"
          }
        }
      }
    },
    "/v1/users/{handle}/pins": {
      "get": {
        "summary": "The repositories pinned to a profile",
        "description": "Filtered to what the caller may see. A pinned repository that has since been made private, or deleted, is absent: `public` is a flag its owner can flip after pinning, so a write-time refusal alone could never cover it.",
        "security": [],
        "responses": {
          "200": {
            "description": "{ pins: [{ kind, org, name, description, public }] }"
          },
          "404": {
            "description": "No such handle"
          }
        }
      },
      "put": {
        "summary": "Replace your pin list (at most 6)",
        "description": "Body: { pins: [{ org, repo, kind? }] }; only `repo` may be pinned. A private repository can never be pinned. Repositories are resolved through the caller's own visibility first, so naming one they cannot see answers \u201cno such repository\u201d \u2014 the same thing a name that does not exist answers \u2014 rather than turning the pin form into an existence oracle for every private repository on the platform.",
        "responses": {
          "200": {
            "description": "The pin list as it now reads"
          },
          "400": {
            "description": "Over the cap, a duplicate, a private repository, an unknown repository, or a kind other than repo"
          },
          "401": {
            "description": "Not signed in"
          },
          "403": {
            "description": "This is somebody else's account"
          },
          "404": {
            "description": "No such handle"
          }
        }
      }
    },
    "/v1/users/{handle}/emails": {
      "get": {
        "summary": "The addresses on your own account, proved or not",
        "description": "Never public, and never readable by another account. This is the authorship-linkage surface \u2014 the set that decides which commits in the world count as this person's work \u2014 so publishing it would hand an impersonator the exact string to put in `git config user.email`, and a spammer a mailbox.",
        "responses": {
          "200": {
            "description": "{ emails: [{ address, verified_at, private, primary, created_at }] }"
          },
          "401": {
            "description": "Not signed in"
          },
          "403": {
            "description": "This is somebody else's account"
          },
          "404": {
            "description": "No such handle"
          }
        }
      },
      "post": {
        "summary": "Claim an address and mail it a confirmation link",
        "description": "Body: { email }. The claim is worth nothing on its own: the row lands unproved, and only a verified address may ever carry authorship. The link goes to the mailbox and never into the response \u2014 a claim confirmed over the API would prove nothing. It is bound both to the address and to this account, so it cannot be spent against a different mailbox or from a different session. At most 10 addresses per account.",
        "responses": {
          "202": {
            "description": "{ address, status, detail }"
          },
          "400": {
            "description": "Not an address, or the account is at its address limit"
          },
          "401": {
            "description": "Not signed in"
          },
          "403": {
            "description": "This is somebody else's account"
          },
          "409": {
            "description": "Already yours, or already in use. Which account holds it is never disclosed."
          }
        }
      }
    },
    "/v1/users/{handle}/emails/verify": {
      "post": {
        "summary": "Spend a confirmation link and mark that one address proved",
        "description": "Body: { token }. Every wrong shape \u2014 malformed, unknown, expired, already spent, or issued to another account \u2014 answers the same 404, so a link cannot be used to ask which addresses are registered here.",
        "responses": {
          "200": {
            "description": "{ address, verified: true }"
          },
          "401": {
            "description": "Not signed in"
          },
          "403": {
            "description": "This is somebody else's account"
          },
          "404": {
            "description": "This confirmation link is not valid any more"
          }
        }
      }
    },
    "/v1/users/{handle}/emails/{email}": {
      "delete": {
        "summary": "Drop an address from your account",
        "description": "The address you sign in with is refused \u2014 an account with no reachable address is one nobody can recover \u2014 and so is an address that is not yours, with the same answer.",
        "responses": {
          "204": {
            "description": "Removed"
          },
          "401": {
            "description": "Not signed in"
          },
          "403": {
            "description": "This is somebody else's account"
          },
          "404": {
            "description": "No such address on this account, or it is the address you sign in with"
          }
        }
      }
    },
    "/v1/orgs/{org}/profile": {
      "get": {
        "summary": "An organization's public face",
        "description": "Display name, description, location, website, contact address, and its public repository count. Open to anonymous callers. An org that has never been edited reads exactly like one whose fields were cleared \u2014 there is no second \u201cno profile yet\u201d state for a page to handle.",
        "security": [],
        "responses": {
          "200": {
            "description": "{ org, display_name, description, location, website, contact_email, public_repos }"
          },
          "404": {
            "description": "No such org"
          }
        }
      },
      "patch": {
        "summary": "Edit an organization's profile (org:admin)",
        "description": "Optional fields; an explicit null clears one. `website` must be http or https and `contact_email` must be an address, for the same reason the personal profile holds them to it: both are rendered on a page anybody loads.",
        "responses": {
          "200": {
            "description": "The profile as it now reads"
          },
          "400": {
            "description": "A field is over its cap, the website is not http(s), or the contact address is not an address"
          },
          "401": {
            "description": "No credential"
          },
          "403": {
            "description": "Needs org:admin"
          },
          "404": {
            "description": "No such org, or the caller is not a member"
          }
        }
      }
    },
    "/v1/auth/login": {
      "post": {
        "summary": "Sign in with email and password; sets an HttpOnly session cookie. Every failure \u2014 unknown address, wrong password, disabled account \u2014 answers 401 identically.",
        "security": [],
        "responses": {
          "200": {
            "description": "{ id, email, name, created_at, orgs: [{ id, name, role }] }"
          },
          "401": {
            "description": "invalid email or password"
          }
        }
      }
    },
    "/v1/auth/logout": {
      "post": {
        "summary": "End this session and clear the cookie. Answers 204 even without a valid session \u2014 a stale cookie still has to go.",
        "security": [],
        "responses": {
          "204": {
            "description": "signed out"
          }
        }
      }
    },
    "/v1/auth/me": {
      "get": {
        "summary": "The signed-in person and the orgs they can reach (session cookie)",
        "security": [],
        "responses": {
          "200": {
            "description": "{ id, email, name, created_at, orgs: [{ id, name, role }] }"
          },
          "401": {
            "description": "not signed in"
          }
        }
      }
    },
    "/v1/auth/accept-invite": {
      "post": {
        "summary": "Redeem an invitation link, creating the account if this is a new person, and sign in. Single use.",
        "security": [],
        "responses": {
          "201": {
            "description": "{ id, email, name, created_at, orgs: [...] } and a session cookie"
          },
          "400": {
            "description": "invite is unknown, expired, already used, or the name/password is unusable"
          }
        }
      }
    },
    "/v1/auth/invite/preview": {
      "post": {
        "summary": "What an invitation is for, before committing to it: the namespace, the role and the address it was sent to. No credentials \u2014 the token in the body is the credential.",
        "responses": {
          "200": {
            "description": "{ org, role, email, expires_at }"
          },
          "404": {
            "description": "every dead link answers identically \u2014 malformed, unknown, expired, already accepted or a wrong secret \u2014 so a link cannot be used to probe which invitations exist"
          }
        }
      }
    },
    "/v1/auth/signup": {
      "post": {
        "summary": "Create an account and its personal namespace, and mail a confirmation link. The handle is checked first and its refusals are plain \u2014 a namespace name is a public URL. Everything after that answers identically whether or not the address is already registered.",
        "responses": {
          "202": {
            "description": "{ status, detail } \u2014 always this, whether a message was sent, the address already has an account (its owner is mailed a note saying nothing was created), or the rate limit was hit"
          },
          "400": {
            "description": "invalid handle, address, name, or a password under 12 characters"
          },
          "409": {
            "description": "that handle is taken or reserved"
          }
        }
      }
    },
    "/v1/auth/verify": {
      "post": {
        "summary": "Redeem a confirmation link and sign in. Single-use; sets the session cookie.",
        "responses": {
          "200": {
            "description": "the account, as /v1/auth/me returns it"
          },
          "401": {
            "description": "the account is disabled"
          },
          "404": {
            "description": "malformed, unknown, spent, expired, or a reset token presented here"
          }
        }
      }
    },
    "/v1/auth/resend-verification": {
      "post": {
        "summary": "Send another confirmation link. Answers identically for a live account, an already-confirmed one, an unknown address and a malformed one.",
        "responses": {
          "202": {
            "description": "{ status, detail }"
          }
        }
      }
    },
    "/v1/auth/forgot-password": {
      "post": {
        "summary": "Mail a password-reset link. Same uniform answer as resend-verification. A disabled account gets no link \u2014 recovering one an operator switched off would undo the switching off.",
        "responses": {
          "202": {
            "description": "{ status, detail }"
          }
        }
      }
    },
    "/v1/auth/reset-password": {
      "post": {
        "summary": "Redeem a reset link, set a new password, and sign in. Ends every other session on the account. Password strength is checked before the link is spent, so a typo costs a retry rather than another trip through the inbox.",
        "responses": {
          "200": {
            "description": "the account, as /v1/auth/me returns it"
          },
          "400": {
            "description": "the new password is under 12 characters"
          },
          "401": {
            "description": "the account is disabled"
          },
          "404": {
            "description": "malformed, unknown, spent, expired, or a verification token presented here"
          }
        }
      }
    },
    "/v1/orgs": {
      "post": {
        "summary": "Create an organization, owned by the signed-in person. It is `free` the moment it exists \u2014 public repositories, members, the free hosted minutes \u2014 and no card is asked for: the payment provider is the merchant of record and only meets a card on its own subscription page, which the first private repository leads to. Nothing at the provider exists for the org until then. Requires a confirmed email address; a service token has nobody to own the result and is refused.",
        "responses": {
          "201": {
            "description": "{ id, name, plan: \"free\", billable_seats, detail }. `detail` says whether this deployment sells anything at all."
          },
          "400": {
            "description": "not a valid namespace name"
          },
          "401": {
            "description": "not signed in"
          },
          "403": {
            "description": "email address not confirmed"
          },
          "409": {
            "description": "that name is taken or reserved"
          }
        }
      }
    },
    "/v1/orgs/{org}/billing": {
      "get": {
        "summary": "What this organization costs and where it stands (org:admin). `billable_seats` is what we would charge for now; `paid_seats` is what the provider has been told. They differ between a membership change and the push that follows it.",
        "responses": {
          "200": {
            "description": "{ org, plan: \"free\" | \"paid\" | \"past_due\", billable_seats, paid_seats, status, current_period_end, may_create_public, may_create_private, may_add_people, price_per_seat_cents, paid_minutes_per_seat, free_minutes, ci_minutes_limit, ci_minutes_used, ci_minutes_remaining, ci_suspended_reason, ci_suspended_at, private_repos }. `price_per_seat_cents` and `paid_minutes_per_seat` are null where nothing is for sale \u2014 a personal namespace, or a deployment with no payment provider \u2014 and the three `may_*` flags are true there. `null` in either minutes field is unlimited, not zero \u2014 \"0 minutes left\" and \"no limit\" are opposite facts. A running job counts toward `ci_minutes_used` from the moment it starts, so the number moves while builds run. `private_repos` counts the active private repositories here, and is read together with `plan`: `free` is where a cancelled subscription lands, private repositories and all, so a page that says the first private repository starts a subscription, to an organization already holding three of them, is wrong twice \u2014 they exist, and subscribing is the only way to write to them again."
          },
          "404": {
            "description": "no such org, or not yours"
          }
        }
      },
      "post": {
        "summary": "Open the provider's billing portal (org:admin), where cards, invoices and cancellation live \u2014 building that here would mean handling card details. There is nothing to manage until a subscription exists: the card arrives with it, on the provider's subscription page. The browser is sent back to this deployment's own billing screen \u2014 a page that can be told where to land afterwards is an open redirect wearing a payment flow.",
        "responses": {
          "200": {
            "description": "{ url, kind: \"portal\" }"
          },
          "402": {
            "description": "`card: nothing to manage yet \u2026 subscribe first` \u2014 no subscription has been opened"
          },
          "400": {
            "description": "a personal namespace is free and has nothing to subscribe"
          },
          "404": {
            "description": "no such org, or not yours"
          },
          "502": {
            "description": "the payment provider could not be reached"
          },
          "503": {
            "description": "billing is not configured on this deployment"
          }
        }
      }
    },
    "/v1/orgs/{org}/billing/subscribe": {
      "post": {
        "summary": "Send the person to the provider's subscription page (org:admin). This is the paid plan's front door and it is a redirect rather than a confirmation, because a promotion code can only be redeemed on the provider's own page: the Checkout carries the seat price for the seats in use today, the saved card already on it, and a promotion-code box. Nothing is written here \u2014 the subscription the page opens arrives on the provider's `customer.subscription.created` webhook, which moves the plan to `paid`; the page comes back to `\u2026/dashboard/settings/billing?subscribed=done` (or `?subscribed=cancelled`), and a repository creation refused with 402 succeeds once the webhook has landed. An org already paying gets its billing view back rather than a page. No idempotency key: a key would hand an expired session back for a day, so a second click before finishing is a second harmless page.",
        "responses": {
          "200": {
            "description": "`{ url, kind: \"checkout\" }` \u2014 the provider's subscription page to send the browser to; or, for an org already paying, the billing view as GET returns it with `plan: \"paid\"`"
          },
          "400": {
            "description": "a personal namespace is free and has nothing to subscribe"
          },
          "402": {
            "description": "`card: \u2026` \u2014 no card on file yet; add one from Billing first. A card is accepted or declined on the provider's page now, never here"
          },
          "404": {
            "description": "no such org, or not yours"
          },
          "502": {
            "description": "the payment provider could not be reached"
          },
          "503": {
            "description": "billing is not configured on this deployment"
          }
        }
      }
    },
    "/webhooks/stripe": {
      "post": {
        "summary": "Stripe's webhook. Verifies the timestamped HMAC in `Stripe-Signature` within a five-minute tolerance, then reconciles checkout completion, subscription updates and failed payments. Replays are acknowledged without acting twice; event types and subscriptions it does not recognise are acknowledged and ignored.",
        "responses": {
          "200": {
            "description": "{ org, plan } \u2014 or { duplicate | unknown_org | ignored }"
          },
          "400": {
            "description": "missing, malformed, forged or stale signature"
          },
          "503": {
            "description": "billing is not configured on this deployment"
          }
        }
      }
    },
    "/v1/auth/password": {
      "post": {
        "summary": "Change your own password. Requires the current one, and ends every other session you hold.",
        "security": [],
        "responses": {
          "204": {
            "description": "changed; this session survives, others are revoked"
          },
          "400": {
            "description": "the new password is too short"
          },
          "401": {
            "description": "not signed in, or the current password is wrong"
          }
        }
      }
    },
    "/v1/orgs/{org}/members": {
      "get": {
        "summary": "The org roster (org:read)",
        "responses": {
          "200": {
            "description": "{ members: [{ user_id, email, name, role, disabled, created_at }] }"
          }
        }
      }
    },
    "/v1/orgs/{org}/members/{user}": {
      "patch": {
        "summary": "Change a member's role: owner | admin | member | viewer (org:admin)",
        "responses": {
          "204": {
            "description": "changed"
          },
          "400": {
            "description": "unknown role"
          },
          "404": {
            "description": "no such member"
          },
          "409": {
            "description": "that would leave the org without an owner"
          }
        }
      },
      "delete": {
        "summary": "Remove a member \u2014 ends their sessions and every credential they hold here, on the next request (org:admin)",
        "responses": {
          "204": {
            "description": "removed"
          },
          "404": {
            "description": "no such member"
          },
          "409": {
            "description": "that would leave the org without an owner"
          }
        }
      }
    },
    "/v1/orgs/{org}/origins/probe": {
      "post": {
        "summary": "Check an origin before creating a mirror against it (org:admin). Body: { origin } \u2014 a git URL, `owner/repo`, or a browser URL; it is normalised. Answers 200 whether or not the origin was reachable: the probe succeeded either way and the finding is in the body. https only, no redirects followed, and any host resolving inside private address space is refused. Rate-limited per org.",
        "responses": {
          "200": {
            "description": "{ reachable, private, default_branch, refs, reason }. `private` true means it exists but needs credentials \u2014 connect GitHub."
          },
          "429": {
            "description": "too many probes for this org"
          }
        }
      }
    },
    "/v1/orgs/{org}/teams": {
      "get": {
        "summary": "Teams in this org (org:read).",
        "responses": {
          "200": {
            "description": "{ teams: [{id, name, description, created_at, member_count}] }"
          }
        }
      },
      "post": {
        "summary": "Create a team (org:admin). Body: { name, description? }. Names are case-folded and unique within the org.",
        "responses": {
          "201": {
            "description": "{ id, name, description, created_at, member_count }"
          },
          "400": {
            "description": "invalid or duplicate name"
          }
        }
      }
    },
    "/v1/orgs/{org}/teams/{team}": {
      "patch": {
        "summary": "Rename a team or change its description (org:admin). Body: { name?, description? }.",
        "responses": {
          "204": {
            "description": "updated"
          },
          "400": {
            "description": "invalid or duplicate name"
          },
          "404": {
            "description": "no such team"
          }
        }
      },
      "delete": {
        "summary": "Delete a team (org:admin). Its membership and every grant it carries go with it, effective on the next request.",
        "responses": {
          "204": {
            "description": "deleted"
          },
          "404": {
            "description": "no such team"
          }
        }
      }
    },
    "/v1/orgs/{org}/teams/{team}/members": {
      "get": {
        "summary": "Who is in this team (org:read).",
        "responses": {
          "200": {
            "description": "{ members: [{user_id, email, name, created_at}] }"
          },
          "404": {
            "description": "no such team"
          }
        }
      }
    },
    "/v1/orgs/{org}/teams/{team}/members/{user}": {
      "put": {
        "summary": "Put someone in a team (org:admin). They must already be a member of the org. Idempotent.",
        "responses": {
          "204": {
            "description": "in the team"
          },
          "400": {
            "description": "not a member of this org, or no such team"
          }
        }
      },
      "delete": {
        "summary": "Take someone out of a team (org:admin).",
        "responses": {
          "204": {
            "description": "removed"
          },
          "404": {
            "description": "not in that team"
          }
        }
      }
    },
    "/v1/orgs/{org}/invites": {
      "post": {
        "summary": "Invite someone by email at a role (org:admin). The invitation is emailed when a mail transport is configured, and the link is also returned so an admin can deliver it by hand when one is not.",
        "responses": {
          "201": {
            "description": "{ id, email, role, expires_at, invite_link, mail: { sent, error? } } \u2014 `sent` is false with no `error` when no transport is configured."
          },
          "400": {
            "description": "unusable address, unknown role, or an invitation is already outstanding"
          }
        }
      },
      "get": {
        "summary": "Invitations issued for this org, accepted ones included (org:admin)",
        "responses": {
          "200": {
            "description": "{ invites: [{ id, email, role, created_at, expires_at, accepted_at }] }"
          }
        }
      }
    },
    "/v1/orgs/{org}/invites/{id}": {
      "delete": {
        "summary": "Revoke an outstanding invitation \u2014 the link stops working immediately (org:admin)",
        "responses": {
          "204": {
            "description": "revoked"
          },
          "404": {
            "description": "no such outstanding invitation"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos": {
      "post": {
        "summary": "Create a repo (p50 < 100 ms)",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "public": {
                    "type": "boolean",
                    "default": false
                  },
                  "default_branch": {
                    "type": "string",
                    "default": "main"
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 512,
                    "description": "One line about the repo. Searchable; visible to anyone who can see the repo."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Repo"
                }
              }
            }
          },
          "402": {
            "description": "`quota:` \u2014 this namespace may not hold what was asked for: a private repository in a personal namespace (private work lives in an organization; the sentence begins `quota: private repositories live in an organization`), a private repository in an organization on the free plan (subscribe from Billing and replay), an organization with no card on file, or a free namespace at the free-tier repository cap. Nothing is created."
          },
          "409": {
            "description": "Name already exists"
          }
        }
      },
      "get": {
        "summary": "List repos (keyset pagination; fleets of millions list fine)",
        "parameters": [
          {
            "name": "after",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Cursor: last id from the previous page"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "maximum": 1000,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of repos with next_after cursor"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/batch/create": {
      "post": {
        "summary": "Create up to 1000 repos in one call",
        "responses": {
          "200": {
            "description": "Per-item results, request order"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/batch/delete": {
      "post": {
        "summary": "Delete up to 1000 repos by name",
        "responses": {
          "200": {
            "description": "Per-item results"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}": {
      "get": {
        "summary": "Get a repo",
        "responses": {
          "200": {
            "description": "The repo"
          }
        }
      },
      "delete": {
        "summary": "Delete a repo (instant tombstone; storage swept by GC)",
        "description": "The row is tombstoned at once and the storage is swept by GC later. Side effects that are not deferred: forks are detached and survive, and CI stops \u2014 every run still going is cancelled with \"the repository was deleted\", its jobs and their mirrored check runs settle as `cancelled`, tasks that were already launched are stopped and their job tokens revoked, and queued jobs are never handed out. Without that last part a deleted repository goes on holding slots of the organisation's CI concurrency until each job's timeout expires.",
        "responses": {
          "204": {
            "description": "Deleted"
          }
        }
      },
      "patch": {
        "summary": "Edit a repo's description, homepage, visibility, or default branch",
        "description": "One handler, four fields, three different authorities. `description` absent leaves it alone; `null` or \"\" clears it, and editing it needs only repo:write. `homepage` behaves identically and carries the same authority, with one extra rule: it must be an absolute http:// or https:// URL. It is rendered as an anchor on a page other people load, so `javascript:` and `data:` are refused for the same reason profile links are \u2014 and a scheme-less \"example.com\" is refused rather than repaired, since prepending one is wrong exactly when it produces a relative URL resolving against this host. Changing `public` requires org:admin and is recorded in the audit trail. `default_branch` requires org:admin and the branch must already exist; a mirror's default branch follows its origin and cannot be set here.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "description": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 512
                  },
                  "homepage": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 512,
                    "description": "Absolute http:// or https:// URL, or null/\"\" to clear. Absent leaves it alone."
                  },
                  "public": {
                    "type": "boolean"
                  },
                  "default_branch": {
                    "type": "string",
                    "maxLength": 200
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The repo as it now is",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Repo"
                }
              }
            }
          },
          "400": {
            "description": "Description or homepage too long or not one line of text, a homepage that is not an absolute http(s) URL, or an invalid branch name"
          },
          "401": {
            "description": "No credentials \u2014 same answer as for a repo that does not exist"
          },
          "403": {
            "description": "A mirror's default branch follows its origin"
          },
          "404": {
            "description": "No such repo, not yours, an unknown branch, or publishing without org:admin"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/forks": {
      "post": {
        "summary": "Fork a repository into another namespace. The fork shares upstream's storage and costs nothing until the two histories diverge; it is created immediately and becomes readable a moment later, which `fork_state` reports. Answers 202 rather than 201 deliberately: the row exists synchronously and the storage pointers are written by a job, so the honest answer is \"accepted, and here is where to look\". Body is optional \u2014 `org` defaults to the caller's own namespace and `name` to the source repository's name.",
        "responses": {
          "202": {
            "description": "{ id, org, name, fork_state: \"pending\", \u2026 the usual repo view }"
          },
          "200": {
            "description": "the fork you already have of this repository at that `org`/`name`, unchanged \u2014 forking twice is how most people find their fork again, so the answer is the fork and not a refusal. Nothing was created."
          },
          "401": {
            "description": "not a person (no session, or a service token)"
          },
          "409": {
            "description": "`org`/`name` is taken by a repository that is not a fork of this one; the body names it and says to fork under another name"
          },
          "402": {
            "description": "`quota:` — the target namespace may not hold what the fork would be: a private source into a personal namespace (private work lives in an organization) or into an organization whose plan admits only public repositories, an organization with no card on file, or a `free` namespace (every personal namespace is one) already at the free-tier repository cap — forks count like created repositories. Nothing is created."
          }
        }
      },
      "get": {
        "summary": "List the repositories forked directly from this one. `count` is the number of forks **this caller may see** and always equals the length of `forks` \u2014 never a stored total. A fork inherits its source's visibility but its owner may make it private afterwards, so publishing the stored number would tell every visitor exactly how many private forks exist. If this ever paginates, `count` stays \"total visible to you\" rather than becoming \"this page\".",
        "responses": {
          "200": {
            "description": "{ forks: [{ org, name, public }], count }"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/grants": {
      "post": {
        "summary": "Grant a role on this repo alone (org:admin). Body names either people \u2014 { user_id, role } or { user_ids: [...], role } \u2014 or a team, { team_id, role }, never both. A grant naming people replaces their org role here in either direction; a team grant only ever raises. `owner` is an org role and is refused. A batch is all-or-nothing: if any id is not a member of this org, nothing is granted.",
        "responses": {
          "204": {
            "description": "granted"
          },
          "400": {
            "description": "unknown role, a role no repo can carry, a user who is not a member of this org, people and a team in one body, or neither"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/grants/{user}": {
      "delete": {
        "summary": "Remove a per-repo grant; the member's org role applies here again (org:admin)",
        "responses": {
          "204": {
            "description": "revoked"
          },
          "404": {
            "description": "no such grant"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/team-grants/{team}": {
      "delete": {
        "summary": "Withdraw a team's grant on this repo (org:admin).",
        "responses": {
          "204": {
            "description": "revoked"
          },
          "404": {
            "description": "no such grant"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/access": {
      "get": {
        "summary": "Everyone who can reach this repo and where their access came from, plus the teams granted on it (org:admin). Each person carries `source`: org_role | direct_grant | team.",
        "responses": {
          "200": {
            "description": "{ people: [{user_id, email, name, role, source, team_id, team_name}], teams: [{team_id, team_name, role, member_count}] }"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/commits": {
      "post": {
        "summary": "Create a commit from tree operations (no git checkout anywhere)",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CommitRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "{ commit, tree, parent, branch }; durable at ack"
          },
          "409": {
            "description": "expected_parent mismatch; body carries current_tip"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/files/{path}": {
      "get": {
        "summary": "Read a file at any revision",
        "parameters": [
          {
            "name": "at",
            "in": "query",
            "schema": {
              "type": "string",
              "default": "HEAD"
            },
            "description": "Commit id, branch, tag, or HEAD"
          },
          {
            "name": "If-None-Match",
            "in": "header",
            "schema": {
              "type": "string"
            },
            "description": "ETag is the blob's content hash \u2014 exact 304s"
          }
        ],
        "responses": {
          "200": {
            "description": "raw content. `Content-Type` is sniffed \u2014 `text/plain; charset=utf-8` for anything textual, a named image type or `application/octet-stream` otherwise; never a type a repository could use to serve script from this origin. `X-Weft-Binary` says which, `X-Weft-Commit` says where from, and `ETag` is the blob oid"
          },
          "304": {
            "description": "Not modified"
          },
          "404": {
            "description": "No such path at that revision"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/tree": {
      "get": {
        "summary": "List the root tree at a revision. `history=1` adds the commit that last touched each entry, found by walking history from the revision under a wall-clock budget (`STRATUM_TREE_HISTORY_BUDGET_MS`, default 2500 ms); when the budget runs out first the response carries `history_truncated: true` and the entries not reached have `last_commit: null`. The plain listing never walks.",
        "parameters": [
          {
            "name": "at",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ commit, entries: [{ name, mode, kind, oid, size }] } \u2014 `size` is bytes for a blob and null for a tree"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/tree/{path}": {
      "get": {
        "summary": "List a subtree at a revision. `history=1` adds the commit that last touched each entry, found by walking history from the revision under a wall-clock budget (`STRATUM_TREE_HISTORY_BUDGET_MS`, default 2500 ms); when the budget runs out first the response carries `history_truncated: true` and the entries not reached have `last_commit: null`. The plain listing never walks.",
        "responses": {
          "200": {
            "description": "{ commit, entries: [{ name, mode, kind, oid, size }] } \u2014 `size` is bytes for a blob and null for a tree"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/log": {
      "get": {
        "summary": "First-parent history, paginated",
        "parameters": [
          {
            "name": "rev",
            "in": "query",
            "schema": {
              "type": "string",
              "default": "HEAD"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "maximum": 500,
              "default": 50
            }
          },
          {
            "name": "after",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Cursor: last commit of the previous page"
          },
          {
            "name": "path",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Only commits that changed this path. Leading and trailing slashes are ignored."
          }
        ],
        "responses": {
          "200": {
            "description": "{ entries, next_after }"
          }
        },
        "description": "First-parent walk. With `path`, only commits that changed that path are returned, each carrying a `change` of `added`, `modified` or `deleted` \u2014 this is what a file page needs to show who last touched a file without downloading the whole history and filtering client-side. A filtered request examines at most 500 commits before handing back `next_after`, so a file changed once at the start of a long history costs several bounded requests rather than one unbounded scan."
      }
    },
    "/v1/orgs/{org}/repos/{repo}/diff": {
      "get": {
        "summary": "Tree diff between two revisions (no rename detection in v1)",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ changes: [{path, status, old_oid, new_oid, \u2026}] }"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/owners": {
      "get": {
        "summary": "Effective OWNERS rule for a path at a revision",
        "parameters": [
          {
            "name": "path",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "at",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "HEAD"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ path, at, rules: [{dir, entries, noparent}], resolved: {users, teams, anyone_with_write, unknown}, error }"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/owners/check": {
      "get": {
        "summary": "Changed paths between two revisions with a sufficiency preview verdict",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "approvers",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated emails to preview: would these approvals suffice?"
          }
        ],
        "responses": {
          "200": {
            "description": "{ from, to, changed_paths, unknown_approvers, verdict: {landable, explanation, per_path} }"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/changes": {
      "post": {
        "summary": "Register a branch tip as a change (one commit = one review unit, keyed by its Change-Id trailer). repo:read when proposing from a fork via `source`; repo:write when proposing from the repository itself.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "from"
                ],
                "properties": {
                  "from": {
                    "type": "string",
                    "description": "Rev (usually a branch) whose tip becomes the patchset"
                  },
                  "target": {
                    "type": "string",
                    "description": "Branch the change intends to land on; the repo default if absent"
                  },
                  "source": {
                    "type": "string",
                    "description": "owner/name of a fork the commits live in, when they do not live in the repository being targeted. This is how somebody with no push credential contributes: fork, push to the repository you own, open the change here. With source set the caller needs only repo:read on the target; without it they still need repo:write, because those commits could only be in the target if an authorised push put them there. The source must be readable by the caller and must actually be a fork of this repository."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "New change or patchset: { change, patchset }"
          },
          "200": {
            "description": "Idempotent ack of a commit already registered"
          },
          "409": {
            "description": "The change is landing/landed/abandoned and refuses new patchsets"
          },
          "403": {
            "description": "Proposing from this repository without write access; the message names forking as the way through"
          },
          "404": {
            "description": "The target, or a named source, is masked from this caller"
          }
        }
      },
      "get": {
        "summary": "List changes, newest first",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Whitespace-separated name:value filters. `is:open` / `is:landing` / `is:landed` / `is:abandoned`; `author:@me` or `author:<email>`; `needs:my-approval`. `repo:<name>` is understood only on the org-wide list and is refused here. An unrecognised term is a 400 that names it \u2014 nothing is ever silently ignored, because a filter that quietly does nothing is how somebody concludes there is no work waiting for them. A well-shaped address that names no account here is an empty page rather than a refusal, so this list is not an address oracle. `author:@me` and `needs:my-approval` need a person: anonymous is 401 and a service token is 403.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "state",
            "in": "query",
            "required": false,
            "description": "The same filter as q=is:\u2026. Given both, they must agree, or 400.",
            "schema": {
              "type": "string",
              "enum": [
                "open",
                "landing",
                "landed",
                "abandoned"
              ]
            }
          },
          {
            "name": "after",
            "in": "query",
            "required": false,
            "description": "Keyset cursor: the `next` of the previous page, opaque. Not an offset \u2014 changes arrive at the top of this ordering, so an offset would skip a row that shifted down into it. A value this list did not mint is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Clamped to 1..500, and to 50 when the query contains needs:my-approval, which costs an OWNERS resolution per row examined.",
            "schema": {
              "type": "integer",
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ changes: [{key, title, state, land_verdict, patchset, \u2026}], next } \u2014 `next` is the cursor for the following page, or null at the end. With needs:my-approval the page may be shorter than `limit`, or empty, with `next` still set: the cursor is where the query stopped, not where the surviving rows did, so a client keeps walking."
          },
          "400": {
            "description": "An unrecognised or repeated q term (the message names it), a state and an is: that disagree, or an `after` this list did not mint"
          },
          "401": {
            "description": "author:@me or needs:my-approval with nobody signed in"
          },
          "403": {
            "description": "author:@me or needs:my-approval under a service token, which is nobody's reviewer"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/changes/{change}": {
      "get": {
        "summary": "One change with all patchsets, the submitted reviews, and the approvals on the latest",
        "responses": {
          "200": {
            "description": "{ change, patchsets, reviews, approvals } \u2014 `reviews` is every submitted review, oldest first, so the conversation can show a reviewer's cover message beside their comments; whether one of them *blocks* is not here, because that needs OWNERS and lives on /verdict. `change.changeset` is the key of the changeset holding this change, or null when it is free to land alone. It is the same binding the land endpoint refuses on with 409, so a client can show \"lands with Ic5000001\" instead of offering Land and reading the refusal."
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/changes/{change}/approve": {
      "post": {
        "summary": "Approve the latest patchset (people only; a service token is refused)",
        "responses": {
          "204": {
            "description": "Approved (idempotent)"
          },
          "401": {
            "description": "Nobody is signed in"
          },
          "403": {
            "description": "Approvals must come from a person, not a service token. Anyone who can read the change may approve it, a stranger to the org included; sufficiency reads write access, so an approval from outside it is recorded and counts for nothing"
          },
          "409": {
            "description": "The change is not open"
          }
        }
      },
      "delete": {
        "summary": "Revoke one's own approval on the latest patchset",
        "responses": {
          "204": {
            "description": "Revoked"
          },
          "404": {
            "description": "No active approval"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/changes/{change}/verdict": {
      "get": {
        "summary": "Sufficiency verdict for the latest patchset: landable, exactly why or why not, and who OWNERS requires",
        "responses": {
          "200": {
            "description": "{ change, state, patchset, commit, verdict: {landable, explanation, per_path}, blocks: [{ id, verdict, body, author, author_email, user_id, patchset_id, state, submitted_at, withdrawn_at, blocking, created_at }], reviewers: { required: [{ user_id, name, email, approved }], anyone_with_write } } — `reviewers.required` is the reviewer set **computed** from the target branch's OWNERS, not nominated by anybody: every user an owning entry resolves to, teams expanded, ordered by name, with `approved` true when that person's approval stands on this patchset. A `*` entry names nobody in particular, so it contributes no required reviewer and instead sets `anyone_with_write` — without which an empty `required` could not be told apart from a change nothing owns. Ungoverned paths and an unparseable OWNERS file contribute nobody either; the verdict reports the parse error. `blocks` is every standing request for changes; `blocking` says whether that one actually stops the change \u2014 true only when its author is somebody OWNERS names for a path this patchset touches, or, where OWNERS says `*` or governs nothing, somebody with write access. A block that is not blocking is recorded, rendered and advisory: any passer-by may object, and only the people the repository asks about these files can hold it up."
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/changes/{change}/review": {
      "post": {
        "summary": "Start (or save) the caller's pending review. Idempotent: calling it twice hands back the same draft, so a client drafting its first comment does not have to know whether one is already open, and two tabs racing cannot make two half-reviews",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "body": {
                    "type": "string",
                    "maxLength": 4000,
                    "description": "The cover message, saved with the draft. Optional: most reviews begin with a comment on a line, not a summary"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ id, verdict, body, author, author_email, user_id, patchset_id, state, submitted_at, withdrawn_at, created_at }"
          },
          "400": {
            "description": "An empty, oversized or control-character body"
          },
          "401": {
            "description": "Nobody is signed in"
          },
          "403": {
            "description": "A review verdict must come from a person, not a service token"
          },
          "409": {
            "description": "The change is not open"
          }
        }
      },
      "get": {
        "summary": "The caller's own pending review and the comments drafted into it. Answers { review: null } rather than 404 when there is none: 'do I have a review open' is a question with a perfectly good negative answer",
        "responses": {
          "200": {
            "description": "{ review: { \u2026 }|null, comments: [{ \u2026, pending: true }] }"
          },
          "401": {
            "description": "Nobody is signed in"
          },
          "403": {
            "description": "A service token cannot hold a review"
          }
        }
      },
      "delete": {
        "summary": "Discard the pending review, drafted comments and all. Nothing anybody else ever saw is lost, which is the whole point of a draft",
        "responses": {
          "204": {
            "description": "Discarded"
          },
          "404": {
            "description": "No pending review to discard"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/changes/{change}/review/submit": {
      "post": {
        "summary": "Submit the review as one act: publishes every drafted comment, records the verdict, writes or revokes the approval, and enqueues exactly ONE notification \u2014 all in one transaction. A twelve-comment pass is one email",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "verdict"
                ],
                "properties": {
                  "verdict": {
                    "type": "string",
                    "enum": [
                      "approve",
                      "comment",
                      "request_changes"
                    ],
                    "description": "approve writes the same approvals row POST /approve writes, so sufficiency, the land gate and the lander are untouched by reviews and cannot disagree with them. request_changes revokes any approval by the same person on this patchset and stands until withdrawn, across patchsets. comment leaves approvals alone"
                  },
                  "body": {
                    "type": "string",
                    "maxLength": 4000,
                    "description": "The cover message. Required for request_changes unless the review carries comments of its own \u2014 a block with no words tells the author no and never what would make it a yes"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ review: { \u2026 }, published: <how many drafts became visible>, approved, approval_revoked }"
          },
          "400": {
            "description": "An unknown verdict, an oversized or hostile body, or a request_changes with neither words nor comments"
          },
          "401": {
            "description": "Nobody is signed in"
          },
          "403": {
            "description": "A review verdict must come from a person, not a service token, for the same reason an approval must"
          },
          "404": {
            "description": "No such change, or the change has no patchsets"
          },
          "409": {
            "description": "The change is not open"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/changes/{change}/review/withdraw": {
      "post": {
        "summary": "Withdraw the caller's own standing request for changes. It withdraws the caller's block and cannot name anybody else's \u2014 a block somebody else could clear is not a block",
        "responses": {
          "204": {
            "description": "Withdrawn. The row is kept with withdrawn_at set: 'asked for changes and later withdrew it' is review history, and deleting it would render it as though they never objected"
          },
          "401": {
            "description": "Nobody is signed in"
          },
          "403": {
            "description": "A service token cannot hold a review"
          },
          "404": {
            "description": "No standing request for changes to withdraw"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/changes/{change}/comments": {
      "post": {
        "summary": "Comment on the change (people and service principals alike; anchored to the latest patchset, optionally to a path or a line in it)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "body"
                ],
                "properties": {
                  "body": {
                    "type": "string",
                    "maxLength": 4000
                  },
                  "path": {
                    "type": "string",
                    "description": "Optionally anchor the comment to a file the change touches"
                  },
                  "line": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 1000000,
                    "description": "With path: anchor to a 1-based line in that file as of the patchset"
                  },
                  "line_end": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 1000000,
                    "description": "With line: last line of a multi-line anchor, inclusive. Must not precede line"
                  },
                  "side": {
                    "type": "string",
                    "enum": [
                      "new",
                      "old"
                    ],
                    "default": "new",
                    "description": "Which half of the diff line counts in. 'old' anchors to a line the change removed, and needs a path"
                  },
                  "parent_id": {
                    "type": "string",
                    "description": "Reply into an existing thread. Threads are one level deep: the parent must be a root. The reply inherits the thread's anchor and must not carry path, line, line_end or side. The parent must be published: a draft is not a thread yet, and a reply into one is refused as absent"
                  },
                  "pending": {
                    "type": "boolean",
                    "default": false,
                    "description": "Draft this into the caller's pending review instead of publishing it. Invisible to every other reader \u2014 a second person, a service token, an admin, an anonymous reader of a public repo \u2014 until the review is submitted, and it notifies nobody. Refused for a service token: a draft belongs to a person, and there is no later in which a token submits it"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "{ id, patchset, author, author_email, author_principal, path, line, line_end, side, parent_id, thread_id, original_line, original_patchset, external_id, pending, review_id, published_at, resolved, resolved_at, resolved_by, body, created_at }"
          },
          "400": {
            "description": "Empty or oversized body, an invalid path, a line without a path, a backwards or unanchored range, an unknown side, or a reply to a reply"
          },
          "401": {
            "description": "Anonymous readers of public repos may read, not converse. A signed-in person comments as themselves whether or not they hold a role here; a service token from another organisation is anonymous here"
          },
          "403": {
            "description": "pending was asked for by a service token"
          }
        }
      },
      "get": {
        "summary": "The change's conversation, oldest first. Flat, with parent_id and thread_id on every row: the order a review was spoken in is meaning, and grouping by thread_id recovers the threads without losing it. Every published comment plus the caller's own unsubmitted drafts, and nobody else's",
        "responses": {
          "200": {
            "description": "{ comments: [{ patchset, author, path, line, line_end, side, parent_id, thread_id, original_line, original_patchset, resolved, resolved_at, resolved_by, body, \u2026 }] }"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/changes/{change}/comments/{comment}/resolve": {
      "post": {
        "summary": "Mark a thread resolved. Root comments only \u2014 a thread resolves, a sentence inside one does not. Permitted to the comment's author, or to anyone who satisfies the commented path under OWNERS (the same resolution that decides whose approval counts); a comment with no path takes standing on any governed path of the change. Resolving does NOT gate landing",
        "responses": {
          "200": {
            "description": "The comment, as { \u2026, resolved: true, resolved_at, resolved_by }"
          },
          "400": {
            "description": "The id names a reply; resolve the thread it is in"
          },
          "401": {
            "description": "Anonymous readers of public repos may read, not resolve"
          },
          "403": {
            "description": "Neither the comment's author nor an owner of that path \u2014 or a service token, which has no judgement to offer about a remark"
          },
          "404": {
            "description": "No such change, or no such comment on it"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/changes/{change}/comments/{comment}/unresolve": {
      "post": {
        "summary": "Reopen a resolved thread, under exactly the same rule as resolving it \u2014 whoever could close it can reopen it, which is what keeps 'resolved' from being a one-way silencer",
        "responses": {
          "200": {
            "description": "The comment, as { \u2026, resolved: false, resolved_at: null, resolved_by: null }"
          },
          "400": {
            "description": "The id names a reply; resolve the thread it is in"
          },
          "401": {
            "description": "Anonymous readers of public repos may read, not resolve"
          },
          "403": {
            "description": "Neither the comment's author nor an owner of that path"
          },
          "404": {
            "description": "No such change, or no such comment on it"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/changes/{change}/suggestions/apply": {
      "post": {
        "summary": "Apply a reviewer's suggestion blocks as one new patchset",
        "description": "A suggestion is a fenced `suggestion` block inside a review comment's body \u2014 there is no suggestion field and no suggestion column; it is parsed where it is read, so an imported or mirrored comment carries one exactly as a comment written here does. The block replaces the lines its comment is anchored to (`path` plus `line`/`line_end`, `side: new`); an **empty** block means delete those lines, which is distinct from a comment carrying no block at all. Every named comment is applied in **one** commit on top of the latest patchset and registered through the same path a push takes, so a reviewer's five remarks become one revision, one CI run and one notification rather than five. Nothing is rewritten: the new commit's parent is the patchset the reviewer read, which is what makes this clean under fast-forward-only landing. The commit message is the latest patchset's, so the `Change-Id` trailer keeps it a patchset of the same change \u2014 a change whose commit carries no trailer is refused, because a commit built from it would open a new change instead.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "comments"
                ],
                "properties": {
                  "comments": {
                    "type": "array",
                    "description": "Comment ids to apply together, at most 50",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "{ change, patchset, applied: [comment id, \u2026], paths: [path, \u2026] } \u2014 one new patchset, whatever the number of suggestions"
          },
          "400": {
            "description": "No comments named, more than 50, or a named comment cannot carry a suggestion: it is still an unsubmitted draft, it is not anchored to a line, it sits on the old side of the diff, it carries no suggestion block, or it carries several for one anchor"
          },
          "403": {
            "description": "Applying commits, so it needs write access \u2014 a reader is told that the change's author, or anybody who can push, applies it"
          },
          "404": {
            "description": "No such change, no such comment on it, or the repository is masked from this caller"
          },
          "409": {
            "description": "The change is not open; the change has no Change-Id trailer to attach a patchset to; two anchors overlap (refused in words rather than silently picking one); or an anchor is stale \u2014 the latest patchset no longer has that file, the file has changed since the patchset the comment was written on, the anchor is past the end of the file, the path is not text, or the lines already read exactly as suggested"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/changes/{change}/checks": {
      "post": {
        "summary": "Report a CI check on the latest patchset (external CI posts these; upsert by name; a failing check blocks landing)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "state"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 100,
                    "description": "e.g. ci/tests"
                  },
                  "state": {
                    "type": "string",
                    "enum": [
                      "pending",
                      "passing",
                      "failing"
                    ]
                  },
                  "url": {
                    "type": "string",
                    "maxLength": 1000,
                    "description": "Where the run's detail lives (http/https)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Check recorded"
          },
          "200": {
            "description": "Existing check of that name updated"
          },
          "400": {
            "description": "Bad name, state, or url"
          },
          "409": {
            "description": "The change is landed or abandoned"
          }
        }
      },
      "get": {
        "summary": "CI checks on the latest patchset",
        "responses": {
          "200": {
            "description": "{ patchset, required_checks: [name], checks: [{ name, state, url, required, source, posted_by, updated_at }] }. The **merged** view the land gate decides on: rows posted against this patchset unioned with the runs reported against its commit. `source` is `patchset` or `commit` and says what the row is a statement about; when one name reports on both sides the patchset row wins. `posted_by` is who reported it \u2014 an intake principal, or the provider for a commit-scoped run.\n\n`required_checks` is every name the **target branch** requires, and is not derivable from `checks`: a required check that has never reported has no row there to carry a `required` flag. A client reading the rows alone counts only what reported and concludes everything passed, over a change the land queue is holding on a name nobody has posted."
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/ci/secret": {
      "post": {
        "summary": "Mint or rotate the repo's CI intake secret (returned once; rotating invalidates the previous one)",
        "responses": {
          "201": {
            "description": "{ secret, rotated_at } \u2014 the secret is shown once and never readable again"
          }
        }
      },
      "get": {
        "summary": "Whether a CI intake secret is configured, and when it last moved. Never the secret itself",
        "responses": {
          "200": {
            "description": "{ configured, rotated_at }"
          }
        }
      },
      "delete": {
        "summary": "Revoke the CI intake secret",
        "responses": {
          "204": {
            "description": "Revoked; deliveries signed with it are answered like any stranger's"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/ci/checks": {
      "post": {
        "summary": "Vendor CI intake: a check verdict signed with the repo's intake secret. No Authorization header \u2014 the X-Weft-Signature-256 header (sha256=<hmac-sha256 of the raw body>) is the credential",
        "parameters": [
          {
            "name": "X-Weft-Signature-256",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "sha256=<hex hmac-sha256 of the raw request body, keyed with the intake secret>"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "commit",
                  "name",
                  "state",
                  "sent_at"
                ],
                "properties": {
                  "change": {
                    "type": "string",
                    "maxLength": 72,
                    "description": "The change key, e.g. I1a2d0001 \u2014 `I` followed by hex, taken from the commit's own Change-Id trailer. OPTIONAL, and its presence selects the shape: with it the verdict attaches to the change's latest patchset; without it the verdict is about the commit alone, which is what a push can send and what fills the Checks tab, the badge and \u2014 through the merged read \u2014 the review page and the land gate. Never a branch name or a pull request title; either is answered 404."
                  },
                  "commit": {
                    "type": "string",
                    "pattern": "^[0-9a-fA-F]{40}$",
                    "description": "The commit the run built; must be the latest patchset's tip"
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 100,
                    "description": "e.g. ci/tests"
                  },
                  "state": {
                    "type": "string",
                    "enum": [
                      "pending",
                      "passing",
                      "failing",
                      "queued",
                      "running",
                      "cancelled",
                      "skipped"
                    ],
                    "description": "The union of two vocabularies; which applies depends on the shape. Change-scoped (with `change`) takes pending, passing, failing. Commit-scoped takes queued, running, passing, failing, cancelled, skipped. Note `pending` is change-scoped ONLY and queued/running are commit-scoped only. A word the chosen shape does not accept is refused 400, naming the scope it was judged under and what the other scope would have taken."
                  },
                  "url": {
                    "type": "string",
                    "maxLength": 1000,
                    "description": "Where the run's detail lives (http/https)"
                  },
                  "summary": {
                    "type": "string",
                    "maxLength": 2000,
                    "description": "One sentence for a human; recorded in the audit trail"
                  },
                  "sent_at": {
                    "type": "integer",
                    "format": "int64",
                    "description": "The sender's clock in unix milliseconds; must be within 5 minutes of the server's"
                  },
                  "external_id": {
                    "type": "string",
                    "description": "Commit-scoped only. Your own id for the run. Send it: without one the row is keyed on (commit, name), so a genuine re-run of one workflow overwrites the first instead of updating the right row."
                  },
                  "ref": {
                    "type": "string",
                    "description": "Commit-scoped only. The branch or tag it ran for; the Checks tab filters on it."
                  },
                  "run_number": {
                    "type": "integer",
                    "format": "int64",
                    "description": "Commit-scoped only. The counter a person recognises in your CI's own UI. Not an identity here; external_id is."
                  },
                  "event": {
                    "type": "string",
                    "description": "Commit-scoped only. What triggered it \u2014 push, pull_request, schedule. The Checks tab filters on it."
                  },
                  "actor": {
                    "type": "string",
                    "description": "Commit-scoped only. Who it ran for. The Checks tab filters on it."
                  },
                  "started_at": {
                    "type": "integer",
                    "format": "int64",
                    "description": "Commit-scoped only. Unix milliseconds. Omit rather than sending 0, which renders as January 1970."
                  },
                  "completed_at": {
                    "type": "integer",
                    "format": "int64",
                    "description": "Commit-scoped only. Unix milliseconds; with started_at it is what the duration column reads."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Change-scoped only: first report under this name on this patchset"
          },
          "200": {
            "description": "Change-scoped: an existing check of that name was updated. Commit-scoped: every report, first or not \u2014 the write is an upsert, re-reporting a run is the ordinary case, and the route does not claim Created for what may be an update"
          },
          "400": {
            "description": "A field is malformed or over its bound (refused, never truncated)"
          },
          "404": {
            "description": "No such repo, no intake secret configured, or the signature did not match \u2014 one answer for all three, so the route cannot confirm a private repo exists"
          },
          "409": {
            "description": "Stale commit, replayed body, skewed clock, or a change that is landed/abandoned"
          },
          "413": {
            "description": "Body over 16 KiB"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/badge.svg": {
      "get": {
        "summary": "Status badge as SVG, rendered here (no external request). Reports the checks on the most recent change landed on the branch",
        "parameters": [
          {
            "name": "branch",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Defaults to the repo's default branch. Compared, never rendered into the SVG"
          }
        ],
        "responses": {
          "200": {
            "description": "image/svg+xml, Cache-Control: max-age=60. Public repos answer anonymously; a private repo is masked exactly as the repo is \u2014 never a badge that says \"private\"",
            "content": {
              "image/svg+xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/changes/{change}/workflows/approve": {
      "post": {
        "summary": "Let a fork's workflows run at this change's current tip (repo:write \u2014 the same authorization landing needs). A change from a fork is `blocked` rather than run, because its workflow file was written by the contributor and running it hands a stranger a repo:read token and a machine. Approval is **per tip**: a new patchset from the fork is blocked again, like GitHub's \"Approve and run\". The blocked placeholder run and its check row are replaced by the real run for the same commit and file.",
        "responses": {
          "202": {
            "description": "{ runs } \u2014 the runs that now exist at this tip, in the shape the workflow-runs route returns. Read back rather than assumed: a run may legitimately be settled instead of started (an organization out of minutes, a `timeout-minutes:` over the fleet's cap)."
          },
          "409": {
            "description": "the change is not open, or nothing is blocked at its current tip \u2014 including because somebody already approved it, or the tip's files ask for no `change` workflows. The cases are not distinguished: they are the same answer to the caller, that there is nothing for this button to start"
          },
          "404": {
            "description": "no such change, or the change has no patchsets. A caller without `repo:write` is refused by the repository auth ahead of this, masked as everywhere"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/changes/{change}/land": {
      "post": {
        "summary": "Enqueue the change for landing (fast-forward promotion by the land worker)",
        "description": "The precheck asks `land_gate`, not \"is any check currently failing\". The old question let two things through that a green tick then misrepresented: a change with no checks at all, and one whose checks had not started. A required check that has **not reported** is now a wait rather than an acceptance \u2014 and a wait, not a refusal, because ejecting somebody the moment they press Land before CI has started is the polling a land queue exists to abolish. The change is accepted, held in the queue, and lands unattended when the check reports green; a required check that never reports at all is ejected by the wait budget with a message naming it. A required check that has failed, or a non-required one that has, refuses here in words rather than being accepted and ejected seconds later.",
        "responses": {
          "202": {
            "description": "{ queued, job, change, gate, waiting_on } \u2014 `gate` is \"ready\" or \"waiting\", and `waiting_on` names the required checks still to report. The lander re-verifies sufficiency, the gate and fast-forward at claim time, so a 202 is a promise to try rather than a promise to land."
          },
          "409": {
            "description": "{ error } always, plus { gate, reason } **only when the gate is what refused**. `gate` is \"blocked\" and `reason` names every blocking check and why, so a UI can say which rather than that something is wrong. The other three refusals \u2014 not open, already landing, not landable \u2014 are not gate decisions and carry `error` alone: emitting `gate` for them would say the gate had an opinion when it was never consulted. Read `reason ?? error`."
          },
          "403": {
            "description": "Mirrors are read-only"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/changes/{change}/abandon": {
      "post": {
        "summary": "Close the change without landing. repo:write, or the change's author — a contributor who proposed from a fork holds only read access here and may still withdraw what they opened. Anyone else without write access gets the 404 a stranger gets.",
        "responses": {
          "204": {
            "description": "Abandoned"
          },
          "404": {
            "description": "Not a writer here and not this change's author"
          },
          "409": {
            "description": "The change is not open, or it is a member of a changeset"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/changes/{change}/interdiff": {
      "get": {
        "summary": "Tree diff between two patchsets of this change \u2014 \"what changed since I last looked\"",
        "description": "`from` and `to` are patchset **numbers**, not revisions; the server resolves each to the commit it was recorded at and diffs those two trees. That is not the same as diffing the latest patchset against its parent: a file touched in patchset 2 and put back in patchset 3 is in that diff and is not in this one, which is the whole reason the endpoint exists. Entries are exactly the shape `/v1/orgs/{org}/repos/{repo}/diff` returns, so a client that renders one renders this; no patch is emitted here either, hunks are assembled from `\u2026/files/*path`. **What it is not**: a two-commit tree diff between the patchsets as pushed, so a rebased patchset also shows whatever trunk picked up in between \u2014 not a rebase-aware three-way interdiff in Gerrit's sense \u2014 and no comment anchor is migrated across the range. `from` newer than `to` is allowed and shows the reverse.",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": true,
            "description": "Patchset number to diff from",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": true,
            "description": "Patchset number to diff to",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ from, to, from_patchset, to_patchset, changes: [{status, path, old_oid, new_oid, old_mode, new_mode}] } \u2014 `from`/`to` are the resolved commit oids, the `_patchset` pair the numbers asked for"
          },
          "400": {
            "description": "from or to missing, not a number, or both naming the same patchset"
          },
          "404": {
            "description": "No such change, this change has no patchset of that number, or the repository is masked from this caller"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/changes/{change}/views": {
      "get": {
        "summary": "The caller's own per-file viewed marks on this change, as they apply to the latest patchset",
        "description": "A mark is stored against the patchset it was made at. A path the newer patchsets rewrote comes back unviewed; a path they left alone keeps its mark. Marks are private to the person who made them \u2014 there is no parameter naming a user, so nobody can read anybody else's. `since` is the newest patchset this person has marked anything at \u2014 the patchset their last pass was against, and the left-hand side to hand `\u2026/interdiff`. It is `null` when they have marked nothing, which means \"no last pass\" and never \"patchset 1\": a client with no answer here asks for no range rather than inventing one.",
        "responses": {
          "200": {
            "description": "{ patchset, since: integer|null, viewed: [path, \u2026] }"
          },
          "401": {
            "description": "Nobody is signed in \u2014 an anonymous reader of a public repository is told to sign in"
          },
          "403": {
            "description": "A service token has no reviewer behind it. A signed-in person reads a public repository as themselves, role or no role, and their marks are their own"
          },
          "404": {
            "description": "No such change, or the repository is masked from this caller"
          }
        }
      },
      "put": {
        "summary": "Tick or untick one path for the caller, against the latest patchset",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "path",
                  "viewed"
                ],
                "properties": {
                  "path": {
                    "type": "string",
                    "description": "A path as the diff names it. The patchset is the server's to choose."
                  },
                  "viewed": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Recorded"
          },
          "400": {
            "description": "Invalid path"
          },
          "401": {
            "description": "Nobody is signed in \u2014 an anonymous reader of a public repository is told to sign in"
          },
          "403": {
            "description": "A service token has no reviewer behind it. A signed-in person reads a public repository as themselves, role or no role, and their marks are their own"
          },
          "404": {
            "description": "No such change, or the change has no patchsets"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/changes/{change}/associations": {
      "get": {
        "summary": "How to weigh each voice on this change: owner | member | contributor | first-time",
        "description": "Derived on every read from membership and from what each person has landed on this repository; nothing is stored. Keyed by the same author_principal string the comments endpoint returns. Service principals are absent rather than labelled.",
        "responses": {
          "200": {
            "description": "{ author: association|null, author_principal: string|null, authors: { \"user:<id>\": association } }"
          },
          "404": {
            "description": "No such change, or the repository is masked from this caller"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/land-queue": {
      "get": {
        "summary": "Changes landing right now, oldest first",
        "responses": {
          "200": {
            "description": "{ queue: [change] }"
          }
        }
      }
    },
    "/v1/orgs/{org}/changes": {
      "get": {
        "summary": "Every change in the organization the caller may read, newest first \u2014 the org-wide list the changeset picker reads, so composing does not begin with a request per repository. No org-level scope is required: each repository is authorized exactly as its own changes list is, so a repo:read token sees its repository and a public repository is readable anonymously. A credential from another organization is masked with 404.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "The same grammar as the per-repo list, plus `repo:<name>`, which narrows to one repository. A name this caller may not read is an empty page, never a refusal that would confirm the repository exists. An unrecognised term is a 400 that names it.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "state",
            "in": "query",
            "required": false,
            "description": "The same filter as q=is:\u2026. Given both, they must agree, or 400.",
            "schema": {
              "type": "string",
              "enum": [
                "open",
                "landing",
                "landed",
                "abandoned"
              ]
            }
          },
          {
            "name": "after",
            "in": "query",
            "required": false,
            "description": "Keyset cursor: the `next` of the previous page, opaque. A value this list did not mint is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Clamped to 1..500, and to 50 when the query contains needs:my-approval, which costs an OWNERS resolution per row examined.",
            "schema": {
              "type": "integer",
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ changes: [{key, title, state, patchset, repo, changeset, \u2026}], next } \u2014 `repo` names the repository the change is in, and `changeset` is the key of the changeset already holding it, or null when it is free to be composed; `viewer_write` says whether the caller may push to `repo` \u2014 the repository row's own answer, so a picker can grey out a change the caller could not compose or land. `next` is the keyset cursor for the following page, or null at the end"
          },
          "400": {
            "description": "Unknown state, an unrecognised or repeated q term (the message names it), a state and an is: that disagree, or an `after` this list did not mint"
          },
          "401": {
            "description": "author:@me or needs:my-approval with nobody signed in"
          },
          "403": {
            "description": "author:@me or needs:my-approval under a service token, which is nobody's reviewer"
          },
          "404": {
            "description": "No such organization. A credential from another organization is not refused: it reads the public repositories' changes the way anyone does, anonymously"
          }
        }
      }
    },
    "/v1/orgs/{org}/changesets": {
      "post": {
        "summary": "Compose open changes from several repositories into one changeset. repo:write on every member.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "key",
                  "title",
                  "members"
                ],
                "properties": {
                  "key": {
                    "type": "string",
                    "description": "Client-chosen key, same alphabet as a Change-Id; unique within the org"
                  },
                  "title": {
                    "type": "string",
                    "description": "At most 200 bytes"
                  },
                  "body": {
                    "type": "string"
                  },
                  "members": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "repo",
                        "change"
                      ],
                      "properties": {
                        "repo": {
                          "type": "string",
                          "description": "Repository name within the org"
                        },
                        "change": {
                          "type": "string",
                          "description": "The change's key (its Change-Id)"
                        }
                      }
                    },
                    "description": "1 to 16 open changes, at most one per repository"
                  },
                  "edges": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "from",
                        "to"
                      ],
                      "properties": {
                        "from": {
                          "type": "object",
                          "required": [
                            "repo",
                            "change"
                          ],
                          "properties": {
                            "repo": {
                              "type": "string",
                              "description": "Repository name within the org"
                            },
                            "change": {
                              "type": "string",
                              "description": "The change's key (its Change-Id)"
                            }
                          }
                        },
                        "to": {
                          "type": "object",
                          "required": [
                            "repo",
                            "change"
                          ],
                          "properties": {
                            "repo": {
                              "type": "string",
                              "description": "Repository name within the org"
                            },
                            "change": {
                              "type": "string",
                              "description": "The change's key (its Change-Id)"
                            }
                          }
                        }
                      },
                      "description": "`from` lands before `to`"
                    },
                    "description": "Landing-order constraints; must be acyclic"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "{ key, title, body, state, created_at, updated_at, viewer_write, reverts, reverted_by: [key], members: [{ repo, change }], edges: [{ from, to }], order: [{ repo, change }], landing, composition, checks: [{ repo, name, state, detail_url, run }] } — `viewer_write` is whether the caller may land, revert, abandon or edit the set: write on every member, which is what each of those routes needs, so a client can withhold controls that would be refused with a masked 404. `order` is the landing order the edges imply, ties by the order members were added. `landing` is null until the changeset has been asked to land, then { id, attempt, started_at, finished_at, outcome, members: [{ repo, change, ref, old, new, state, note }] }: the plan in landing order with each member's progress — `state` is `pending`, `done`, `failed` or `reverted`; `note` says why a step failed, names the revert commit of a reverted one, or says why a landed member could not be reverted; `outcome` is null while landing, then `landed` or `failed`; `attempt` counts the jobs that have driven it. `reverts` is the key of the changeset this one was made to revert (`POST …/revert`), or null; `reverted_by` lists the keys of the changesets made to revert this one, oldest first, whatever state they are in. `composition` identifies this set of members at these commits — a hash over each member's repo and its latest patchset commit — over the members listed, so a member whose repository has been deleted is left out of it as it is of `members` \u2014 and is null when no member is left or one has no patchset yet; a new patchset on any member, or a membership change, is a new composition, and its runs supersede the previous one's. `checks` is the composed CI for that current composition only, ordered by repo then name: one row per job of a workflow whose `on:` contains `changeset`, `name` being `<workflow> / <job>` as on a commit, `state` one of queued, running, passing, failing, cancelled, skipped, and `run` the workflow run it came from. Composed verdicts are never written to the member commit's `check_runs` — a file with `on: [change, changeset]` would otherwise report the composed answer as the member's own."
          },
          "400": {
            "description": "Bad key or title, no members, more than 16, two from one repository, or edges that name a non-member or form a cycle"
          },
          "402": {
            "description": "The org is read-only and a member is in a private repository"
          },
          "404": {
            "description": "A member is not a change the caller may write to"
          },
          "409": {
            "description": "The key exists, or a member is not open, or is already in an open changeset"
          }
        }
      },
      "get": {
        "summary": "Changesets in the org, newest first — only those whose every member the caller may read",
        "parameters": [
          {
            "name": "state",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "open",
                "landing",
                "landed",
                "abandoned",
                "failed"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 500
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ changesets: [changeset] }"
          },
          "400": {
            "description": "Unknown state"
          }
        }
      }
    },
    "/v1/orgs/{org}/changesets/{changeset}": {
      "get": {
        "summary": "One changeset with its members, edges and landing order. repo:read on every member; otherwise 404, whether or not it exists.",
        "responses": {
          "200": {
            "description": "{ key, title, body, state, created_at, updated_at, viewer_write, reverts, reverted_by: [key], members: [{ repo, change }], edges: [{ from, to }], order: [{ repo, change }], landing, composition, checks: [{ repo, name, state, detail_url, run }] } — `viewer_write` is whether the caller may land, revert, abandon or edit the set: write on every member, which is what each of those routes needs, so a client can withhold controls that would be refused with a masked 404. `order` is the landing order the edges imply, ties by the order members were added. `landing` is null until the changeset has been asked to land, then { id, attempt, started_at, finished_at, outcome, members: [{ repo, change, ref, old, new, state, note }] }: the plan in landing order with each member's progress — `state` is `pending`, `done`, `failed` or `reverted`; `note` says why a step failed, names the revert commit of a reverted one, or says why a landed member could not be reverted; `outcome` is null while landing, then `landed` or `failed`; `attempt` counts the jobs that have driven it. `reverts` is the key of the changeset this one was made to revert (`POST …/revert`), or null; `reverted_by` lists the keys of the changesets made to revert this one, oldest first, whatever state they are in. `composition` identifies this set of members at these commits — a hash over each member's repo and its latest patchset commit — over the members listed, so a member whose repository has been deleted is left out of it as it is of `members` \u2014 and is null when no member is left or one has no patchset yet; a new patchset on any member, or a membership change, is a new composition, and its runs supersede the previous one's. `checks` is the composed CI for that current composition only, ordered by repo then name: one row per job of a workflow whose `on:` contains `changeset`, `name` being `<workflow> / <job>` as on a commit, `state` one of queued, running, passing, failing, cancelled, skipped, and `run` the workflow run it came from. Composed verdicts are never written to the member commit's `check_runs` — a file with `on: [change, changeset]` would otherwise report the composed answer as the member's own."
          },
          "404": {
            "description": "No such changeset, or a member the caller may not read"
          }
        }
      }
    },
    "/v1/orgs/{org}/changesets/{changeset}/verdict": {
      "get": {
        "summary": "Landability of the whole changeset now, with every member's own verdict and check gate beside it, in landing order. repo:read on every member; otherwise 404.",
        "responses": {
          "200": {
            "description": "{ changeset, state, landable, gate: ready|waiting|blocked, explanation, waiting_on: [\"repo/change: check\"], members: [{ repo, change, state, patchset, commit, landable, explanation, gate, waiting_on: [check], reason, verdict: { landable, explanation, per_path }, approvals: [{ email, name, created_at }] }] } — `landable` is true when every member is approved and no required check has failed; `explanation` is the first non-landable member's, prefixed `repo/change: `; `waiting` is not a review refusal — nothing is left for a person to do — but `POST …/land` refuses it until the checks have reported, and `waiting_on` names them. The changeset's composed CI (`checks` on `GET …/changesets/{changeset}`) is folded into `gate` with the same precedence, blocked over waiting over ready: any composed check failing, cancelled or skipped blocks and `explanation` names it (`composed check {name} in {repo} is {state}`); otherwise any composed check queued or running waits and is listed in `waiting_on` as `{repo}: {name}` (a member's own check is `repo/change: check`); a changeset with no composed checks, or none that can be composed, leaves the gate to its members"
          },
          "404": {
            "description": "No such changeset, or a member the caller may not read"
          }
        }
      }
    },
    "/v1/orgs/{org}/changesets/{changeset}/workspace": {
      "get": {
        "summary": "The changeset as one checkout: every member at its proposed head, and the composed clone URL that checks them all out there. repo:read on every member; otherwise 404.",
        "responses": {
          "200": {
            "description": "{ key, title, state, composition, tip, clone_url, ssh_clone_url, members: [{ repo, change, title, path, commit, fetch_ref, clone_url, ssh_clone_url }], note } \u2014 `clone_url` (and `ssh_clone_url`, null when the server has no SSH front door) is a read-only git repository served for this changeset: one commit on branch `workspace` whose tree holds one submodule per member, at `path` (the repository's name), pinned to `commit`, with `.gitmodules` naming each member by a relative URL so `git clone --recurse-submodules <clone_url>` checks every member out at its proposed head over the same transport and credential. `tip` is that commit; `composition` is the same hash the changeset's composed CI runs are named by, and both move together whenever a member gains a patchset or the membership changes. `fetch_ref` is where each member's commit can be fetched from its own repository even when no branch points at it. Pushing to `clone_url` is refused in-band. `note` is null except while the changeset is landing, when it says that some members may already be on their trunks and the workspace is the proposed state, not the trunks. Members are the composable set: a member whose repository has been deleted is left out, and a changeset with no member left has `composition`, `tip` null and `members` empty"
          },
          "404": {
            "description": "No such changeset, or a member the caller may not read"
          }
        }
      }
    },
    "/v1/orgs/{org}/changesets/{changeset}/diffstat": {
      "get": {
        "summary": "How big this review is, member by member: the line counts behind `+412 − 77`, computed from the same tree diff /diff reports path by path. repo:read on every member; otherwise 404, masked identically to the changeset read.",
        "responses": {
          "200": {
            "description": "{ changeset, total: { files, insertions, deletions, truncated }, members: [{ repo, change, patchset, files, insertions, deletions, truncated }] } — members are in landing order, and each member's numbers are its latest patchset against its parent commit. `truncated` is true when at least one file was not counted: over 512 KiB (the same limit the diff view refuses to fetch as text), binary, a submodule pointer, or a rewrite past the request's work bound. The counts are then the total over the files that *were* counted, and `files` stays exact because it comes from the tree walk — a number invented for a file the server declined to read would be worse than an absent one. On the total, `truncated` means some member's is."
          },
          "404": {
            "description": "No such changeset, a member the caller may not read, or a member with no patchsets (which names no commit, and there is no size of nothing)"
          }
        }
      }
    },
    "/v1/orgs/{org}/changesets/{changeset}/members": {
      "post": {
        "summary": "Add an open change to an open changeset. repo:write on every member, the new one included.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "repo",
                  "change"
                ],
                "properties": {
                  "repo": {
                    "type": "string",
                    "description": "Repository name within the org"
                  },
                  "change": {
                    "type": "string",
                    "description": "The change's key (its Change-Id)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The changeset, as GET returns it"
          },
          "400": {
            "description": "The changeset already holds 16 members"
          },
          "409": {
            "description": "The changeset is not open, the change is not open, another change from that repository is already a member, or the change is in another open changeset"
          }
        }
      }
    },
    "/v1/orgs/{org}/changesets/{changeset}/members/{repo}/{change}": {
      "delete": {
        "summary": "Remove a member and every edge that touched it. repo:write on every member.",
        "responses": {
          "204": {
            "description": "Removed"
          },
          "404": {
            "description": "Not a member of this changeset"
          },
          "409": {
            "description": "The changeset is not open, or this is its last member — abandon the changeset instead"
          }
        }
      }
    },
    "/v1/orgs/{org}/changesets/{changeset}/edges": {
      "put": {
        "summary": "Replace the landing-order edges wholesale. repo:write on every member.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "edges"
                ],
                "properties": {
                  "edges": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "from",
                        "to"
                      ],
                      "properties": {
                        "from": {
                          "type": "object",
                          "required": [
                            "repo",
                            "change"
                          ],
                          "properties": {
                            "repo": {
                              "type": "string",
                              "description": "Repository name within the org"
                            },
                            "change": {
                              "type": "string",
                              "description": "The change's key (its Change-Id)"
                            }
                          }
                        },
                        "to": {
                          "type": "object",
                          "required": [
                            "repo",
                            "change"
                          ],
                          "properties": {
                            "repo": {
                              "type": "string",
                              "description": "Repository name within the org"
                            },
                            "change": {
                              "type": "string",
                              "description": "The change's key (its Change-Id)"
                            }
                          }
                        }
                      },
                      "description": "`from` lands before `to`"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The changeset, as GET returns it, with the new `order`"
          },
          "400": {
            "description": "An edge names a non-member, a change before itself, or the edges form a cycle"
          },
          "409": {
            "description": "The changeset is not open"
          }
        }
      }
    },
    "/v1/orgs/{org}/changesets/{changeset}/abandon": {
      "post": {
        "summary": "Close the changeset without landing; its members are released and may land or be abandoned alone again. repo:write on every member.",
        "responses": {
          "204": {
            "description": "Abandoned"
          },
          "409": {
            "description": "The changeset is not open"
          }
        }
      }
    },
    "/v1/orgs/{org}/changesets/{changeset}/land": {
      "post": {
        "summary": "Land every member, all or nothing. repo:write on every member.",
        "description": "Pre-flight first, and it writes nothing: every member must be `open`, its sufficiency verdict at the latest patchset green, its required checks **passing** — a check that has not reported is a refusal here where a single change would be held, because the plan below is made against the trunks as they stand now — and its patchset a fast-forward of its target. The changeset's composed CI is part of that gate too: a composed check failing, cancelled or skipped is a refusal, and one still queued or running is a `waiting` refusal here, the same as a member's own. Landing does not cancel composed runs. Then the commit point: one landing record with the full plan, and the changeset turns `landing` in the same transaction. From that row on the landing *will* finish, on this node or any other: members are CASed in landing order one manifest at a time; a member whose trunk moved in the window fails the landing, and every member already landed is put back with a **revert commit** on top of the landed one — never a rewind. `GET …/changesets/{changeset}` carries the landing's progress member by member until the changeset is `landed` or `failed`. A landed member is `landed` with `land_verdict` \"landed with changeset {key}\"; a reverted one is `open` again with a verdict naming the revert and what failed, and needs a new patchset to land again; a member that never got its turn is `open` with an `ejected:` verdict.",
        "responses": {
          "202": {
            "description": "{ queued: true, job, changeset, landing, plan: [{ repo, change, ref, old, new }] } — the plan in landing order: for each member the target ref, the tip it was judged against and will be CASed from, and the patchset commit it lands."
          },
          "404": {
            "description": "No such changeset, a member the caller may not write, or a member with no patchsets"
          },
          "409": {
            "description": "{ error, gate, waiting_on } — `error` names the first member in landing order standing in the way (`repo/change: why`), or says `changeset is {state}` when it is not open. `gate` is what stands in the way, composed CI folded in \u2014 \"waiting\" when every member is approved and a required check has not reported, \"blocked\" for anything a person has to act on (a member not yet approved, a failing check, a trunk that moved) \u2014 and never \"ready\" in a refusal. `waiting_on` lists `repo/change: check` for every required check still to report; a `waiting` gate is a refusal here, so land again when it has reported. A member that is not a fast-forward of its trunk is `repo/change: not fast-forward from {tip}`, with `gate` \"blocked\"."
          }
        }
      }
    },
    "/v1/orgs/{org}/changesets/{changeset}/revert": {
      "post": {
        "summary": "Revert a landed changeset: a new changeset, one call. repo:write on every member.",
        "description": "Body { key, title?, body? }. In every repository a member of this changeset landed in, makes one commit on a new `revert/{key}` branch off the target branch **as it is now** that puts back every path the member changed — the deleted file returns, the rewritten one is its old self at its old mode, the added ones are gone — and leaves every other path alone, so work landed since is kept. Each commit is registered as a change on the member's target exactly as a push would be (pinned, CI triggered, owners notified), and the changes are composed into changeset `key` with this changeset's edges **reversed**: what landed after its dependency is undone before it. Nothing about this changeset moves, and no trunk moves: the new changeset is reviewed under the same OWNERS and landed with `POST …/land` like any other. Only members recorded as landed are reverted — every member of a `landed` changeset, and of a `failed` one the members the unwind left landed (`GET …/changesets/{changeset}` shows them `done` with a `not reverted` note). Every member is checked before any branch is written, and any one refusal refuses the whole call: a path somebody has changed on trunk since the landing, a target branch that is gone, a `revert/{key}` branch already there, a member whose repository has been deleted. Title defaults to `Revert \"{title}\"`, body to `Reverts changeset {key}.`.",
        "responses": {
          "201": {
            "description": "The new changeset, as `GET …/changesets/{key}` returns it, with `reverts` naming this one. Members are in the order the reverts were made; `order` is the landing order."
          },
          "400": {
            "description": "{ error } — `key` is not a valid changeset key"
          },
          "404": {
            "description": "No such changeset, or a member the caller may not write"
          },
          "409": {
            "description": "{ error, conflicts? } — `changeset is {state}: nothing of it has landed` for an open, landing or abandoned changeset; `changeset {key} already exists`; `nothing of changeset {key} is landed: every member that landed was reverted` for a failed changeset the unwind fully put back; `{repo}/{change}: the repository no longer exists`. When a member cannot be reverted cleanly, `error` names the first in landing order — `{repo}/{change}: {ref} has changed since it landed at {paths}`, `{repo}/{change}: {ref} no longer exists`, `{repo}/{change}: revert/{key} already exists` — and `conflicts` lists every such member as { repo, change, why, changed?: [path], no_branch?, branch_exists? }. Nothing was written."
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/protections": {
      "get": {
        "summary": "Protected branches, alphabetical",
        "responses": {
          "200": {
            "description": "{ protections: [{ branch, created_at }] }"
          }
        }
      },
      "post": {
        "summary": "Protect a branch (admin): from then on it moves only through the land queue \u2014 pushes, commits, resets, reverts and deletes are refused",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "branch"
                ],
                "properties": {
                  "branch": {
                    "type": "string",
                    "maxLength": 200
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Protected"
          },
          "200": {
            "description": "Was already protected"
          },
          "400": {
            "description": "Invalid branch name"
          },
          "403": {
            "description": "Mirrors are read-only"
          },
          "404": {
            "description": "Unknown branch"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/protections/{branch}": {
      "delete": {
        "summary": "Remove a branch protection (admin)",
        "responses": {
          "204": {
            "description": "Unprotected"
          },
          "404": {
            "description": "The branch was not protected"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/refs": {
      "get": {
        "summary": "All branches and tags",
        "responses": {
          "200": {
            "description": "{ head, refs }"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/branches": {
      "post": {
        "summary": "Create a branch",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "from"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "from": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "{ oid }"
          },
          "409": {
            "description": "Branch exists"
          }
        }
      },
      "get": {
        "summary": "List branches, sorted, with the repository's default marked. `/refs` returns every ref unpaginated, which is right for a client that wants everything and wrong for a switcher that wants one kind.",
        "responses": {
          "200": {
            "description": "{ branches: [{ name, full, oid, default }], head }"
          },
          "404": {
            "description": "no such repo, or not visible to this caller"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/branches/{name}": {
      "delete": {
        "summary": "Delete a branch (API-only; the git wire never deletes)",
        "responses": {
          "204": {
            "description": "Deleted"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/reset": {
      "post": {
        "summary": "Soft ref move \u2014 the undo primitive; orphans stay SHA-reachable until GC",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "to"
                ],
                "properties": {
                  "branch": {
                    "type": "string",
                    "default": "main"
                  },
                  "to": {
                    "type": "string"
                  },
                  "expected_head": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ oid }"
          },
          "409": {
            "description": "expected_head mismatch; body carries current"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/revert": {
      "post": {
        "summary": "Append a commit undoing the branch head (history preserved)",
        "responses": {
          "201": {
            "description": "{ commit }"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/tags": {
      "post": {
        "summary": "Create a tag",
        "responses": {
          "201": {
            "description": "{ oid }"
          }
        }
      },
      "get": {
        "summary": "List tags, sorted. `/refs` returns every ref unpaginated, which is right for a client that wants everything and wrong for a client that wants one kind. `default` is on every ref for symmetry with `/branches` and is false for a tag, which is never a repository's HEAD.",
        "responses": {
          "200": {
            "description": "{ tags: [{ name, full, oid, default }], head }"
          },
          "404": {
            "description": "no such repo, or not visible to this caller"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/tags/{name}": {
      "delete": {
        "summary": "Delete a tag",
        "responses": {
          "204": {
            "description": "Deleted"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/export": {
      "post": {
        "summary": "Start an async bundle export",
        "responses": {
          "202": {
            "description": "{ job, state }"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/export/{job}": {
      "get": {
        "summary": "Export job status",
        "responses": {
          "200": {
            "description": "{ job, state, error, download }"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/export/{job}/download": {
      "get": {
        "summary": "Download the bundle (standard git bundle)",
        "responses": {
          "200": {
            "description": "application/octet-stream"
          }
        }
      }
    },
    "/v1/orgs/{org}/export": {
      "post": {
        "summary": "Org-wide bulk export: one job per repo",
        "responses": {
          "202": {
            "description": "{ exports: [{repo, job}] }"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/webhooks": {
      "post": {
        "summary": "Subscribe a URL to push events (HMAC-signed deliveries)",
        "responses": {
          "201": {
            "description": "{ id, url, secret } \u2014 secret shown once"
          }
        }
      },
      "get": {
        "summary": "List subscriptions",
        "responses": {
          "200": {
            "description": "{ subscriptions }"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/webhooks/{id}": {
      "delete": {
        "summary": "Remove a subscription",
        "responses": {
          "204": {
            "description": "Removed"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/metrics": {
      "get": {
        "summary": "Serving metrics: counts, bytes, p50/p99 per kind (clone/fetch/push/api/freshness)",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "Unix ms; default now-24h"
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "format",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "csv"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "JSON summary, or text/csv with format=csv"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/compact": {
      "post": {
        "summary": "Compact this repo now, synchronously \u2014 fold the write-ahead log into a new epoch (repo:write). Normally the background compactor's job; exposed for tests and for forcing a checkpoint before a bulk read.",
        "responses": {
          "200": {
            "description": "{ outcome } \u2014 Compacted, NotNeeded or Skipped"
          },
          "502": {
            "description": "the object store refused the write"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/gc": {
      "post": {
        "summary": "Delete epochs no longer referenced, honouring the retention window (repo:write)",
        "responses": {
          "200": {
            "description": "{ epochs_seen, epochs_deleted, objects_deleted }"
          },
          "502": {
            "description": "the object store refused the delete"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/cdn-pack": {
      "post": {
        "summary": "Build the CDN-offload pack for this repo's current tip (repo:write). Clones then fetch the bulk of their objects from the CDN via packfile-uri instead of from the server.",
        "responses": {
          "200": {
            "description": "{ outcome }"
          },
          "502": {
            "description": "the object store refused the write"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/cdn/{pack}": {
      "get": {
        "summary": "Fetch an offload pack. This is the URL advertised to git as a packfile-uri; when a CDN is configured the advertisement points at the CDN instead and this route is the origin behind it. 404 when no CDN is configured.",
        "security": [],
        "responses": {
          "200": {
            "description": "the packfile (application/x-git-packfile)"
          },
          "404": {
            "description": "no such pack, or CDN offload is not configured"
          }
        }
      }
    },
    "/v1/orgs/{org}/usage": {
      "get": {
        "summary": "Daily usage rows (active repos, requests, egress) + plan",
        "responses": {
          "200": {
            "description": "{ plan, days }"
          }
        }
      }
    },
    "/v1/orgs/{org}/audit": {
      "get": {
        "summary": "Query the immutable audit trail",
        "parameters": [
          {
            "name": "repo",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "principal",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "user",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Everything one person did, by user id"
          },
          {
            "name": "action",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Exact action, e.g. token.mint"
          },
          {
            "name": "since",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "until",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "Upper bound on at (epoch ms)"
          },
          {
            "name": "order",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "asc"
            },
            "description": "asc (default, oldest first) or desc (newest first)"
          },
          {
            "name": "after",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "Cursor: last seq"
          },
          {
            "name": "before",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "Cursor for order=desc: rows older than this seq"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "maximum": 1000
            }
          },
          {
            "name": "format",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "csv"
              ]
            },
            "description": "csv returns text/csv instead of JSON"
          }
        ],
        "responses": {
          "200": {
            "description": "{ entries, next_after, next_before }, or text/csv when format=csv"
          }
        }
      }
    },
    "/v1/orgs/{org}/mirrors": {
      "post": {
        "summary": "Register a mirror of an origin repo (initial ingest runs async)",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "origin"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "provider": {
                    "type": "string",
                    "enum": [
                      "github",
                      "generic"
                    ],
                    "default": "github"
                  },
                  "origin": {
                    "type": "string",
                    "description": "owner/name for github; a git URL for generic"
                  },
                  "installation_id": {
                    "type": "string",
                    "description": "The GitHub App installation that can read the origin \u2014 one this organization connected through `POST /v1/orgs/{org}/github/install`. Omit it for a public origin: that one is fetched as itself, with no credential, and nothing needs installing."
                  },
                  "public": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "{ repo, clone_url, status }"
          },
          "404": {
            "description": "`installation_id` is not one this organization connected. The same answer for an installation another organization holds and for one that does not exist: which installations exist is not this organization's business, and the id would otherwise be a bearer token to somebody else's source."
          },
          "422": {
            "description": "the origin was checked and could not be reached. The body carries the probe (`{ error, probe: { reachable, private, reason, \u2026 } }`) so a client can offer the GitHub App install when `private` is what happened. Skipped when an `installation_id` is supplied \u2014 a private origin refusing an anonymous check is the expected answer, not a reason to refuse creation."
          }
        }
      }
    },
    "/v1/orgs/{org}/mirrors/{repo}/sync": {
      "post": {
        "summary": "Sync a mirror now (synchronous)",
        "responses": {
          "200": {
            "description": "{ outcome, last_synced_commit, sync_error }"
          },
          "502": {
            "description": "Origin unreachable"
          }
        }
      }
    },
    "/v1/orgs/{org}/tokens": {
      "post": {
        "summary": "Mint a token; the plaintext is shown once. Optional expires_in_secs (seconds from now, 1 second to one year) gives the credential a deadline it dies at on its own \u2014 the shape a machine wants, where cleanup cannot be relied on; omit it for a token that lives until revoked. A signed-in member mints a personal token owned by them, whose scopes may not exceed what they can do anywhere in the org (their org role, or a per-repo grant if one gives them more). A service token (owned by nobody) needs org:admin.",
        "responses": {
          "201": {
            "description": "{ id, token, expires_at }. expires_at is null unless expires_in_secs was sent, and is echoed so a caller holds the deadline beside the secret rather than recomputing it from its own clock."
          },
          "400": {
            "description": "unknown scope, a scope beyond what you can do anywhere in this org, or an expires_in_secs that is not positive or exceeds one year \u2014 refused rather than clamped, because either means a duration was computed wrongly and a credential dead on arrival surfaces as a puzzling 401 elsewhere"
          },
          "403": {
            "description": "minting an org service token needs org:admin"
          }
        }
      },
      "get": {
        "summary": "Tokens, without their secrets: every token in the org for an admin, your own for a member. Also returns mintable_scopes \u2014 what this caller may put on a new token.",
        "responses": {
          "200": {
            "description": "{ tokens: [{ id, label, scopes, repo_id, user_id, created_at, revoked_at, expires_at }], mintable_scopes: [...] }. expires_at is when the credential stops working by itself, null for never; it is enforced by the authentication path rather than by a sweep, so an expired token is still listed and is already inert."
          }
        }
      }
    },
    "/v1/orgs/{org}/tokens/{id}": {
      "delete": {
        "summary": "Revoke a token \u2014 dead on the next request. An admin may revoke any; a member only their own.",
        "responses": {
          "204": {
            "description": "revoked"
          },
          "404": {
            "description": "no such token of yours"
          }
        }
      }
    },
    "/v1/orgs/{org}/ssh-keys": {
      "post": {
        "summary": "Register an SSH public key. Body: { public_key, label?, token_id? }. Without token_id the key is personal \u2014 it signs in as you, and follows your role. With token_id it is a deploy key carrying that token's authority (org:admin).",
        "responses": {
          "201": {
            "description": "{ id, token_id, user_id, algo, public_key, fingerprint_sha256, label, created_at, revoked_at }"
          },
          "400": {
            "description": "unusable key, unknown or revoked token"
          },
          "403": {
            "description": "registering a deploy key needs org:admin"
          },
          "409": {
            "description": "this key is already registered"
          }
        }
      },
      "get": {
        "summary": "SSH keys, revoked included: every key in the org for an admin, your own for a member",
        "responses": {
          "200": {
            "description": "{ keys: [...] }"
          }
        }
      }
    },
    "/v1/orgs/{org}/ssh-keys/{id}": {
      "delete": {
        "summary": "Revoke an SSH key \u2014 cuts access on the next connection. An admin may revoke any; a member only their own.",
        "responses": {
          "204": {
            "description": "revoked"
          },
          "404": {
            "description": "no such key of yours"
          }
        }
      }
    },
    "/webhooks/{provider}": {
      "post": {
        "summary": "Inbound origin webhook (github | generic); X-Hub-Signature-256 required",
        "security": [],
        "responses": {
          "202": {
            "description": "{ matched }"
          },
          "401": {
            "description": "Bad signature"
          }
        }
      }
    },
    "/healthz": {
      "get": {
        "summary": "Liveness",
        "security": [],
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/metrics": {
      "get": {
        "summary": "Prometheus exposition (process-level)",
        "security": [],
        "responses": {
          "200": {
            "description": "text/plain"
          }
        }
      }
    },
    "/v1/github/setup": {
      "get": {
        "summary": "GitHub sends a browser here after an App install or an edit to one. Two proofs, for two questions. Whose installation: the `code` GitHub appends when the App requests user authorization during installation is exchanged for that person's token, and the installation must be among the ones `GET /user/installations` lists for them \u2014 otherwise `connect=notyours` and nothing is bound. Which organization: the single-use `state` minted by `POST /v1/orgs/{org}/github/install`; without one (GitHub sends none after an edit) the signed-in person's own recent connect stands in \u2014 exactly one pending org binds, none is `missing`, several is `which`. Answers a redirect back into the dashboard either way, carrying `connect=ok|notyours|missing|expired|which|taken|error` \u2014 every dead state is `expired`, so a callback cannot be used to learn which flows exist.",
        "parameters": [
          {
            "name": "installation_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "state",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "The single-use state from `POST /v1/orgs/{org}/github/install`. GitHub sends none after an installation is edited; the signed-in person's own pending connect then names the organization."
          },
          {
            "name": "code",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "GitHub's user-authorization code, appended when the App requests user authorization during installation. Exchanged for the person's token to check the installation is theirs; required when the deployment has an OAuth client configured."
          }
        ],
        "responses": {
          "303": {
            "description": "redirect to the dashboard with the outcome in `connect`"
          }
        }
      }
    },
    "/v1/orgs/{org}/github/install": {
      "post": {
        "summary": "Begin connecting a GitHub App installation to this organization. Requires org:admin and a verified address. Answers the URL to send the person to, with a single-use state already in it.",
        "responses": {
          "200": {
            "description": "{ url, state, expires_in }"
          },
          "501": {
            "description": "this server has no GitHub App configured"
          }
        }
      }
    },
    "/v1/orgs/{org}/github/installations": {
      "get": {
        "summary": "Installations this organization has connected. An installation belongs to exactly one organization, so this never lists somebody else's.",
        "responses": {
          "200": {
            "description": "{ installations: [{ installation_id, provider, account, created_at }] }"
          }
        }
      }
    },
    "/v1/orgs/{org}/github/installations/{id}/repos": {
      "get": {
        "summary": "What an installation can actually read \u2014 the list a person picks from instead of typing an id and a name that have to agree. An installation this organization has not connected answers 404, the same as one that does not exist.",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ repositories: [{ full_name, private, default_branch, description, size }] }"
          },
          "404": {
            "description": "no such installation, for this organization"
          },
          "502": {
            "description": "GitHub did not answer"
          }
        }
      }
    },
    "/v1/orgs/{org}/github/installations/{id}": {
      "delete": {
        "summary": "Stop offering an installation here. GitHub keeps it installed \u2014 only GitHub can uninstall \u2014 and repositories already mirrored through it keep working.",
        "responses": {
          "204": {
            "description": "forgotten"
          },
          "404": {
            "description": "no such installation"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/sync-status": {
      "get": {
        "summary": "Where a mirror has got to: `syncing` before the first sync finishes, `ready` when the last one worked, `failed` with a reason when it did not. Creation answers 202 and ingests in the background, so this is what a screen polls instead of looking stuck.",
        "responses": {
          "200": {
            "description": "{ state, origin, provider, last_sync_at, commit, error, clone_url }"
          },
          "400": {
            "description": "not a mirror"
          }
        }
      }
    },
    "/v1/search/topics": {
      "get": {
        "summary": "Topics in use, most-used first",
        "description": "The topics maintainers have actually filed repositories under, with how many the caller can see carrying each. Ordered by count and then by name, so the order is stable between calls rather than reshuffling among equal counts. Scoped exactly as repo search is: a topic carried only by repositories the caller cannot see is not listed, because a list of topic names is an existence oracle for the work behind them. This is what the discovery page builds its topic chips from.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Topics and the number of visible repositories carrying each",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "topics": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string"
                          },
                          "repos": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/search/repos": {
      "get": {
        "summary": "Search repos across namespaces",
        "description": "Open to anonymous callers: they see public repos. Signed in (session or org token), the answer also includes repos in namespaces you belong to. Matching is a case-insensitive substring of the repo name, its namespace, its description, or any of its topics; an empty `q` returns everything visible, which is what the discovery page browses with. `topic` narrows to repositories carrying exactly that topic, and is separate from `q` on purpose: `q` is what somebody typed and matches a topic as loosely as it matches prose, while `topic` is what a topic pill means and has to be exact. Paging is keyset over `(namespace, name, id)` and the cursor is applied inside the visibility filter, so a tampered cursor moves the window and never widens it.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 128
            },
            "description": "Substring to match against the name, namespace, description or topics. Empty or absent matches everything visible."
          },
          {
            "name": "topic",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Narrow to repositories carrying exactly this topic. Case-insensitive, because topics are stored lowercased. A topic no repository carries — or a string that could not be a topic at all — returns an empty page rather than a 400, because a link somebody typed by hand should come back empty rather than as an error."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "after",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Cursor: the `next` value from the previous page."
          }
        ],
        "responses": {
          "200": {
            "description": "A page of hits, and `next` when there is another page",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "repos": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/RepoHit"
                      }
                    },
                    "next": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Query longer than 128 characters"
          },
          "401": {
            "description": "A credential was presented and is not valid"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/watch": {
      "get": {
        "summary": "What this person hears about from this repository",
        "description": "Three levels, and the absence of a choice is `participating` \u2014 the default is not stored, so nobody has to be enrolled. A token-authenticated caller is refused: watching is a person's setting and a service token has no mailbox.",
        "responses": {
          "200": {
            "description": "The current level"
          },
          "401": {
            "description": "No session, or a token rather than a person"
          },
          "404": {
            "description": "No such repository, or none you may see"
          }
        }
      },
      "put": {
        "summary": "Choose what to hear about from this repository",
        "description": "`all`, `participating` or `ignore`. Choosing the default clears the stored row rather than writing one, so \"never decided\" and \"decided on the default\" stay one state. `participating` means changes you opened, took part in, or that the repository's OWNERS file says require you as a reviewer.",
        "responses": {
          "200": {
            "description": "The level now in force"
          },
          "400": {
            "description": "Not one of all, participating, ignore"
          },
          "401": {
            "description": "No session, or a token rather than a person"
          },
          "404": {
            "description": "No such repository, or none you may see"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/ci/poll": {
      "post": {
        "summary": "Poll this repository's GitHub Actions runs now",
        "description": "Enqueues a poll of the runs GitHub already has, through the org's existing App installation. Requires repo:write rather than org:admin \u2014 unlike an import this writes nothing anybody has to undo by hand, it restates verdicts a provider already reached, idempotently, into rows nobody edits. Refused before anything long-running starts, in a sentence naming the way through, for a repository whose origin is not a GitHub App installation: a poll that is accepted and then silently does nothing for a minute is a much worse answer. Needs `actions: read` on the App \u2014 see the GET below for how a missing permission is reported.",
        "responses": {
          "202": { "description": "A poll was enqueued" },
          "400": { "description": "This repository has no GitHub App origin to poll" },
          "404": { "description": "No such repository, or none you may write to" }
        }
      },
      "get": {
        "summary": "How the last poll went",
        "description": "Exists for one field. An App installation lacking `actions: read` fails in the single way that renders identically to success \u2014 an empty Checks tab \u2014 and a page that cannot tell the two apart tells a maintainer their CI is not set up when the truth is that we may not look at it. `denied` is that distinction; with it the tab can say so and offer the re-approve link. `connected` is whether there is a GitHub origin to poll at all, which is the third state and is neither of the other two.",
        "responses": {
          "200": {
            "description": "{ provider, connected, polled, denied, error, high_water, resuming_from, retry_in_ms }"
          },
          "404": { "description": "No such repository, or none you may see" }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/commits/{sha}/checks": {
      "get": {
        "summary": "The check verdicts standing beside one commit \u2014 one row per workflow, the newest of each",
        "description": "Not the commit's whole check history. A provider polls, so one build arrives as queued, then running, then failing, then passing after a re-run; returning all of them puts \"build: failing\" from twenty minutes ago above \"build: passing\" from two, and a reader who scans the first line concludes the commit is broken. Keyed on the workflow's name, because the name is what a reader recognises \u2014 the cost being that if two providers report a workflow of the same name for one commit, the newer wins and the other is not shown. Readable by anyone who may read the repository, a signed-out visitor included; a private repository answers exactly as one that does not exist. A commit with no runs, and a sha that could never have been stored, are both an empty list with 200: the 404 on this route belongs to the repository, and spending it on the commit too would make \"no such project\" and \"nothing has built this yet\" indistinguishable. The sha is deliberately not checked against a reachable object \u2014 a run can be reported for a commit since rewritten away, and refusing to show it would lose the only record that it happened.",
        "security": [],
        "parameters": [
          { "name": "sha", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "{ runs: [CheckRun] } \u2014 at most one per workflow name, newest" },
          "404": { "description": "No such repository, or none you may see" }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/required-checks/{branch}": {
      "get": {
        "summary": "List the checks required on a branch",
        "description": "Alphabetical. Takes repo read: which checks must pass before a change lands is the rules of the road, not privileged information. Not gated on the branch currently being protected \u2014 a requirement outlives the protection it was created under.",
        "parameters": [
          { "name": "branch", "in": "path", "required": true, "description": "Branch name; may contain slashes (release/2.0), which is why this is the route's trailing wildcard.", "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "{ branch, required_checks: [{ name, created_by, created_at }] }" },
          "400": { "description": "Invalid branch name" },
          "404": { "description": "No such repository, or none you may see" }
        }
      },
      "post": {
        "summary": "Require a named check on a protected branch",
        "description": "Org admin, exactly like protecting the branch: deciding which checks must pass before trunk moves is the same class of decision as deciding that trunk moves only through review. The branch must already be protected \u2014 on an unprotected branch anyone can push straight past the requirement, so the row would read as safety while being none. Requiring the same check twice acknowledges rather than erroring or duplicating. The name is deliberately allowed to be wider than the intake's own rule so that a mirrored Actions workflow ('Build and test (ubuntu-latest)') can be required.",
        "parameters": [
          { "name": "branch", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": {
            "type": "object", "required": ["name"],
            "properties": { "name": { "type": "string", "description": "Check name, up to 100 bytes; no control characters, no leading or trailing whitespace." } }
          } } }
        },
        "responses": {
          "201": { "description": "Now required" },
          "200": { "description": "Already required" },
          "400": { "description": "Invalid branch or check name" },
          "409": { "description": "The branch is not protected" },
          "404": { "description": "No such repository, or not an admin of it" }
        }
      },
      "delete": {
        "summary": "Stop requiring a check",
        "description": "Org admin. No protection check, unlike POST: removal must work on a branch whose fence has since come down, or a stale requirement would be unremovable and would come back into force the moment the branch was protected again.",
        "parameters": [
          { "name": "branch", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "name", "in": "query", "required": true, "description": "The check name to stop requiring.", "schema": { "type": "string" } }
        ],
        "responses": {
          "204": { "description": "No longer required" },
          "404": { "description": "That check was not required on that branch, or no such repository" }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/workflows": {
      "get": {
        "summary": "What .weft/ asks to have run, at a rev. Read-only \u2014 nothing here starts anything. Each file is parsed and its matrix expanded, so the answer is the concrete jobs that would run and the order they would be allowed to start in; a file that cannot be used comes back with problems naming the key, the line, and what to do instead.",
        "parameters": [
          {
            "name": "at",
            "in": "query",
            "required": false,
            "schema": { "type": "string" },
            "description": "Branch, tag or commit. Defaults to HEAD. A rev nobody has is 404, not an empty list \u2014 an empty list would claim the ref exists and has no workflows."
          }
        ],
        "responses": {
          "200": {
            "description": "{ workflows: [{ file, ok, name, on: [push|change], jobs: [{ key, job, matrix, needs: [key] }] }] } for a usable file, or { file, ok: false, name?, problems: [{ line, key, message, hint, text }] } for one that is refused. `key` is the expanded job name, e.g. `build (linux)`; a matrix job appears once per cell. Repositories with no .weft/ answer with an empty list."
          },
          "404": {
            "description": "no such repository, or an unknown rev"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/checks/runs": {
      "get": {
        "summary": "This repository's CI runs, newest first",
        "description": "Every row here is a verdict: reached by Weft's own hosted runner for a `.weft/*.yml` workflow (`provider` is `stratum`, and `detail_url` is that run's page on this forge, `/<org>/<repo>/checks/runs/<run id>` \u2014 a refused workflow file gets a row named for the file, and a `.weft/` directory the trigger could not read gets one named for the directory, each linking to the run that carries the reason), polled from the org's GitHub App installation, or posted to the signed intake at .../ci/checks. Which of the three is `provider`, and callers should not branch on it: the point of one shape is that a Buildkite project's runs read exactly like a GitHub Actions project's. `workflows` is the distinct set of names across the whole repository and rides along with every page, so a left rail can be drawn without a second request. `next_before` is the cursor for the next page and is null on the last one. Readable by anyone who may read the repository; a private repository answers a stranger exactly as one that does not exist. `state` outside the six is refused by name rather than ignored \u2014 a filter silently dropped shows rows that do not match what was asked for, which reads as the filter being broken. `limit` is clamped rather than refused. A repository nobody has reported a run for answers an empty list with 200.",
        "security": [],
        "parameters": [
          { "name": "branch", "in": "query", "schema": { "type": "string" } },
          {
            "name": "state",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": ["queued", "running", "passing", "failing", "cancelled", "skipped"]
            }
          },
          { "name": "event", "in": "query", "schema": { "type": "string" } },
          { "name": "actor", "in": "query", "schema": { "type": "string" } },
          { "name": "workflow", "in": "query", "schema": { "type": "string" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer" } },
          { "name": "before", "in": "query", "schema": { "type": "integer" } }
        ],
        "responses": {
          "200": {
            "description": "{ runs: [{ id, repo_id, commit_sha, ref_name, provider, external_id, name, run_number, event, state, detail_url, actor, started_at, completed_at, created_at, updated_at }], workflows: [string], next_before: integer|null }"
          },
          "400": {
            "description": "A state outside the six, or a limit or cursor that is not a whole number"
          },
          "404": {
            "description": "No such repository, or none you may see"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/checks/runs/{id}": {
      "get": {
        "summary": "One check run",
        "description": "Scoped to the repository in the path: a run belonging to another repository answers exactly as an id that was never issued, because the lookup filters on repo_id inside the query rather than fetching the row and comparing afterwards.",
        "security": [],
        "responses": {
          "200": { "description": "The run" },
          "404": { "description": "No such run in this repository, or no such repository" }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/contributors": {
      "get": {
        "summary": "Who has worked on this repository, most first",
        "description": "Derived from the commits themselves rather than from activity on this platform, on the same rules as the profile heatmap \u2014 which is what makes a project that mirrored in yesterday show its real contributors instead of an empty grid. Readable by anyone who may read the repository, a signed-out visitor included; a private repository answers exactly as one that does not exist. `limit` is optional and clamped rather than refused \u2014 a large number is somebody wanting more of the answer than the rail shows \u2014 but a `limit` that is not a whole number is refused by name, because nobody meant it and quietly serving the default would answer a question that was not asked. A repository nobody has committed to answers an empty list with 200: that is a fact about a real repository, where a 404 would be a different and false statement.",
        "security": [],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ contributors: [{ user_id, handle, commits, last_at }] } \u2014 ranked by commits descending, ties broken by handle so the order never moves between two loads"
          },
          "400": {
            "description": "A limit that is not a whole number"
          },
          "404": {
            "description": "No such repository, or none you may see"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/meta": {
      "get": {
        "summary": "What the repository is, beside what is in it: the language mix by bytes, the detected licence, the community files (CONTRIBUTING / CODE_OF_CONDUCT / SECURITY) found at the root or in `.github/`, the path of the README, and the topics. Everything but the topics is derived from the tree at the default branch \u2014 no new git plumbing and nothing stored. Readable by anyone who may read the repository, a signed-out visitor included; masked exactly as the code is for anyone who may not. `license.spdx` is null with `recognised: false` when the file is there but the fingerprint table is not certain what it is \u2014 a wrong licence is materially worse than no licence, so it never guesses. Several licence files (`LICENSE-APACHE` beside `LICENSE-MIT`) is its own answer rather than silence: `path` and `spdx` are null and `files` lists them, because naming one would mislead and saying nothing makes a dual-licensed project read as unlicensed. `license` is null only when there is no licence file at all. `languages_truncated` is true when the tree walk hit its bound, in which case the byte counts are of what was measured rather than of the whole tree.",
        "responses": {
          "200": {
            "description": "{ topics: [string], languages: [{ name, bytes }] (biggest first), languages_truncated: bool, license: { path: string|null, spdx: string|null, name: string|null, recognised: bool, files: [string] } | null, community: [{ kind, path }], readme: string|null }. `readme` is the path of the project's README (root before `.github/`, and `.md` / `.txt` / `.rst` / no extension all recognised), or null when there is none — a scalar rather than a fourth `community` kind because the client fetches and renders its bytes rather than linking to it, and null rather than \"\" because absence is a fact the About rail renders rather than a file named nothing. Carries an ETag over the HEAD commit and the topics, and `Cache-Control: private, no-cache`: a conditional request that matches answers 304 without walking the tree — the README path is a function of the tree, which is a function of the commit already in the tag, so it needs no extra input."
          },
          "404": {
            "description": "No such repository, or none you may see"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/topics": {
      "put": {
        "summary": "Replace the repository's topics with exactly this set \u2014 the whole list, not an addition. Requires repo:write. Topics are lowercased at the write (the column carries CHECK (topic = lower(topic))); everything that is not a case difference is refused with a 400 rather than mangled.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "topics"
                ],
                "properties": {
                  "topics": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "At most 20 distinct topics, each at most 35 characters of ASCII letters, digits and interior hyphens. Refused, not truncated, past either bound. Duplicates collapse."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ topics: [string] } \u2014 normalised and sorted, as stored"
          },
          "400": {
            "description": "A topic that is not a case difference away from storable, or too many of them"
          },
          "404": {
            "description": "No such repository, or none you may write to"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/star": {
      "get": {
        "summary": "Star counts for this repository. Readable by anyone who may read the repo, a signed-out visitor included. `stars` is this forge's own count; `origin` is what the upstream reported when the mirror last read it, or `null` when there is no imported number \u2014 the two are never summed.",
        "responses": {
          "200": {
            "description": "{ stars, starred, origin: { stars, at, url } | null }"
          }
        }
      },
      "put": {
        "summary": "Star this repository. Idempotent: starring twice is starring once. A person's act \u2014 a service token is refused 401.",
        "responses": {
          "200": {
            "description": "{ stars, starred, origin }"
          },
          "401": {
            "description": "not a person (no session, or a service token)"
          }
        }
      },
      "delete": {
        "summary": "Unstar this repository. Unstarring something you never starred is not an error.",
        "responses": {
          "200": {
            "description": "{ stars, starred, origin }"
          },
          "401": {
            "description": "not a person (no session, or a service token)"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/issues": {
      "post": {
        "summary": "File an issue. Requires repo:read, not repo:write \u2014 an issue tracker gated on write access is useless to the people it exists for. Also requires a confirmed email address.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "title"
                ],
                "properties": {
                  "title": {
                    "type": "string",
                    "description": "1-400 characters; refused, not truncated, beyond that"
                  },
                  "body": {
                    "type": "string",
                    "description": "Up to 65536 characters"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new issue, including its allocated number"
          },
          "400": {
            "description": "A bound was exceeded; the message names which"
          },
          "401": {
            "description": "No person is asking \u2014 a service token cannot author an issue"
          },
          "403": {
            "description": "Email address not yet confirmed"
          }
        }
      },
      "get": {
        "summary": "List issues. Open and closed counts are both returned whatever the state filter, because the index shows them as a pair.",
        "parameters": [
          {
            "name": "state",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "open",
                "closed",
                "all"
              ],
              "default": "open"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "newest",
                "oldest"
              ],
              "default": "newest"
            },
            "description": "Both sorts are keyed on the issue number, because the page cursor is a number: ordering by one column while paging by another skips or repeats rows at a page boundary. sort=updated is refused by name with 400 until it has a compound cursor, rather than silently answered in a different order."
          },
          {
            "name": "label",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "author",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "description": "Substring of the title"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 30,
              "maximum": 100
            }
          },
          {
            "name": "before",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "description": "Issue number to page back from"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ issues: [...], counts: {open, closed}, next }"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/issues/{number}": {
      "get": {
        "summary": "One issue, with its labels and comment count",
        "responses": {
          "200": {
            "description": "The issue"
          },
          "404": {
            "description": "No such issue"
          }
        }
      },
      "patch": {
        "summary": "Edit the title or body, or close/reopen. The issue's author or somebody with write access; an author may close their own issue.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string"
                  },
                  "body": {
                    "type": "string"
                  },
                  "state": {
                    "type": "string",
                    "enum": [
                      "open",
                      "closed"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The issue as it now stands"
          },
          "403": {
            "description": "Neither the author nor a writer"
          },
          "404": {
            "description": "No such issue"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/issues/{number}/comments": {
      "post": {
        "summary": "Comment on an issue. repo:read and a confirmed email, same as filing.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "body"
                ],
                "properties": {
                  "body": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new comment"
          },
          "401": {
            "description": "No person is asking"
          },
          "403": {
            "description": "Email address not yet confirmed"
          }
        }
      },
      "get": {
        "summary": "The conversation, in insertion order (by seq, not by id \u2014 ULID tails are random within a millisecond)",
        "responses": {
          "200": {
            "description": "{ comments: [...] }"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/issues/{number}/labels": {
      "put": {
        "summary": "Replace an issue's labels. Write access only \u2014 triage is a maintainer's map of their own backlog. Refused with 403 rather than masked, because reaching it means the caller could already read the repository.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "labels"
                ],
                "properties": {
                  "labels": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "maxItems": 20
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The issue with its new labels"
          },
          "400": {
            "description": "Unknown label, or too many"
          },
          "403": {
            "description": "Read access but not write"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/milestones": {
      "get": {
        "summary": "The repository's milestones, by the number they came with",
        "responses": {
          "200": {
            "description": "{ milestones: [{ number, title, description, state, due_on, open_issues, closed_issues }] }. Numbers are the origin's own, so a reference written before a migration keeps meaning what it said. Both issue counts are given rather than one progress fraction: an empty milestone and a finished one compute to the same number and are opposite situations."
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/labels": {
      "get": {
        "summary": "The repository's labels",
        "responses": {
          "200": {
            "description": "{ labels: [...] }"
          }
        }
      },
      "post": {
        "summary": "Create a label. repo:write.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "color"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 50
                  },
                  "color": {
                    "type": "string",
                    "description": "A design-system token name, never a hex. A stored hex is unfixable later: change the palette and it is silently wrong in both themes with no migration able to know what was meant. An unknown name is refused and the message names the valid set."
                  },
                  "description": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new label"
          },
          "400": {
            "description": "Unknown colour token, or a name already in use"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/labels/{label}": {
      "delete": {
        "summary": "Delete a label, removing it from every issue carrying it. repo:write.",
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "400": {
            "description": "No such label"
          }
        }
      }
    },
    "/v1/orgs/{org}/repos/{repo}/import": {
      "post": {
        "summary": "Begin importing this repository's issues from its GitHub origin. Org admin. Numbers are preserved, so the repository's tracker must be empty \u2014 an import that renumbered would break every #reference written down elsewhere.",
        "responses": {
          "202": {
            "description": "Queued. Nothing is imported yet; poll GET on this path."
          },
          "400": {
            "description": "No GitHub origin to import from \u2014 connect it as a mirror first"
          },
          "409": {
            "description": "The tracker already has issues, so the imported numbers cannot be kept"
          }
        }
      },
      "get": {
        "summary": "How far the import has got, per phase. Each phase resumes independently; issues reports the next page URL, or 'done'.",
        "responses": {
          "200": {
            "description": "{ labels, milestones, issues } \u2014 null for not started"
          }
        }
      }
    },
      "/v1/orgs/{org}/repos/{repo}/workflow-runs": {
        "get": {
          "summary": "This repository's workflow runs, newest first, each with its jobs",
          "description": "What `.weft/` actually ran \u2014 as opposed to `/workflows`, which is what it *would* run. Run JSON is `{ id, file, name, commit_sha, ref_name, event, change_key, state, error, blocked_reason, created_at, updated_at, completed_at, changeset, composition, jobs: [...] }`; `event` is `push`, `change` or `changeset`, and on a `changeset` run `changeset` is `{ key }` and `composition` is the hex composition it was started for — both null otherwise; `state` is one of running, passed, failed, cancelled, blocked \u2014 a run that exists, is not running and has no verdict: a fork's change waiting for a maintainer to let it run, an organization out of hosted-runner minutes, or one whose hosted workflows are suspended. The sentence is in `error` and the machine-readable reason is in `blocked_reason` — `fork`, `budget` or `suspended`, and null unless the run is blocked. Branch on `blocked_reason`, never on the words in `error`: only a `fork` run can be started by `POST …/changes/{change}/workflows/approve`, and the sentences are written for a person and get rewritten. The run's mirrored check row is `queued` rather than failing, because nothing is wrong with the commit. Job JSON is `{ id, job_id, key, matrix, state, attempts, error, detail_url, log_chunks, started_at, completed_at }`; `key` is the expanded cell name and `matrix` is an object, already parsed. Every job also mirrors itself into `check_runs` as `<workflow> / <cell>`, so a run's verdicts appear on the Checks tab and in the land gate without asking here at all — except a `changeset` run, whose jobs mirror onto the changeset instead (`checks` on `GET …/changesets/{changeset}`) and never onto the commit. `limit` defaults to 20 and is clamped to 100 rather than refused: a limit is a hint about how much the renderer wants, and a 400 turns a harmless over-request into a broken page whose only recovery is guessing a maximum the caller cannot see.",
          "parameters": [
            {
              "name": "limit",
              "in": "query",
              "required": false,
              "schema": {
                "type": "integer",
                "minimum": 1,
                "maximum": 100,
                "default": 20
              }
            },
            {
              "name": "event",
              "in": "query",
              "required": false,
              "description": "Only runs started by this event \u2014 `push`, `change` or `changeset`. Applied in the query rather than after `limit`, for the reason `commit_sha` is: a repository's composed `changeset` runs are far rarer than its pushes, so a client that filtered a page of runs after the fact would show none of them on a busy repository. The Checks tab asks for `changeset` to list the composed runs, which report to the changeset and never to a commit here and so appear nowhere among the check rows. An event word this server does not use answers an empty list rather than a 400.",
              "schema": { "type": "string" }
            },
            {
              "name": "commit_sha",
              "in": "query",
              "required": false,
              "description": "Only runs at this commit. Applied in the query rather than after `limit`, so a busy repository cannot push a commit's runs out of the window \u2014 which is how a page loses a verdict and still looks correct.",
              "schema": { "type": "string" }
            },
            {
              "name": "change_key",
              "in": "query",
              "required": false,
              "description": "Only runs for this change. Combine with `commit_sha` for \"this change at this tip\", which is what the approval panel asks. An empty value is treated as absent; an unknown one answers an empty list.",
              "schema": { "type": "string" }
            }
          ],
          "responses": {
            "200": {
              "description": "{ runs: [Run + jobs] }. A repository that has never run anything answers an empty list."
            },
            "404": {
              "description": "no such repository, or none you may read"
            }
          }
        }
      },
      "/v1/orgs/{org}/repos/{repo}/workflow-runs/{id}": {
        "get": {
          "summary": "One workflow run with its jobs",
          "description": "Scoped to the repository in the path. A run belonging to another repository answers 404 rather than 403 \u2014 an id that resolves differently for a stranger is an existence oracle. Run JSON is `{ id, file, name, commit_sha, ref_name, event, change_key, state, error, blocked_reason, created_at, updated_at, completed_at, changeset, composition, jobs: [...] }`; `event` is `push`, `change` or `changeset`, and on a `changeset` run `changeset` is `{ key }` and `composition` is the hex composition it was started for — both null otherwise; `state` is one of running, passed, failed, cancelled, blocked \u2014 a run that exists, is not running and has no verdict: a fork's change waiting for a maintainer to let it run, an organization out of hosted-runner minutes, or one whose hosted workflows are suspended. The sentence is in `error` and the machine-readable reason is in `blocked_reason` — `fork`, `budget` or `suspended`, and null unless the run is blocked. Branch on `blocked_reason`, never on the words in `error`: only a `fork` run can be started by `POST …/changes/{change}/workflows/approve`, and the sentences are written for a person and get rewritten. The run's mirrored check row is `queued` rather than failing, because nothing is wrong with the commit. Job JSON is `{ id, job_id, key, matrix, state, attempts, error, detail_url, log_chunks, started_at, completed_at }`; `key` is the expanded cell name and `matrix` is an object, already parsed. Every job also mirrors itself into `check_runs` as `<workflow> / <cell>`, so a run's verdicts appear on the Checks tab and in the land gate without asking here at all — except a `changeset` run, whose jobs mirror onto the changeset instead (`checks` on `GET …/changesets/{changeset}`) and never onto the commit.",
          "responses": {
            "200": {
              "description": "the run and its jobs"
            },
            "404": {
              "description": "no such run in this repository"
            }
          }
        }
      },
      "/v1/orgs/{org}/repos/{repo}/workflow-runs/{id}/cancel": {
        "post": {
          "summary": "Stop a running workflow run",
          "description": "Marks every queued and running job `cancelled` with the reason `cancelled by <name>`, settles the run, updates each job's mirrored check row, and stops the tasks that were actually executing. Requires `repo:write`, because cancelling somebody's build destroys work \u2014 a `repo:read` credential, which is what a job token is, may not do it. Cancelling a run that has already settled is a 409 rather than a silent success: the page the caller is looking at is out of date, and they should reload rather than wonder whether the button worked.",
          "responses": {
            "200": {
              "description": "the cancelled run with its jobs"
            },
            "404": {
              "description": "no such run in this repository"
            },
            "409": {
              "description": "the run had already passed, failed or been cancelled"
            }
          }
        }
      },
      "/v1/orgs/{org}/repos/{repo}/workflow-jobs/{id}/log": {
        "get": {
          "summary": "A job's build log as text",
          "description": "`text/plain`. The complete log once the job has uploaded it, and the chunks produced so far while it is still running \u2014 the two are the same bytes read at different times, so no caller has to know which half of a job's life it is looking at. A chunk that never arrived is skipped rather than failing the read. A job in another repository is 404.",
          "responses": {
            "200": {
              "description": "the log so far, as text/plain"
            },
            "404": {
              "description": "no such job in this repository"
            }
          }
        }
      },
      "/v1/orgs/{org}/repos/{repo}/workflow-jobs/{id}/log/stream": {
        "get": {
          "summary": "A job's build log as it arrives (server-sent events)",
          "description": "`text/event-stream`. Emits `event: chunk` carrying `{ \"text\": ... }` for each new log chunk, in order, `event: queued` once for a job that has not started, and `event: done` carrying `{ \"state\": ... }` when the job leaves running/queued, after which the stream closes. The chunk is JSON rather than the raw bytes because an SSE `data:` field cannot carry a newline — the wire format splits a multi-line payload across several `data:` lines and loses the payload's trailing one, which would run the last line of each chunk into the first line of the next. Held open for at most six hours. The feed reads the same stored chunk objects the plain log route does rather than an in-memory fan-out from the runner: in a fleet, the node a runner posts to is not the node a reader is connected to, and an in-process channel would show the log to whoever happened to land on the right one and a spinner to everybody else.",
          "responses": {
            "200": {
              "description": "an event stream of chunk, queued and done events"
            },
            "404": {
              "description": "no such job in this repository"
            }
          }
        }
      },
      "/v1/runner/jobs/{id}": {
        "get": {
          "summary": "Everything a runner needs to check out and execute one job",
          "description": "For hosted runners; not a route a person calls. The credential is a **job token**: minted for one attempt of one job, `repo:read` on one repository, with an expiry, and recorded on the job row. A token that is valid for the repository but was not minted for this job is 403 \u2014 otherwise one build could report another's verdict. Refusals are ordered 401 (no or unknown token), 404 (no such job), 403 (wrong job), 410, so a job's state is only ever disclosed to the credential that owns it. One deliberate exception, and it is what makes cancellation work: a token that no longer authenticates \u2014 cancelling a job revokes its token before asking the platform to stop the container \u2014 is still answered **410 with the state** when it is the token this job was dispatched with and the job has stopped running. Anything else is 401. Without it the container being cancelled is refused at the door with a 401, which a runner retries, and it runs every remaining step before finding out. **410** `{ error, state }` means the job is no longer running \u2014 cancelled, superseded, already reported, or its repository deleted \u2014 and the runner's contract is to kill what it is doing and exit 0. That is the whole cancellation mechanism: there is no channel into the container, so \"stop\" is something a runner learns by being told on its next call. The answer is the job's frozen JobSpec (`image`, `timeout_minutes`, `env`, `steps`) plus `id`, `run_id`, `attempt`, `key`, `job`, `clone_url`, `fetch_ref`, `commit_sha`, `ref_name`, `event`, `change_key` and `matrix`. `clone_url` is built from the runner URL the runner already reached this API on rather than the public host \u2014 they are the same behind a CDN and differ on a private network \u2014 and never carries a credential \u2014 the token goes in a header the runner sets itself, so it cannot end up in a remote's config, a reflog, or a `ps` listing. `fetch_ref` is `refs/heads/<branch>` for a push and `refs/patchsets/<sha>` for a change, because a change's commits are not on a branch. When `event` is `changeset` the answer also carries `changeset`: `{ key, members: [{ repo, change, clone_url, fetch_ref, commit_sha, token }] }` in member order, every member of the changeset at the head it proposes, which the runner checks out side by side under a workspace directory and runs the steps in this job's own repository. The job's own member has `token: null` — it is checked out with the job token — and every other member carries a **plaintext `repo:read` token minted for that one repository**, expiring with the job token and revoked with it when the job finishes or is stopped; a re-fetch of the spec mints a fresh set and revokes the previous one. It is deliberately not one org-wide token: a member's CI script is code its author wrote, and an org-wide read would let it read repositories the author cannot see. The top-level `fetch_ref`, `commit_sha` and `change_key` stay this job's own member's.",
          "responses": {
            "200": {
              "description": "the job spec plus its identity and checkout fields"
            },
            "401": {
              "description": "no, unknown, revoked or expired job token"
            },
            "403": {
              "description": "that token was not minted for this job"
            },
            "404": {
              "description": "no such job"
            },
            "410": {
              "description": "{ error, state } \u2014 the job is no longer running; stop and exit"
            }
          }
        }
      },
      "/v1/runner/jobs/{id}/log": {
        "post": {
          "summary": "Upload one chunk of a running job's log",
          "description": "For hosted runners. Body `{ seq: integer >= 1, text: string }`, at most 262144 bytes of text. The object is written before the counter moves, so a chunk the database knows about is always one the store has \u2014 the other order leaves a hole a reader can see. A `seq` at or below what has already been recorded is a retry: the object is overwritten and the answer is still 200, because the counter only moves forward and a re-sent chunk must not hide chunks a reader has already been shown. Answering also renews the job's lease: a job producing output is a job that is alive, so a build that logs steadily never needs a separate heartbeat. The credential is a **job token**: minted for one attempt of one job, `repo:read` on one repository, with an expiry, and recorded on the job row. A token that is valid for the repository but was not minted for this job is 403 \u2014 otherwise one build could report another's verdict. Refusals are ordered 401 (no or unknown token), 404 (no such job), 403 (wrong job), 410, so a job's state is only ever disclosed to the credential that owns it. One deliberate exception, and it is what makes cancellation work: a token that no longer authenticates \u2014 cancelling a job revokes its token before asking the platform to stop the container \u2014 is still answered **410 with the state** when it is the token this job was dispatched with and the job has stopped running. Anything else is 401. Without it the container being cancelled is refused at the door with a 401, which a runner retries, and it runs every remaining step before finding out. **410** `{ error, state }` means the job is no longer running \u2014 cancelled, superseded, already reported, or its repository deleted \u2014 and the runner's contract is to kill what it is doing and exit 0. That is the whole cancellation mechanism: there is no channel into the container, so \"stop\" is something a runner learns by being told on its next call.",
          "responses": {
            "200": {
              "description": "{ lease_until: epoch millis }"
            },
            "400": {
              "description": "malformed body, or a seq below 1"
            },
            "401": {
              "description": "no, unknown, revoked or expired job token"
            },
            "403": {
              "description": "that token was not minted for this job"
            },
            "404": {
              "description": "no such job"
            },
            "410": {
              "description": "{ error, state } \u2014 the job is no longer running; stop and exit"
            },
            "413": {
              "description": "the chunk is over 262144 bytes"
            }
          }
        },
        "put": {
          "summary": "Upload a job's complete log",
          "description": "For hosted runners. Raw `text/plain`, at most 16777216 bytes. This is the authoritative copy: a chunk POST that failed twice is dropped by the runner rather than retried forever, so the chunk sequence may have holes and this object does not, and reads prefer it the moment it exists. Refused with 413 rather than truncated \u2014 a silently truncated log is worse than a refused one, because it looks complete. The credential is a **job token**: minted for one attempt of one job, `repo:read` on one repository, with an expiry, and recorded on the job row. A token that is valid for the repository but was not minted for this job is 403 \u2014 otherwise one build could report another's verdict. Refusals are ordered 401 (no or unknown token), 404 (no such job), 403 (wrong job), 410, so a job's state is only ever disclosed to the credential that owns it. One deliberate exception, and it is what makes cancellation work: a token that no longer authenticates \u2014 cancelling a job revokes its token before asking the platform to stop the container \u2014 is still answered **410 with the state** when it is the token this job was dispatched with and the job has stopped running. Anything else is 401. Without it the container being cancelled is refused at the door with a 401, which a runner retries, and it runs every remaining step before finding out. **410** `{ error, state }` means the job is no longer running \u2014 cancelled, superseded, already reported, or its repository deleted \u2014 and the runner's contract is to kill what it is doing and exit 0. That is the whole cancellation mechanism: there is no channel into the container, so \"stop\" is something a runner learns by being told on its next call.",
          "responses": {
            "200": {
              "description": "{}"
            },
            "401": {
              "description": "no, unknown, revoked or expired job token"
            },
            "403": {
              "description": "that token was not minted for this job"
            },
            "404": {
              "description": "no such job"
            },
            "410": {
              "description": "{ error, state } \u2014 the job is no longer running; stop and exit"
            },
            "413": {
              "description": "the log is over 16777216 bytes"
            }
          }
        }
      },
      "/v1/runner/jobs/{id}/lease": {
        "post": {
          "summary": "Heartbeat for a job that is working and not talking",
          "description": "For hosted runners. A long compile produces nothing to log for minutes at a time, and without this its lease would expire and the dispatcher would hand the work to a second runner while the first was still doing it. The credential is a **job token**: minted for one attempt of one job, `repo:read` on one repository, with an expiry, and recorded on the job row. A token that is valid for the repository but was not minted for this job is 403 \u2014 otherwise one build could report another's verdict. Refusals are ordered 401 (no or unknown token), 404 (no such job), 403 (wrong job), 410, so a job's state is only ever disclosed to the credential that owns it. One deliberate exception, and it is what makes cancellation work: a token that no longer authenticates \u2014 cancelling a job revokes its token before asking the platform to stop the container \u2014 is still answered **410 with the state** when it is the token this job was dispatched with and the job has stopped running. Anything else is 401. Without it the container being cancelled is refused at the door with a 401, which a runner retries, and it runs every remaining step before finding out. **410** `{ error, state }` means the job is no longer running \u2014 cancelled, superseded, already reported, or its repository deleted \u2014 and the runner's contract is to kill what it is doing and exit 0. That is the whole cancellation mechanism: there is no channel into the container, so \"stop\" is something a runner learns by being told on its next call.",
          "responses": {
            "200": {
              "description": "{ lease_until: epoch millis }"
            },
            "401": {
              "description": "no, unknown, revoked or expired job token"
            },
            "403": {
              "description": "that token was not minted for this job"
            },
            "404": {
              "description": "no such job"
            },
            "410": {
              "description": "{ error, state } \u2014 the job is no longer running; stop and exit"
            }
          }
        }
      },
      "/v1/runner/jobs/{id}/finish": {
        "post": {
          "summary": "Report a job's verdict",
          "description": "For hosted runners. Body `{ state: \"passed\" | \"failed\", error: string | null, abuse?: \"mining\" }`. `abuse` is how a runner reports that it stopped the job because of what it was doing rather than because the steps failed, and it **suspends the organization**: every later trigger for it is `blocked` with the reason, and everything it has running is cancelled. It is a report, not an instruction \u2014 it is accepted from a credential that untrusted `run:` lines were executing next to, so the only thing it can cause is that organization\u0027s own work being stopped, and there is no value of it that turns anything back on. Clearing a suspension is an operator action by SQL: there is no operator role on this server to hang a route off. `cancelled` is deliberately not accepted: a runner reports what its steps did, and letting a job announce itself cancelled would let a build escape a red verdict by claiming it was stopped. Recording the verdict also cascades skips to everything downstream, settles the run if that was the last job, updates every job's mirrored check row, deletes this attempt's log chunks, and revokes the job's own token: the verdict is the last call that credential is entitled to make, and it was minted to outlive the whole timeout, so a later call with it answers 401 rather than 410. The credential is a **job token**: minted for one attempt of one job, `repo:read` on one repository, with an expiry, and recorded on the job row. A token that is valid for the repository but was not minted for this job is 403 \u2014 otherwise one build could report another's verdict. Refusals are ordered 401 (no or unknown token), 404 (no such job), 403 (wrong job), 410, so a job's state is only ever disclosed to the credential that owns it. One deliberate exception, and it is what makes cancellation work: a token that no longer authenticates \u2014 cancelling a job revokes its token before asking the platform to stop the container \u2014 is still answered **410 with the state** when it is the token this job was dispatched with and the job has stopped running. Anything else is 401. Without it the container being cancelled is refused at the door with a 401, which a runner retries, and it runs every remaining step before finding out. **410** `{ error, state }` means the job is no longer running \u2014 cancelled, superseded, already reported, or its repository deleted \u2014 and the runner's contract is to kill what it is doing and exit 0. That is the whole cancellation mechanism: there is no channel into the container, so \"stop\" is something a runner learns by being told on its next call.",
          "responses": {
            "200": {
              "description": "{}"
            },
            "400": {
              "description": "malformed body, or a state that is not passed or failed"
            },
            "401": {
              "description": "no, unknown, revoked or expired job token"
            },
            "403": {
              "description": "that token was not minted for this job"
            },
            "404": {
              "description": "no such job"
            },
            "410": {
              "description": "{ error, state } \u2014 the job is no longer running; stop and exit"
            }
          }
        }
      }
  }
}
