Skip to content

Changelog

The beta at beta.plune.ai ships continuously, so entries are dated, not versioned: an entry here is on the beta. Only what you can see or call is listed — screens, endpoints, rules. The road in — @plune-ai/cli and the @plune-ai/playwright reporter — is versioned and keeps its own changelog.

2026-10-05 — Your language and theme follow your account, and a save no longer overwrites a newer one

Section titled “2026-10-05 — Your language and theme follow your account, and a save no longer overwrites a newer one”
  • The language and the theme are kept with your account. Choose them on Account → Profile and signing in on another browser opens in them. If your account holds none yet, the choice your browser already has is saved to it at your next sign-in; with no choice anywhere, Plune speaks your browser’s language if it is English or Українська (English otherwise), in the dark theme. GET /v1/me returns locale (en or uk) and theme (dark or light), and PATCH /v1/me takes them next to name — any of the three, at least one; null takes a choice back. Account.
  • A save no longer overwrites a newer one. When two people save the same case within moments, the second save is now made on top of what the first wrote, where it used to put its older copy back. PATCH /v1/test-cases/{id} and a bulk action read the case again and retry, up to three times; a case that keeps changing answers 409 with the case changed while it was being saved — reload and retry, and in a bulk action its id lands in refused while the other ids run. Accepting fields of a pending draft (PATCH /v1/test-cases/{id}/draft) answers 409 for a third reason, the case kept changing while the draft was being accepted, and the draft stays waiting. A Markdown import refuses such a block, by line, and applies the rest.

2026-09-30 — A result’s panel, JSON and text files, and a run’s commit and branch

Section titled “2026-09-30 — A result’s panel, JSON and text files, and a run’s commit and branch”
  • A result opens in a panel beside the list. A click on a result’s row on the run page opens its panel — where it ran, what happened, its screenshots and its files — instead of the row that used to open in place (since 2026-09-29). #result-<id> in the address opens a result, so it can be shared; ↑ ↓ step between results, and the panel’s width is yours to change. Up to 1024 px it is a sheet over the list, and on a phone the whole screen. A result in a run.
  • A test’s JSON, logs and text files are kept with its result, and open in the panel. POST /v1/results/{id}/files takes application/json and text/plain — a log is text/plain — as UTF-8 without NUL bytes, up to 512 KiB a file and 10 a result, apart from the screenshots’ 20. A text is cleaned of colour codes and credentials before it is kept, and GET /v1/files/{id} returns it as text/plain; charset=utf-8 (both since 2026-09-29). In the panel a file Plune keeps says Open where it said in the CI run ↗, and opens in a window of its own — a JSON indented and coloured, a log or a text with its line numbers. JSON and text files.
  • @plune-ai/playwright 0.5.0, and plune run import in @plune-ai/cli 0.17.0, send them. What a test attaches as application/json or text/plain, with a body or a path, goes to its result — a passed test’s too — and what is left behind is counted in the run’s summary. Until a client sends them, no text file arrives. JSON and text files.
  • A run names the commit and the branch it ran on. @plune-ai/playwright 0.4.0, and plune run import in @plune-ai/cli 0.16.0, open a run with both, read from the CI it ran on — GitHub Actions, GitLab CI or Jenkins. A result’s panel on the run page lists them under Where it ran: Branch, and Commit by its first seven characters, a row each when the run has the value. A run from an older client, from a developer’s machine or from another CI has neither. A run replayed from .plune/pending-results.jsonl keeps them too (plune run report 0.16.0). The commit and the branch.
  • GET /v1/runs/{id}/summary hands them out. run.branch, run.sha — the whole commit — and run.runner, the tool that reported the run (playwright, or the report format plune run import read; not the CI’s machine), each null when the run holds none. Live since 2026-09-29, and empty until a client sends them, which the clients above now do.

2026-09-29 — A run’s screenshots, the Analytics page, and companies

