# GitHub changelog

What changed in each GitHub simulator release, newest first.

What changed in each GitHub simulator release, newest first.

## v0.0.16 — 2026-11-06

1. **The page says where the simulator departs from GitHub.** A **Differs from GitHub** line beside a behavior, or an entry under Limitations, marks each departure. "Unconfirmed" marks behavior not yet checked against GitHub.

2. **The GitHub page reads plainly.** Its Auth, Seeding state and Webhooks sections set out the caller's permissions, the record keys and the event each change sends as tables, in short sentences.

3. **The page has an Errors and limits section.** It shows each REST error body and 422 form, the GraphQL error fields and that the simulator never throttles on its own. It says how `per_page`, `page` and the `Link` header page a list, names the lists that page differently and says what a repeated merge or label write returns.

## v0.0.15 — 2026-11-06

1. **The page states what only this simulator has.** Shared connection steps live on the installation page.

## v0.0.14 — 2026-11-06

1. **A push re-settles `mergeable_state` for the new head.** A push that moves an open pull request's head now re-settles its `mergeable_state` from the new head's results, as GitHub recomputes it for the new commit: an `unstable` pull request reads `clean` once its author pushes a head that carries no results yet, and `unstable` again when an unfinished or failing result lands on it. A head that still conflicts with its base stays `dirty`, and the `synchronize` event carries the re-settled verdict.

2. **`GET /orgs/{org}` answers the organization.** A scenario's organization reads back in GitHub's organization shape, its login matched in any case, and a user's login or an unknown name answers GitHub's `404 Not Found` envelope. terraform-provider-github reads it at configure time to tell an organization owner from a user.

3. **Auto-merge and the merge queue.** `gh pr merge --auto` (`enablePullRequestAutoMerge`) records an auto-merge request on a blocked pull request (`auto_merge` over REST, `autoMergeRequest` over GraphQL, the `auto_merge_enabled` event and webhook), and the twin merges it on the enabler's behalf with the recorded method as soon as the base's protection is met; `--disable-auto` clears it, a no-op with nothing to clear and refused `Can't disable auto-merge for this pull request.` on a closed one, and a landed request stays on the merged record as GitHub keeps it. A repository must allow auto-merge unless the base holds a merge queue, and off a queue base a pull request nothing blocks is refused `Pull request is in clean status`, as GitHub answers. A merge queue is a seeded `state_merge_queue` row on a branch: `enqueuePullRequest`, or an auto-merge request on such a base, adds a pull request whose protection is met as an entry whose merge group is built as the branch `gh-readonly-queue/<base>/pr-<number>-<sha>`, starts the repository's `merge_group` workflows on it and sends the `merge_group` webhook; entries land in order, each by fast-forwarding the base to its group, once the group's required contexts pass; a pull request that moves, closes, retargets or turns draft leaves the queue, a base moved from outside it rebuilds the groups, `dequeuePullRequest` removes one and rebuilds the groups behind it, and `Repository.mergeQueue` lists them. GitHub holds a queue only on an organization's repository, so the queue's answers beyond the refusals a base without one draws are the twin's own, pinned by its tests.

4. **Repository rulesets gate merges.** `POST /repos/{owner}/{repo}/rulesets` creates a ruleset; `GET` lists them, `GET`, `PUT` and `DELETE .../rulesets/{ruleset_id}` read, update and delete one, and `GET .../rules/branches/{branch}` lists the rules that apply to a branch. An active ruleset's required status checks and approving review count block the pull requests into the branches its conditions name and refuse their merge with GitHub's `Repository rule violations found` answer, listing every violation, until they are met; a repository admin merges past one only as a bypass actor, and a branch a ruleset applies to reads `protected`.

5. **No `github-pulls_garrytan_gbrain` comment is dated after the scenario's clock.** Two Trunk bot comments, on #5194 and #5294, carried an `updated_at` and a body from after `2026-09-23T00:00:00Z`; they are left out, and each pull request's `comments` count follows (393 comments, from 395).

6. **The first calls of gh, octokit (Ruby), @octokit/rest and terraform-provider-github are measured on every round.** The requests each client makes before anything else are now tests compared against real GitHub in every full round: `GET /user`, `GET /`, the GraphQL viewer read gh sends with its preview and API-version headers, a repository and its pulls, and terraform's organization check.

7. **Write files and refs without a clone.** The contents API (get, create or update, and delete a file) and the git data API (create, update and delete refs, list matching refs, create and get blobs, trees and commits) are served, and every write lands in the repository as a push does: `git fetch` finds it, an open pull request on the branch synchronizes, and the `push` webhook and push-triggered workflows follow. Deleting an open pull request's head branch, by `git push --delete` or through the API, closes the pull request and logs `head_ref_deleted` and `closed`, and deleting a branch open pull requests are based on closes them or retargets them onto a merged head's base.

8. **GraphQL reads branch protection.** A ref's `branchProtectionRule` and a repository's `branchProtectionRules` answer the protection set through `PUT .../branches/{branch}/protection`: its pattern, required status checks and approving reviews, admin enforcement and its other switches, as GitHub answers them. A status-check rollup context's `isRequired(pullRequestId:)` or `isRequired(pullRequestNumber:)` is true when that pull request's base requires a context of exactly its name, so `gh pr checks --required` lists the checks a protected base requires; with neither argument, or one naming no pull request, it is refused with GitHub's error.

9. **A push to a protected branch follows its protection rules.** A `git push` to a branch protected through `PUT .../protection` is now judged by that protection's required status checks, required reviews, force-push and deletion rules: a commit with no passing result for a required check, any direct push while pull request reviews are required, a force push and a delete are refused with GitHub's `remote: error: GH006` lines and `protected branch hook declined`. A repository admin's push goes through unless `enforce_admins` is on; a force push or delete is refused to everyone unless the protection allows it. A scenario's seeded `protected` flag, with no protection record behind it, still refuses every push.

10. **Update-branch, reopen and base changes re-settle `mergeable_state`.** Updating a pull request's branch, reopening it and changing its base now re-settle its `mergeable_state` for the head and base it lands on, as a push does: the merge commit update-branch writes carries no results yet, so an `unstable` pull request reads `clean`, or `blocked` while its base requires a check the new commit lacks, and its `synchronize` event carries that verdict; a pull request reopened after a push while closed reads the new head's verdict, whether it is reopened through the REST pulls or issues route or `reopenPullRequest`; and a base change reads `blocked` onto a base whose protection requires a context the head lacks, and releases a `blocked` verdict when it moves to another base whose protection the head meets, or that has none.

11. **A push of a tree git's fsck refuses is refused.** A `git push` whose pack carries a tree holding a `.git` entry in any spelling, a symlinked or non-blob `.gitmodules`, or a non-blob `.gitattributes` is refused whole, as GitHub refuses it: the report reads `unpack index-pack failed`, every ref command in the push answers `failed`, deletes included, and no ref moves. A corrupt pack answers the same report, where it used to name the read error.

12. **Pull request workflows see the pull request.** A `pull_request` workflow's `github.event` now carries `pull_request`, the pull request as its webhook sends it, beside `number`, so a job's `if:` and its steps read `github.event.pull_request.number`, its head and its base as they do on GitHub.

13. **A matrix built from a dependency's output runs.** A job whose `strategy.matrix` reads a dependency's outputs, such as `${{ fromJSON(needs.plan.outputs.legs) }}`, now expands once that dependency finishes, into one job and check run per combination named as GitHub names them, and the jobs that need it read the combinations' result together, where before it showed one `neutral` job and its dependents went unevaluated.

14. **A state dump seeds back as it was, and the next record gets a fresh id.** `GET /_rystic/inspect` posted to `POST /_rystic/seed` after a reset restores every record where it was, per-caller records included, which used to land under a doubled key. The next record created after any seed passes every uuid and event id the simulator minted that the seed holds. `GET /_rystic/inspect?view=sequences` lists every id counter, and a world document's new `sequences` key restores them, so ids the simulator renders from a counter continue where the dump left off.

15. **A branch read shows the tip commit of a scenario loaded from a git pack.** `GET /repos/{owner}/{repo}/branches/{branch}` names the tip commit's tree, message, author and parents in a scenario whose history comes from a git pack, where it answered them blank.

16. **A ruleset's merge queue rule holds a queue.** A branch of an organization's repository that an active ruleset's `merge_queue` rule targets holds a merge queue configured by the rule's parameters, so `enqueuePullRequest`, `dequeuePullRequest`, `gh pr merge --auto` and `Repository.mergeQueue` work on it as on GitHub, and the queue's answers are now measured against GitHub's own. A ruleset that pairs a `merge_queue` rule with a wildcard branch pattern is refused, as GitHub refuses it.

