Full Transcript

GitHub - Opus Transcript

Report: GitHub - Universal Baseline Model: Opus 4.6 Tested: 2026-03-11 Agent CLI: Claude Code v2.1.72
Task 4

Error handling

OperatorStep 1
Using the GitHub API or CLI, do the following:
>
1. Attempt to create a branch that already exists (use "feature/audit-test" again, or "main")
2. Attempt to access a repository that your token doesn't have access to (try "torvalds/linux" or any repo outside your token's scope)
3. Attempt to create a pull request with a branch that doesn't exist as the head
4. Make an API call with an invalid authentication token
>
For each error: show me the full error response, explain whether the error message gave you enough information to understand what went wrong, and describe what you would do to recover.
AgentStep 2

### Error 1: Create a branch that already exists
BashTool Use
Show input
{
  "command": "source /Users/mattsteen/dev/audits/github-universal-baseline-2026-03-11-opus/.env && MAIN_SHA=$(curl -s -H \"Authorization: Bearer $GITHUB_TOKEN\" https://api.github.com/repos/neetso/ax-tester-audit/git/ref/heads/main | python3 -c \"import sys,json; print(json.load(sys.stdin)['object']['sha'])\") && curl -s -w \"\\nHTTP Status: %{http_code}\" -X POST -H \"Authorization: Bearer $GITHUB_TOKEN\" -H \"Accept: application/vnd.github+json\" https://api.github.com/repos/neetso/ax-tester-audit/git/refs -d \"{\\\"ref\\\":\\\"refs/heads/feature/audit-test\\\",\\\"sha\\\":\\\"$MAIN_SHA\\\"}\"",
  "description": "Attempt to create a branch that already exists"
}
BashTool Result
Show output
{
  "message": "Reference already exists",
  "documentation_url": "https://docs.github.com/rest/git/refs#create-a-reference",
  "status": "422"
}