Section titled “2026-09-29 — A run’s screenshots, the Analytics page, and companies”
  • A Playwright run’s screenshots reach Plune with their results. @plune-ai/playwright 0.3.0, and plune run import of a Playwright JSON report in @plune-ai/cli 0.15.0, send the pictures each test kept as files to its result — a passed test’s as well as a failed one’s: PNG, JPEG or WebP, up to 2 MB each and 20 a result. Screenshots.
  • And the run page shows them. A result with screenshots says how many beside its status, and its row opens — a passed one’s too — to Screenshots · N above the error text. A picture opens large, ← → move through the result’s pictures and Esc closes; on a phone it takes the whole screen. The run’s line now says how long the run took, and names the key the run was imported under — from CI, Run ID 36552637315, the CI run’s own id, rather than a bare number. Screenshots.
  • Analytics, over every project you are in. Analytics in the bar outside a project (/analytics) counts over all your projects at once — or the ones you pick — for a period: the pass rate, test cases, runs, manual, automated and coverage; the cases by project and type, the latest run of each project and the results by priority; and a row per project, sortable, each leading to its runs. Date, Projects and a drawer of Filters — run labels, case tags, environment, executed by, and a suite or folder of one project — narrow it, and the address keeps the filters, so a link opens the same view. Analytics.
  • GET /v1/analytics answers over every project you are in, or the ones named in projects, for a period and filters: the pass rate, test cases by type, runs, coverage, the latest run’s statuses, results by priority, a row per project, and the values to filter by. A project you are not in answers 404, the same as one that does not exist, and a token reads its own project only.
  • Companies, through the API. A company holds projects and the people who work on them, with roles of its own — owner, admin, member, reader (/v1/companies). A company role gives the right to join the company’s projects and the role you join with; a project’s data stays open to its members only. POST /v1/projects/{id}/transfer moves a personal project into a company for good, and deleting a company takes its projects to the Bin for six months. GET /v1/projects names each project’s company.
  • Companies in the dashboard. Companies in the bar outside a project (/companies) lists the companies you are in, with your role, their projects and people counted; New company makes one, you its owner. A company’s page has three tabs. Projects: every project of the company by name, Join as … on one you are not in, and — for its owners and admins — Move a project here, which says the move is one-way before it happens. People: invite by address with a role, change a role, remove, leave, and withdraw an invitation still waiting. Settings, for owners and admins: the name, and for the owner Delete company, confirmed by typing the company’s name. Companies.
  • Projects in groups, by company. Personal first — every project in no company — then each company by name, with how many people it has, your role in it and Open company →. A company where you are in none of its projects yet says so, with the way to its page; search narrows inside the groups. With no company, the page is one list as before. The Projects page.
  • The company in the bar and the switcher. Inside a company’s project the bar names the company before the project — for its people a link to its page — and below 1024 px wide the section gives way to it. The project switcher groups its list the same way, and opens under the name at every width. Inside a project.
  • A project holds up to 20 000 test cases, not 5 000: a suite past 5 000 tests no longer stops becoming cases halfway through. A big suite.
  • The top bar keeps one line on a phone. The project’s name at its left is a name and a chevron, not a bordered button; a name longer than the room ends in «…», and the switcher’s list shows it whole. On a phone Search is an icon beside ?. On a phone.

2026-09-28 — Projects, your account, search, and a light theme

Section titled “2026-09-28 — Projects, your account, search, and a light theme”
  • A page of your projects. Projects (/projects) lists every project you are in, as cards or a table — the browser remembers which — each with your role and its last run: when, and how much of it passed. Search narrows the list by name; New project makes one. The bare address opens your project straight away when you are in one, and Projects when you are in more. GET /v1/projects gives each project its lastRun — { at, total, passed }, or null before the first run.
  • One bar, a help menu and an account menu. The ? holds Documentation, API reference, What’s new and Send feedback, each in a new tab; the avatar holds who is signed in, Account and Sign out. Outside a project the bar carries the Plune mark and Projects; inside, the project switcher, whose list now opens under its name. Navigation.
  • Your account, outside every project. Your name, sign-in address, role, language and theme moved from a project’s Settings → General to Account → Profile (/account) — none of them belonged to a project. Account.
  • API tokens are the account’s. Account → API tokens lists every token you hold, from every project; a token is minted for the project picked beside Generate a token, and a project you only read is not offered. A project’s Settings → General, its Getting started and an empty runs list link there with that project picked. POST /v1/tokens documents its 403 (you only read the project) and 404 (you are not in it). CLI 0.14.3 says the same: plune login and its messages about a missing or rejected token send you to Account → API tokens.
  • Ctrl K opens search on any page (⌘K on a Mac), and so does Search in the bar. Inside a project it finds the project’s test cases, runs and plans as you type, up to five of each, and your projects by name; outside a project, your projects. The arrows and Enter open a result; See all opens the list narrowed by the same letters. Navigation.
  • The runs and plans lists narrow by name. /runs?q=… and /plans?q=… show the runs or plans whose name holds the letters, say so, and link back to all of them. GET /v1/runs takes q — part of the run’s name, any case.
  • A result can keep its screenshots. POST /v1/results/{id}/files?name=… takes a PNG, JPEG or WebP of up to 2 MB as the request body, at most 20 per result, while its run is open; GET /v1/files/{id} returns the file, and GET /v1/runs/{id}/results lists a result’s files. A project keeps up to 1 GiB of them; past that, the oldest go first.
  • A light theme. Dark or Light on Account → Profile, for this browser; dark until you choose.
  • A moment’s outage is not a sign-out. When the API fails to say who you are — for a few seconds while the beta updates, say — the page shows its own error with a way to try again, instead of sending you to sign in.

