Skip to Content
SimulatorsGitHub changelog

Simulators

GitHub changelog

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:

    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.

Last updated on