17. **The merge queue answers as GitHub does.** `enqueuePullRequest` answers the queue as the entry joined it: the entry `QUEUED` with no `headCommit` or `baseCommit`, its pull request open. From the next request `Repository.mergeQueue` reads the entry `AWAITING_CHECKS` with its group built, and an entry whose group conflicts leaves the queue then. The group commit is authored by the enqueuer and committed by GitHub. A landing reads `merged_by` the enqueuer, and the issue's events gain `removed_from_merge_queue` by `github-merge-queue[bot]` and `merged` and `closed` by the enqueuer. A queue's `checkResponseTimeout` and `minimumEntriesToMergeWaitTime` read in seconds. An enqueue is refused in GitHub's words: `Pull request Required status check "<context>" is expected.`, `Pull request is already in the queue`, `Pull request is in draft and Pull request not in mergeable state`, and `No merge queue found for branch '<base>'` on an organization's repository.

18. **A fork's branch moves the upstream pull request in the upstream.** A push, a ref update or a contents write to a branch of a fork the scenario holds now moves the upstream pull request whose head it is in the upstream repository, as GitHub does: the upstream's `/pulls/{n}/files` lists the changed files; the upstream serves `refs/pull/{n}/head` at the new commit to a fetch; the test merge is built against the upstream's base branch, so a conflict with a later upstream change reads `dirty`; and the `synchronize` event names the upstream, so a webhook on the upstream receives it. Deleting the fork's branch closes the pull request with its `head_ref_deleted` and `closed` events in the upstream's timeline and a `closed` event naming the upstream, and a pull request of the upstream whose head is the upstream's own branch of the same name is left alone. A pull request a scenario seeds without a `base.repo` is read in the repository its key names, and reopening, retargeting or updating one stored under any key form keeps a single record.

19. **A pull request's files are its changes since it branched.** In a scenario with a repository, a pull request's `files`, `changed_files`, `additions` and `deletions` are now GitHub's three-dot diff, its head against the merge base with its base branch, wherever it is opened, pushed, updated or retargeted: a commit only the base branch holds no longer reads as a file the pull request removes. A signed commit committed as GitHub itself (`GitHub <noreply@github.com>`) now reads `verified: true` with reason `valid` on every REST route that shows it, and any other signed commit reads `unknown_key`. On the REST branch, commit and compare reads, a commit's author and committer are credited by their email; a GitHub noreply address credits only the login it names, and any other ident is credited by its name, then by the login its address's local part names; and GitHub's own ident credits its `web-flow` account when the scenario seeds one.

20. **Creating a blob accepts every encoding, as GitHub does.** `POST /repos/{owner}/{repo}/git/blobs` no longer refuses an `encoding` other than `utf-8` or `base64`. Only the exact value `base64` decodes `content`, and it decodes leniently: characters outside the base64 alphabet are skipped, a `=` in the third or fourth place of a group of four ends the read, and nothing is refused. Any other value, or none, stores the text as its UTF-8 bytes. A missing or `null` `content`, an empty body, and a body that is a JSON string, number, boolean or `null` answer 422 `Validation Failed` with a `missing_field` error; a `content` that is not a string answers 422 `content must be a string`; and a body that is not valid JSON answers 400 `Problems parsing JSON`.

21. **A completed check run is announced.** A check run that completes now reaches `check_run` hooks and the egress log as `action: completed`, as GitHub sends it: through `PATCH .../check-runs/{check_run_id}` when it completes the run or changes a completed run's conclusion, through a create that supplies a conclusion (after `created`) and for each Actions job as it finishes.

22. **Git writes to a repository that does not exist answer 404.** `POST /repos/{owner}/{repo}/git/refs`, `PATCH` and `DELETE /repos/{owner}/{repo}/git/refs/{ref}` on a repository the scenario does not hold now answer 404 `Not Found`, as GitHub does, where they answered 409 `Git Repository is empty.` or 422, and `DELETE /repos/{owner}/{repo}/contents/{path}` answers 404 there whatever its body holds. Creating or updating a ref checks its body first, as GitHub does: a missing `ref` or `sha`, a value of the wrong type, a `sha` that is not 40 or 64 lowercase hex characters and a body that is not a JSON object answer 422 with GitHub's message for each, and on all three ref routes a body that does not parse as JSON answers 400 `Problems parsing JSON` before anything else.

23. **A push and a REST write answer every branch protection rule.** A `git push`, a ref write through `.../git/refs` and a contents write to a branch protected through `PUT .../protection` are now judged by all of its rules, as GitHub judges them: `lock_branch` refuses every change (`Cannot change this locked branch`), `required_linear_history` refuses a push that brings a merge commit (naming the commit) and `restrictions` refuses anyone its users do not list. A user listed in the pull request rule's `bypass_pull_request_allowances` pushes past that rule. A refused push prints GitHub's current layout, `error: GH006: Protected branch update failed for refs/heads/<branch>.` and then one `- <reason>` per broken rule, and a repository admin's push past a rule prints GitHub's `Bypassed rule violations` notice. Creating a protected branch again after it was deleted is judged by no rule, and a push naming a stale commit for a protected branch is declined by its protection first, both as on GitHub. A ref write or contents write refused by protection answers GitHub's message instead of a generic one, 422 for a ref write and 409 with `Could not create file: `, `Could not update file: ` or `Could not delete file: ` for a contents write, and an admin's REST write now passes the rules `enforce_admins` lets an admin pass. On a push and on `PUT .../merge`, a required check still running reads `Required status check "<context>" is pending.` rather than `is expected.`, and several unmet required checks answer by count: `2 of 2 required status checks are expected.` or `2 of 2 required status checks have not succeeded: 1 expected and 1 failing.`.

24. **A ruleset's every rule refuses a push, and its merge rules refuse a merge.** Every rule of a repository ruleset now judges a `git push` to a branch it applies to, as GitHub judges it: `deletion` refuses a delete, `creation` a create, `update`, `pull_request` and `non_fast_forward` an update (a forced one for `non_fast_forward`), `required_status_checks` an update or a create, `required_linear_history` a push that brings a merge commit and `required_signatures` one that brings a commit GitHub did not sign, naming the commits. A refused push prints GitHub's `error: GH013: Repository rule violations found for refs/heads/<branch>.` with its review link and one `- <reason>` per broken rule, the branch protection's unmet rules after the rulesets', and reports `push declined due to repository rule violations`. A repository admin is judged like anyone else, and a bypass actor passes with GitHub's `Bypassed rule violations` notice (`always`) or silently (`exempt`). A merge by a method a ruleset's `allowed_merge_methods` leaves out, or that the repository disables, answers `405` with GitHub's words (`Merge commits are not allowed on this repository.`) instead of `Pull Request is not mergeable`, over REST and `mergePullRequest`. A `pull_request` rule requiring review threads resolved blocks a pull request while a review thread stands on it.

25. **A merge queue's group branch takes no push, ref write or contents write.** A push, a ref write or a contents write onto the `gh-readonly-queue/<base>/pr-<number>-<sha>` branch a merge queue builds for an entry is now refused, from a repository admin too, as GitHub refuses it. A `git push` that updates, force-pushes or deletes it is declined `protected branch hook declined` after `error: GH006: Protected branch update failed for refs/heads/<branch>.` and GitHub's reason, `- A pull request for this branch has been added to a merge queue. Branches that are queued for merging cannot be updated. To modify this branch, dequeue the associated pull request.`, while the push's other branches land; when a ruleset also refuses the push, it answers GH013 with this reason after the ruleset's. A ref update or delete through `.../git/refs` answers 422 with that message, and a contents write 409 with `Could not create file: `, `Could not update file: ` or `Could not delete file: ` before it. The group stays where the queue built it, so the entry lands as built. A client can still create and delete other branches under `gh-readonly-queue/`.

26. **A push of a commit, tag or tree git's fsck refuses is refused.** A `git push` whose pack carries a tree holding one name twice or its entries out of git's order, a submodule entry naming the all-zero id, a commit or annotated tag whose author, committer or tagger line git refuses, or a commit that lacks its tree, author or committer line, is refused whole, as GitHub refuses it: the report reads `unpack index-pack failed`, every ref command in the push answers `failed`, and no ref moves. A malformed timezone and a zero-padded mode still land, as GitHub lets them through.

27. **A pushed tree with a zero-padded mode clones back.** A tree pushed with an entry mode written `040000` or `0100644`, which GitHub accepts, is served back byte for byte under the name it was pushed with, so a fresh clone or fetch of its branch succeeds where it used to fail with a bad-hash error. The API reads such a mode as git does: the tree read and the tree create answer `100644`, and a commit that only re-spells a mode lists no changed files in its pull request, its push event, its commit read or a compare, so a rebase merge over it keeps the base branch's changes. Every tree the simulator writes itself is the one git writes.