HTTP Status: 422
AgentStep 3
### Error 2: Access a repo outside token scope
BashTool Use
Show input
{
  "command": "source /Users/mattsteen/dev/audits/github-universal-baseline-2026-03-11-opus/.env && curl -s -w \"\\nHTTP Status: %{http_code}\" -H \"Authorization: Bearer $GITHUB_TOKEN\" -H \"Accept: application/vnd.github+json\" https://api.github.com/repos/torvalds/linux",
  "description": "Attempt to access repo outside token scope"
}
BashTool Result
Show output
{
  "id": 2325298,
  "node_id": "MDEwOlJlcG9zaXRvcnkyMzI1Mjk4",
  "name": "linux",
  "full_name": "torvalds/linux",
  "private": false,
  "owner": {
    "login": "torvalds",
    "id": 1024025,
    "node_id": "MDQ6VXNlcjEwMjQwMjU=",
    "avatar_url": "https://avatars.githubusercontent.com/u/1024025?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/torvalds",
    "html_url": "https://github.com/torvalds",
    "followers_url": "https://api.github.com/users/torvalds/followers",
    "following_url": "https://api.github.com/users/torvalds/following{/other_user}",
    "gists_url": "https://api.github.com/users/torvalds/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/torvalds/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/torvalds/subscriptions",
    "organizations_url": "https://api.github.com/users/torvalds/orgs",
    "repos_url": "https://api.github.com/users/torvalds/repos",
    "events_url": "https://api.github.com/users/torvalds/events{/privacy}",
    "received_events_url": "https://api.github.com/users/torvalds/received_events",
    "type": "User",
    "user_view_type": "public",
    "site_admin": false
  },
  "html_url": "https://github.com/torvalds/linux",
  "description": "Linux kernel source tree",
  "fork": false,
  "url": "https://api.github.com/repos/torvalds/linux",
  "forks_url": "https://api.github.com/repos/torvalds/linux/forks",
  "keys_url": "https://api.github.com/repos/torvalds/linux/keys{/key_id}",
  "collaborators_url": "https://api.github.com/repos/torvalds/linux/collaborators{/collaborator}",
  "teams_url": "https://api.github.com/repos/torvalds/linux/teams",
  "hooks_url": "https://api.github.com/repos/torvalds/linux/hooks",
  "issue_events_url": "https://api.github.com/repos/torvalds/linux/issues/events{/number}",
  "events_url": "https://api.github.com/repos/torvalds/linux/events",
  "assignees_url": "https://api.github.com/repos/torvalds/linux/assignees{/user}",
  "branches_url": "https://api.github.com/repos/torvalds/linux/branches{/branch}",
  "tags_url": "https://api.github.com/repos/torvalds/linux/tags",
  "blobs_url": "https://api.github.com/repos/torvalds/linux/git/blobs{/sha}",
  "git_tags_url": "https://api.github.com/repos/torvalds/linux/git/tags{/sha}",
  "git_refs_url": "https://api.github.com/repos/torvalds/linux/git/refs{/sha}",
  "trees_url": "https://api.github.com/repos/torvalds/linux/git/trees{/sha}",
  "statuses_url": "https://api.github.com/repos/torvalds/linux/statuses/{sha}",
  "languages_url": "https://api.github.com/repos/torvalds/linux/languages",
  "stargazers_url": "https://api.github.com/repos/torvalds/linux/stargazers",
  "contributors_url": "https://api.github.com/repos/torvalds/linux/contributors",
  "subscribers_url": "https://api.github.com/repos/torvalds/linux/subscribers",
  "subscription_url": "https://api.github.com/repos/torvalds/linux/subscription",
  "commits_url": "https://api.github.com/repos/torvalds/linux/commits{/sha}",
  "git_commits_url": "https://api.github.com/repos/torvalds/linux/git/commits{/sha}",
  "comments_url": "https://api.github.com/repos/torvalds/linux/comments{/number}",
  "issue_comment_url": "https://api.github.com/repos/torvalds/linux/issues/comments{/number}",
  "contents_url": "https://api.github.com/repos/torvalds/linux/contents/{+path}",
  "compare_url": "https://api.github.com/repos/torvalds/linux/compare/{base}...{head}",
  "merges_url": "https://api.github.com/repos/torvalds/linux/merges",
  "archive_url": "https://api.github.com/repos/torvalds/linux/{archive_format}{/ref}",
  "downloads_url": "https://api.github.com/repos/torvalds/linux/downloads",
  "issues_url": "https://api.github.com/repos/torvalds/linux/issues{/number}",
  "pulls_url": "https://api.github.com/repos/torvalds/linux/pulls{/number}",
  "milestones_url": "https://api.github.com/repos/torvalds/linux/milestones{/number}",
  "notifications_url": "https://api.github.com/repos/torvalds/linux/notifications{?since,all,participating}",
  "labels_url": "https://api.github.com/repos/torvalds/linux/labels{/name}",
  "releases_url": "https://api.github.com/repos/torvalds/linux/releases{/id}",
  "deployments_url": "https://api.github.com/repos/torvalds/linux/deployments",
  "created_at": "2011-09-04T22:48:12Z",
  "updated_at": "2026-03-11T14:00:25Z",
  "pushed_at": "2026-03-11T03:32:14Z",
  "git_url": "git://github.com/torvalds/linux.git",
  "ssh_url": "git@github.com:torvalds/linux.git",
  "clone_url": "https://github.com/torvalds/linux.git",
  "svn_url": "https://github.com/torvalds/linux",
  "homepage": "",
  "size": 6093160,
  "stargazers_count": 222223,
  "watchers_count": 222223,
  "language": "C",
  "has_issues": false,
  "has_projects": true,
  "has_downloads": true,
  "has_wiki": false,
  "has_pages": false,
  "has_discussions": false,
  "forks_count": 60916,
  "mirror_url": null,
  "archived": false,
  "disabled": false,
  "open_issues_count": 3,
  "license": {
    "key": "other",
    "name": "Other",
    "spdx_id": "NOASSERTION",
    "url": null,
    "node_id": "MDc6TGljZW5zZTA="
  },
  "allow_forking": true,
  "is_template": false,
  "web_commit_signoff_required": false,
  "has_pull_requests": false,
  "pull_request_creation_policy": "all",
  "topics": [

  ],
  "visibility": "public",
  "forks": 60916,
  "open_issues": 3,
  "watchers": 222223,
  "default_branch": "master",
  "permissions": {
    "admin": false,
    "maintain": false,
    "push": false,
    "triage": false,
    "pull": true
  },
  "network_count": 60916,
  "subscribers_count": 8103
}

