# Linear changelog

What changed in each Linear simulator release, newest first.

What changed in each Linear simulator release, newest first.

## v0.0.6 — 2026-11-05

1. **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.

2. **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 scenario without events, `--tape` and `--live` keep the clock manual even when your shell exports `RYSTIC_PACE`.

3. **GraphiQL to read and query the twin from the browser.** `/_rystic/graphiql`, linked from the state UI header, is GraphiQL over Linear's schema trimmed to the queries and mutations this twin serves; each query runs against the twin.

4. **A malformed reference id is refused as Linear refuses it.** `commentCreate`, `attachmentCreate`, `attachmentLinkURL`, `projectCreate`, `projectMilestoneCreate` and `projectUpdateCreate` given an `issueId`, `projectId` or `teamIds` entry that is not a valid id now answer `INVALID_INPUT` "Argument Validation Error", with `userPresentableMessage` and a `validationErrors` entry naming the field, its value and the failed check. Before, they answered `GRAPHQL_VALIDATION_FAILED` "Expected value of type …".

5. **`comments` and `documents` refuse a page size below 1 as Linear does.** `first` below 1 answers `INVALID_INPUT` with `userPresentableMessage` "first must not be less than 1." and a `validationErrors` entry naming `first`, its value and the failed `min` check. Before, they answered "Argument Validation Error" with no detail.

6. **A badly typed variable or a misused directive is refused before anything runs.** A variable whose value its declared type does not hold (`5` for a `String!`, an unknown input field, a value outside an enum) now answers 400 `BAD_USER_INPUT` with graphql-js's "Variable "$v" got invalid value …" text, the code Linear gives a missing variable. An unknown, misplaced or repeated directive, or one missing a required argument, answers `GRAPHQL_VALIDATION_FAILED`. Before, the operation ran.

7. **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`, a required input field left out or an unknown one) now answers `GRAPHQL_VALIDATION_FAILED` with graphql-js's text, as a `null` for a required argument already did. Before, the query or mutation ran with the value.

8. **`issues`, `users` and `workflowStates` apply their `filter`.** A filter on a list's own fields (`title`, `number`, `email`, `name`, `type`, …) or one hop into a related record (`team: { key: … }`, `state: { type: … }`, `assignee: { email: … }`, `labels: { name: … }`), joined by `and` and `or`, now narrows the list as Linear does. Before, each list answered every record whatever the filter said. A filter field the twin does not read is refused rather than ignored.

9. **`issue(id:)` takes an identifier, `includeArchived` lists trashed issues, and a new attachment answers its `sourceType`.** `issue(id: "ENG-101")` now resolves the issue as Linear does, not only its UUID. `issues(includeArchived: true)` now also lists issues deleted with `issueDelete`, each with `trashed: true`. `attachmentCreate` and `attachmentLinkURL` now answer and store `sourceType: "unknown"` where they answered `null`, including a create on a url linked before.

10. **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.

11. **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.

12. **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 code (or, without one, its message) 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.

13. **The docs are one page, with a page per feature.** The archive ships `README.md` in place of `HOWTO.md`, with the same sections in the same order as every Rystic simulator's page, and `CHANGELOG.md` beside it. Filters and the GraphiQL explorer each have their own page under `docs/features/`, and every example on these pages runs as written against a fresh simulator.

14. **The docs page links the admin token section.** The page now sends you to the admin token's section of the shared-simulators page.

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

16. **Answers carry Linear's rate-limit and complexity headers, and a query over 10,000 points is refused.** Every executed answer carries `x-complexity` and the `x-ratelimit-requests-*` and `x-ratelimit-complexity-*` headers, with each reset given in Unix milliseconds, so backoff code can be tested against the budgets it reads. A query's complexity is priced as Linear prices it, before it runs: a property is 0.1, an object 1, an object selecting only `id` 0.2, and most connections multiply their selection by `first` or `last` (50 when neither is given). The two budgets match an API key's: 2,500 requests and 3,000,000 points an hour, refilling continuously. A query over 10,000 points answers Linear's 400 `INPUT_ERROR`, and spending past either budget answers 429 `RATELIMITED` with `Retry-After`. Resetting the simulator fills both budgets.

17. **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.

## v0.0.5 — 2026-10-12

1. **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`.

