# GitHub API compatibility

IssueMe pins REST requests to `X-GitHub-Api-Version: 2022-11-28` (`GITHUB_API_VERSION` in `src/constants.ts`) and sends `GraphQL-Features: sub_issues` on `/graphql`. This document separates what GitHub documents from what IssueMe has tested, so a documented endpoint is never mistaken for a verified one. Reviewed against docs.github.com on 2026-10-09.

Evidence levels used below:

- **Documented**: the public GitHub documentation describes the endpoint or mutation. This does not prove the token, repository, or organization can use it.
- **Mocked**: `test/` covers the request shape and response handling with an injected `fetch`; no network access.
- **Live**: an operator ran the opt-in plan in [`live-github-verification.md`](live-github-verification.md) and recorded the result. No row is marked live unless such a run is recorded.

## REST version decision

| Version | Documented support | IssueMe use |
| --- | --- | --- |
| `2022-11-28` | Supported until 2028-03-10; used for requests without a version header. | Pinned (`GITHUB_API_VERSION`). |
| `2026-03-10` | Current; end of support not yet scheduled. | Not adopted. |

Decision: keep `2022-11-28`.

- The documented `2026-03-10` breaking changes that touch issue endpoints remove the singular `assignee` response field and deprecate the `beta` media type and the default `pull_request` property. IssueMe reads `assignees` arrays, requests `application/vnd.github+json`, and detects pull requests with a null/undefined check, so none of these blocks IssueMe on either version. They are also not a reason to move.
- GitHub documents versions as behavior switches for breaking changes, not as gates for new endpoints. The dependency, related-issue, timeline, and issue-type endpoints below are additive paths. The `2026-03-10` header in their examples is the documentation default, not a stated minimum.
- A version change without live regression coverage would alter behavior for every existing tool at once. Unresolved compatibility is therefore scoped to the affected feature: a tool whose endpoint GitHub rejects reports that failure for that tool only.

Regression coverage: `test/github-transport-client.test.mjs` asserts the pinned header value, that it is listed in `GITHUB_DOCUMENTED_API_VERSIONS`, and that the `sub_issues` GraphQL feature flag is sent only to `/graphql`. Changing the pin requires updating that list, this document, and re-running the live plan.

## Capability matrix