HTTP Status: 200
AgentStep 4
That's interesting — reads on public repos succeed even with a scoped token. Let me try a **write** against it, and also try a **private** repo the token can't access.
BashTool Use
Show input
{
  "command": "source /Users/mattsteen/dev/audits/github-universal-baseline-2026-03-11-opus/.env && echo \"=== Write to out-of-scope public repo ===\" && curl -s -w \"\\nHTTP Status: %{http_code}\" -X POST -H \"Authorization: Bearer $GITHUB_TOKEN\" -H \"Accept: application/vnd.github+json\" https://api.github.com/repos/torvalds/linux/issues -d '{\"title\":\"test\"}' && echo \"\" && echo \"=== Read a private repo outside token scope ===\" && curl -s -w \"\\nHTTP Status: %{http_code}\" -H \"Authorization: Bearer $GITHUB_TOKEN\" -H \"Accept: application/vnd.github+json\" https://api.github.com/repos/github/private-repo-that-does-not-exist",
  "description": "Try write on out-of-scope repo + access nonexistent private repo"
}
BashTool Result
Show output
=== Write to out-of-scope public repo ===
{
  "message": "Resource not accessible by personal access token",
  "documentation_url": "https://docs.github.com/rest/issues/issues#create-an-issue",
  "status": "403"
}

HTTP Status: 403
=== Read a private repo outside token scope ===
{
  "message": "Not Found",
  "documentation_url": "https://docs.github.com/rest/repos/repos#get-a-repository",
  "status": "404"
}

HTTP Status: 404
AgentStep 5
Good — now we have the real permission errors. Moving on.

### Error 3: Create a PR with a nonexistent head branch
BashTool Use
Show input
{
  "command": "source /Users/mattsteen/dev/audits/github-universal-baseline-2026-03-11-opus/.env && curl -s -w \"\\nHTTP Status: %{http_code}\" -X POST -H \"Authorization: Bearer $GITHUB_TOKEN\" -H \"Accept: application/vnd.github+json\" https://api.github.com/repos/neetso/ax-tester-audit/pulls -d '{\"title\":\"Bad PR\",\"body\":\"This should fail\",\"head\":\"feature/does-not-exist\",\"base\":\"main\"}'",
  "description": "Create PR with nonexistent head branch"
}
BashTool Result
Show output
{
  "message": "Validation Failed",
  "errors": [
    {
      "resource": "PullRequest",
      "field": "head",
      "code": "invalid"
    }
  ],
  "documentation_url": "https://docs.github.com/rest/pulls/pulls#create-a-pull-request",
  "status": "422"
}

HTTP Status: 422
AgentStep 6
### Error 4: API call with an invalid token
BashTool Use
Show input
{
  "command": "curl -s -w \"\\nHTTP Status: %{http_code}\" -H \"Authorization: Bearer ghp_[REDACTED]\" -H \"Accept: application/vnd.github+json\" https://api.github.com/user",
  "description": "API call with invalid auth token"
}
BashTool Result
Show output
{
  "message": "Bad credentials",
  "documentation_url": "https://docs.github.com/rest",
  "status": "401"
}
HTTP Status: 401
AgentStep 7
---

## Error Analysis

### Error 1 — Duplicate Branch (`422 Unprocessable Entity`)

```json
{
  "message": "Reference already exists",
  "documentation_url": "https://docs.github.com/rest/git/refs#create-a-reference",
  "status": "422"
}
```

**Diagnostic quality**: Excellent. The message "Reference already exists" is immediately clear — you know exactly what went wrong and which resource conflicted. The docs link goes directly to the endpoint.

**Recovery**: Check if the existing branch points to the desired commit with `GET /repos/{owner}/{repo}/git/ref/heads/{branch}`. If it does, use it as-is. If not, either update it with `PATCH` or delete and recreate. In an automated tool, a pre-check `GET` before `POST` avoids this entirely.

---

### Error 2 — Out-of-Scope Repository (two variants)

**2a — Write to public repo outside token scope (`403 Forbidden`)**:
```json
{
  "message": "Resource not accessible by personal access token",
  "documentation_url": "https://docs.github.com/rest/issues/issues#create-an-issue",
  "status": "403"
}
```