2. **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.

3. **`linear_sprint_board` plays a timeline.** Over the four hours after the pinned clock, Hana replies on ENG-103 and moves it to Done, Deploy Bot attaches a production deploy to it, Tomas starts ENG-102 and Yuki files a Backlog follow-up, each through `POST /graphql` under an API key the world binds to that person; advance the clock to watch the sprint move.

4. **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.

5. **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.

6. **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`.

## v0.0.4 — 2026-10-11

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. **The docs say what an empty workspace's `viewer` answers.** Booted without `--scenario`, the twin answers `viewer` with its placeholder user until you seed a person; the plans section said it was refused.

## v0.0.3 — 2026-10-11

1. **Free runs every control route but latency and fault injection.** On Free, scenarios, seeding, the clock, inspect, the state UI and replay now work as on Pro, and the twin boots empty without `--scenario`. `/_rystic/network` and `/_rystic/ws-drop` answer 403 with `"feature":"faults"` and stay Pro and Enterprise.

## v0.0.2 — 2026-10-11

1. **An issue and a comment never share an ID.** Each kind of record the twin creates draws its IDs from its own range: the first issue you file is `554dd3a1-3e25-42f1-8000-000000000001` and the first comment `925e28d1-dad3-4fd4-8000-000000000001`, where both were `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.

2. **Introspection answers each argument's type.** `__type`'s `fields { args }`
   now carry `type` as the specification nests it (`kind`, `name`, `ofType`), so
   a client that reads argument signatures, such as `args { name type { kind
   name ofType { kind name } } }`, gets them instead of a validation error.

3. **Nested lists answer the records that name them.** An issue's `parent`, `children`, `cycle`, `project`, `labels`, `relations` and `inverseRelations`, a cycle's, label's and project's `issues`, a project's `teams`, a team's `projects`, a user's `assignedIssues`, and the organization's `teams` and `users` now list what the workspace holds, where they answered empty or null. An issue filed with a `parentId` or `labelIds`, or updated to one, shows up under its parent's `children` and each label's `issues`, and a new issue's `creator` reads the caller. `issueCreate` takes the parent by UUID or by identifier such as `ENG-102`; `issueUpdate` still drops an identifier, so name the parent by its UUID there.

4. **`viewer` answers in a world with no scenario, and a seed names its caller under `member`.** A twin started without a scenario resolves its caller, a placeholder user with an empty name, so `viewer` answers a user instead of an error. A world or a seed names the person `viewer` answers by keying that `state_user` record `member`, its `id` its own; every shipped world does. A seed that keys none gets the placeholder, where `viewer` used to answer the user marked `isMe`.

5. **The guide lists what the twin serves and what it does not, from the model.** `HOWTO.md` and the docs page carry the served operations and the known gaps generated from the twin's model and its open issues: the schema operations not served, the non-null fields the model does not declare, the branches the fidelity suite does not measure, what the model leaves out, and the known differences from Linear. They also say how to seed the person `viewer` answers: a `state_user` keyed `member`.

6. **A JSON field or a small payload object the twin does not model no longer fails the response.** `reactionData`, `Team.securitySettings` and `searchIssues`' `archivePayload` answer an empty value where they answered `Cannot return null for non-nullable field …`, so `@linear/sdk`'s `issues()`, `issue(id)`, `createIssue`, `updateIssue`, `createComment` and `searchIssues` return data. `teams()`, `projects()` and `createProject` still fail on `Team.visibility` and `Project.status`.

7. **Malformed requests are refused before they run, as Linear refuses them.**
   A missing required variable now answers 400 `BAD_USER_INPUT`, a variable
   whose type does not fit its argument (`$id: ID!` where `String!` is expected)
   400 `GRAPHQL_VALIDATION_FAILED`, a document with several operations and no
   `operationName` 400 `INPUT_ERROR`, a subscription 400 with "Cannot query
   field … on type "Subscription"", and a JSON-array batch body 400
   `BAD_REQUEST` "Operation batching disabled." Before, each one executed or
   answered a different error.