2026-09-24 — What failed, on the run page

Section titled “2026-09-24 — What failed, on the run page”
  • A failed automated result says what failed. The row shows the error’s first line (on a test timeout, the action that was still waiting), the declared test.steps down to the one that failed, where it failed and the names of what the runner kept as files; clicking it opens every error of the attempt in place. /runs/<run>#result-<result> opens a run at one result, and each line of a case’s Result history links there. Failed automated results.
  • failure on a result. POST /v1/runs/{id}/results takes an optional failure { headline, steps, location, artifacts, ciUrl }, and GET /v1/runs/{id}/results returns it — or null. A location that is not a path from the repository root or whose line is not a positive integer, or a ciUrl that is not http(s), refuses the batch and names the field. Over-long parts are shortened and marked, never refused: 20 steps keep the 10 outer and the 10 inner, the artifacts stop at 50. A ciUrl over 2048 characters is left out.
  • The CI run. meta.ciUrl on POST /v1/runs must be an http or https address; the summary carries it as run.ciUrl, and the run’s header shows CI run ↗.
  • No credentials in the text. errorContext is cleaned on the way in and again on the way out, so results stored before today also read without colour codes and with credential-looking values replaced by [redacted:<kind>]. On the way in, a text over 256 KB is kept as its start and end around …[truncated N bytes]…, and errorContextMeta { redacted, truncated } says what was done then; the cleaning on the way out does not change it.
  • A whole failed run in one go. The results route takes up to 10 MiB a request, up from 512 KiB.
  • Later the same day, after a review. A Name: value line whose name merely contains key, token, secret, password or session keeps its value when the name ends in Error or Exception (TokenExpiredError: jwt expired), or the value is a JSON number, true, false or null after a quoted key ("tokenCount": 3,) — a number in a logged header still goes. In a sentence, only the runs that look like a credential are replaced. A session cookie printed as an object — { name: 'plune_session', value: '…' } in a diff or a saved login, both values of a diff where it changed, a value in quotes — is replaced too, and every text of a stored failure is cleaned again on the way out. A NUL in the text, or a text of several megabytes, no longer fails the batch; …[truncated N bytes]… counts only the bytes left out; and a location whose file is no longer a path from the repository root once cleaned is left out. On the run page, #result-<result> also moves the keyboard focus to that result, and a failure’s head stacks on a narrow screen.
  • Sent by @plune-ai/playwright 0.2.6 and by plune run import 0.14.0 of a Playwright JSON report; 0.2.7 and 0.14.1, out the same day, carry the fixes from the review, and 0.2.8 and 0.14.2, out the same day too, those from its second pass. Older clients report as before — the text alone, which the row shows as its first line.
  • Comments. GET and POST /v1/test-cases/{id}/comments, DELETE /v1/comments/{id} — plain text with its line breaks, at most 8 192 bytes, oldest first, the author as a userId. Deleting is soft and the author’s, or an owner’s on anyone’s; a reader reads and writes nothing. Both writes are on the case’s audit feed as test_case/comment. The case page ends with Comments · N — a box and Comment for a member, Delete on your own. Cases → Comments.
  • With this the team programme’s first wave is complete: members and roles, projects, names, who did what, who runs what, and what people said. Next: notifications and attachments — not yet.
  • Assignee on a case. PATCH /v1/test-cases/{id} { "assigneeId" } — a member of the project, or 400 user '…' is not a member of this project; null takes the case back; never set on create. Leaving or being removed from a project clears the person off every case they held there. Cases → Assignee.
  • Filter by it. GET /v1/test-cases?assignee=me|none|<userId> — the eighth parameter, repeatable; me is whoever asks. The filter bar’s Assignee facet: Me · Unassigned · the members.
  • Assign… on the selection bar. POST /v1/test-cases/bulk { "action": { "kind": "assign", "assigneeId" } } — a stranger is refused once, before anything moves.
  • Prefer case assignee at launch. assignment: "prefer-assignee" on POST /v1/runs and on POST /v1/plans/{id}/launch copies the cases’ assignees into configuration.assignees — a snapshot. The run page shows the person on each «To run» row and offers Mine · n / All · n; the summary carries run.assignees (and now run.createdBy). Manual runs → Who runs what.
  • Your name. PATCH /v1/me { "name" } — Settings → General, «Your name»; GET /v1/me carries it, and so does every row of GET /v1/members. The avatar’s initials come from the name once there is one. Members → Names.
  • Who wrote it, who launched it, who marked it. A case carries createdBy (the caller of POST /v1/test-cases or a duplicate), a run carries createdBy (the first shard that created it, on every road in), a source: manual result carries executedBy (whoever submitted or last corrected it). The case page says created by …, the run page Launched by …, a mark on «To run» by … · 2 minutes ago. Rows from before say nothing rather than guess.
  • Not yet (then): assignees and comments — assignees shipped the same day, above.
  • Every record belongs to a project, and every request acts in one. The tenant boundary is membership now, not ownership: you read a case because you are a member of its project. Every account keeps its default project, and nothing changes for a client that names none — X-Plune-Project: <id> names another; a project you are not in answers 404 like an invented id. GET /v1/projects lists yours with your role; POST /v1/projects makes one with you as its owner; PATCH /v1/projects/{id} renames it. The dashboard shows the project at the left of the top bar on every screen, with a switcher and New project…; Settings → General renames it. Projects.
  • A CLI token is bound to the project it was minted in. Every request under it lands there, whatever header a client sends; the token list says which. Tokens from before write to your default project.
  • Members, three roles, invitation by address. owner · member · reader — GET/POST /v1/members, PATCH/DELETE /v1/members/{userId}, DELETE /v1/invitations/{id}. An address with an account joins at once; any other gets a mail, passes the beta door on the strength of the invitation, and joins the moment it signs in. A reader is answered 403 read-only member on every write, and the dashboard says so in a banner on every screen. The last owner cannot be demoted or removed (409). Settings → Members is the screen: the Invite form, a ⋯ menu per row, the pending list with Revoke. Membership changes are in the project’s audit feed. Members.
  • Not yet (then): display names, «created by» and «executed by» — shipped the same day, above; assignees and comments — the next waves of the same programme.