28. **A slow client no longer holds up the others.** The simulator reads a request's whole body before it starts serving it and sends the answer after, so a client that uploads slowly, or reads a large answer slowly, no longer delays other clients' requests. Answers are unchanged; one larger than 4 MiB still makes other requests wait while it is sent.

29. **Workflows run on a tree with a symlink or a submodule.** A push whose tree holds a symlink or a submodule now gets its workflow jobs run. The job's checkout carries each symlink as a symlink and leaves a submodule's directory empty, as `actions/checkout` does without `submodules`; a symlink whose target leaves the checkout is left out. Such a push used to end every job `neutral` with a snapshot error.

30. **A pushed commit is checked as GitHub checks it, and clones back.** A `git push` whose pack carries a commit with two author lines, its committer line before its author line, another header between its parents and its author, a parent line between its author and committer, a tree, parent, author or committer keyword with nothing after it where that line belongs, a line before its tree line, or a last header line with no newline is refused whole, as GitHub refuses it, and so is any commit git's fsck refuses whether or not a blank line follows its headers. A commit GitHub accepts, with a second committer line or a parent line after its committer or no blank line after its headers, lands and is served back byte for byte, so a fresh clone of its branch succeeds where it used to fail with a bad-hash error.

31. **A push of a `.gitmodules` or `.gitattributes` git's fsck refuses is refused.** A `git push` whose trees name a `.gitmodules` holding a submodule url, path or update setting that would run as an option or a command, or a submodule name that climbs out of the repository, a `.gitattributes` with a line of 2048 bytes or more, either file naming a tree, or a `.gitattributes` naming nothing, is refused whole, as GitHub refuses it: the report reads `unpack index-pack failed`, every ref command in the push answers `failed`, and no ref moves. The blob is read whether the push brought it or the repository already held it under another name. A `.gitmodules` or `.gitattributes` entry is judged by what it names, at any mode but a symlink, so a submodule entry named `.gitmodules` lands as GitHub lets it through when the repository does not hold the commit it names, and so do an unparsable `.gitmodules` and a 2047-byte attributes line.

32. **A write beside a pushed subdirectory leaves that subdirectory as pushed.** A file created, updated or deleted through the contents API now rewrites only the directories on its own path, and a rebase merge replays each commit's tree as git cherry-picks it. A subdirectory the change leaves as it was keeps the name it was pushed with, zero-padded modes inside it included, so the new commit's tree is the one GitHub writes for the same change and a clone of the branch finds every such directory under its pushed name. Squash and regular merges already kept them.

33. **The call log shows what each call asked for and whether it succeeded.** Each `GET /_rystic/calls` entry now carries `args`, the call's query and body arguments with credentials redacted (long values cut, file contents left out), and `ok`, false for an answer that is not 2xx. A GraphQL answer carrying `errors` at HTTP 200 is not ok either, and its first error's `type` is the entry's `error`. A path the simulator does not serve is marked `unknown_route: true`. `?tail=N` returns only the last N calls, and `dropped` counts the calls the log no longer keeps.

34. **The docs are one page, with a page per feature.** The archive ships `README.md` in place of `HOWTO.md` and `SCENARIOS.md`, with the same sections in the same order as every Rystic simulator's page, and `CHANGELOG.md` beside it. Git repositories, GraphQL and the `gh` CLI, search, issues and the timeline, workflows and CI runs, branch protection and rulesets, auto-merge and the merge queue, GitHub Apps, the pull request UI and the review-bot scenarios each have their own page under `docs/features/`, and every example on these pages runs as written against a fresh simulator.

35. **Scenario descriptions use customer terms.** The Scenarios table and `--list-scenarios` describe each scenario in American spelling, with no internal references.

36. **Placeholders in the docs show on the site.** A placeholder such as `<id>` in the docs now shows on the site, where it was left out.

37. **The review-bot scenarios page describes the bot and its test.** It now says what `bot.py` does and which settings it reads, that it needs Python's `requests`, how `test_bot.py` finds a Kalshi simulator, and that approving any broken variant means a review missed the bug.

38. **The Scenarios table is easier to scan.** Each row says in a line what the scenario is for, and a list below the table says what each scenario holds.

39. **Pro starts with a 14-day trial.** Free has ended, so every simulator is on Pro and Enterprise, and the docs name only those plans.

40. **Known clients name their registry.** The table at the end of Connect your client names each client's package registry beside it, such as npm, PyPI or GitHub Releases.

41. **Limitations is shorter to read.** It opens with the known issues and one sentence counting the rest. The detailed lists follow, and the website folds them behind "Show all limitations".

42. **The docs page links to the boot configuration page for boot inputs.** The link used to open a heading the control-plane page now only points away from.

43. **The page has no Plans section.** The Quickstart names the plans that run the simulator, and the pricing page lists what each plan includes.

44. **The page keeps only the simulator's own facts on reset, seeding and sign-in.** The shared pages cover the general mechanics.

45. **The feature pages read more plainly, and four topics have pages of their own.** Sentences are shorter, and behaviors that differ from GitHub are marked "Differs from GitHub" where they're described. Rules with several outcomes, such as what settles a pull request's mergeable state, are tables. How protection and rulesets judge a push is now on Protected pushes. Branching and committing through the API is on Commits through the API. How a workflow job's result is decided is on CI results and the evaluation engine. Auto-merge has its own page beside Merge queues. Each new page opens with an example that runs as written.

## v0.0.13 — 2026-10-29

1. **Reactions on issues and issue comments.** `POST` and `GET .../issues/{issue_number}/reactions` and `.../issues/comments/{comment_id}/reactions`, and `DELETE` on each with `/{reaction_id}`, are served; a repeated reaction answers 200 with the first one, and the `reactions` rollup on an issue, an issue comment and a review comment counts them.

2. **Labels, assignees and milestones.** A repository's labels and milestones can be listed and created and its assignable users listed; an issue's or pull request's labels can be listed, added, set and removed and its assignees added and removed; and `POST .../issues` and `PATCH .../issues/{issue_number}` store `labels`, `assignees` and `milestone` instead of dropping them.

3. **Commits, comparisons, branches and refs.** `GET .../commits/{ref}` answers a commit with its stats and files (or its diff with `Accept: application/vnd.github.diff`), `GET .../compare/{base}...{head}` compares two commits, and `GET .../branches`, `.../branches/{branch}` and `.../git/ref/{ref}` read the repository's branches and refs.

4. **Review comments and body-less COMMENT reviews answer as on GitHub.** A COMMENT review whose only content is inline comments, and a pending review holding inline comments submitted as COMMENT, no longer need a body. A review comment that names `pull_request_review_id` is refused 422 as GitHub refuses it, where it used to open a second review.

5. **The state UI is included on Free.** `/_rystic/ui` is available on every plan, including Free and the no-signup trial. The Pro-only verified UI download is a separate feature.

6. **Setting the clock never stamps records before the timeline events it fired.** When `POST /_rystic/clock {"time": ...}` fires timeline events due at its target, the next request's records stamp after theirs instead of reusing their instant; a set past every event still stamps one millisecond past its target, as before.

7. **A paced clock keeps wall time, and `rystic run --speed 0` stops it.** Under `RYSTIC_PACE`, each tick moves the clock by the wall time that passed since the last, so a tick missed while the twin was busy no longer makes timeline events fire late. `rystic run` hands the twin only the pace it resolved, so `--speed 0`, a world without events, `--tape` and `--live` keep the clock manual even when your shell exports `RYSTIC_PACE`.

8. **Repository and organization webhooks.** `/repos/{owner}/{repo}/hooks` and `/orgs/{org}/hooks` create, list, read, update, ping and delete hooks, with their `config` routes. With `RYSTIC_WEBHOOK_DELIVERY=on` the twin POSTs each hook the `push`, `pull_request`, `pull_request_review`, `pull_request_review_comment` and `check_run` events it subscribed to, and a `ping` when it is created, signed `X-Hub-Signature-256` with the hook's secret as `@octokit/webhooks` verifies it. `rystic grid` turns delivery on in every cell and hands each cell's bot its own `$PORT`.

9. **The twin's own writes log their issue events, and every world's log agrees with its records.** Closing, reopening, renaming, locking, unlocking and merging an issue or pull request, over REST or GraphQL, and readying or drafting a pull request append the event GitHub logs, so `GET .../issues/{n}/events` and `.../timeline` show it with GitHub's keys. A seeded closed, merged or locked issue or pull request logs the events that put it there, and `github-pulls_garrytan_gbrain` no longer logs a merge on two pull requests it holds open.

10. **New issue events, reactions and users take IDs above the seeded ones.** An event the twin logs, a reaction it creates and an assignee it adds by an unknown login now take the next ID above the largest one the world already holds, rather than starting from 1. An issue's events list in ID order now lists a new event after the seeded ones, and a new record never overwrites a seeded one whose ID is 1.