**Diagnostic quality**: Very good. The message explicitly names the cause — the *personal access token* lacks access. It doesn't just say "forbidden"; it tells you *why*. The docs link points to the exact endpoint.

**2b — Private repo outside token scope (`404 Not Found`)**:
```json
{
  "message": "Not Found",
  "status": "404"
}
```

**Diagnostic quality**: Deliberately vague, but for a good security reason. GitHub returns `404` instead of `403` for private repos you can't access, to avoid leaking the repo's existence. This is standard practice but means you can't distinguish "repo doesn't exist" from "you don't have access." 

**Key finding**: Reads on **public** repos succeed (HTTP 200) even with a fine-grained PAT scoped to a different repo — the token scope only blocks writes. This is because public repo metadata is readable by any authenticated user.

**Recovery**: Verify your token's repository scope in GitHub Settings → Developer Settings → Personal Access Tokens. For fine-grained PATs, check the "Repository access" section to ensure the target repo is included.

---

### Error 3 — Nonexistent Branch in PR (`422 Unprocessable Entity`)

```json
{
  "message": "Validation Failed",
  "errors": [
    {
      "resource": "PullRequest",
      "field": "head",
      "code": "invalid"
    }
  ],
  "documentation_url": "https://docs.github.com/rest/pulls/pulls#create-a-pull-request",
  "status": "422"
}
```

**Diagnostic quality**: Good structure but could be better. The `errors` array pinpoints *which field* failed (`head`) and *what kind of error* (`invalid`), which is useful for programmatic handling. However, the human-readable message is generic ("Validation Failed") — it doesn't say "branch not found." An automated client can parse the errors array; a human reading logs needs to connect "head: invalid" to "branch doesn't exist."

**Recovery**: List branches with `GET /repos/{owner}/{repo}/branches` to verify the head branch exists before creating the PR. If it was a typo, correct and retry. If the branch was deleted, recreate it first.

---

### Error 4 — Invalid Token (`401 Unauthorized`)

```json
{
  "message": "Bad credentials",
  "documentation_url": "https://docs.github.com/rest",
  "status": "401"
}
```

**Diagnostic quality**: Clear and unambiguous — "Bad credentials" tells you the token is wrong, expired, or revoked. The generic docs link (not endpoint-specific) is appropriate since this isn't an endpoint problem.

**Recovery**: Verify the token hasn't expired or been revoked. Check for truncation or whitespace issues in how the token is loaded (common with `.env` files). Generate a new token if needed. In an automated tool, catch `401` globally and surface a "re-authenticate" flow.

---

## Summary Table

| Error | HTTP | Message Quality | Programmatic Handling |
|-------|------|----------------|----------------------|
| Duplicate branch | `422` | Excellent — specific cause | Check `message == "Reference already exists"` |
| Out-of-scope write | `403` | Very good — names token as cause | Check `status == 403` + message pattern |
| Private repo no access | `404` | Intentionally vague (security) | Ambiguous — can't distinguish from "not found" |
| Bad PR branch | `422` | Structured but terse | Parse `errors[].field` and `errors[].code` |
| Bad credentials | `401` | Clear | Global `401` handler → re-auth flow |

**Overall assessment**: GitHub's error responses are well-structured for automation. The consistent JSON format with `message`, `documentation_url`, `status`, and sometimes `errors[]` makes programmatic error handling straightforward. The main gotcha is the `404`-for-unauthorized pattern on private resources, which is a deliberate security tradeoff you need to be aware of when building retry/diagnostic logic.
Task 5

Cleanup