2026-09-17 — A plan as the runner’s --grep

Section titled “2026-09-17 — A plan as the runner’s --grep”
  • The road for CI is one command. plune plan grep <id> (CLI 0.13.0) reads GET /v1/plans/{id}/cases and prints the plan as one --grep pattern — npx playwright test --grep "$(plune plan grep <id>)", npx vitest run -t "$(…)" — so the platform decides which tests run and CI runs only those. One pattern fits Playwright, jest, mocha and vitest 4 and 5, whichever way they spell the boundary between a describe and a title. The curl | jq road the plans page used to show is retired. Test plans.
  • A case in the bin is never detached. The full-run rule of 2026-09-15 looked at every case with a result from the source, deleted ones included; it now looks at live cases only, as every other read does.
  • A person walks a manual or mixed run in the dashboard. While a run of kind manual or mixed is open, its page has a To run section: every expected case as a row with its steps (a toggle per step — passed, failed, skipped), a message and Passed · Failed · Skipped, where the button is the request — one result per case through POST /v1/runs/{id}/results, keyed manual:<caseId> with source: manual and the new steps field. A mark is corrected in place with Change — PATCH /v1/results/{id}, for a manual result of an open run only; a reporter’s result stays append-only. Finish run asks about the cases left; a closed run names its notRun over the results. In a mixed run the automated cases wait for the reporter below and share the counts. On Runs, New run ▾ › Manual run… starts one without a plan — all manual cases or a filtered selection, automated as manual, a title and an environment. Manual runs.
  • A plan is a collection of case filters. POST /v1/plans with a title, a kind (manual, automated, mixed) and one to twenty collections, each a query string of the cases list — narrowing together inside a collection, adding up between them, an empty one is every case. Nothing is copied: GET /v1/plans/{id}/cases resolves the plan when asked, by the kind’s rule, and POST /v1/plans/{id}/launch starts a run of it with configuration.expected set to what it collects at that moment and meta.planId for provenance; GET /v1/runs?planId= lists the runs of one plan. On the dashboard: Plans in the sidebar — the list with a live count, a form with the cases screen’s filter bar per collection and a live «Matched cases», the plan’s page with Launch, and New run ▾ › From a plan… on Runs. Test plans.