11. **`GET /_rystic/egress` shows the twin's URLs.** A webhook event's URLs on GitHub's hosts in the egress log are on the origin you read it on, as in the delivered hook; `?raw=1` returns them on GitHub's hosts.

12. **Swagger UI to read and call the twin from the browser.** `/_rystic/swagger`, linked from the state UI header, is Swagger UI over the published API trimmed to the operations this twin serves; Try it out sends each request to the twin.

13. **A requested reviewer is found by the login as sent.** The seeded reviewer account is now held under the login exactly as a review request sends it, as well as under its lower-case form, so requesting a review from that account finds it whatever case the login is written in.

14. **A GraphQL variable of the wrong type or a misused directive is refused before anything runs.** A variable whose value its declared type does not hold (`"10"` for an `Int`, `5` for a `String!`) now gets an error with graphql-js's `Variable "$v" got invalid value …` text, and so does an unknown, misplaced or repeated directive, or one missing a required argument. Before, the query or mutation ran.

15. **A GraphQL argument written with a value of the wrong type is refused before anything runs.** A literal in the query that its argument's declared type does not hold (an enum value outside the enum, `"5"` for an `Int`, `null` for a required argument, a required input field left out or an unknown one) now gets graphql-ruby's "Argument '…' has an invalid value (…)" error. Before, the query or mutation ran with the value.

16. **Issue intake sees every metadata change.** Adding or removing a label, an assignee or a milestone, through the label and assignee routes or `PATCH .../issues/{issue_number}`, logs GitHub's `labeled`, `unlabeled`, `assigned`, `unassigned`, `milestoned` or `demilestoned` event with its keys, and an issue created with labels, assignees or a milestone logs them too. `GET /repos/{owner}/{repo}/issues/events` and `GET .../issues/events/{event_id}` read the repository's whole event feed. A label and a milestone can each be read, edited and deleted, and `DELETE .../issues/{issue_number}/labels` clears an issue's labels. Issue, issue comment, label and milestone writes now emit the `issues`, `issue_comment`, `label` and `milestone` webhooks; a label, assignee or lock change on a pull request emits `pull_request`. `PATCH .../issues/{issue_number}` takes the deprecated singular `assignee`.

17. **Commit statuses.** A CI bot that reports through statuses rather than check runs can now post them: `POST /repos/{owner}/{repo}/statuses/{sha}` creates a status, `GET .../commits/{ref}/statuses` lists a ref's statuses newest first, and `GET .../commits/{ref}/status` answers the combined status — the latest status per context, `failure` if any context fails, `pending` while any is pending or none exists. A ref may be a sha or a branch name. Creating a status sends the `status` webhook, and GraphQL's `statusCheckRollup`, which `gh pr checks` reads, lists each context beside the head's check runs.

18. **`mergeable_state` follows the head's results.** An open pull request reads `unstable` while a check run on its head is unfinished or failed, or the latest commit status of any context is not `success`, and `clean` once they all pass, as GitHub moves it. A world's `blocked`, `dirty` or `unknown` verdict stands.

19. **Webhook deliveries can be read back and redelivered.** Each delivery now carries GitHub's headers: a GUID `X-GitHub-Delivery`, `X-GitHub-Hook-Installation-Target-ID` and `-Type`, a `GitHub-Hookshot/` user agent, and, beside `X-Hub-Signature-256`, the SHA-1 `X-Hub-Signature`. `GET .../hooks/{hook_id}/deliveries` lists every attempt sent (a hook's newest 500), newest first, with cursor paging and a `status` filter. `GET .../deliveries/{delivery_id}` shows its request and the receiver's response. `POST .../deliveries/{delivery_id}/attempts` sends it again with the same payload and GUID, to the hook's current URL under its current secret. A delivery whose receiver fails is recorded with its status and not retried, as on GitHub, and a repository hook's `last_response` follows its latest delivery. The same routes serve organization hooks.

20. **A pull request's CI evaluates its test merge.** A `pull_request` workflow checks the head merged into the base branch's current tip, as GitHub's does: `github.sha` is that merge commit, which `refs/pull/{n}/merge` serves and an open pull request's `merge_commit_sha` names, and the check runs stay on the head. The merge is git's three-way merge, line by line, so a change that passes alone but breaks once merged with a moved base fails. A head that conflicts with its base starts no workflow, reads `mergeable: false`, `mergeable_state: "dirty"`, and its merge answers `405` and update-branch `422`. A `merge` or `squash` lands the same merged tree its checks evaluated, and reopening a pull request makes commits pushed to its branch while it was closed its head again.

21. **Workflow runs, workflow_dispatch and run logs.** `/repos/{owner}/{repo}/actions/workflows`, `.../workflows/{workflow_id}/dispatches`, `.../actions/runs`, `.../runs/{run_id}/jobs`, `.../runs/{run_id}/logs` and `.../actions/jobs/{job_id}/logs` are served, so `gh workflow run` and `gh run view --log` work. A dispatch records a queued run with its inputs; a harness seeds runs, their conclusions and each job's log text through `/_rystic/seed` (`state_workflow_run`, `state_workflow_job`).

22. **A GitHub App can authenticate.** `GET /app`, its installations, the installation lookups by repository, organization and user, and `POST /app/installations/{installation_id}/access_tokens` answer an App's JWT, which the twin reads by structure (any RSA key signs). The exchange mints a `ghs_` token that acts as the app's bot user, `tb-agent[bot]` in every authored world: its writes are the bot's, its standing follows the installation's grant, and `GET /installation/repositories` and `DELETE /installation/token` answer it. An App JWT on any route but the App's own answers `401 Bad credentials`, where it used to act as the repository owner. The App routes are not measured against GitHub yet.

23. **A push's workflows become Actions runs and jobs.** Each workflow a push, a pull request or a `workflow_dispatch` starts is one workflow run in its own check suite, with one job per check run and the evaluator's output as the job's log, so `gh run list`, `gh run view --log-failed` and GraphQL `checkSuite.workflowRun` show them. `POST .../runs/{run_id}/rerun`, `.../rerun-failed-jobs`, `.../jobs/{job_id}/rerun` and `.../runs/{run_id}/cancel` are served, with run attempts at `.../runs/{run_id}/attempts/{attempt_number}`, and each step fires `workflow_run`, `workflow_job` and `check_suite` webhooks.

24. **The GitHub Enterprise Server URL layout.** A client pointed at the twin as GitHub Enterprise Server (github-mcp-server with `GITHUB_HOST`, octokit with `baseUrl: <host>/api/v3`, PR-Agent) reaches every REST route under `/api/v3` and GraphQL at `/api/graphql`, each answering exactly as the unprefixed route. Answers under `/api/v3` carry `X-GitHub-Enterprise-Version: 3.17.0`.

25. **`HEAD /` answers 200.** The twin answers `HEAD /`, and `HEAD /api/v3/` under the GitHub Enterprise Server layout, as api.github.com answers `HEAD /`: 200 with the `GET` answer's headers and no body. Renovate calls `HEAD <endpoint>/` before anything else on a GitHub Enterprise endpoint.

26. **Reopening a pull request through the issues route picks up its head branch.** `PATCH /repos/{owner}/{repo}/issues/{issue_number}` with `{"state":"open"}` on a closed pull request now does what `PATCH .../pulls/{pull_number}` does: commits pushed to the head branch while it was closed are its head again and `mergeable` / `mergeable_state` are re-derived, so a head that conflicts with its base reads `false` / `"dirty"` and its merge and update-branch refuse. Before, the reopen kept the head and verdict it was closed with, and a merge landed that stale head.

27. **A job whose dependency was skipped is skipped.** A skipped job's outputs read empty, so a job that needs it is gated on its `if:` as GitHub gates it: skipped under the implicit `success()`, and under `always()` skipped or run as its condition reads `needs.<id>.result == 'skipped'` and the empty outputs. Such a job no longer completes `neutral`, so a run whose live jobs pass concludes `success`.

28. **Copilot review fields.** Pull request reviews now carry `is_copilot_code_review: false` and `copilot_code_review_effort: null`, and review comments carry `is_copilot_code_review: false` and `copilot_code_review_sources: []`, on every route that returns them, lists included, as GitHub began answering in October 2026. No review in the twin is Copilot's, so the values never vary.

29. **Reopening a pull request sends `pull_request` with `action: reopened`.** A reopen through `PATCH .../pulls/{pull_number}`, `PATCH .../issues/{issue_number}` or the `reopenPullRequest` mutation now emits the notification GitHub sends for it, so the repository's `pull_request` workflows start on the reopened head and every hook subscribed to `pull_request` is delivered the event with the reopened pull request, its repository and the caller as `sender`. That head includes the commits pushed to the branch while the pull request was closed. Editing or closing a pull request through `PATCH .../pulls/{pull_number}` still sends nothing.