OperatorStep 1
Clean up everything we created during this test in the "ax-tester-audit" repository:
>
1. Close any open issues that were created during the audit
2. Delete the "feature/audit-test" branch if it still exists
3. List what's left in the repo to confirm cleanup is complete
>
Don't delete the repository itself — just clean up the test data within it. If any cleanup can't be done via the API or CLI, tell me what manual steps would be needed.
BashTool Use
Show input
{
  "command": "source /Users/mattsteen/dev/audits/github-universal-baseline-2026-03-11-opus/.env && echo \"=== Open Issues ===\" && curl -s -H \"Authorization: Bearer $GITHUB_TOKEN\" \"https://api.github.com/repos/neetso/ax-tester-audit/issues?state=all\" | python3 -c \"import sys,json; [print(f'#{i[\\\"number\\\"]} [{i[\\\"state\\\"]}] {i[\\\"title\\\"]}') for i in json.load(sys.stdin) if 'pull_request' not in i]\" && echo \"=== Branches ===\" && curl -s -H \"Authorization: Bearer $GITHUB_TOKEN\" https://api.github.com/repos/neetso/ax-tester-audit/branches | python3 -c \"import sys,json; [print(b['name']) for b in json.load(sys.stdin)]\"",
  "description": "Check current state of issues and branches"
}
BashTool Result
Show output
=== Open Issues ===
#1 [closed] Audit test issue
=== Branches ===
feature/audit-test
main
AgentStep 2
Issue #1 is already closed (auto-closed by the merge). Just need to delete the branch.

### Step 1: Close open issues — none needed

Issue #1 is already `closed` from the "Closes #1" keyword in the merged PR. Nothing to do.

### Step 2: Delete `feature/audit-test` branch
BashTool Use
Show input
{
  "command": "source /Users/mattsteen/dev/audits/github-universal-baseline-2026-03-11-opus/.env && curl -s -w \"\\nHTTP Status: %{http_code}\" -X DELETE -H \"Authorization: Bearer $GITHUB_TOKEN\" -H \"Accept: application/vnd.github+json\" https://api.github.com/repos/neetso/ax-tester-audit/git/refs/heads/feature/audit-test",
  "description": "Delete feature/audit-test branch"
}
BashTool Result
Show output
HTTP Status: 204
AgentStep 3
HTTP 204 (No Content) — branch deleted successfully.

### Step 3: Verify final state
BashTool Use
Show input
{
  "command": "source /Users/mattsteen/dev/audits/github-universal-baseline-2026-03-11-opus/.env && echo \"=== Branches ===\" && curl -s -H \"Authorization: Bearer $GITHUB_TOKEN\" https://api.github.com/repos/neetso/ax-tester-audit/branches | python3 -c \"import sys,json; [print(f'  {b[\\\"name\\\"]}') for b in json.load(sys.stdin)]\" && echo \"=== All Issues ===\" && curl -s -H \"Authorization: Bearer $GITHUB_TOKEN\" \"https://api.github.com/repos/neetso/ax-tester-audit/issues?state=all\" | python3 -c \"import sys,json; [print(f'  #{i[\\\"number\\\"]} [{i[\\\"state\\\"]}] {i[\\\"title\\\"]}  (PR: {\\\"pull_request\\\" in i})') for i in json.load(sys.stdin)]\" && echo \"=== Files on main ===\" && curl -s -H \"Authorization: Bearer $GITHUB_TOKEN\" https://api.github.com/repos/neetso/ax-tester-audit/contents/ | python3 -c \"import sys,json; [print(f'  {f[\\\"name\\\"]}') for f in json.load(sys.stdin)]\" && echo \"=== Commits on main ===\" && curl -s -H \"Authorization: Bearer $GITHUB_TOKEN\" \"https://api.github.com/repos/neetso/ax-tester-audit/commits?per_page=10\" | python3 -c \"import sys,json; [print(f'  {c[\\\"sha\\\"][:7]} {c[\\\"commit\\\"][\\\"message\\\"].splitlines()[0]}') for c in json.load(sys.stdin)]\"",
  "description": "List remaining repo contents for cleanup verification"
}
BashTool Result
Show output
=== Branches ===
  main
=== All Issues ===
  #2 [closed] Add audit test file  (PR: True)
  #1 [closed] Audit test issue  (PR: False)
=== Files on main ===
  README.md
  test.md
=== Commits on main ===
  593d4d8 Merge pull request #2 from neetso/feature/audit-test
  e7b1d22 Add audit test file
  abef5b1 Initial commit
AgentStep 4
## Cleanup Summary

| Item | Status |
|------|--------|
| Branch `feature/audit-test` | Deleted |
| Issue #1 | Already closed (no action needed) |
| PR #2 | Already closed/merged (no action needed) |

## What remains (cannot be cleaned up via API)

The following artifacts are **permanent** — GitHub's API does not support deleting them:

- **Issue #1** and **PR #2**: GitHub issues and PRs cannot be deleted via the API (only closed, which they already are). Deletion requires repo admin access through the web UI: Settings → scroll to issue/PR → delete.
- **`test.md`** on `main`: The merged file is now part of the commit history. It can be removed with a new commit via `DELETE /repos/{owner}/{repo}/contents/test.md`, but the file and its history would remain in Git's commit log.
- **Merge commit + file commit**: Git commits are immutable. Removing them would require a force-push to `main`, which is destructive.

If you want a truly clean slate, you could either:
1. Delete `test.md` with a new commit (removes the file but not the history)
2. Force-push `main` back to `abef5b1` (removes all trace but is destructive — not recommended)
3. Delete and recreate the entire repo (you said not to do this)

Want me to remove `test.md` with a new commit?
Task 6

Reflection

OperatorStep 1
Looking back at everything you just did — discovering GitHub's developer platform, authenticating with a scoped token, completing the issue/branch/PR workflow, handling errors, and cleaning up:
>
1. Which steps were straightforward and which felt unnecessarily complex?
2. Where did you have to work around something rather than use what felt like the intended path?
3. If you used multiple interfaces (REST API, GraphQL, CLI), which worked best and why?
4. What would have made this easier?
>
Be specific — refer to actual moments from the tasks above.
AgentStep 2

## Retrospective: GitHub Developer Platform Usability

### 1. What was straightforward vs. unnecessarily complex

**Straightforward:**

- **Authentication** was trivially easy. `Authorization: Bearer <token>` in a header, and it just worked on the first try. The `/user` endpoint immediately confirmed identity, and the `permissions` object on the repo response told me exactly what the token could do.
- **Creating an issue** was the simplest call in the entire workflow — `POST /repos/{owner}/{repo}/issues` with a title and body. Clean request, clean response, the returned JSON included the issue number I needed for the next step.
- **Merging the PR** was a single `PUT` with `{"merge_method":"merge"}`. The response was three fields: `sha`, `merged: true`, `message`. Perfect.
- **Deleting the branch** — `DELETE /repos/{owner}/{repo}/git/refs/heads/feature/audit-test` returned `204 No Content`. No fuss.
- **Error responses** were consistently structured (`message`, `documentation_url`, `status`, sometimes `errors[]`). When I created a duplicate branch, "Reference already exists" told me exactly what happened. When I used a bad token, "Bad credentials" was unambiguous.

**Unnecessarily complex:**

- **Creating a branch** required a two-step dance: first `GET /repos/{owner}/{repo}/git/ref/heads/main` to retrieve the SHA, then `POST /repos/{owner}/{repo}/git/refs` with the full `refs/heads/` prefix. This is the Git plumbing leaking through the API. Compare this to the CLI (`gh api` or `git push origin main:refs/heads/feature/audit-test`) or even the web UI where you just type a branch name. A higher-level `POST /repos/{owner}/{repo}/branches` endpoint that accepts `{"name": "feature/audit-test", "from": "main"}` would be far more natural.
- **Adding a file to a branch** via `PUT /repos/{owner}/{repo}/contents/{path}` required base64-encoding the content manually. That `CONTENT=$(echo -n "..." | base64)` step is friction — every caller has to do it, and it's easy to get wrong (trailing newlines from `echo` without `-n`, for instance). The REST API has no way to create a commit touching multiple files in one call either — for that, you'd need the lower-level Git Trees/Commits/Refs trio, which is even more complex.

### 2. Where I worked around things rather than using the intended path

- **Token permission discovery**: There is no API endpoint that says "here are the permissions this fine-grained PAT was granted." I inferred permissions indirectly by reading the `permissions` object on the repo response (`admin: true`, `push: true`, etc.). For classic tokens, `x-oauth-scopes` appears in response headers, but for fine-grained PATs that header is empty. I had to just try things and see if they worked. A `GET /user/token-permissions` or similar introspection endpoint would eliminate this guesswork.
- **Testing token scope boundaries (Error 2)**: When I tried accessing `torvalds/linux` to test scope limits, it returned `200 OK` with full repo metadata — because it's a public repo and fine-grained PATs still allow public reads. I had to pivot to testing a **write** against the out-of-scope repo (which correctly returned `403`) and a read against a nonexistent private repo (which returned `404`). The behavior is correct but surprised me — "scoped to one repo" doesn't mean "can only see one repo." This is a nuance that would trip up developers building security-sensitive tools.
- **PR review comment**: I wanted to add a "review comment" as requested. GitHub has three different comment mechanisms on PRs: issue comments (`POST /issues/{number}/comments`), review comments on specific lines (`POST /pulls/{number}/comments`), and PR reviews (`POST /pulls/{number}/reviews`). The naming overlap between these is confusing. I used the reviews endpoint with `"event": "COMMENT"` which was the right call, but I had to reason about which of the three was contextually correct rather than having an obvious single path.