2026-09-17 — Steps Database and snippets

Section titled “2026-09-17 — Steps Database and snippets”
  • Every step of a manual case, in one place. Each action becomes a row of the project’s Steps Database on save, with how many live cases carry it; GET /v1/settings/steps lists them (?q= for a prefix). Rename one — PATCH /v1/settings/steps/{id} — and every case that carries it is rewritten in one transaction, a new version and an audit entry each; merge several with POST /v1/settings/steps/merge; add one by hand; delete only what nothing carries, or all of those at once with DELETE /v1/settings/steps?unused=true. The dashboard’s case form suggests the project’s steps as you type, and Settings → Steps is the list with rename, merge, notes and Delete unused. Steps and snippets.
  • Snippets. Titled Markdown inserted into a case as a copy — /v1/snippets, with a template flag for the ones a new case may start from. In the form: Insert snippet ▾ (at the caret on the Markdown tab; as steps or description on the form tab) and Start from ▾ on a new case; Settings → Snippets to write them.
  • One table, a row per iteration. A case of any type carries a parameters table the way Testomat has dynamic parameters: column names and rows, ${Name} (or {{Name}}) in the steps, description, preconditions and prompt, ${Name} only in the title, every row one iteration. On the API examples on POST/PATCH /v1/test-cases (null clears), and each result in GET /v1/test-cases/{id}/results now carries the params it ran. In the Markdown document the <!-- example --> table is stored rather than warned about. On the dashboard: a Parameters section in the form, the table with a row per iteration on the case page, and the row a result ran as chips on its history line. The fields of a case.
  • The tree is a file, and the file is the tree. Every case — or one suite, or one case — as a Markdown document in Testomat’s classical format: <!-- suite --> / <!-- test --> blocks, the title as a heading with its @tags, ## Steps with Expected: under each step, plus Plune’s own columns (type: eval, spec:, ## Preconditions, ## Prompt / ## Assertions). Edit it in any editor, hand it to an agent, and push it back: matched by id, a draft for a block without one, refusals by line, nothing deleted, a report before anything is written. On the API: GET /v1/test-cases/markdown (?suiteId=, ?state=), GET /v1/test-cases/{id}/markdown, GET /v1/suites/{id}/markdown, POST /v1/test-cases/markdown?dryRun=true. On the dashboard: the form’s Markdown tab with a preview, Copy as Markdown on cases and nodes, Markdown ▾ › Export .md / Import .md… over the tree with the dry-run report first. Cases as Markdown.

2026-09-16 — One tree for cases and suites; the API explained, with a collection to try it; the bin