30. **Branch protection gates merges.** `PUT /repos/{owner}/{repo}/branches/{branch}/protection` protects a branch, `GET` reads its protection and `DELETE` removes it; its required status checks and required pull request reviews have their own `GET`, `PATCH` and `DELETE` routes. A pull request into a protected base reads `mergeable_state: "blocked"` while a required context has no passing result on its head (a commit status, or a check run of that name) or approvals are short, and its merge answers `405` naming the first unmet requirement, unless a repository admin merges while `enforce_admins` is off. The branch reads `protected: true`, and lists under `GET .../branches?protected=true`, while it holds protection. Rulesets, push rules for API-protected branches and GraphQL's `branchProtectionRule` are not served yet.

31. **The docs page lists the clients known to work.** A new Clients known to work section names each official client checked against this simulator, the release checked, the ways it connects that work and fail, the day it was checked, and any known gap that covers a failure.

32. **A pull request's file rows have one owner test, and update-branch on a fork head writes nothing to the base repository.** The files listing and the re-projection that replaces a pull request's rows on push, open, update-branch and retarget now decide which stored rows are the pull request's by the same test, so a row seeded with a null `pull_number` or an upper-case `/REPOS/` in its `contents_url` can no longer survive a re-projection and leave the listing longer than `changed_files`. Update-branch on a pull request whose head is a fork's merges the base into the fork's head: the merge's first parent is the pull request's `head.sha`, never a same-named branch of the base repository, and no `refs/heads/<head.ref>` or branch lands in the base repository, as GitHub creates none.

## v0.0.12 — 2026-10-12

1. **The scripts in this archive are MIT-licensed.** `check.sh` and the archive's other scripts are published under the MIT License at https://github.com/Rystic-Inc/rystic-scripts, so you can copy, change and share them. `LICENSE.txt` no longer covers them; it still governs the twin itself.

2. **`bots/test_bot.py` boots your installed Kalshi twin.** With neither `KALSHI_BASE_URL` nor `KALSHI_TWIN_BIN` set, it runs the kalshi-twin version `rystic pull kalshi-twin` installed, with the API key or license `check.sh` would find, instead of trying to build the twin from source. When that twin fails to start, the test prints its log.

3. **A world can carry its own timeline.** A `--seed` file, or a complete world document posted to `POST /_rystic/seed`, may add `credentials`, binding presented credentials to actors, and `events`: calls the twin makes on its own API at set sim times as one of those actors, through the same handlers and egress as your requests. A top-level `credentials` or `events` key the twin used to ignore now takes effect. `RYSTIC_PACE=<speed>` plays the clock, and the timeline, in real time, and an event's `at` may be `+<duration>`. `/_rystic/inject`, `--inject` and `RYSTIC_INJECT` now need `state_set` instead of `scenarios`.

4. **A timeline call can hand what it minted to later events.** A `call` event may add `bind`, `{"<name>": "<JSON Pointer>"}`, saving fields of its answer, such as an ID the twin minted; a later event uses one as `${name}` in a call's path, headers or body or in a seed event's record keys and values (a register name cannot be bound). A schedule naming a `${name}` no earlier call binds is refused at load, and an event whose binding call failed records an error instead of firing.

5. **`github-pulls_open_queue` plays a timeline.** Over the hour after the pinned clock, tb-reviewer-login approves #41, tb-contributor thanks the reviewer on it, tb-maintainer requests changes on #42, retitles the draft #43 and squash-merges #41 into `tb-base-branch`, each through the REST API under a token the world binds to that login; advance the clock to watch the queue move.

6. **A clock jump fires each timeline event at its own time.** When one `POST /_rystic/clock` or paced tick passes several events, each fires with the clock at its `at`, so the records it creates carry that time, and the clock then lands where the move asked. An event already due when the clock moves fires at the current time.

7. **A clock jump stamps each timeline event at its exact instant.** When a clock move passes timeline events, the records each one makes carry exactly its `at`, to the millisecond, whatever traffic came before it, so one jump leaves the same records as stepping the clock to each event in turn.

8. **A timeline call can carry raw bytes, XML or a JSON array.** A `call` event may send `body_text` or `body_base64` instead of `body`; the twin sends it as written, under the route's own content type (XML, bytes, JSON or a form), so a scheduled call can upload an object's bytes or post an XML document, and `${name}` fills in `body_text`. A `body` may be a JSON array where the route reads one. A call that answers XML binds from its elements by JSON Pointer, such as `/InitiateMultipartUploadResult/UploadId`.

9. **A read-only collaborator cannot remove review requests.** `DELETE /repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers` refuses a caller who can only read the repository with `403 Must have write access to repository.`, as requesting reviewers already did.

10. **Refusals and issue search answer as GitHub does.** A self-approval answers errors `["Review Can not approve your own pull request"]`, a review comment on a path outside the diff `["Path could not be resolved"]`, an invalid `event` on review create `Unprocessable Entity` and on submit `Validation Failed` with GitHub's errors — each measured against GitHub. Issue search applies `reason:completed`, `reason:"not planned"` and `owner:`, which it used to read as text. A search qualifier the twin does not evaluate still answers `501`, now in GitHub's error envelope with a `message`.

11. **`GET /user/codespaces` lists your codespaces.** It answers `{total_count, codespaces}` with every codespace the caller created from a pull request, newest first, paged by `per_page` and `page` and narrowed by `repository_id`; without a credential it answers `401 Requires authentication`.

12. **Other search types are refused, and what is out of scope is listed.** GraphQL `search` with a `type` other than `ISSUE` or `ISSUE_ADVANCED` is refused with an error naming `g_search_type_served`, where it used to answer issues. Code browsing, code and commit search and issue subscriptions are recorded as out of scope under Limitations.

## v0.0.11 — 2026-10-10

1. **Seeded worlds read like GitHub's own records.** Every URL a seeded user, organization, membership, repository, commit, file, review, team or check run carries sits on GitHub's hosts — the API ones rewritten to the twin's origin, as created records already were — so a client that completes a record lazily (PyGithub's `pull.user.name`) comes back to the twin instead of failing on `example.invalid`. A pull request's `head.repo` and `base.repo` are the whole repository object (`fork`, `clone_url`, `ssh_url`, `pushed_at` and the rest), its `user` and every `owner` the whole simple-user, and `comments` and `review_comments` count exactly the rows the comment routes list. The imported `github-pulls_garrytan_gbrain` world keeps its `example.invalid` URLs.

2. **A body that is not JSON is refused, not applied as an empty edit.** `PATCH /repos/{owner}/{repo}/issues/{issue_number}` and `PATCH /repos/{owner}/{repo}/pulls/{pull_number}` answer bytes their JSON decoder rejects with GitHub's `400 {"message":"Problems parsing JSON"}` before looking anything up, and write nothing — both used to answer 200 and bump `updated_at`. A truncated object is not that case: GitHub reads a body missing its closing brace as the object and a dangling value as a value its schema check refuses, and the test sends what no JSON grammar accepts. A `PATCH` on a seeded pull request answers the projection `GET` renders, with all eight `_links` and its derived `merge_commit_sha`, not the record as seeded.

3. **A pull request serves its diff and patch.** `GET /repos/{owner}/{repo}/pulls/{pull_number}` with `Accept: application/vnd.github.diff` (or `.v3.diff`) answers the unified diff as text under `Content-Type: application/vnd.github.diff; charset=utf-8`, `.patch` the format-patch mbox of its commits, and `application/vnd.github.full+json` the JSON body with `body_html` and `body_text` — including calls made by Octokit's `pulls.get({mediaType:{format:"diff"}})`.

4. **The Limitations say what the twin serves.** The docs page and the HOWTO state where a seeded URL leads (to the twin, where `GET /users/{login}` and every other route outside its coverage answers `404`), the headers it sends unlike GitHub (no `ETag` or `304`, no `X-GitHub-Request-Id`, `X-RateLimit-*` only on `GET /rate_limit`, no `charset` on JSON), the past-the-last-page `Link`, the list query values and the requested-reviewers readback no round has measured, the truncated JSON bodies GitHub reads leniently, the `Date:` of a commit made through the API, and what `body_html` renders from.

5. **The diff and patch representations read as GitHub writes them.** `GET /repos/{owner}/{repo}/pulls/{pull_number}` under `Accept: application/vnd.github.diff` names each `index` line's blobs by their first seven characters, and under `.patch` numbers a lone commit `[PATCH]`, carries the `create mode` line in the stat block and ends after the diff with no version trailer — measured byte for byte against the real service — so a tool that parses GitHub's `format-patch` output parses the twin's. The one remaining difference is the `Date:` header of a commit created through the API: the twin stamps it in UTC, GitHub in the committing account's time zone.