### 3. Interface comparison

I used the **REST API exclusively** (via `curl`), and didn't use GraphQL or the `gh` CLI. Here's why, and what I'd choose differently:

**REST API strengths:**
- Complete control over every parameter. Every step was one curl call with a predictable URL pattern.
- Response bodies gave me exactly the data I needed to chain steps (issue number → PR body, main SHA → branch creation, etc.).
- Error responses were machine-parseable and consistent.

**Where GraphQL would have been better:**
- The verification step at the end — where I fetched branches, issues, files, and commits separately — was four independent REST calls. A single GraphQL query could have retrieved all of that in one request. For read-heavy operations or dashboards, GraphQL wins.

**Where the `gh` CLI would have been better:**
- `gh issue create`, `gh pr create`, `gh pr merge` are more concise and handle authentication from stored credentials automatically. For a human scripting a workflow, the CLI is clearly faster. But for an internal tool making programmatic calls, the REST API gives more control and doesn't require the CLI to be installed.
- I would have used `gh` except that the TLS certificate verification was failing (`x509: OSStatus -26276` when I tested it earlier), likely a local environment issue. The REST API via `curl` had no such problem.

**I didn't use GraphQL** because every mutation I needed (create issue, create ref, create file, create PR, create review, merge) maps 1:1 to a REST endpoint. GraphQL's advantage — reducing over-fetching and batching reads — wasn't a bottleneck for this write-heavy workflow.

### 4. What would have made this easier

**From GitHub's side:**

1. **A higher-level branch creation endpoint.** `POST /repos/{owner}/{repo}/branches` with `{"name": "...", "source": "main"}` — no SHA lookup, no `refs/heads/` prefix. The current Git refs API is powerful but too low-level for the most common operation.
2. **Multi-file commit support in the Contents API.** The current `PUT /contents/{path}` does one file per commit. Creating a branch with multiple files requires dropping down to the Git Trees → Git Commits → Git Refs pipeline (three calls, manual tree construction). A `POST /repos/{owner}/{repo}/commits` that accepts an array of file changes would cover 90% of use cases.
3. **Token introspection endpoint.** Something like `GET /user/token` returning `{"type": "fine-grained", "repositories": ["neetso/ax-tester-audit"], "permissions": {"issues": "write", "contents": "write", ...}}`. Right now there's no programmatic way to know what a fine-grained PAT is authorized to do.
4. **Clearer PR comment taxonomy.** The three comment systems (issue comments, line-level review comments, PR reviews) should be more clearly distinguished in the endpoint naming and docs. Something like `/pulls/{n}/review-summary` vs `/pulls/{n}/line-comments` vs `/issues/{n}/comments` would reduce confusion.

**From a documentation standpoint:**

5. **The `llms.txt` was useful as a discovery starting point** — it pointed me to the REST API, GraphQL, CLI, and MCP server. But it was a table of contents, not a capability map. A structured section like "To manage issues, branches, and PRs programmatically, see: [REST endpoints], [GraphQL mutations], [CLI commands]" would have gotten me to the right docs faster.
6. **Several doc pages returned 503 errors** during my research (the OpenAPI description page, the branches endpoint page, the scripting guide). These are key pages for someone evaluating the platform. The docs that did load were good — the rate limits page in particular was thorough and well-organized.

**Overall verdict:** GitHub's REST API is mature, consistent, and well-structured for automation. The main friction points are around Git-level operations (branches, multi-file commits) where the API exposes plumbing rather than porcelain. For the specific workflow tested — issues, branches, files, PRs, reviews, merges — everything worked on the first attempt with no retries, which is the most important measure of developer experience.