| Capability | API | Documented availability | Documented permissions / gates | IssueMe status | Evidence |
| --- | --- | --- | --- | --- | --- |
| Issues list/get/create/update/close/reopen, labels, milestones, assignees, comment add/edit/delete, search | REST `/repos/{owner}/{repo}/...`, `/search/issues`, `/user` | Documented on both versions | Issues read for reads, Issues write for mutations; `/user` returns the token's identity | Implemented | Mocked; live-verified 2026-10-10 (create, update, comment add/edit/delete, labels, assignees, milestones, close/reopen, search, sync, permanent deletion) |
| Native sub-issues | GraphQL `parent`, `subIssues`, `addSubIssue`, `removeSubIssue`, `reprioritizeSubIssue` with `GraphQL-Features: sub_issues` | Documented | Issues write; the feature must be available to the repository | Implemented | Mocked; live-verified 2026-10-10 (create sub-issue, add, list, reorder, remove) |
| Projects v2 list, fields, add item, update field | GraphQL `projectsV2`, `fields`, `addProjectV2ItemById`, `updateProjectV2ItemFieldValue` | Documented | Classic token: `read:project` for reads, `project` for mutations. Fine-grained tokens cannot access user-owned Projects at all (documented GitHub limitation, no permission exists); organization Projects need the organization "Projects" permission | Implemented. `ProjectV2.owner` is `User` or `Organization` only; the earlier `Repository` inline fragment was invalid and is removed | Mocked; live-verified 2026-10-10 with a classic token (`project`, `repo`) on the user board `issueme-testing`: list (repository and user scope), fields, add, idempotent re-add, single-select update, bulk `add_to_project`. With the fine-grained token the same calls returned `github_projects_v2_forbidden` (`Resource not accessible by personal access token`) |
| Development links | GraphQL `timelineItems` filtered to connected, cross-referenced, referenced, and closed events | Documented | Issues read; PR and commit visibility depends on the token | Implemented | Mocked; live-verified 2026-10-10 (one linked pull request on an existing issue, zero links on a run issue) |
| Issue dependencies (blocked by / blocking) | REST `GET`/`POST .../issues/{n}/dependencies/blocked_by`, `DELETE .../dependencies/blocked_by/{issue_id}`, `GET .../issues/{n}/dependencies/blocking` | Documented. `issue_id` is the blocking issue's integer database `id`, not its number or node ID. The `GET` collections paginate with `per_page` (max 100) and `page`. `POST` documents 403/404/410/422; `DELETE` documents 400/401/403/404/410. Cycle and self-reference rules are not documented beyond the generic 422. | Not stated; 403 on mutation implies write access. Rapid mutation may trigger secondary rate limits. | Implemented: `issueme_list_issue_dependencies`, `issueme_add_issue_dependency`, `issueme_remove_issue_dependency` (both endpoints open and in scope, blocking issue by database id, preflighted idempotency, structured 422/404 refusals) | Mocked (`test/issue-dependencies-tool.test.mjs`); live-verified 2026-10-10 under `2022-11-28` on a user-owned repository: add, no-op repeat, self-dependency refusal, both directions listed, remove, timeline `blocked_by_added` with the blocker number |
| Related issues | REST `GET`/`POST .../issues/{n}/relates_to`, `DELETE .../relates_to/{issue_id}` | Documented with the same `issue_id` semantics; direction/symmetry is not stated | Not stated; 403 documented on mutation | Implemented: `issueme_list_related_issues`, `issueme_add_related_issue`, `issueme_remove_related_issue` (reports only the requested issue's collection; GitHub mirrors the link on both issues) | Mocked (`test/related-issues-tool.test.mjs`); live-verified 2026-10-10 on a user-owned repository: add, no-op repeat, both-side listing, remove |
| Issue timeline | REST `GET .../issues/{n}/timeline` with `per_page`, `page`, `exclude` | Documented. Event schemas include labeled/unlabeled, milestoned/demilestoned, renamed, assigned/unassigned, state change with `state_reason`, issue type changes, sub-issue and parent events, and blocked-by/blocking events. | Not stated; 400/404/410 documented | Implemented: `issueme_list_issue_timeline`; `relates_to_added`/`relates_to_removed` are known events without a related-issue payload | Mocked (`test/issue-timeline-tool.test.mjs`); live-verified 2026-10-10 (labeled, assigned, milestoned, closed with reason, reopened, parent/sub-issue, blocked_by, relates_to, cross-referenced) |
| Issue comments beyond the cache cap | REST `GET .../issues/{n}/comments` (`since`, `per_page` max 100, `page`), `GET .../issues/comments/{comment_id}` | Documented | Issues read | Implemented: `issueme_list_issue_comments`, `issueme_get_comment` | Mocked (`test/issue-comments-tool.test.mjs`); live-verified 2026-10-10 (two-page continuation, get by id, ownership refusal, reads on a closed issue) |
| Issue types | REST `type` on issue create/update and `type` filter on list (`*`, `none`); `GET /orgs/{org}/issue-types` | Documented. Create/update silently drop `type` without push access. The organization endpoint is organization-only. | Push access to persist a type; `read:org` on classic tokens to list organization types | Implemented: `issueme_list_issue_types`, `type` on create/update/list, `issue_type` in cache records; the transport allows exactly `/orgs/{resolved owner}/issue-types` and nothing else under `/orgs/*` | Mocked (`test/issue-types.test.mjs`); live 2026-10-10 on a user-owned repository: `issue_types_unavailable` reported without an organization request, and `type: "Bug"` on create/update was silently dropped by GitHub and surfaced as the documented `partial_success`. Organization-owned behavior is live-unverified |
| Close reasons | REST `state_reason`: `completed`, `not_planned`, `duplicate` (with `duplicate_issue_id`), `reopened`, `null`; ignored unless `state` changes | Documented | Issues write | Implemented: `completed` and `not_planned` are sent on close, `reopened` on reopen; records retain `state_reason` and summaries `stateReason` for all documented values and `null`, unknown values are omitted. Setting `duplicate` is not exposed. | Mocked (`test/close-reasons.test.mjs`); live-verified 2026-10-10 (`completed`, `not_planned`, `reopened`, and `null` on open issues) |
| Projects v2 item read, remove, clear field, archive, position | GraphQL `ProjectV2.items` with `fieldValues`; `deleteProjectV2Item`, `clearProjectV2ItemFieldValue`, `archiveProjectV2Item`, `unarchiveProjectV2Item`, `updateProjectV2ItemPosition` | Documented in the GraphQL reference. Inputs are `projectId` and `itemId`, plus `fieldId` for clear and optional `afterId` for position. `ProjectV2Item` exposes `isArchived` and `type` (`ISSUE`, `PULL_REQUEST`, `DRAFT_ISSUE`, `REDACTED`). Typed values: text, number, date, single select (`name`, `optionId`), iteration (`title`, `iterationId`, `startDate`, `duration`). | Classic token `project` scope (fine-grained tokens cannot reach user-owned Projects) | Implemented: reads (`issueme_list_project_items`, `issueme_get_project_item`), removal (`issueme_remove_issue_from_project`), clearing (`issueme_clear_project_item_field`), archive/unarchive (`issueme_archive_project_item`), position (`issueme_move_project_item`, open-only, anchor validated on the same board, verified against the returned `items` order; schema-introspected: `afterId` omitted or null moves to the top) | Mocked (`test/project-items-tool.test.mjs`, `test/project-item-maintenance-tool.test.mjs`); live-verified 2026-10-10: list/get items with typed values, clear (and already-clear no-op), archive/unarchive (and already-archived no-op), remove (and verified-absent no-op), all repeated on the closed backing issue under the approved exception. Live fact: a deleted item id answers with a GraphQL `NOT_FOUND` error plus null node, now tolerated as "absent" |
| Issue templates | REST `GET /repos/{owner}/{repo}/contents/.github/ISSUE_TEMPLATE` and `.../contents/{file}` (base64 content). GraphQL `Repository.issueTemplates` exists but returned an empty list for a repository with four YAML issue forms (live, 2026-10-10), so it only covers legacy Markdown templates | Contents API documented; template inheritance from the owner's `.github` repository is server-side | Contents read (fine-grained) or `repo` (classic) | Implemented: `issueme_list_issue_templates` reads the directory or a legacy `ISSUE_TEMPLATE.md`, parses front matter and form headers without validating forms, windows content with continuation, and reports organization defaults as unresolved | Mocked (`test/issue-templates-tool.test.mjs`); live-verified 2026-10-10 on this repository's four forms plus `config.yml` |
| Lock/unlock, issue and comment pins, reactions, suggestions, organization issue fields | REST | Documented. Suggestions require the repository feature and triage access. | Varies per endpoint | Optional track only (Tasks 16 to 22); not part of the core track | None |
| Cross-repository transfer, organization type/field administration, duplicate management | REST/GraphQL | Documented | Administrative | Deferred; out of scope | None |

## Unknowns

- Whether the dependency, related-issue, timeline, and issue-type endpoints respond identically under `2022-11-28` and `2026-03-10`. Documentation does not gate them by version; only a live run can confirm.
- Fine-grained permission names for dependency and related-issue mutations. The documentation lists status codes, not permissions.
- Whether the `issue_dependencies_summary` object on issue responses is present under `2022-11-28`.
- GitHub backlog sync during planning failed with `401 Unauthorized`. Nothing here is validated against the live repository.

## Review procedure

1. Re-read the references listed under "Authoritative API references" in `specs/spec-issue-management-gap-tasks.md`.
2. Update this table and `GITHUB_DOCUMENTED_API_VERSIONS` when GitHub publishes or retires a version.
3. Run `npm run validate`. Run the live plan only with explicit authorization and disposable fixtures, then record each affected row as live-verified, unavailable, or still unverified.

Sources read on 2026-10-09 (docs.github.com): REST issue dependencies, REST issues, REST organization issue types, REST issue comments, REST issue timeline, REST API versions, REST breaking changes, GraphQL reference for Projects.