8. **`attachment(id:)` answers Linear's not-found error.** Asking for an
   attachment that does not exist now answers `INPUT_ERROR` with `statusCode`
   400, the message `Entity not found: Attachment: could not find by url.` and
   `Could not find referenced attachment with this url.`, as Linear does, instead
   of an `INVALID_INPUT` "Argument Validation Error".

9. **A request without `Authorization` is refused before it runs.** A GraphQL request with no `Authorization` header now answers `401` with Linear's `AUTHENTICATION_ERROR` error before any field resolves, so a refused mutation writes nothing; it used to run as the world's caller. An empty `Authorization` header gets the same refusal, which is the twin's own: Linear's answer to an empty header is not measured. Any non-empty value, a bare key or a `Bearer` token, is still accepted, so a test that sends no header now needs a placeholder key.

10. **Introspection answers each field's type and an input object's fields.**
    `__type`'s `fields` now carry `type` (`kind`, `name`, `ofType`), and
    `__type(name: "IssueFilter") { inputFields { name type { kind name } } }`
    lists an input object's fields, instead of a validation error.

## v0.0.1 — 2026-10-01

1. **A local Linear for your agent's tests.** The first release of the Linear
   twin: 58 GraphQL operations on Linear's one endpoint, `POST /graphql` — issues
   (create, update, archive, unarchive, delete, search), comments, labels,
   relations, projects with milestones and status updates, documents and
   attachments, and read-only teams, users, workflow states, cycles and the
   organization — served from a model validated against the real Linear API.
   Point your client's API URL at `http://localhost:8080/graphql` (`apiUrl` in
   `@linear/sdk`) and your existing queries and mutations run against it
   unchanged.

2. **Five named workspaces, and the one your test needs.** `--scenario
   linear_sprint_board` boots an engineering team on the day cycle 7 closes — six
   people, six workflow states, three labels, eight issues with comments,
   relations and attachments — and is the world Free boots. `linear_triage_inbox`
   holds five untriaged support issues, `linear_roadmap_quarter` three projects
   with milestones and status updates, `linear_solo_workspace` one person and one
   team, and `linear_archive_vault` archived and trashed records; `SCENARIOS.md`
   says what each is for. On Pro and Enterprise, `POST /_rystic/seed` lands
   teams, people, workflow states, cycles and any issue, comment, label, project
   or document directly. `POST /_rystic/reset` rewinds the ID sequence and the
   clock, so two identical runs give byte-identical issues down to `id`,
   `identifier`, `number` and `createdAt`.

3. **It never reaches the network, and it needs no key.** Any `Authorization`
   value is accepted, as a bare API key or a `Bearer` token, and the twin dials
   nothing: no test issue lands in a real workspace and no webhook fires. Not in
   this release, on purpose: workspace administration (creating teams, inviting
   users, workflow and cycle settings), integrations, OAuth applications,
   webhooks, billing and initiatives. A query for any of them answers
   `GRAPHQL_VALIDATION_FAILED` rather than empty data.

4. **What is measured, and what is not.** Fidelity is scored differentially
   against the real Linear API, per response field, not by eyeball: the last
   full-suite round matched **64 of 71 probes on 2026-10-01**, with 2 more
   inconclusive, on a 73-probe suite. That is an early number and this release
   ships it as-is. An issue read resolves its team, workflow state, assignee and
   comments, but driving the twin as an agent would found differences the score
   does not show, listed in `HOWTO.md` under Limitations: some non-null fields
   (`Issue.priorityLabel`, `Issue.branchName`, `Project.state`) fail the whole
   response; `issue.labels` and `issue.attachments` answer empty; a new issue
   without a `stateId` has no state; a new issue in a world is numbered from 1;
   `issues` ignores `filter`; and `__schema` introspection is refused. Each
   release states its own number.

---

Source: https://www.rystic.ai/docs/simulators/linear/changelog · Markdown: https://www.rystic.ai/docs/simulators/linear/changelog.md