Section titled “2026-09-16 — One tree for cases and suites; the API explained, with a collection to try it; the bin”
  • Test cases is one tree. Folders › suites › cases on one screen, with the case open in a panel beside it — click a name and read it there, Open for its page. The Suites section is gone: /suites forwards, a link to a node (/cases?open=<id>) unfolds the path to it. The filter narrows the tree to the matches and their ancestors; the keys walk it (↑ ↓ → ← Space Esc); below 1024 px the case is a drawer over the tree. One selection covers cases and nodes — a suite’s checkbox takes its cases along — and the toolbar in the filter bar’s place offers Move to…, Priority, Retire, Delete, Copy IDs and Duplicate for all of them. Organizing cases → One tree.
  • Rows drag. Lift any row of the selection and the whole selection travels: a slot between cases or a frame around a suite or folder says where; a folder refuses a case — and a suite a suite — with the rule drawn before you let go, and takes no drop. Alt copies instead of moving. On the API: POST /v1/test-cases/bulk move takes a position, and POST /v1/test-cases/{id}/duplicate takes an optional { suiteId, position } — without a body the copy now lands right under its original, and the dashboard opens it with the name selected to rename. A case’s ⋯ gained Move up / Move down. Organizing cases → Drag and drop.
  • The side panel is yours to fold. It opened as icons alone on Test cases and in full everywhere else, which the first tester read as a broken menu. Now it is full on every section until you fold it — a click on its empty space or the « above Sign out — and it stays the way you left it in that browser. It is also pinned to the window: a tree of three thousand rows scrolls under it, Sign out does not leave the first screen. And a long case name in a deep folder no longer pushes its tags and state onto a second line — it ends in an ellipsis, the whole name on hover.
  • A selection on the case list, and one action over it. Tick rows (Shift+click for a range), and the bar over the list offers Move to…, Priority, Retire and Delete for all of them in one dialog — which stays open with the answer: how many were done and, by name, what was refused. On the API: POST /v1/test-cases/bulk with up to 200 ids and one action, answered 200 with done and refused. Duplicate in the row’s ⋯ and on the case page (POST /v1/test-cases/{id}/duplicate) makes a draft copy without the original’s keys; an automated case is refused — its identity is the spec. Organizing cases → Many cases at once. The suite tree got the same: tick folders and suites, then Move to… or Delete for all of them, each picked root walking the single node’s road.
  • The bar over the content names the section — «Test cases», not /cases. And the edit form’s Type badge says why it is a badge: the type is set once, and a test that left the code is the case’s state (detached after a complete run, then retired), not its type.
  • Settings → Bin. Everything deleted in the last six months — suites (one row per deletion, with what went along), cases, runs — with the date each is gone for good, and Restore on each row: no more asking us. A case that went with its suite comes back with the suite. On the API: GET /v1/bin, and the three …/restore endpoints now answer the account that owns the project.
  • Dates on the wire are always UTC ISO 8601. A run’s startedAt/finishedAt, result and audit createdAt used to arrive in the database’s own text form; every timestamp now reads 2026-09-16T08:02:13.432Z, and the reference marks each as date-time.
  • Every endpoint says what it is for and who calls it, and every request body comes with an example that the platform’s own tests send against the API before it is published — the reference is rebuilt from the same document. POST /v1/runs now documents both of its bodies: the run a reporter opens (schemaVersion: 2) and the finished eval run plune sync uploads (schemaVersion: 1).
  • A Postman collection, built from the API’s own description — beta-api.plune.ai/postman.json, linked from the API’s Swagger page too. Folders per area, baseUrl and token as variables, example bodies, saved example answers; imports into Postman, Insomnia, Bruno and Hoppscotch.

2026-09-15 — Every screen as the design draws it; a run says when it ran

Section titled “2026-09-15 — Every screen as the design draws it; a run says when it ran”
  • A case whose test left the code is marked, not forgotten. When a run that declares itself complete (configuration.full: true on the run — the whole suite, not --grep; the CLI sets it from PLUNE_FULL_RUN=1 since 0.12.0) finishes with nothing missing, every active case that source used to report and did not this time moves to detached, with the run named as the reason. The next result for such a case brings it back to active by itself. Manual cases, other sources and retired cases are never touched.

  • Delete works on any case and any suite — and is reversible. A case with results can be deleted; its results stay in the runs they belong to. Deleting a suite takes every suite and case beneath it, and the dialog says how many before you confirm. Six months to put it back (from 2026-09-16 — Settings → Bin); after that it is gone for good. The “move N cases first” refusal is gone.

  • Every screen was checked against its design frame, element by element, and moved where it differed — fourteen screens in one pass. What you will notice: the sign-in and 404 cards, the onboarding as a numbered list with a copy field per command, the runs list in the columns of the design (Run · Kind · Status · Cost · Duration · When, the verdict and the counts in one cell), the run page with its score cards, the case page with Edit as the main action, the review queue as a plain list with the item on its own page (← Previous · Next →, was → now for a disputed result), the trends page as bar charts of the last 14 runs, and Settings with its sections in a column instead of tabs. The panel, the top bar and the content column were already right.

  • A failed request says so with a word. The error block is a card with a request failed badge, the reason, the advice, and the actions under them — Try again and, where the advice is to write to us, Write to us as a button rather than a link at the end of the sentence.

  • A run nobody named shows the minute it started — 2026-09-15 15:26 in your own clock — instead of the date alone, which read the same for every run of one day. A run named by the CLI or CI keeps its name; from @plune-ai/cli 0.11.0 (adapter 0.2.3) a run is named <directory> · 2026-09-15 15:26 · ci by itself, and PLUNE_RUN_TITLE still replaces the whole name.

  • The dead ends are one card. Whether the address does not exist, the page could not render, or the API could not be reached, the screen is the same card: what happened, the likely cause, and a way out — the last of those used to be a bare sentence with nothing to press.