6. **A seeded draft pull request is mergeable.** In `github-pulls_open_queue`, the draft #43 answers `mergeable: true, mergeable_state: "clean"` — the settled values GitHub computes for a draft, measured against the real service on a full-suite round — where it answered `mergeable: null, mergeable_state: "unknown"` on every read, so a merge bot that polls until `mergeable` resolves now stops. A draft opened through the API already answered the settled values.

7. **The pull-request list falls back to GitHub's defaults.** `page=0`, `per_page=0`, a non-numeric page and an unknown `state` or `sort` on `GET /repos/{owner}/{repo}/pulls` answer `200` with the defaults, as GitHub does, where the twin answered `422 Validation Failed`; and a `head` value without `user:` or `organization:` is ignored rather than matched against the branch name, so `head=<ref>` lists every open pull request as it does on GitHub. Each value is a test confirmed against the real service. An unknown `state` on the issues list stays `422`, which is what GitHub answers there.

8. **A review comment's review exists.** A top-level comment through `POST /repos/{owner}/{repo}/pulls/{pull_number}/comments` creates the COMMENTED review its `pull_request_review_id` names, so `GET …/pulls/{pull_number}/reviews/{review_id}` answers that review and the reviews list includes it, as GitHub does.

9. **A submitted review clears its author from the review request.** Once a requested reviewer submits a review through `POST /repos/{owner}/{repo}/pulls/{pull_number}/reviews`, `GET …/pulls/{pull_number}/requested_reviewers` and the pull request's `requested_reviewers` no longer list them, as GitHub documents, so `gh pr view` drops their "(Requested)" mark and a bot waiting on the request sees it clear. A pending review leaves the request standing until it is submitted.

10. **What a pull request's conversation page reads now answers.** `GET .../issues/{n}/events` and `GET .../issues/{n}/timeline` list a pull request's or issue's events and its derived timeline (comments, reviews, review comments, commits); `Accept: application/vnd.github.html+json`, `.text+json` and `.full+json` answer `body_html` and `body_text`; `GET /repos/{owner}/{repo}` answers `permissions` for the caller; and every `avatar_url` leads to an image the twin serves itself. `github-pulls_garrytan_gbrain` now carries every pull request's conversation comments and issue events, and its collaborators are the logins GitHub marks as holding push, so other users read.

11. **Organization access is consistent.** Membership and repository routes match the caller's login and membership state without case sensitivity, ignore memberships without a user, and keep organization affiliation when the caller is also a collaborator. Organization admins receive admin permissions on their repositories; billing managers and pending invitations grant no repository access. `/user/orgs`, `/user/memberships/orgs`, and `/user/repos` error responses include `status`.

12. **Answers carry what GitHub's response schemas fix.** Where a handler leaves a field absent or null, the twin answers with the required constant, the required field's documented default, or the variant tag that GitHub's response schema fixes for that route and status, error envelopes included; a value the handler wrote is never replaced.

13. **A body's media type renders the same under its v3 spelling.** `Accept: application/vnd.github.v3.full+json`, `.v3.html+json`, `.v3.text+json` and `.v3.raw+json` answer an issue's, pull request's, comment's or review's body exactly as the unversioned media types do: `body_html` and `body_text` are one GitHub-flavored Markdown rendering of `body`, so a client that names the API version in its media type gets the rendered fields.