2026-09-14 — Retire a case, bring it back, confirm it

Section titled “2026-09-14 — Retire a case, bring it back, confirm it”
  • The case page moves the case. The ⋯ menu on a case offers exactly what the lifecycle table allows a person from where the case stands — Retire, Return to work, Confirm (an approved case out of in_review), Activate (a draft) — through the same was → becomes dialog as “Move to…”, with an optional Why. The reason and the date show beside the state badge for as long as the case stays there; the audit log keeps every reason for good. The lifecycle of a case.
  • Retired, but still reported. Results for a retired case are recorded as before and the state stays deprecated; the case page counts the results that arrived after the retirement and dates the last one — the cue to take the test out of the code too.
  • The form no longer sends a state. The Status select is gone from the edit form; saving a title or a description never moves a case again. deprecated reads as retired everywhere the dashboard shows a state.
  • allowedStates on a single case. GET /v1/test-cases/:id says where the caller may move the case from here — a session and a token get different answers (additive; the list does not carry it).

2026-09-14 — A refused token mint says so

Section titled “2026-09-14 — A refused token mint says so”
  • A refused mint is visible. Generating a token past a limit used to do nothing on screen — the refusal was never caught. Now it shows above the list with the server’s reason and, for a throttle, how long to wait.
  • Ten live tokens per account, three seconds between mints. The ceiling was twenty and the mint throttle five per hour — which locked out exactly the person looking around on their first day. Volume is now bounded by the ceiling (every token listed and revocable), the throttle only turns a burst of clicks into one mint. The list says “N of 10”; at the ceiling the button is disabled with the way out named. GET /v1/tokens gains limit (additive).
  • Retry-After tells the truth. Every throttled answer now says the seconds until a slot frees, not the length of the whole window — sign-in included.
  • The suite tree is a filesystem. A folder holds folders and suites, a suite holds cases — nothing holds both, and every write that would break the rule is refused with the same sentence. A non-empty suite becomes a folder through POST /v1/suites/:id/convert-to-folder; its cases move into a child suite of the same name in one transaction. Organizing cases.
  • An automated case files itself. On every road a case is born — created, approved from the queue, or trusted on arrival — its specRef decides where it lives: directories become folders, the file becomes a suite, and the suite remembers its file.
  • Case fields. priority (five levels), precondition, description, and position among siblings. Tags live in the title — @smoke at the end of a name is the tag; tags[] in the API is sugar that rewrites that tail. Settings → Tags lists them with counts, renames and merges. Case fields.
  • The filter is the URL. GET /v1/test-cases?suiteId&tag&priority&priorityMin&type&state&q — repeats inside one dimension are OR, dimensions are AND, a folder expands to its descendants, and an unknown value answers 400 with the allowed ones spelled out. The dashboard reads the same object from the address bar, so a selection is a link. Filtering.
  • Screens. Cases grouped in tree order with breadcrumb headings, priority glyphs and tag chips; Move to… and Delete with a “was → becomes” dialog; suites created with a type, reordered with ↑/↓, converted in place; filters in a drawer and the tree as a drill-down below 1024 px.

2026-09-10 — The beta gets a door, and a source can be trusted

Section titled “2026-09-10 — The beta gets a door, and a source can be trusted”
  • Invite-only. Sign-up is limited to an allowlist of mailboxes (an address and its +alias forms are one mailbox). A sign-in request for an address that is neither invited nor already registered gets the same 202 as any other and no email — the answer is not a way to ask whether an address is on the list; the sign-in screen says the beta is invite-only and links to the waitlist. GET /health reports door: invite-only | open.
  • Trust a source once, instead of a test at a time. Settings → Trusted sources (PATCH /v1/settings {trustedDiscoverySources: ["junit"]}): tests a listed runner finds become cases on arrival — the verdict is created — instead of waiting in the review queue; everything created this way is marked as trusted in the audit log, and the setting has a writer, a date and a way back. --create on the reporter is still the offer; what becomes of it is decided here.
  • An approved case can be deleted. A case born from the review queue used to refuse deletion because the decision that created it pointed at it. Now the case goes and the decision stays — verdict, category, note and time untouched, with an audit row saying the case is gone. Results still refuse: they are evidence of past runs. The delete button asks first.

2026-09-09 — The first run tells the truth; a run can be deleted

Section titled “2026-09-09 — The first run tells the truth; a run can be deleted”
  • A first run no longer says nothing happened. Results from a suite that has no cases yet do not become results — they wait in the review queue as discovered tests. The run page used to show 0 of 0 and “recorded no results”; now it says how many tests are waiting and links to the queue. Only pending entries count: an approved one has become a case and is no longer waiting for you.
  • A run can be deleted, and put back. DELETE /v1/runs/:id — the account owner, no confirmation, recursively (results, disputes, discovered tests); a second call is 404. POST /v1/runs/:id/restore exists for the application owner only. A run was the one thing the platform could create and not remove.
  • Settings shows what this account is — the address and the role — above the tokens, and GET /v1/me returns role next to the address.
  • The token screen names both homes. A CLI token goes into plune login on the generating road and into a CI environment variable on the free road; the screen used to mention only the first.
  • One voice. The Ukrainian interface addresses the reader one way throughout; 34 strings changed. English is unaffected.

2026-09-08 — A run has a name; the queue learns discovered tests

Section titled “2026-09-08 — A run has a name; the queue learns discovered tests”
  • Runs carry title, environment and labels. The reporter variables PLUNE_RUN_TITLE, PLUNE_ENV and PLUNE_LABELS now land somewhere: the title heads the run page, the other two show as tags. GET /v1/runs?environment= splits the histories of a suite run against staging and against production. PLUNE_GROUP is still refused — run groups wait for their own feature.
  • Discovered tests are a third kind of queue entry. A reporter that ran a test the platform has no case for offers it through POST /v1/review-items/discovered; the answer is 200 with a verdict per test — queued, duplicate, refused, known, or created when the source is trusted. Approving one creates the case and attaches every key, so from the next run the test resolves instead of being found again. A refusal outlives the run that provoked it. The review queue.
  • A proposal shows where it came from. Source (page, operation, or requirement) and technique on every queue row; rationale and the cost of the proposing run when you open it. An approved case shows the same on its own page — for cases created before this change too.
  • Breaking. technique on a proposal is limited to 32 characters; a longer name is refused where it used to pass.
  • The runs table gives its width to the column with something to read; an environment tag no longer wraps inside itself.

2026-09-07 — The queue reads in pages and remembers why

Section titled “2026-09-07 — The queue reads in pages and remembers why”
  • Breaking. GET /v1/review-items is paginated. The answer is {items, nextCursor}, limit 1–200 (default 50). A client that read items and stopped sees the first page only — read until nextCursor is null, and pass the cursor back verbatim; an invalid one answers 400 rather than silently restarting from the top.
  • One decision over an explicit list. POST /v1/review-items/decisions takes {itemIds, state, rejectionCategory?, note?} for up to 200 items and answers with a report — {applied, casesCreated, skipped[], duplicates[]} — because one stale item must not undo the other nineteen. There is deliberately no “approve everything pending”.
  • Breaking. A rejection needs a reason. rejectionCategory — one of duplicate, incorrect, out-of-scope, low-value, not-testable, other — and a note of 10–2000 characters, on any write after which the item stays rejected, from the dashboard and from the API alike. The reason is visible on the card and can be changed; the previous pair stays in the audit log.
  • Queue rows are lighter than a single item: proposedTitle and the names of the fields that differ from an existing case; the full body comes from GET /v1/review-items/{id}.

2026-09-05 — Where the public record starts

Section titled “2026-09-05 — Where the public record starts”

The beta as invitees first saw it: a bilingual dashboard (English by default, Ukrainian as the second language), and data model v2 underneath — case states, external keys as identity, run statuses, and an audit log on every change. Earlier history belongs to the team, not to this page.