14. **Page links name the page size you asked for.** On the page-numbered reads (`GET /search/issues`, `GET /user/repos`, `GET /user/orgs`, `GET /user/memberships/orgs`, and an issue's `events` and `timeline`), each `Link` target carries the `per_page` the request sent, as GitHub does, even above the 100 items a page holds. A `per_page` that is not a positive integer names the size served.

15. **Pull request mutations answer the pull request's reviews.** `mergePullRequest`, `convertPullRequestToDraft`, `markPullRequestReadyForReview` and `createPullRequest` answer `reviews`, `latestReviews`, `latestOpinionatedReviews` and `reviewDecision` from the reviews submitted on the pull request, as `repository.pullRequest` does, instead of always empty.

16. **`X-GitHub-Api-Version` is checked on every route, as GitHub does.** `2022-11-28` (the default when the header is absent) and `2026-03-10` are accepted; any other value answers `400` with GitHub's error body before the operation runs.

17. **A read answers `If-None-Match` with `304`.** A `GET` carries a weak `ETag` where GitHub's does; send it back in `If-None-Match` and an unchanged read answers `304 Not Modified` with no body, so a client's cache revalidates against the twin as it does against GitHub. `GET /search/issues` and `GET /rate_limit` carry no `ETag`, as on GitHub, so a conditional request there reads the answer again. `If-Modified-Since` is not answered.

18. **Web URLs lead back to the twin.** `html_url`, `_links.html`, `clone_url` and every other URL on `github.com` or one of its subdomains (`docs.github.com`, `codeload.github.com`) now names the twin's origin, as `api.github.com` URLs already did; `RYSTIC_PRESERVE_SERVICE_URLS=1` keeps GitHub's hosts. Through `rystic run --intercept`, `github.com` and its subdomains route to the twin.

19. **Past the last page, `Link` points back at it.** On the page-numbered reads (`GET /search/issues`, `GET /user/repos`, `GET /user/orgs`, `GET /user/memberships/orgs`, and an issue's `events` and `timeline`), a page past the end names the last page as both `prev` and `last`, and page 1 as `first`, as GitHub does.

20. **Repositories and commit authors read as GitHub embeds them.** `GET /user/repos` lists each repository as GitHub's full repository object with the caller's `permissions`; a pull request's `base.repo` and `head.repo`, on its reads and on `PATCH …/pulls/{pull_number}`, carry neither `permissions` nor `collaborators`; and `GET …/pulls/{pull_number}/commits` names each commit's `author` and `committer` by GitHub's 19-field user.

21. **A pull request's `404` carries only GitHub's keys.** A `404` on a pull request, or on its commits, files, comments, reviews, review comments, reactions or check-run annotations, answers exactly `message`, `documentation_url` and `status`, as GitHub does, without the `url` the twin added. `GET …/issues/{issue_number}/events` answers its `404` with the same three keys.

22. **`GET /user` and `GET /rate_limit` answer what GitHub does.** The authenticated caller reads its private-user view, with `plan`, `disk_usage`, `collaborators`, the private repository and gist counts, `two_factor_authentication` and `notification_email`, and `GET /rate_limit` lists every resource category GitHub answers, `audit_log`, `audit_log_streaming` and `enterprise_token_inventory` included.

23. **The Limitations name the codespace timestamps.** The docs page and the HOWTO say that a codespace's `created_at`, `updated_at` and `last_used_at` are written in UTC, where GitHub writes them at the account's UTC offset, so a client compares them as times rather than strings.

24. **A path ending in a slash answers `404`, as on GitHub.** `GET /repos/{owner}/{repo}/pulls/`, or any route with a trailing slash, answers GitHub's `404 Not Found` (`message`, `documentation_url` and `status`) where the twin served the route without the slash; drop the slash to reach it.

25. **`GET /repos/{owner}/{repo}` answers GitHub's full repository object.** The repository read carries all 96 of its fields, the merge-commit settings and the `*_url` templates among them, and names its `owner` by GitHub's 19-field user. Its URLs on GitHub's hosts lead back to the twin, except `git_url` and `ssh_url`, which keep GitHub's `git://` and SSH addresses.

26. **A push runs the repository's CI checks.** A push, and a pull request that opens, moves or reopens, now starts the repository's own `.github/workflows`: one check run per job as `github-actions`, named as GitHub names it, completed from the twin's evaluation engine — a verdict the world already recorded for the same tree replays, a surrogate you register (`POST /_rystic/eval/evaluators`, run by `rystic evaluator`) evaluates, and anything else shows `neutral`. A push also renders GitHub's `push` event into `GET /_rystic/egress`.

27. **CI evidence stays tied to the current submission.** Agent-created check runs and results from older submissions no longer count as trusted RL reward evidence; rerequests retain their prerequisites, workflow conditions retain their context, and executable files keep their permissions. Evaluation documents now require schema version 2 and a task scope; import reference history before starting an episode, and pin command evaluators to a container image digest and platform. Local execution requires explicit trusted opt-in, while calibration requirements can specify sample counts, coverage and applicability.

## v0.0.10 — 2026-10-05

1. **Search applies the rest of GitHub's grammar, and refuses what it cannot evaluate.** `GET /search/issues` and `gh pr list --search` now apply negation (`-author:`, `-label:`, `-is:draft`), repeated qualifiers as GitHub combines them, the advanced `AND` / `OR` / parentheses syntax `gh` sends, free text with `in:title|body|comments`, `head:`, `base:`, `comments:`, `no:label`, `review:`, `involves:`, `mentions:`, `commenter:` and in-query `sort:`, each measured against api.github.com; an unknown login or repository answers GitHub's `422` over REST and matches nothing over GraphQL. A qualifier the twin cannot evaluate answers `501` with an `X-Rystic-Marker` instead of quietly broadening the result; REST search pages with GitHub's `Link` header, so a client that follows it gets every page, and a pull request found by search or read through the Issues routes carries `draft` and its own `node_id`.

2. **A codespace's `environment_id` comes from its own range.** The first codespace you create gets `81cebaf1-e762-4d0c-8000-000000000001` rather than `00000001-0000-4000-8001-000000000001`. Ids still count up in their last digits and restart on reset; a test that pinned an old ID needs the new one.

3. **Seeded teams report `type: "organization"`.** The teams `github-pulls_org_teams` seeds, which `GET /repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers` returns, carried `type: "Team"`, a value GitHub never sends. They now carry `"organization"`, GitHub's ownership type for an organization's team and the one the twin gives a team it was not seeded with, so a client typed on that field's enum accepts them.

4. **The authenticated user's repositories and organizations answer.** `GET /user/repos` lists the repositories the caller owns or collaborates on, with GitHub's `visibility`, `affiliation`, `type`, `sort`, `direction`, `since`, `before` and paging parameters, and `GET /user/orgs` lists the caller's organizations. Both answer `401` without a credential, as GitHub does, so `gh api user/repos` and `gh api user/orgs` no longer 404.

5. **Organizations have members.** `GET /user/orgs` lists the organizations the caller holds an active membership in, `GET /user/memberships/orgs` answers those memberships with their `role` and `state`, and `GET /user/repos?affiliation=organization_member` reads the same membership. `github-pulls_org_teams` seeds the organization its teams belong to, `tb-org`, with `tb-owner`, `tb-reviewer-login` and `tb-maintainer` as members, and its teams' URLs name it; every other world seeds none, so those lists stay empty there.

## v0.0.9 — 2026-10-01

1. **`check.sh` fails fast on a port that never answers.** Its port check and boot wait give each `/_rystic/version` probe two seconds, so an address where something accepts connections but never replies fails the check with a message instead of hanging.

2. **The repository, the rate limit, the API root and the authenticated user answer.** `GET /repos/{owner}/{repo}`, `GET /rate_limit`, `GET /` and `GET /user` are served as GitHub serves them, so an agent's first probes no longer 404 and `gh auth status` reports a login instead of an invalid token. The rate limit reports a full budget that resets at the next hour of the twin's clock, and URL templates in responses keep their braces, as GitHub writes them.

3. **`gh pr list` and `gh issue list` run with their filter flags.** With `--author`, `--assignee`, `--label`, `--draft` or `--search`, gh first asks for `SearchType`'s values, and the twin answered `Field 'enumValues' doesn't exist on type '__Type'`. It now answers with GitHub's values, so the list runs through the twin's search. HOWTO.md lists the qualifiers that search applies.

4. **Issue search over REST, and both searches apply date qualifiers.** `GET /search/issues` answers the world's issues and pull requests with `total_count`, paging and `sort`. It and the GraphQL `search` that `gh pr list --search` sends now apply `created:`, `updated:`, `closed:` and `merged:` ranges, `draft:` and `is:unmerged`, so counting pull requests in a date window matches the world instead of ignoring the range.

## v0.0.8 — 2026-09-30

1. **The state UI shows the repository as GitHub.** On Pro and Enterprise, `/_rystic/ui` opens GitHub-style Pull requests and Issues pages for the twin — a pull request's conversation and merge box, commits, checks and diff with review threads and check annotations — where you can act as any seeded user: comment, review, comment on a line, react, request reviewers, merge, close, lock and set a check run's status through the twin's own API, beside Actors & calls and Webhook events cards.

2. **Unmodified clients reach the twin at GitHub's hostnames.** `rystic run github-pulls-twin --intercept` boots the twin behind the rystic CLI's HTTPS gateway, and `rystic exec -- <command>` routes that command's calls to `api.github.com` and `github.com` to it: `gh`, `git clone https://github.com/...`, `curl` and SDKs with their default base URL, when their transport honors the proxy and CA settings.

3. **Free boots the imported `garrytan/gbrain` repository.** `--scenario github-pulls_garrytan_gbrain` (or `RYSTIC_SCENARIO`) now boots it on Free, and `POST /_rystic/reset` puts back whichever world the twin booted — issues, comments, pull requests, reviews, check runs, pushed branches and git objects included, with the next issue and pull request numbers where the boot left them.

## v0.0.7 — 2026-09-26

1. **HOWTO.md and `check.sh` start from an API key.** HOWTO.md leads with `rystic login rys_…`, says which worlds and control-plane routes Free, Pro and Enterprise reach, and corrects the auth rules: Codespaces need a `Bearer` or `token` header, and a login the world does not seed is refused authored writes, merges, dismissals and review requests but not a PATCH, a check run or a reaction. SCENARIOS.md marks `github-pulls_open_queue` as the one Free world and keys a pull request file `<number>#<path>`, and both give the ID bands new records get. `check.sh` runs the Free and Pro builds on your API key (`RYSTIC_TOKEN`, or the key `rystic login` stored) as well as the Enterprise build on its license file, looked up in the CLI's order.

## v0.0.6 — 2026-09-25

1. **More complete pull-request responses and token authentication.** Pull-request and review responses include missing user and repository fields. GraphQL pull-request creation errors retain the nullable payload alongside errors, and merge and review-dismissal refusals match observed response envelopes. Codespaces can be deleted by name. Clients can authenticate with either GitHub's `Bearer` or `token` authorization label.

2. **Self-serve on the metered edition replaces the free edition.** A Rystic account's token runs the metered build: `rystic login rys_…` once, then `rystic run` or `docker run -e RYSTIC_TOKEN=…`. It carries every scenario and control-plane route your plan includes, and bills active 30-second blocks against the plan's allowance. The fixed-world free download and its usage report are gone.

## v0.0.5 — 2026-09-24

1. **`gh repo clone` works with gh 2.101.0 and later.** That release's
   clone query selects `Repository.sshUrl` on the repository and its parent
   and stops at the schema when the field is missing, so against 0.0.4 the
   clone failed before it started. The twin now answers `sshUrl` as GitHub
   spells it, `git@<host>:<owner>/<repo>.git`, on every repository.

## v0.0.4 — 2026-09-23

1. **A request GitHub's schema refuses gets GitHub's answer.** In 0.0.3,
   eight routes answered a required field left out, a value of the wrong
   type, or one outside its enum with `422 {"message": "Validation Failed",
   "errors": [...]}`. GitHub refuses those before the resource sees them,
   with `422 {"message": "Invalid request.\n\n<lines>"}` and no `errors` key:
   `"title" wasn't supplied.`, `For 'properties/body', 5 is not a string.`,
   `For 'properties/side', "UP" is not a member of ["LEFT", "RIGHT"].` The
   twin now answers the same way when you create or edit an issue, create or
   edit an issue comment, lock an issue, open a pull request, create a
   review comment, or react to one. Validation Failed with `errors[]` stays
   where GitHub sends it, for a value the schema admits and the resource
   refuses: a blank title, an unknown branch, a duplicate. Editing an issue
   with an unknown `state_reason` answers GitHub's own `422 {"message":
   "Please specify a valid state reason."}`.

## v0.0.3 — 2026-09-22

1. **Every world is a real git repository.** Each scenario's repository is
   served at `/{owner}/{repo}.git/` on the twin's port: `git clone
   http://localhost:8080/tb-owner/tb-repo.git` and `git fetch origin
   refs/pull/{n}/head` walk git's dumb HTTP protocol, `git push` speaks the
   smart one, and every sha the API reports is a commit you can check out.
   Pushing to an open pull request's head branch moves the pull request —
   head sha, `commits`, `files`, a `synchronize` notification — exactly as
   GitHub does; pushing to a protected branch is refused. A pull request's
   `files[].patch` and its commits' diffs now come from the same commits, so
   the two never disagree.

2. **The `gh` CLI works against the twin.** `POST /graphql` answers the
   queries and mutations the `gh` porcelain sends, so `gh pr list`, `gh pr
   view`, `gh pr checkout`, `gh pr checks`, `gh pr comment`, `gh pr review`,
   `gh pr merge`, `gh repo clone`, `gh issue create`, `gh issue list` and
   `gh issue comment` run unchanged, and every mutation writes through the
   same handler its REST route uses, so what `gh` did shows on the REST
   surface. Point it at the twin with environment alone — `gh` speaks plain
   HTTP to `github.localhost` and honors a proxy, and git honors the
   lowercase spelling:

   ```bash
   export GH_HOST=github.localhost GH_TOKEN=ghp_anything \
          HTTP_PROXY=http://localhost:8080 http_proxy=http://localhost:8080
   gh pr list -R tb-owner/tb-repo
   ```

   The GraphQL surface is the subset `gh` speaks, not GitHub's schema; a
   field outside it answers GitHub's `undefinedField` error. `gh issue
   view --comments` lists the comments the store holds.

3. **The API's writes land in the repository you clone.** Creating a pull
   request, retargeting its base, merging it and updating its branch all move
   the served git repository now, so a `git fetch` after any of them finds
   what the API described: a merge is a commit on `origin/<base>` carrying the
   head's changes — with `merge_method` `merge` the base and the head are its
   two parents, while `squash` and `rebase` land single-parent commits — and a
   pull request opened on a branch you pushed carries that branch's commits
   and files. `gh pr merge` followed by `git pull` behaves as it does against
   GitHub. Fetching still changes nothing — `refs/pull/{n}/merge` is derived
   when a client asks for it and never written — so a clone leaves the world
   exactly as it found it.

4. **Issues and their comments over REST.** `/repos/{owner}/{repo}/issues`
   lists and creates them, `/issues/{n}` gets and updates one — `state`
   closes and reopens it, with a `state_reason` — and `/issues/{n}/lock`
   locks and unlocks its conversation. Comments are `/issues/{n}/comments`
   to list and create, `/issues/comments` for a whole repository's, and
   `/issues/comments/{id}` to get, edit or delete one. Pull requests are
   issues here as they are on GitHub: they come back in the list and by
   number, each carrying a `pull_request` key, and `/issues/{n}/comments` is
   where a pull request's conversation comments live — the route
   `gh pr comment` writes to, distinct from the diff-anchored review
   comments. Issues and pull requests draw their numbers from one sequence
   per repository, and a repository with issues disabled answers 410 on the
   issue routes; pull-request numbers and their comments are unaffected. The
   eighth world, `github-pulls_issue_tracker`, seeds five issues and a
   commented pull request around that shared sequence.

5. **A real repository to review: `github-pulls_garrytan_gbrain`.** The first
   imported world — the 300 most recently updated pull requests of
   `garrytan/gbrain` as GitHub served them (121 open, 15 merged), their
   5,693 changed files with GitHub's own diffs, 6,266 check runs, 140 real
   users, and the repository itself as a packfile: `git clone
   http://localhost:8080/garrytan/gbrain.git` downloads one 85 MB pack in
   about two seconds, `git fetch origin refs/pull/N/head` reaches any
   imported pull request, and `gh pr checkout` lands on the API's head sha.
   Ids, logins and shas are GitHub's. `import_repo.py --git --gzip` is how
   another repository becomes a world.

6. **Who may write is one rule, answered as GitHub answers it.** A caller's
   standing on a repository — the owner is admin, a collaborator holds its
   declared permission, everyone else the world knows holds read, a login
   the world does not seed has none — is consulted by every guarded write.
   Merging, dismissing a review and requesting reviewers refuse a credential
   naming nobody with `401 Bad credentials` and a caller without write access
   with `403`, where before they accepted both; the writes that record an
   author no longer create issues, pulls, reviews or comments with `user:
   null`. Standing is asked of the resolved user, never the `X-Rystic-Actor`
   label, so a review can no longer be requested from the pull request's own
   author. Every world seeds all four documented logins, and
   `tb-contributor` is a user without write access, as the name says. From
   the same review round: an issue's and a pull request's IDs are one rule,
   and created rows mint into their own bands; error bodies are GitHub's
   three families, and an unknown path answers in the service's voice; a
   pull request's mergeability, its reviews and its check runs report what
   the world actually holds.

## v0.0.2 — 2026-09-18

1. **Two worlds whose diffs carry a real program.** `github-pulls_bot_pr_broken`
   seeds a merged pull request (#7) whose single file is a working Kalshi
   market-maker bot — `files[].patch` is the whole ~150-line Python file — and
   an open pull request (#8) that changes one line so the buy and sell legs
   swap sides, with a red `bot-smoke` check run whose failure annotation
   points at that line. `github-pulls_bot_pr_queue` opens five such pull
   requests (#11–#15), one bug each: swapped sides, a syntax error, the wrong
   orders path, prices in cents, a required order field dropped. The twin has
   no contents API, so this is how code comes out of it: read the base, apply
   the change, run the result. Seven worlds now; `--list-scenarios` names
   them.

2. **The bot ships as plain files too.** `bots/bot.py`, the five
   `bots/variants/*.py` and `bots/test_bot.py` ride the archive beside the
   binary, and `rystic features github-pulls-twin` describes them under
   `reference bot`. The test runs the bot for 30 s against a `kalshi-twin` in
   its `tight_book` world and requires a bid and an ask resting at once, no
   crossed quote and nothing left resting; every variant is held to failing
   that.

3. **`SCENARIOS.md`.** One page naming every baked-in world and what it is
   for, with IDs, the cast and the bot-world recipe — the cheat sheet the
   Kalshi and Stripe twins already carried.

4. **Writes on a seeded pull request now land.** In 0.0.1 every baked-in
   world keyed its pull requests and branches by bare number and name while
   the handlers look them up as `<owner>/<repo>#<number>` and
   `<owner>/<repo>#<name>`, so a `GET` on a seeded pull request resolved but
   `PATCH`, merge, review dismiss, reply and reviewer requests on it answered
   404. The worlds are re-keyed the way the handlers read them, and
   `GET /_rystic/registers` states the key grammar. One consequence that
   matches GitHub: opening a second pull request on a head that already has
   an open one is 422; a head whose pull request is closed takes a new one.

## v0.0.1 — 2026-09-17

The first release of the GitHub Pulls twin.

1. **A local GitHub pull-request surface for your review automation.** 37
   routes across 13 resources under `/repos/{owner}/{repo}`: pull requests
   (list, create, get, update, files, commits, merge, update-branch),
   reviews and their submit and dismiss events, review comments with replies
   and reactions, requested reviewers, check runs with annotations and
   re-requests, and codespaces. Point your GitHub client's base URL at the
   twin — PyGithub, octokit and `gh` all take one — and your existing suite
   runs against it unchanged. Nothing you send it counts against a rate
   limit, lands in a real repository, or needs a token.

2. **Five worlds baked in.** `--list-scenarios` names them: an open queue of
   four pull requests, a finished review thread with reactions, a head sha
   with four check runs standing, a merged-and-closed backlog, and org teams
   requested for review. `--scenario <name>` boots straight into one, so a
   test starts from the review state it needs with no seeding step.

3. **Review as one user, approve as another.** GitHub's review rules depend
   on who is calling — an author cannot approve their own pull request, only
   a user with write access can dismiss a review. The twin reads the caller
   from the `X-Rystic-Actor` header and applies the seeded permissions, so
   the permission branches of your integration are testable without a second
   real account.

4. **It never reaches the network, and it needs no token.** Any
   `Authorization` header is accepted, and the twin dials nothing — which is
   the point: a suite that hits `api.github.com` fails when the hourly limit
   is spent, and one that creates throwaway repositories leaves them behind.

5. **Seed the world your test needs.** `POST /_rystic/seed` takes a register
   map — repositories, branches, commits, users, teams, pull requests,
   reviews, comments, reactions, check runs — and lands it directly.
   `GET /_rystic/registers` lists what this twin accepts a seed for; a
   register the model does not carry is refused with a status rather than
   silently dropped.

6. **The same seed twice is the same world twice.** `POST /_rystic/reset`
   empties the twin, `POST /_rystic/seed-rng` pins the generator so IDs,
   node_ids and merge shas repeat run to run, and `POST /_rystic/clock` fixes
   the twin's notion of now so `created_at` and `updated_at` are values your
   test chose. Two identical seed flows leave byte-identical
   `/_rystic/inspect` dumps.

**What is measured, and what is not.** Fidelity is scored differentially
against real `api.github.com` on a per-run throwaway repository, per response
field, not by eyeball: the last full-suite round was **136 of 138 probes on
2026-09-06**, and the suite has since grown to 140 probes. Known shortfalls
are listed in `HOWTO.md` under Limitations — there is no git behind the
records, so diffs and file lists are seeded facts and merging does not
rewrite branch history; no webhooks are delivered. Rounds are re-run daily
and each release states its own number.

**Not in this release.** Issues, issue comments, labels as a resource,
commit statuses, branch protection and repository or organization
administration are not modeled.

---

Source: https://www.rystic.ai/docs/simulators/github-pulls/changelog · Markdown: https://www.rystic.ai/docs/simulators/github-pulls/changelog.md
