Aller au contenu
quest'n'click

Changelog

Chaque version, telle que CHANGELOG.md l'enregistre au dernier tag. Le changelog du moteur est écrit en anglais.

4.5.0

dernière23 octobre 2026Release sur GitHub

LOG #268–#289: "Wiki", the documentation moved. wiki/en and wiki/fr are now the documentation: 210 pages in English and in French, each paired with its twin section by section, published as the GitHub wiki and as /docs/<version>/ on Pages from the release's own documentation archive, never rebuilt from main. The old docs/ pages are transition pages: every old heading and anchor stays and links to its new place and to the 4.4.0 text. An assistant finds the pages with two new MCP tools, search_docs and list_docs, and reads one section with read_doc {id, section}; read_doc {name} keeps 4.4.0's eight names and its answer. The engine's package carries its documentation (docs-bundle/), so read_doc works in a created project. Every guarantee a page states is tied to a named test, and the published page says whether that test ran in the candidate. Writing the pages found three bugs, fixed below (the scenario map's lease, doctor in a created project, page:world's file name). The 132 mutation exemptions due at 4.5.0 read again: five killed by a test. One new people's pass, docs-beginner-reader; none of the twenty-two has passed yet. Nothing to change in a game (Upgrade notes: 4.5). The tag is signed with the maintainer's SSH key on their yes of 23 October for v4.5.0 (D37, D50).

Fixed

  • search_docs no longer reads the word "studio" as the variable STUDIO (4.5.0). An identifier written in capitals (an environment variable such as STUDIO, PATH or PORT) now counts only when the query holds it exactly, or is that one identifier in another case (studio_session_key); before, any question with the word "studio" ranked the environment variable page high. The section named exactly by the identifier comes first (PATH before WORKER_PYTHON_PATH), its excerpt shows the description after the name, and an excerpt no longer doubles the page's own bold nor touches the stars of code (ops/**/*.yml).

  • read_doc {id} from an installed package now says where the page lives online (4.5.0). Its structured content gives url.current, the wiki's page, labelled as the current documentation rather than this version's, and url.versioned: for a release build (a candidate's build, not dirty) the Pages permalink that build has once it is the one published (/docs/<version>/r<n>/<lang>/<id>.html#<section>, checked at publication.json), and for any other build null with the reason.

  • A published page's guarantees say whether their test passed in the candidate (4.5.0). The documentation archive is now built after the candidate's tests, from their report (npm run wiki -- archive --test-report <file> --candidate <run>), so a guarantee whose test passed shows "executed on <commit>" on the published page, as check --publish already required. Before, the archive was built without the report and every guarantee read "referenced" only.

  • Reviews say who made them (4.5.0). A translation pair's and a claim's review record whether a person or an AI read it, and whether the reviewer wrote what they read. A page whose pairs or claims were only checked by the AI that wrote them says "self-checked by the author (AI), not reviewed", and check --publish wants a person's review before it publishes it. translations --ack and review-claim now take --kind human|ai and --relation author|independent.

  • The scenario map's span can be leased on a shared Studio (4.5.0). POST /api/leases, /api/leases/release and /api/leases/takeover answered 400 for the span groupings (groupings.json), though the Spans panel lists it. The routes now accept exactly the spans studio-server/src/ops/spans.ts gives files (plus room:<id> and lang:<code>), so the two lists cannot drift apart again.

  • npx quest-n-click doctor runs in a created project (4.5.0). It imported Playwright at the start and stopped with ERR_MODULE_NOT_FOUND, since a created project does not install it. Without the package, the doctor now reports Playwright as optional and missing, with the fix: npm install --save-dev playwright, then npx playwright install chromium.

  • npm run page:world and npm run page:puzzles write a .html file (4.5.0). By default they wrote dist-pages/world and dist-pages/puzzles, without an extension; an explicit --out file.html is still used as given.

  • A Markdown link whose text wraps onto the next line is seen (4.5.0). The wiki's link scanner read one line at a time: [the\npage](x.md) was neither checked nor rewritten for the published wiki. A link whose text breaks across one line of its paragraph is now read whole, as CommonMark reads it.

  • The hosted Studio's end-to-end run no longer fails at random on "Load the latest preview" (4.5.0, #255): the script read the last ready preview, then clicked; when a preview finished building in between, the Studio rightly loaded the newer one and the check failed (seen on a PR's CI, HTTPS pass). It now waits for the head's preview first. Nothing changes in the Studio.

  • An image cached for offline play is served even when the worker's lookup fails (4.5.0, #257): Firefox's service worker now and then failed a cached image offline ("A ServiceWorker intercepted the request and encountered an unexpected error"), a sprite frame then missing. When the image route fails, the worker now looks the image up in every cache before giving up, and says why it failed; npm run e2e:pwa prints that reason. Nothing to change in a game.

  • The browser jobs of CI no longer time out installing Playwright's system packages (4.5.0, #261): seven Linux jobs (e2e, reality, pwa-firefox, cross-runtime, fresh-install, second-game, reference) run in Playwright's own image, the lockfile's version pinned by digest, where the browsers and their libraries are already installed; --with-deps fetched 52 MB from an apt mirror that sometimes served 50 kB/s. A test keeps the image's version equal to the lockfile's. ffmpeg likewise comes from a static build on GitHub first, apt only if it fails.

  • The WebKit e2e run no longer fails at random on "Context is stopped" after its save check (4.5.0, #263): the save round trip reloads the page; when the offline warm-up had a Cache API call in flight, WebKit logged "Cache API operation failed: Context is stopped" and the run failed, every check passed (4 times in 5 days on CI). WebKit logs it itself, even for a promise the page catches, so the run now tolerates it during that reload as it already did during the teardown: reported, not counted, on WebKit only, in that exact form. Nothing changes in the engine.

  • The hosted Studio's end-to-end run no longer waits on a Play frame that never answers after "Load the latest preview" (4.5.0, #264): on the Firefox pass, the script took the frame as soon as its address showed the expected revision and spent 30 s on it, and the second click still expected the revision the first one read. Both clicks now go through one helper that reads the latest revision at each click, looks the frame up again at each try with each evaluation bounded, and loads the page once more (said in the log) if it stays silent. Nothing changes in the Studio.

Changes

  • The documentation's map, wiki/manifest.json, and npm run wiki (4.5.0). Every page of the coming bilingual wiki has an entry: 213 pages across the domains of the plan's catalogue, all planned for now except the four generated indexes. Each entry gives a stable id, a type, an audience, a context, its French and English titles, its public URLs (EN-Create-Rooms, FR-Creer-Salles), the source files it rests on and the questions a reader would ask. npm run wiki -- check keeps the manifest, the pages and their links consistent; it fails on errors only and lists the links to pages not written yet. stats counts the pages. build --target wiki writes the folder the GitHub wiki will be published from: a bilingual home page, a sidebar per language, a footer, each ready page under its URL, and indexes by task, symptom, identifier and vocabulary. The repository also gains a bilingual glossary (wiki/data/glossary.json, with synonyms such as lease, bail and verrou) and page templates (wiki/_templates/). npm run ci:plan also names the wiki pages whose source files a change touches. Nothing is published yet.

  • The coverage matrix of the documentation's move, npm run wiki -- coverage (4.5.0). Before anything moves to the wiki, every section of the old documentation has a recorded destination. wiki -- coverage --init takes the anchor inventory of the 32 pages of docs/en and of docs/fr and of the root pages (both READMEs, CONTRIBUTING, SECURITY, CODE_OF_CONDUCT, CREDITS): every heading with the anchor GitHub gives it, the explicit <a id> and <a name> anchors, every code block, and the sentences that look like a limit (only, not, uniquement, pas encore…), proposed to a reviewer. wiki/coverage.json holds one row per section: its destinations in the wiki, keep, rewrite or drop (a drop says why), and two states, planned and migrated. Today all 1,036 sections are planned and none is migrated. Only roadmap milestones that are fully shipped are dropped, each naming its CHANGELOG version or git tag; a milestone that still lists open items moves to the roadmap page. A dropped section counts as reviewed only once each sentence that looks like a limit has been ticked. wiki -- coverage merges a new inventory and keeps what a person wrote, following a renamed heading by its text; a changed section must be rechecked and acknowledged (--ack <key>); it also writes wiki/legacy.json, the order in which the eight pages read_doc serves will be rebuilt, and each manifest page's replaces. wiki -- coverage and wiki -- stats print the two counters, "destinations planned" and "migrated and reviewed". docs/ is now frozen: wiki -- check and a test fail when a page changes without the matrix being brought back in step.

  • read_doc in a created project now works (4.5.0). The engine's package carries its documentation, docs-bundle/, and npx quest-n-click mcp reads it: read_doc {name: "CONTENT_GUIDE"} (and the seven other names) answers the same single text block as in the engine's repository, where 4.4.0 found nothing to read. The bundle holds an index, the wiki's published pages (none yet), an exact copy of the eight historical pages and a manifest with the sha256 of every file; it names the version, the commit and the documentation revision it was built from, and nothing about publication. Two packs of one commit give the same bytes.

  • search_docs and list_docs, two new MCP tools, and read_doc by id (4.5.0). search_docs finds the documentation pages by words or a question, in French or English, accents and case aside, following the glossary from one language to the other (« verrou » also finds "lease"); each result gives the page and section, why it matched and an excerpt, and a query with no confident match gets an explicit empty answer. list_docs lists the pages by audience, context, type, language or state. read_doc {id, section, lang} reads one section, 20 000 characters at a time with a cursor for the rest, with where the text comes from at its top. read_doc {name} keeps its schema and its answer: the eight names, one whole page, in English. In the repository the new tools read wiki/ (draft and ready pages, said to be a preview); in a package, the bundle's ready pages. The local MCP server and the Studio's Assistant (local and demo) offer them. Known limit: the remote MCP of a hosted Studio does not, its tools being requests to an API that has no documentation route; an agent there uses the local MCP of the same engine version.

  • The documentation says for which versions it is true, what backs each claim, and how stable each surface is (4.5.0). A wiki page already gave the versions it applies to; a section can now narrow them with a comment right after its anchor, <!-- applies: 4.2.2..4.4.0 since 4.2.0 -->, shown to the reader under the heading, and search_docs with a version leaves out a section that does not apply (or marks it notApplicable with includeArchived). A page that names its surfaces shows, for each, five statuses kept apart instead of one "experimental" banner: the contract (experimental, stable or deprecated, from the new docs/dev/contracts.json, whose wording a test checks against the support page), the environments CI verifies and as of when, the automated checks, the people's passes with their state and issue, and the page's known limits. A guarantee (> [!GUARANTEE id=…]) is tied to a named test and shows exactly what is known of it: "referenced" when the test exists with that name, "executed on <commit>" when the candidate's test report says it passed on the documented commit, "scope reviewed" when a person recorded that the test covers it (npm run wiki -- review-claim <id> --by <name>; a change to the claim or the test resets it). A measure (> [!MEASURE id=…]) must give its date, version, volume, conditions and a source pinned to a commit. A room-example block in a page goes through the engine's validator and a studio-config block through the Studio Server's own configuration loader, so a page cannot show what the product refuses.

  • npm run wiki -- check --publish, the publication level (4.5.0). It requires --product-ref <tag|sha> (and, to publish a candidate or a release, --candidate <run id>, the run the test reports must come from) and checks: published pages link to published pages only; no translation is "to review" (wiki/translations.json, recorded by npm run wiki -- translations --ack <id#section> --by <name> once both languages were read; a change on either side makes the pair to review, a warning on a pull request); every guarantee was executed on the product (--test-report, a Vitest JSON report with the commit.json the candidate writes beside it); the documentation's revision is the clean checkout, descends from the product and changed only wiki/<lang>/*.md, the manifest, the translations, the reviews and the images since; --released refuses a page or a section that applies only from a later version; --state preview is refused once a release 4.5.0 or later is tagged. The candidate now uploads the node suite's JSON report and its commit as the artifact candidate-test-report.

  • The documentation is accepted by real questions (4.5.0). wiki/acceptance/questions.json holds 60 questions that a player, an author, a team Studio admin, a contributor or a Bridge operator would type, in English and in French. Each one gives the pages a right answer lands on and the facts it must contain. The file also holds the counter-examples of the plan's table:

    • a feature that does not exist (real-time multiplayer, a Steam export) must lead to "What quest'n'click does not do" or to an explicit empty result;
    • npm run studio in the engine repository and in a created project, and the Bridge's backup and the Studio Server's, are told apart by their context;
    • the v2 → v3 id migration is never shown as current for 4.5.0;
    • a 409 or a 503 leads to the hosted Studio's page or the Bridge's depending on the context, and without one shows both, each naming its service.

    npm run wiki -- acceptance [--lang en|fr] [--json] runs them through search_docs and prints three measures: the expected page in the top 3, no inapplicable answer first for the counter-examples, and the share of empty results that were justified. A question whose pages are not written yet is counted as pending, apart from the misses; today every question is pending. --strict fails on a miss, a failed counter-example or a pending question. It is for the switch-over. npm run wiki -- check validates the file.

  • Two protocols for the documentation's readers (4.5.0). docs/dev/acceptance/AI-PROTOCOL.md explains how to run a fresh AI agent that has only the documentation tools (or only the public URL) on every question, and how to grade its answer fact by fact: found, missing or misread. npm run wiki -- acceptance --export-prompts <dir> writes the prompts, without the answers. A new people's pass, docs-beginner-reader, asked from 4.5.0 on: a beginner looks for the answers to ten of the questions in the published documentation (docs/dev/acceptance/BEGINNER-READER.md).

  • The documentation of each release is published as an archive, a versioned site and the wiki (4.5.0). A release from 4.5.0 on attaches quest-n-click-docs-<version>.tar.gz: the documentation bundle, the rendered site (one page per page and language, its images, a search that answers as MCP's search_docs does, sitemap.xml, llms.txt) and the GitHub wiki's pages, with the sha256 of every file. The candidate builds it twice and compares the bytes; the release carries the very same file in its sums, attestations and ship verify. Once ship verify has passed, the new docs-publish workflow checks that archive again, runs npm run wiki -- check --publish with the candidate's own test report, and publishes: the wiki gets the pages (npm run wiki -- publish --target wiki), and publication.json records which revision of each version is approved and which is the latest; a release candidate publishes only /docs/<version>/ on Pages, never the wiki. GitHub Pages serves /docs/<version>/r<n>/ permalinks, /docs/<version>/ and /docs/latest/ aliases, all copied from the released archives and never built from main (npm run wiki -- pages). A correction between two releases goes through a docs/<version> branch and becomes an immutable revision docs-v<version>-r<n>; an older version's fix never moves the wiki backwards; a rollback is the deliberate way back and a promotion the way to finish a publication that was cancelled. Pages serves an archive only once its build provenance is verified against the workflow that must have made it, and a doc revision only once the publication record names it. The game's offline worker no longer answers a /docs/ page with the game. npm run wiki -- transition writes the transition pages that will replace docs/en and docs/fr (at the switch-over), each old heading and anchor kept and linked to its new place and to the 4.4.0 text.

  • The first wiki pages, written in French and in English, and the environment variable reference (4.5.0). Five pages of wiki/ are ready, each written natively in both languages and found by search_docs:

    • "Restore the Studio on a fresh installation" / « Exercice de restauration sur une installation vierge »: the hosted Studio's restore drill, step by step. It gives the restore time measured on the 4.2.3 reference deployment (28.7 s and 59.7 s, two runs on two commits, one sample each), with its date, volume and conditions, the guarantee that a restore into a database already holding a project is refused, and the test that proves it. It also says what a restore can lose (the RPO) and why going back after a migration means restoring an older backup into a new add-on. The page says plainly that it was not replayed on a live installation.
    • "Write refused on the hosted Studio: span locked (423)" and "… stale base revision (409)": the exact message, the causes (a lease another member holds for 120 s, renewed while their Studio stays open; the exclusive lease a job such as Prepare or an engine upgrade takes; a file saved after you read it), how to tell them apart, and what to do (wait, ask, an owner's audited takeover, apply your edit on the latest version).
    • "Bridge and connector troubleshooting": what the Reality Bridge's 409 and 503 mean, a signal sent twice, a connector down. A search for "409" without a context now shows the Studio's page and the Bridge's, each naming its service.
    • "Environment variables": every variable quest-n-click reads (142, from the Studio Server's secrets to the CI workflows' own) and the ones Clever Cloud and its add-ons inject, each with its own section, its origin, whether it is a secret and can come from a _FILE, and the files that read it.
  • The environment variable register (4.5.0). wiki/data/env.json describes each variable in French and in English. npm run wiki -- env regenerates the reference page and its identifiers from the register and from the code: process.env (destructuring included), import.meta.env, the Python tools, the shell scripts of ops/ and clevercloud/, and the workflows of .github/workflows/. npm run wiki -- check fails on a variable the code reads without a description, on a described one the code no longer reads, and on a computed read (process.env[name]) nobody resolved.

  • The reference pages that rest on the code are generated from it (4.5.0, wiki lot B11). npm run wiki -- generate [<page>…] writes, in English and in French, the part of eight wiki pages that the code knows: every npm script of the engine repository and of a created project (reference.npm-scripts), the six command-line tools with their subcommands, options and exit codes (reference.cli), the error register, by HTTP status for the Studio and by status and code for the Reality Bridge, with the solver's statuses and the checking commands' exit codes (reference.error-codes), the five statuses of each product surface (reference.support-matrix), the DSL (create.dsl), the public API's signatures (engine.api), every route of /__studio/api (studio.api) and every MCP tool with its input fields (studio.mcp-tools). What a person adds, a description in each language, lives in wiki/data/ (npm-scripts.json, cli.json, errors.json, studio-api.json, mcp-tools.json); npm run wiki -- check fails when a script, a route, a tool, a subcommand or an error code has no description, when a described one is gone from the code, or when a page's generated part or its manifest identifiers drift. docs/en|fr/DSL.md and API.md are still generated as before until the switch-over.

  • The 132 mutation exemptions read again (4.5.0): each entry of docs/dev/mutants.json came due at 4.5.0 and was checked against the code. Five were not what they said and are now killed by a test: a speedrun envelope without a world proof no longer carries an empty worldEvidence key in memory, the Studio journal's lease that ends in the very millisecond it is asked for is taken, a durable write handed text is pinned on a short write (a file system that writes less than asked), and a bisection called past the head is refused. One reason was wrong in a side claim and is corrected; 19 are sharpened. The other 127 are read again at 4.6.0.

  • The documentation is written: 209 wiki pages in English and in French (4.5.0). Eleven lots wrote every page of the catalogue under wiki/en and wiki/fr (Index, Start, Play, Install, Create a game, Engine, the local Studio, the team Studio, Clever Cloud, the Reality Bridge, Contribute, Reference), each page ready, its twin paired section by section, its guarantees linked to the test that holds them. Four indexes are generated from them. npm run wiki -- check passes with no error and no warning (the translations and the guarantees' scopes are checked by the AI that wrote them; a person's review is still due before publication); every question and counter-example of wiki/acceptance/questions.json passes in both languages (npm run wiki -- acceptance --strict); 1034 of the 1036 old sections are migrated, every limit candidate ticked. Until the switch-over (C2) the published documentation stays docs/.

  • The documentation search tells contexts apart (4.5.0). With a context, search_docs answers with the section headed by that context ("The engine repository", "A created project") of a page that spans several, and a status code (409, 503) asked within a context leads to that service's troubleshooting page before the error register. An excerpt shows the field that holds the most words found, so it names its service ("Studio backup", not only "Backups"). A search over the whole wiki is about three times faster.

  • The documentation moved to the wiki (4.5.0). wiki/en and wiki/fr are now the documentation, published as the GitHub wiki; the README, the package READMEs, the created project's README, the command line's and the tools' messages, the Studio, the skills and the templates point to it. docs/en/*.md and docs/fr/*.md become transition pages: each old heading and explicit anchor stays, so an old link still lands, and links to its new place in the documentation of its version and to the complete 4.4.0 page (blob/v4.4.0/docs/…). npm run wiki -- check refuses a transition page edited by hand or one that lost an old anchor; npx tsx tools/dsl-doc.ts and npx tsx tools/api-doc.ts write the wiki's DSL and API references.

  • read_doc {name} reads its page from the wiki (4.5.0). The eight historical pages (CONTENT_GUIDE, ENGINE, TOOLS, STUDIO, WORKFLOW, AUDIO, UPGRADING, CLASSICS) are rebuilt from the wiki's sections in the order of the old page, in the repository and in the package alike, still one text block and still accepted with 4.4.0's arguments. Each stays within 1.5 times its 4.4.0 size: where the new sections say more, the longest give their heading, their first paragraph and the read_doc {id, section} that returns the rest. The MCP server's instructions now say: search_docs, then read_doc by id and section.

  • The placement editor turns a character or a prop around (4.5.0, #252): the editor's Selection folder (?edit=<room>, and the Studio's Rooms tab, which embeds it) has a mirror ↔ checkbox for the selected prop or character and a mirror ↕ one for a prop: the layout's flip and flipV, which only the phone placement page could set. A character whose room sets facing keeps it, as before (the room's facing wins over the layout's flip): the panel says so under the checkbox. Nothing to change in a game.

  • The reference chapter's Chromium work runs in three CI jobs side by side (4.5.0, #265): reference (chromium) keeps the build, the proofs, the keyboard run, visual, perf, weight, music and taps; the French and generic runs move to reference-fr, the hero choice example to heroes, both on the reference gate. The job that ended every run (about 15 min of sequential work) becomes three of 5 to 8 min; each rebuilds its game.

4.4.0

22 octobre 2026Release sur GitHub

LOG #256–#267: "Author's Desk", the release an outside reading of 4.3 asked for: making a whole game simpler to make and to check. A playable example of choosing the hero in two languages, games/heroes, with its browser test (building it found three bugs, fixed below). The Studio's hero × language matrix (the Voices tab). Before/after captures of the rooms the semantic diff names (a local Studio). The hosted preview measured phase by phase and made about three times faster on the reference deployment: from a write to a ready preview, median 17.8 s → 5.6 s, max 26.0 s → 11.7 s over ten revisions each (QUALIF-4.2.3-REFERENCE §6). From Horacio (LostInBrittany), thank you: a hotspot that acts on a single tap (#221), one map pin for several places (#223), the phone's column on a short landscape screen (#227). The 137 mutation exemptions due at 4.4.0 read again, then read a second time: four bugs fixed with tests. Two new people's passes (heroes-two-languages, studio-hero-matrix); none of the twenty-one has passed yet. Nothing to change in a game (UPGRADING §41; strict SemVer: the new fields are optional). The tag is signed with the maintainer's SSH key on their yes of 22 October for v4.4.0 (D37).

Fixed

  • The phone layout's column fits a short landscape screen (4.4.0, #227): on a phone in landscape under its browser's bars (e.g. 844 × 320), the right-hand column was 300 px wide, each bag slot a square 87 px tall, the verbs 44 to 56 px, and the tools row (map, pause, music) went out at the bottom of the screen; six items or more (the bag's page buttons) cut it on a 568 × 320 phone too. The column now fits its height: the slots stop being square first, then the verbs come down to 44 px, and only when 44 px no longer fits do verbs and slots share the room; the tools row never shrinks, and past a floor the column scrolls instead of cutting. The layout also reads the room inside the safe areas (notch, home indicator) instead of the whole screen. Nothing to change in a game.

  • The Studio checks a translation's verb keys too (4.4.0): a translation that gives two verbs the same key (fr: verbs.take.key › "F" is already the key of "close") was refused by npm run validate only; the Studio's guard, its processing worker and its in-browser demo said nothing. validate now runs that check whenever it is given the translations, so every caller says the same.

  • A translation's voice clips ship with the game (4.4.0): npm run assets encoded only audio.voices, so the clips of audio.voicesByLang (a French recording) never reached public/assets/audio/voices/ and a French page played nothing. They are now prepared like the others (keyed <lang>:<id> in assets.gen.json).

  • npm run voices -- check reads each language's own text (4.4.0): 4.3.1 taught status that a clip keyed on a switch only the translation has (Reviens sain{form?gull=e|*=} et sau{form?gull=ve|*=f} !) belongs to that line; check still read the source text and warned "a clip no line claims".

  • npm run e2e in two languages in one run (4.4.0): the e2e harness kept the verb labels of the first page it read, so a game script that reloaded the page in another language (?lang=fr) tapped the English labels. It reads them again after each load.

  • A Reality page that polls no longer piles up listeners (4.4.0): the HTTP port's wait between two polls (or two reconnections of the stream) added an abort listener to the port's signal and removed it only on an abort, never when the wait ended on time. A page polling once a second held some 86 000 more closures a day, for as long as it stayed open. Each wait now removes its listener when it ends, either way.

  • An email's HTML read with a Turkish capital İ hides its scripts and comments again (4.4.0): the email connector's htmlToText looked for tags in the message's lower case at the original's positions, and a character whose lower case is longer (İ is two characters in lower case) shifted them: after an İ, a <script>'s or a comment's content leaked into the text an answer is read from, and visible text after a <style> could vanish. Tags are now matched in ASCII lower case, as HTML names them, which keeps every position.

  • The Studio's simulated Bridge no longer leaks a listener per wait (4.4.0): each wait for a signal left an abort listener on the connection, as the Reality port's sleep did (fixed in the same release); a long Studio session gathered thousands.

  • The CLI's login says why the provider refused it (4.4.0): studio-server login ended on a bare ResponseBodyError when the OpenID provider refused the device login; it now names the provider's code and reason, e.g. not_allowed (Offline tokens not allowed for the user or client). STUDIO-SERVER says what that one needs: the client scope offline_access on the CLI client and the realm role offline_access for each member.

Changes

  • A hotspot can act on a single tap, as an exit can (4.4.0, #221): a hotspot that says onTap: 'go' answers a single tap with no verb picked as an exit with input.exitOnTap does (#188): the hero walks there and acts on arrival with the double tap's verb (defaultVerb). E.g. an endless aisle that says why one does not walk down it, the LucasArts "Walk to" that gives a comment. Without it a tap on a hotspot only walks there, as before; input.exitOnTap stays about exits. Input only: rules, solver and saves do not see it. Nothing to change in a game.

  • One map pin for a city with several places (4.4.0, #223): a place's portrait can be a list, its faces side by side on the pin, and a place that says joins: '<place>' sits on that place's pin instead of having its own: once it is unlocked its face is added there, a tap on a face travels to that face's place, from any of them the pin is "here" (only the current face ringed), and moving between two places of one pin draws no journey. E.g. a city whose reception and office upstairs are both destinations. A pin with one place renders as before; the validator wants the joined place to exist, in the same region, and not to join another itself. Nothing to change in a game.

  • The Studio's hero × language matrix (4.4.0): the Voices tab has a second view, "Heroes × languages", read-only and in the in-browser demo too. For each line with an id whose text (in any language) has a text switch or {hero}, a column per hero (each value the switched flags take, headed by the variant's portrait and name) and per language; each cell shows the text that hero reads, the clip that plays, and the keyed clip that is missing (the plain id named when it plays instead). A row flags an untranslated language, a translation that drops a choice the source makes, and a hero's value no switch of the line lists without *=; "Lines with a problem" keeps only those. The same data as GET hero-matrix on the Studio API, and as heroMatrix(game, locales) in src/engine/tools/hero-matrix.ts.

  • The semantic diff draws the rooms it names, before and after (4.4.0): in the Check tab, under "Changes between revisions", Capture the rooms (before / after) shows each room the changes name (its layout, its source, its texts in a language; the first room when only shared files changed, such as the cast or site.json; six at most) at both revisions, side by side, with the rectangle where they differ outlined, or "no visible change". A moved prop, a font or a text that overflows is seen at once. Each revision is built as a preview and drawn in headless Chromium at a fixed 960 × 600; the PNGs are kept under .cache/studio/. New route GET diff/<from>..<to>/captures. A local Studio only (npm run studio, with Playwright): a hosted Studio answers that the captures need one, its worker having no browser (docs/en/STUDIO.md "Check" lists the other limits).

  • A hosted preview's time, phase by phase (4.4.0): a preview's log line ("event":"preview") now says where its time goes: queuedMs (from the write to the build's start: the coalescing, a build before it, the retries), snapMs, jobMs (the worker's job as the Studio waits for it, its queue on the worker included), storeMs and the whole wait, sinceWriteMs. Each job the Studio asks of the worker logs a worker-call line (putMs, postMs, getMs, delMs), and the worker's own worker-job line says queueMs, fetchMs, unpackMs, childMs and putMs. Two histograms read them, studio_preview_phase_seconds{phase} and studio_worker_job_phase_seconds{kind,phase}; studio_preview_build_seconds is said to start after the snapshot, as it always did.

  • A faster hosted preview (4.4.0): the worker runs one job at a time per lane, a preview's build in a lane of its own, so it no longer waits behind the health's validator and solver (up to 45 s) or a load; each lane holds 8 jobs at most. The Studio packs one snapshot per revision, shared by the preview, the health, the published builds and the engine's upgrade (the lock, the repair and the walk once, not three times); stores a build's files 8 at a time; does not write again a blob it stored less than an hour ago; and answers once it has read the worker's result, the staging deleted after (a delete that fails is logged, worker-staging-left). Nothing changes for a game.

  • A playable example of choosing the hero (4.4.0): games/heroes, "Who Am I Today?", one garden and one puzzle for every 4.3 hero-choice feature at once. Three heroes on portrait cards at New Game (Pixel, Biscuit, Gaby the gull), their names in the lines ({hero}), Grandma's words switched for each ({form?…}), one rule only the gull gets, English and French (the French table adds its own agreements: Prête for the gull), Grandma's lines voiced per hero in both languages (garden.dust~form=gull in audio.voices and audio.voicesByLang.fr), and a save that keeps the hero. The sample game's art. GAME=heroes npm run dev plays it, GAME=heroes npm run solve -- --prove proves it for every hero, npm run e2e -- <url> --game heroes --prod picks each hero on its card in English and in French, saves, reloads, continues and finishes (in CI's reference job). CONTENT_GUIDE's hero section points to it.

  • The 137 mutation exemptions read again (4.4.0): each entry of docs/dev/mutants.json came due at 4.4.0 and was checked against the code. Four were not what they said and are now killed by a test (a delivery with an empty player id bound the save to it, the Bridge's re-sign cache hits were never counted, a badge fetch's own request timeout, a logout token whose events is null); reading them found the two fixes above. The other 133 keep their place, 16 with a reason rewritten, and are read again at 4.5.0.

4.3.1

21 octobre 2026Release sur GitHub

LOG #254–#255: "Voices by Language", a patch on 4.3.0 after an outside reading of it. A translation that agrees a line its source never agrees (Ready?, Prêt{form?witch=e|knight=|robot=} ?) now gets its voices by value through the whole chain: export, import, orphans, the validator's warning (the command line, the Studio's server and its in-browser demo alike) and playback. npm run solve is said to find one winning path, and --prove to prove every state, every hero included, in the guides and in the solver's own sentence. A new mutation set, voices, gated from the start. Nothing to change in a game (UPGRADING §40): no content field, save, schema or fingerprint moves (strict SemVer, as since 4.2).

Fixed

  • A translation's own switches get their voices (4.3.1): a translation may agree a line the source language never agrees, e.g. English Ready?, French Prêt{form?witch=e|knight=|robot=} ?. npm run voices -- export --lang fr already listed the three French clips (ready~form=witch…), but import refused them as unknown lines, status called their clips orphans, the Studio refused their status, and npm run validate said nothing when one was missing: those read the switches of the text as written, not as translated. Each language's clips now come from its own text everywhere, the command line, the Studio's server (its guard and the processing worker) and its in-browser demo alike; a switch a translation drops no longer asks for clips by value in that language either. The engine already played the right clip; a test now holds the whole chain. And a line whose switches list only a default ({form?*=there}) showed its braces in the recording table; it now reads as spoken.
  • npm run solve says it found one path, not a proof (4.3.1): the hero-choice guide said npm run solve "proves every hero"; it stops at the first winning path, which one hero may be enough for. A game where only the robot can finish printed solved; --prove reports its softlocks. The guide, CLASSICS, DESIGN and WORKFLOW now name npm run solve -- --prove for that, and the witness's sentence, shared by the command line, its JSON, the Studio and the MCP tool, reads "the ending is reached on one path (…; only --prove checks every state, every answer of a choice included)".

4.3.0

21 octobre 2026Release sur GitHub

LOG #246–#253: "Hero Choice". The player chooses the hero at New Game, among any number of heroes a game declares: portraits in a choice, a name per character variant, and text switches ({hero}, {form?knight=him|witch=her}) so the other characters' words follow the hero, in every language; voices follow the same keys (D49, ADR 0022). Most of this release comes from an outside contributor, Horacio (LostInBrittany), thank you: the hero choice itself (#202), a key per verb (#205), a proportional pixel font for the interface by default (#206), a job's late answer that no longer overwrites its result (#211), a naming convention for titles and branches (#208) and a conversation saved after each topic (#219); each was reviewed, completed on its branch and merged into main, the 4.3 line (D48). A game that uses none of it keeps its saves, its logic fingerprint and schemaVersion 3; its interface font changes unless it sets its own (UPGRADING §39). Breaking: the former web-scumm names, aliases through 4.2.x, are gone (D47). A game that still imports web-scumm/… runs npx quest-n-click migrate once, after installing quest-n-click (UPGRADING §39); the formats keep their names. The tag is signed with the maintainer's SSH key on their yes of 21 October, « yes sign v4.3.0 », the yes for v4.3.0 only (D37).

Breaking

  • The web-scumm names are gone (4.3.0, D47): the release no longer ships web-scumm-<version>.tgz, create-web-scumm, web-scumm-bridge, web-scumm-connectors or web-scumm-studio-server (nor their SBOMs), the packages no longer carry the web-scumm, web-scumm-bridge, web-scumm-connector, web-scumm-studio-server and web-scumm-studio-worker commands, and web-scumm/content and the other old imports no longer resolve. A game still under the old name runs npm uninstall web-scumm, installs quest-n-click, then npx quest-n-click migrate, which now renames its web-scumm/… imports, its tsconfig's paths and its scripts' commands (--check names what is left; UPGRADING §39 also gives the grep/sed by hand). On a hosted Studio such a game does not load until its owner runs the engine's upgrade, which renames its imports first, as a revision of its own judged by loading only (a game that cannot be finished yet is never stuck) and not undoable, then migrates its schema as before. Format identifiers (web-scumm-save, sessions, .wsrun, storage keys, headers, WEB_SCUMM_*) keep their names, and releases up to 4.2.5 keep their files.

Fixed

  • A prepared job keeps its result on screen (4.3.0, #210): on a hosted Studio, the Assets panel could replace "prepared from revision 3: 218 files" with "Preparing the assets: done" a moment after the job ended, when a status check sent before the end answered after it. Once a job has ended, the page hears nothing more of it, nor of a Cancel sent just before the end and refused after it (the panel used to add "(the job has ended)" to its last word). This also made the Firefox pass of npm run e2e:studio-hosted fail now and then.
  • A badge signed with a weak or foreign key is refused (4.3.0): the badge connector takes the key from the issuer's documents, and the algorithm's name said nothing of its strength. An eddsa-jcs-2022 proof is now checked with an Ed25519 key only (an RSA-512 key, whose signature is exactly 64 bytes, made a valid proof); a JWS (an OB3 JWT, an OB2 signed assertion and its publicKeyPem, a status list) signed RS256 needs a modulus of at least 2048 bits, and ES256 a P-256 key. A 512-bit RSA key can be factored, so a badge it signed could be forged. Such badges are now invalid.
  • The Bridge checks its limits before it starts (4.3.0): each of config.json's limits must be an integer, at least 1 for streamPage, bodyBytes and streamBufferBytes (streamPage: 0 made a fetch by cursor loop forever) and at least 0 for the others, and a name that is no limit is refused. Before, any value was taken. A Bridge given such a value does not start, and says which limit, the least it takes and what it was given; on a shared Bridge that stops every tenant (UPGRADING §39 says how to look first).
  • A run whose world cannot be hashed is refused, not a crash (4.3.0): a .wsrun whose world holds a number JSON reads as Infinity (1e400), or two keys equal once NFC-composed, is invalid-replay / world-shape; the verifier answered inconclusive / crash. Loading such a stored world says the same.
  • A conversation saves after each topic (4.3.0, #218): a conversation is one action, and the autosave came only when it ended, at "Bye". What a topic changed (a flag, an item, a minigame won) was lost if the page died before: on a phone, a tab killed in the background or a battery gone flat sent the player back before the topic. The state is now saved after each topic's do, once the topic is marked as seen, when the conversation is the rest of its action (the talk verb, or a { talk } that ends a rule); a talk followed by more commands is still saved at the action's end, so a save never holds half a do. A loaded save opens no conversation: the player is in the room, the topics seen marked. Nothing to change in a game.

Changes

  • The player chooses the hero at New Game (4.3.0): any number of heroes, one character with a variant per hero. A game asks in its intro with a choice whose options set a flag ({ set: ['form', 'witch'] }); the rest follows from that flag. Nothing changes for a game that does not use it (no new condition, command or state, saves and schemaVersion 3 unchanged). CONTENT_GUIDE, "Choosing the hero"; UPGRADING §39.

  • A character variant may carry a name (4.3.0): while the variant applies, the speech box, the transcript, the talk header and {name} in a fallback line call the character by it. Translated like any text (char:<id>/variants[<i>].name).

  • Text switches and {hero} (4.3.0): {hero} is the active hero's name; {form?knight=him|witch=her|*=it} is the text listed for the flag's current value, *= covering the rest. Filled when a text is shown (lines, choices, looks, hints, toasts, captions, item and object names, objective titles), after the translation, so each language writes its own words inside the braces (content{form?witch=e|*=}). The validator reports a malformed switch, a flag no command sets, a value listed that nothing writes and a value written that a switch does not cover; npm run i18n -- status lists the translations that drop a choice their source's switches make. Room, verb, character and variant names are shown as written: a switch there is an error. A 4.2 text holding a literal {hero} or {word?…=…} is now filled (UPGRADING §39).

  • Portraits in a choice (4.3.0): img on a choice option shows it as a card, the image with the option's text as its label and its accessible name; the cards wrap and scroll, so a phone fits any number of heroes.

  • Voices for lines with switches (4.3.0): a line whose words follow a flag ({form?m=him|f=her}) is recorded once per value: its clips go in audio.voices under <id>~<flag>=<value> (grandma_saw~form=f); with several flags in one line, sorted by name and joined with ~ (greet~form=f~mood=calm), values written as text (true, 2). The engine plays the clip for the flags as they stand and falls back to the plain id when that clip is missing or a flag is not set yet, so a game can ship before every recording exists. voicesByLang uses the same keys. npm run voices (and the Studio's Voices tab) lists one row per value, each with its text, and npm run validate warns when a voiced line lacks a value's clip. The keys are only map keys: the file names stay the author's (AUDIO.md, "Voices").

  • The Studio previews each hero (4.3.0): the Play tab's Text switches picker sets the flag a game's switches read (and shows {hero}'s name), and the Rooms tab shows under a switched text how it reads (local Studio; STUDIO.md).

  • npm run lint and the chosen hero (4.3.0): hero-looks-default (info) says when two or more values of the hero's flag match no variant, so those heroes would look like the written one; a rule that only sets a flag read by a character's variant or a text switch (the intro's choice) is no longer reported as action-dead.

  • A key per verb: O for Open, as in the LucasArts games (4.3.0, #203): a verb's optional key ({ id: 'open', label: 'Open', color: '#8fd3ff', key: 'o' }) picks it at the keyboard as a tap on its button would, in any case and without Ctrl, Alt or Meta, and does nothing while the engine is busy, a line is on screen or something covers the scene. The button underlines the letter in its label and announces it (aria-keyshortcuts). The key is translated with the label (verb:<id>/key). npm run validate refuses a key that is not one character, Space and a key two verbs share, in the game and in each translation. With settings: true the player can turn the keys off (a "Verb keys" row, ui.verbKeys, shown only when a verb has a key: WCAG 2.1.4), and the validator warns about keys in a game without a settings menu. Without key, nothing changes.

  • A proportional pixel font for the interface (4.3.0, #204). The default --font-ui is now Galmuri11 (OFL 1.1, a 125 KB Latin subset) instead of DotGothic16, a Japanese face whose fixed-width Latin left gaps around i and l and set the period a pixel above the line. Galmuri11 has a narrow i, a wide m and a period on the baseline, close to the LucasArts dialogue fonts, and a size-adjust keeps a game's text at the same size. DotGothic16 now stands behind every interface family, so Japanese text and the few symbols Galmuri11 lacks still render. A game that wants the old look sets skin.fonts.ui: 'DotGothic16'; a game that already sets skin.fonts.ui keeps its font, and a character its font lacks now falls back to DotGothic16 before 'Courier New' (UPGRADING §39). tools/subset-font.py checks the sha256 of the pinned Galmuri release before it cuts the subset.

  • main is the 4.3 line (4.3.0, D48): after v4.2.5 the six open pull requests (#202, #205, #206, #208, #211, #213) were reviewed, completed on their branches and merged into main, which prepares 4.3.0. The 4.2.x freeze of the content format and the public API (D40, eased by D46) no longer binds main; a 4.2.x fix, if one is needed, branches from v4.2.5.

  • A naming convention for titles and branches (4.3.0, #207): CONTRIBUTING.md says how an issue, a pull request and a branch are named: an issue starts with its template's prefix ([bug], [feature], [field]), a pull request reads <Area>: <what changes> (#issue) (a release's pass starts with its version), a branch starts with its kind (feature/, fix/, docs/…) and the issue's number. The pull request template's pointer to the branch prefixes now finds them there.

  • Pillow 13 (4.3.0, #213): the asset tools (npm run assets, tools/cut-sheet.py, check-alpha.py, pick-bg.py, the code wheel's PDF) and a hosted Studio's processing worker use Pillow 13.0.0. Nothing changes for an author: the sample game's images come out byte for byte the same, and no tool used an API that Pillow 13 removed or deprecated. Run python3 -m pip install -r requirements.txt again. The worker's lock (ops/studio/worker-requirements.lock) carries the new wheel's sha256, checked against PyPI.

  • The 158 mutation exemptions read again (4.3.0): each entry of docs/dev/mutants.json came due at 4.3.0 and was checked against the code. 28 were not what they said and are now killed by a test; two led to the fixes above and stay, their reason rewritten. The other 128 keep their place with a reviewed reason, and every entry now names its line and is read again at 4.4.0. 130 remain.

4.2.5

20 octobre 2026Release sur GitHub

LOG #244–#245: the project is now called quest'n'click. It was web-scumm, named in homage to SCUMM, the LucasArts engine whose spirit it follows, and is renamed so its name sparks no needless debate (D47). Nothing changes for a game: the formats keep their identifiers, so saves, runs and sessions load as before, and a game made for 4.2.4 installs, builds and plays under its old names. The old names (web-scumm, create-web-scumm, web-scumm-bridge, web-scumm-connectors, web-scumm-studio-server, the web-scumm/… imports) work until 4.3: each 4.2.x release also ships them as full copies. A release's files are named quest-n-click-… from this one, and its attestations name the repository wanoo/quest-n-click. The tag is signed with the maintainer's SSH key on their yes of 20 October, « Yes, sign v4.2.5 », the yes for v4.2.5 only (D37).

Changes

  • web-scumm becomes quest'n'click (4.2.5, D47): the project, its repository (github.com/wanoo/quest-n-click, the old links redirect) and its Pages site (wanoo.github.io/quest-n-click; the old address redirects there through wanoo.github.io) take the name quest'n'click. The packages and commands are now quest-n-click, create-quest-n-click, quest-n-click-bridge, quest-n-click-connectors (command quest-n-click-connector) and quest-n-click-studio-server (commands quest-n-click-studio-server, quest-n-click-studio-worker); a game imports quest-n-click/content, /player, /minigames, /testing, /reality; a release's files are named quest-n-click-<version>.tgz and quest-n-click-<tag>-…. Nothing to change in a game: through 4.2.x each release also ships web-scumm-<version>.tgz and the other old archives (the same files, the old package name), the old commands run after a one-line notice, and web-scumm/content and the other old imports still resolve; the aliases go in 4.3. The formats keep their identifiers (web-scumm-save, web-scumm-session, .wsrun, web-scumm-remix-1, the IndexedDB and localStorage names, the X-Web-Scumm-* headers, WEB_SCUMM_*), so saves, runs, sessions and deployments load and run as before. Releases up to v4.2.4 keep their files' names and their attestations' repository, wanoo/web-scumm. The READMEs open on the new logo and a short "Formerly web-scumm" note; UPGRADING §38 says how to switch, when you like.

  • The sample games' credits say quest'n'click (4.2.5): the Pantry Key's credits ("A quest'n'click sample game", "Sound effects: generated by quest'n'click") and the Night Market's credits and title-screen line ("The reference chapter of quest'n'click"), in English and French. Their presentation fingerprint moves with them, so the reference run is recorded again; their rules, saves and proofs do not change.

4.2.4

20 octobre 2026Release sur GitHub

LOG #241–#243: four pull requests from an outside contributor, Horacio (LostInBrittany), found while making his game "Jack cherche un taf", each reviewed, completed on its branch and merged: face: 'hero' turns a character toward the hero (#194); the sprite sheets' background, transparent or a chosen colour (#187); an exit gone through on a single tap (#191); actorGap, the hero's distance to a character following their size (#199). A game that uses none of them plays, saves and proves as on 4.2.3: the same fingerprints, golden saves and reference runs. The content format gains three optional, presentation-only fields, input.exitOnTap, an exit's onTap and actorGap, under D46 (a 4.2.x may add such a field when it needs no schema, save envelope or IR change); the public API and the save envelope are 4.2.0's. The tag is signed with the maintainer's SSH key on their yes of 20 October, « Yes, sign v4.2.4 », the yes for v4.2.4 only (D37).

Fixed

  • face: 'hero' turns a character toward the hero (4.2.4): { face: 'hero', who: 'grandma' } turned grandma to the right wherever the hero stood, since the hero is not a hotspot, a prop nor an actor; the face command now reads the hero's position from the state, as it already did for the hero turning toward someone. Found while making "Jack cherche un taf", in an opening cutscene.

Changes

  • The sprite sheets' background (4.2.4): "sheetBackground" in site.json, next to artStyle, is "transparent" or a #RRGGBB colour (default #2B2E45, so a game without it gets the same prompts as before). npm run prompts writes every sheet's background from it; in transparent mode the prompts ask for real alpha, forbid a painted checkerboard and keep carved or open shapes (mouths, eyes) opaque. tools/cut-sheet.py cuts a sheet on a transparent background on its alpha, keying nothing by colour (a flat-background sheet cuts exactly as before). Two small scripts help choose: tools/check-alpha.py tells real alpha from a painted checkerboard, and tools/pick-bg.py ranks the flat colours by their distance to a character's drawing (PROMPTS.md, "Choosing the sheet background").

  • An exit can be gone through on a single tap (4.2.4, #188): input: { exitOnTap: true } in game.ts makes a single tap on an exit with no verb picked walk there and go through on arrival, with the double tap's verb, as in the LucasArts classics; a locked exit says its locked line. A picked verb still wins, a tap elsewhere during the walk still redirects the hero (and nothing is gone through), a double tap is unchanged. An exit's onTap: 'go' | 'walk' overrides the game's setting either way; the Studio offers it on the exit's sheet. Off by default: a game without either plays exactly as before. Input only (presentation in the fingerprint): the rules, the solver and saves do not see it.

  • actorGap: the hero's distance to a character follows their size (4.2.4, #198): without an approach point in the layout, the hero stopped 44 units from any character, so tall characters who gesture as they talk put a hand in each other's head. actorGap on the game (e.g. 0.8) sets the distance to that share of the two characters' mean height, times the depth scale where the character stands; omitted, nothing changes, and an approach point still wins. depthScale and scaleAt move to core/stage.ts. Found while making "Jack cherche un taf".

4.2.3

20 octobre 2026Release sur GitHub

"Coordination" (LOG #218–#240): the last of the three 4.2.x steps (D40, D44). A team organises its game in the hosted Studio: a scenario map of chapters, puzzles and characters built on the game's stable ids, comments anchored on those ids, and a diff between two revisions that names what changed. A write says the revision its editor read, and a late one is refused with what changed, never merged on its own. AI agents work on the hosted Studio through its remote MCP, as their member; an owner pushes the game to a Git repository and imports it back; a game made on 4.1.x is upgraded on the server. The Assets tab works hosted (uploads, sheets cut on the worker), Prepare and the cuts can be followed and cancelled, and a worker installed from the release archives prepares assets. An operator gets metrics, a daily backup to the bucket and a runbook, run on Clever Cloud's test organisation. The Studio Server stays experimental until its people's passes are made (studio-hosted, studio-collab, studio-preview, studio-assets-hosted, and 4.2.3's studio-mcp-remote and studio-oidc-generic). Nothing a game or a player uses changed: the public API, the content format and the save envelope are 4.2.0's (D40). The tag is signed with the maintainer's SSH key on their yes of 19 October, « Yes, sign v4.2.3 » (D37, D44).

Fixed

  • A Studio Server worker installed from the release archives prepares assets (4.2.3): the engine's package carries the worker's Python lock (ops/studio/worker-requirements.lock), and the worker looks for the packages its build hook installed in its own folder (.python, or WORKER_PYTHON_PATH) rather than beside the engine. STUDIO-SERVER gives the hook for a worker from the archives. In 4.2.2 Prepare assets needed a worker deployed from the repository.

  • The daily backup ran nothing (4.2.3, found by the reference deployment): with the configuration in STUDIO_CONFIG_JSON (no --config), the Studio Server's commands dropped their first argument, so import <dir>, invite --email … and the daily backup --to-bucket of ops/studio/backup-cron.sh failed (a new file is required). Each now reads its arguments whole.

  • An operation another process confirmed is shipped (4.2.3): a game imported by the CLI against the same database emitted no event in the running instance, so it reached the bucket only at the next write there or a restart, and the RPO grew meanwhile. The import now ships its operation before it ends, and the instance also ships every 30 s.

  • invite reads its flags strictly (4.2.3): without --role, or with a flag where a value belongs, it took the first argument instead (invite owner --email a@b.fr invited an owner); it now refuses, as its usage says.

  • A slow start never serves older prepared assets (4.2.3): when an instance started, it wrote the latest prepared set again in the background; a Prepare that ended meanwhile could see the start finish after it, serve the older set again and delete the newer one's folder. The older set now steps aside.

  • A deploy no longer refuses writes for minutes (4.2.3, found by the reference deployment): on Clever Cloud the old instance lives on, renewing the service lease, for minutes after the new one is up, and was never seen to get SIGTERM, so writes got 503s for about 2.5 min at each deploy. The new instance now asks for the lease and the old one gives it back at its next renewal (10 s); an older instance never takes it from a newer one. A write that waited for another instance's journal lock is a 503 to retry, no longer a 409 a client could take as a conflict, and only the instance serving the writes repairs its folder (at once when it takes the lease, then every 30 s). The first deploy of 4.2.3 replaces an instance that does not ask or give back: it still waits as before; the next ones do not.

  • A worker on a machine whose clock is a little behind no longer refuses jobs (4.2.3, found by the reference deployment): the Studio signs a job to live 5 min to the millisecond, and the worker refused one that lived longer by its own clock, so a few milliseconds of skew between the two machines refused about half the jobs (health and previews failed with "refused the job (403)"). The worker now allows 30 s of skew, logs why it refuses a job, and the Studio says the worker's reason.

Changes

  • The gated mutation sets read again before 4.2.2's tag (4.2.2): a survivor of mutation (core) found by the 4.2.2 candidate (a slot's revision never copied) is killed by a test, and the 16 new survivors of studio-ops in the journal's reads of a past revision are killed (15) or named (1). No behaviour changed; the tag v4.2.2 was made on this commit, and its note is filed here.

  • A plan for the coordination (4.2.3): docs/dev/PLAN-4.2.3-COORDINATION.md (in French), ten guarantees each with its limit and its test (H1–H10), the pull requests in order, the exit criteria, what stays open after 4.2.3, and decisions D44 (the name, GitLab as the second OpenID provider, a deploy key, the diff's captures local) and D45 (no captures in 4.2.3's diff).

  • A write says what its editor read (4.2.3): on a hosted Studio each write carries the revision its editor read (x-studio-base). When one of its files was written since, it is refused (409) with that file as it was read and as it is now (the same tab's own writes since never refuse it): the Studio shows the lines that changed, keeps the draft, and offers to apply the edit on the latest version, or for a whole document (a layout, the storyboard) to replace the other change, said as such; never on its own.

  • The scenario map (4.2.3): a Studio tab, Map, where a team declares its chapters, puzzles and characters, each a list of what the game already names (a room, a rule id, an item, a character, an objective, a checkpoint), shown with the file:line that writes it, or "not declared". Kept in games/<id>/groupings.json, a span of its own on a shared Studio; a grouping never writes what it names. The map is derived from the game's IR at each read, never stored. Routes: GET/PUT groupings, GET map.

  • The Studio Server's workshop mutation set is gated (4.2.3): the 104 survivors of studio-workshop (the backup, the journal shipped, the collection, the health, the previews, the builds and the prepared assets) are read one by one: 94 killed by short tests (tests/studio-workshop-{backup,health,previews,prepared}-mutants.test.ts), 10 named in docs/dev/mutants.json. A survivor not named now fails the set's job, as for the other sets.

  • The MCP on a hosted Studio (4.2.3, ADR 0029): a Studio Server serves its game's MCP tools at /mcp (MCP's Streamable HTTP, stateless), for an access token of the Studio's audience, never the session cookie; it says where its authorization is (/.well-known/oauth-protected-resource, RFC 9728). Each tool is a request to the server's own API with the caller's token: an agent writes as its member, with their role, their spans, in the journal. npm run mcp -- --server <url> points the local MCP server at a hosted Studio the same way (the CLI's token).

  • The game in Git (4.2.3): a hosted Studio configured with git (a repository, a branch, a folder; its deploy key the secret STUDIO_GIT_KEY) lets an owner push the revisions since the last push as one commit, each author a Co-authored-by, never forced (a branch that moved is refused), and import the branch as one revision. The Play tab shows where the game goes and the last revision pushed.

  • A hosted game made on 4.1.x is upgraded to this engine's authoring schema from the Studio Server (4.2.3): an owner's POST /api/engine/migrate runs web-scumm migrate on the processing worker under their exclusive lease (the stable ids, then schemaVersion: 3, saveVersion + 1 and the ids' migration step in game.ts), judges the migrated game there (the validator, then the solver), and writes it as one undoable revision; a game it would break, or that the solver cannot judge in time, is refused (422) and nothing is written. A game already at schema 3 has nothing to do. STUDIO-SERVER (en, fr): "Upgrading the engine of a hosted game".

  • Comments on a shared Studio (4.2.3): in the Notes tab, threads anchored on a stable id of the game (or the game) and the revision they were written at, open to reviewers (kept by the Studio, never in a game file); an anchor the game no longer names is said orphan; an owner brings the notes of notes.json in.

  • What changed between two revisions, by name (4.2.3): the Check tab's "Changes between revisions" (and GET diff/<from>..<to>) says, file by file, the ids a source added, removed or changed with the fields that changed, the value paths of a JSON file, or "file changed" when nothing more can be said. The sources are read, never run.

  • The gates of 4.2.3: two human passes asked from 4.2.3 on, studio-mcp-remote (an AI agent on a hosted Studio through its remote MCP) and studio-oidc-generic (the Studio Server behind a second OpenID Connect provider, GitLab).

  • The Assets tab on a hosted Studio (4.2.3): a team on the Studio Server now uploads sounds and decors, cuts generated sprite sheets and keys single cells from the Assets tab, as on a local Studio. An upload is 10 MiB at most (413) and its type is read from its bytes (PNG, JPEG or WebP images; Ogg, MP3 or WAV sounds; 415 otherwise); it is written as one revision, never run. The cutting runs on the processing worker under the project's exclusive lease, and its cells, with the backups of those recut, are one revision. The uploaded sheet itself is not kept on a hosted Studio.

  • For scripts calling the Studio Server (4.2.3): POST /api/assets/prepare now answers 202 with a job instead of the prepared run; poll GET /api/jobs/<id> until its state is done (or failed, cancelled), and read the run in its result.

  • Prepare assets and the cuts can be followed and cancelled (4.2.3): on a hosted Studio they are jobs; their states (waiting, running and its step, done, failed, cancelled) come live, in a small panel with a Cancel button. A job cancelled before it writes leaves nothing, and gives the project's exclusive lease back.

  • The Studio Server's metrics and daily backup (4.2.3, H9): with the secret STUDIO_METRICS_TOKEN, GET /metrics answers in Prometheus' text format to that token only (never the session, never the game's origin): the journal's operation latency, the worker's jobs in flight and their duration by kind, the lease conflicts (423), the previews' build time, the RPO, the revision. studio-server backup --to-bucket [--keep n] writes the backup to the instance's bucket (backups/<date>-r<revision>.json) and keeps the n most recent; clevercloud/cron.json runs it every day on an app that sets STUDIO_BACKUP_CRON=1. STUDIO-SERVER gains its runbook ("Running it: the reference deployment": the overlapping redeploy and the engine upgrade, the alerts, the restore drill).

  • The Studio Server's runbook, after its reference run on Clever Cloud (4.2.3, H9): STUDIO-SERVER (en, fr) now says how to create the realm on Clever's managed Keycloak (its first admin password is temporary, admin-cli refuses it, the console's realm import works), which fields of ops/studio/keycloak/realm.json to change for a hosted Studio, to leave APP_FOLDER unset, to put -- before a key's value in clever env set, to link both apps (clever link --alias) before clever deploy --alias, that the shell of clever ssh has the app's variables and that clever ssh --command waits for the old instance to go, how to keep a part of clever logs, that /metrics on the game's origin is a 401 (its session gate), what an overlapping redeploy costs (measured: writes refused 10 to 16 s once the lease is handed over, about 2.5 min from a version without the handover; twice the database connections), and how to compare a restore drill through the API.

  • The exclusive lease's runner is under mutation testing (4.2.3): studio-auth's tests now pin how a member's long operation takes the project's lease, gives it back and renews it (19 mutants that survived before are killed).

  • The 4.2.3 fixes are under mutation testing (4.2.3): a closed shipper and a start dropping an older prepared set are pinned by studio-workshop's tests.

  • The cost of a Studio Server redeploy, measured with the lease handover (4.2.3): STUDIO-SERVER (en, fr) now says what a redeploy costs on Clever Cloud once both versions hand the service lease over: the lease taken 6 to 10 s after the new instance listens, writes refused (503 with Retry-After) for 10 to 16 s while Clever still routes to the old instance, none lost; and that the first deploy from a version without the handover still refuses writes for about two and a half minutes. The /readyz alert goes back to 2 min, expected once on that first deploy.

4.2.2

19 octobre 2026Release sur GitHub

"Playable Workshop" (LOG #206–#217): the second of the three 4.2.x steps (D40, D42). On a hosted Studio, everyone plays the shared revision without a dev server: the processing worker builds a preview of each revision, and a save carried into another revision asks before it is resumed (ADR 0026). Every diagnostic names its revision; the header gives the game's health at the head and, when it breaks, the revision that broke it. Undo and redo come back, each member's own. The team publishes immutable, numbered builds with signed manifests, prepares the game's assets on the worker (sprite sheets and uploads stay local until 4.2.3), and an operator backs the Studio up, ships its journal to the bucket (the RPO is in /readyz) and restores it. Vite has left the Studio Server. It stays experimental until its people's passes are made (studio-hosted, studio-collab, and 4.2.2's studio-preview and studio-assets-hosted). Nothing a game or a player uses changed: the public API, the content format and the save envelope are 4.2.0's (D40). The tag is signed with the maintainer's SSH key on their yes of 18 October, « yes sign 4.2.2 » (D37, D42).

Changes

  • Dependencies (4.2.2): zod 4.6.7, Playwright 1.64.1, @axe-core/playwright 4.14, @vitest/coverage-v8 5.0.5; the bridge's locks follow zod. Nothing changes for a player, an author or a host.

  • The hosted Studio shows the preview of a revision (4.2.2): after each change the processing worker builds the game at the head (a new job kind, preview; a burst of changes builds its last revision only), the Studio stores its files by content and serves them on the game's origin at /preview/<rev>/, behind the game session. The room view loads a newer preview once it is built, unless a placement is not saved; the label under it names the revision shown, the one building, and a revision that does not build, with its reason. The editing tools enter the preview's build (VITE_STUDIO_PREVIEW); the player's build is unchanged.

  • Vite has left the Studio Server (4.2.2): 4.2.1's live preview (Vite in middleware mode on the game's origin) is gone, with its allow-list of engine paths; the game's origin serves a revision's built files and the prepared assets of the prepared set and publicDir, and the published builds. The worker still needs the engine's Vite to build.

  • A save made in another revision asks before it is resumed (4.2.2, ADR 0026): in a hosted Studio's preview, a save records the revision and the fingerprint of the content it was made in (beside it: the save format does not change). Resumed where the content differs, on Continue or from a slot, the game asks: Resume here (it may fail if this revision removed what the save refers to), Restart, or Stay in the save's own revision. The same content under another revision number asks nothing. Escape cancels the question. The player's builds are unchanged.

  • A plan for the playable workshop (4.2.2): docs/dev/PLAN-4.2.2-ATELIER-JOUABLE.md, eight guarantees each with its limit and its test, the pull requests in order, the exit criteria, and decision D42 (the preview's path, the fresh rebuild, the RPO target, public builds).

  • Every diagnostic says its revision (4.2.2): on a hosted Studio each read answer carries the revision it was made at (x-studio-revision); the Check tab shows it, and marks its results stale as soon as the head moves on.

  • Lint, the solver and the playtests run on the processing worker (4.2.2), with the game's own commands (before, a hosted Studio ran them on the data it read back, without them); the solver is bounded to 45 s there.

  • The game's health by revision, and the revision that broke it (4.2.2): a hosted Studio's header says the verdict at a revision (the validator, then the solver from a new game, on the processing worker, after each burst of changes). When a revision fails where the last judged one passed, a bisection names the first failing revision, with what its operation did and its files, in ⌈log₂ n⌉ checks; a dev can ask for one (POST /api/health/bisect). A solver cut short says "unknown", never "valid".

  • Personal undo on a shared Studio (4.2.2): Undo and Redo come back on a hosted Studio, each member's own: their last operation is undone by a new revision (its files back to their content before), whatever the others wrote meanwhile; refused when another wrote one of its files since (409) or holds its span (423). A new edit drops the member's redo; the stacks are kept in the database.

  • Published builds (4.2.2): a hosted Studio's Play tab publishes the head as an immutable, numbered build of the player's game, served at /play/<n>/ on the game's origin; /play/ is the last green build (its revision judged valid). Each build's manifest (revision, engine, snapshot and file hashes) is signed with the Studio's job key; an owner can rebuild one fresh and compare it byte for byte. publicBuilds (off by default) opens /play/ without a session.

  • The asset tools on the processing worker, qualified (4.2.2): their Python packages are locked with hashes for Clever Cloud's native runtime (ops/studio/worker-requirements.lock) and installed by the worker's build hook (STUDIO-SERVER gives the variable); measured on the spike's worker: 7 s to install, 4 s for a full prepare of the sample game (docs/dev/QUALIF-4.2.2-ASSETS.md).

  • Prepare assets on a hosted Studio (4.2.2): the Play tab's "Prepare assets" (a dev) runs prepare on the processing worker from the head's art and sounds, under the project's exclusive lease; the prepared set is kept by the Studio and served before publicDir, and the game's assets.gen.json is written as one operation. Sprite sheets and uploads stay on a local Studio (4.2.3).

  • Backup, restore and the journal shipped (4.2.2): a hosted Studio ships each confirmed operation to the bucket (/readyz gives the RPO); studio-server backup writes the project's tables in one snapshot, restore --replay brings them back into an empty database then replays the journal shipped since, each operation checked against the head it continues (replay resumes a replay that stopped); gc lists (and with --delete deletes) the blobs no row names and the worker's old staging.

  • The gates of 4.2.2: two human passes asked from 4.2.2 on, studio-preview (people play the shared revision of a hosted Studio without a dev server, carry a save across revisions, read the health chip, undo their own work, publish a build) and studio-assets-hosted (prepare on the worker, its set played); a mutation set studio-workshop (undo, health, previews, builds, prepared assets, backup, the journal shipped, the collection), measured until its survivors are read.

4.2.1

18 octobre 2026Release sur GitHub

"Shared Floor" (LOG #188–#205): the Studio online for a team, the first of three 4.2.x steps (D40). One game per server, several people at once: they log in through an OpenID Connect provider (Keycloak or another), get a role in the Studio, and edit the same game live, each change one operation of a durable journal, each part of the game held by one member at a time. The game's code runs in a separate processing worker that holds no secret, and the game is played on its own origin, apart from anyone's Studio session. It is deployed as two native Node.js applications on Clever Cloud with PostgreSQL and Cellar, from the repository or from the new web-scumm-studio-server archive. It is experimental until its two people's passes are made (studio-hosted, studio-collab): the game in the Studio is a live preview, not frozen at a revision (per-revision previews come in 4.2.2), and a real Safari has not tried the game's frame yet. Nothing a game or a player uses changed: the public API, the content format and the save envelope are 4.2.0's (D40). An author may notice that the Studio and the tools that write a game need Node 22.13+ (their writes go through the journal, in SQLite) and refuse an edit that would add a validation error (UPGRADING §34). The tag is signed with the maintainer's SSH key on their yes of 18 October (D37).

Fixed

  • The e2e run tolerates WebKit's "Context is stopped" during its teardown (WebKit stopping its context as the run ends, a Cache API call cut short): reported as a warning, as e2e:pwa does since 4.1.20, only for an error that came after every check passed, on WebKit, in that exact form.

Changes

  • A plan to put the Studio online for a team (4.2.1 → 4.2.3): docs/dev/PLAN-4.2.1-4.2.3-STUDIO-SERVER.md, behind OpenID Connect on Clever Cloud's native Node.js runtime, with ADR 0025 and decisions D39 to D41.

  • The Studio's operation core moves to studio-server/src/ops/ (4.2.1, neutral). core.ts, source.ts, apply.ts and errors.ts leave tools/studio/ for the Studio Server's package, where the transactional operations of the next pull requests grow. Nothing an author sees changes: the dev server, the MCP server, npm run studio-apply and the demo Studio run the same code from its new place. The browser Studio now names its API through one constant (src/studio/api-base.ts) instead of spelling /__studio/api in six modules.

  • Every Studio write is one journaled operation (4.2.1). The files an edit touches are written together or not at all, durably, and an edit a crash interrupted is finished when the Studio opens again (.cache/studio-journal/). A change made outside the Studio (an editor, an agent, git) is recorded before the next edit touches that file; a file another process rewrote while an edit was prepared is a 409, never a lost write.

  • One guard for every edit, on the game as it would be (4.2.1): text edits, layouts, new entities and voices are refused (422) when they would add a validation error, like structured values since 3.4. Nothing is written first and taken back any more.

  • Adding a prop, hotspot or actor is one step of undo (4.2.1): the room and its layout come back together.

  • The authoring tools write a game through the Studio's journal (4.2.1):

    • npm run ids, i18n, voices (import), import-layout, provenance -- --lock, page:storyboard -- --md, prompts and remix write their game files as one operation of the journal;
    • asset uploads (sheets, cells, decors, sounds) too. Backups and the cells a cut produced land together, or none of them;
    • a Studio open on the same game sees each operation, and a file it rewrote meanwhile is a 409.
    • The batch tools (ids, i18n, provenance) are journaled, not guarded: they rename paths, which changes the text of existing validation errors.
  • The placement editor saves through the Studio's API (4.2.1): PUT /__studio/api/room/<id>/layout, one guarded operation. The dev server's /__layout route is gone; layoutWriter() stays exported as a deprecated plugin that does nothing.

  • studio-server, the Studio as a server (4.2.1, first part; authentication comes next and it serves the loopback only until then). studio-server serve --config <file> serves the Studio's API under /api/, with:

    • the journal's cursor (/api/revision, /api/ops?epoch&after);
    • the probes /livez, /startz (started) and /readyz (serving writes);
    • a service lease so that one instance serves the writes;
    • operation ids from the client (x-studio-op): a retry gets the same answer and is applied once.

    Other commands:

    • studio-server import <dir> makes a game the instance's project;
    • studio-server export <dir> writes the game at its head with a manifest of hashes.

    On Postgres and an S3 store (Cellar), the database and the blobs are the truth and the instance's folder is rebuilt at start and checked every 30 s.

  • The Studio Server behind OpenID Connect (4.2.1).

    • Login: with Keycloak or any OIDC provider (Authorization Code + PKCE). The rights live in the Studio: owner, dev or reviewer, given by one-time invitations to a verified email (studio-server invite, POST /api/invitations). The owner's routes: members, roles, sessions, audit.
    • Mutations: a mutation from the browser needs the exact Origin and the X-Studio-Request header. The CLI and MCP use Bearer access tokens for the Studio's audience, obtained with studio-server login --server <url> (device flow).
    • Access ends:
      • at the next request when a member is removed;
      • at once on the provider's back-channel logout;
      • within 15 min when the provider disables the user, or when it stays unreachable (the session is then suspended until it answers).
    • Without OIDC: the server stays on the loopback.
  • A hosted Studio never runs the game's code itself (4.2.1, ADR 0028).

    • With OIDC, validation and the loads behind the Studio's reads run in studio-worker, a separate application with no secret of the platform. Configure worker: { url, instance } and the secret STUDIO_WORKER_KEY; create the keys with studio-server worker-keys --out <folder>. The worker needs WORKER_PUBLIC_KEY and WORKER_INSTANCE.
    • Captures are unavailable on a hosted Studio for now.
    • Without OIDC (the local server, on the loopback), nothing changes.
    • A game that imports files outside its own folder cannot run on a hosted Studio.
  • Working together on a shared Studio (4.2.1).

    • Each file has a span: a room (its code and layout), a language, the story, the items, and so on. A write takes the spans it touches. If another member is editing one of them, the write is refused (423) with their name, and the client keeps its draft. A lease lasts 2 min and is renewed by the heartbeat (POST /api/presence). Members can take spans ahead (POST /api/leases) and release them.
    • Owners can take a span over (audited) and take the whole game for an exclusive change.
    • GET /api/events streams every revision (with its files and spans), the leases and who is here. A stream closes as soon as its member is removed or logged out, or when its token ends.
    • Undo is not available on a shared Studio until 4.2.2.
  • The game runs on its own origin (4.2.1): a hosted Studio serves everything that runs the game on playUrl, never on the Studio's origin. The Studio opens it with a one-time ticket (POST /api/play-ticket). The game's access ends at the next request when the member is removed or their Studio session ends.

  • A guide to put a Studio online (4.2.1): docs/en/STUDIO-SERVER.md (French: docs/fr/STUDIO-SERVER.md) deploys the Studio Server and its worker as two native Node.js applications on Clever Cloud, with PostgreSQL, Cellar and Keycloak or another OpenID provider. An example configuration (ops/studio/studio.config.example.json), a test Keycloak realm (ops/studio/keycloak/realm.json) and an optional local stack (ops/studio/compose.yml) come with it. npm run studio-server and npm run studio-worker start them; serve also takes its configuration from STUDIO_CONFIG_JSON.

  • The hosted Studio is tested against a real Postgres and S3 in CI (4.2.1): a studio-hosted job runs its hosted tests on every change to the Studio Server; its code counts in the coverage, with floors on its trust boundaries.

  • The Studio Server's journal is held by mutation testing (4.2.1): the set studio-ops (the journal, its durable writes, the spans) is gated, every surviving mutant killed by a test or named.

  • The Studio Server's trust boundaries are held by mutation testing (4.2.1): the set studio-auth (the gate, the OpenID client, the routes and store of authentication, the spans' leases, the game's origin) is gated, every surviving mutant killed by a test or named.

  • The Studio Server is tested against a real Keycloak in CI (4.2.1): a login through Keycloak's own form, the identity it gives, a back-channel logout Keycloak sends, and a user an admin disables.

  • The hosted Studio opens in a browser (4.2.1): the Studio Server serves the Studio's page (npm run studio-client builds it; CC_POST_BUILD_HOOK on Clever Cloud), which talks to /api and logs in through the provider. The game runs on its own origin as a live preview (Vite behind the game's session, not frozen at a revision): the Rooms editor and the Play tab open there with a ticket, and the editor hands its layout to the Studio, which writes it as the member. Undo, the Assistant and the Assets tab are not on a hosted Studio yet; the Play tab's inspector panels need the local Studio. New optional settings publicDir and clientDir (docs/en/STUDIO-SERVER.md).

  • The Studio Server as a release archive (4.2.1): web-scumm-studio-server-<version>.tgz carries the commands web-scumm-studio-server and web-scumm-studio-worker, the Studio's page already built, and its own dependencies (their versions locked in its npm-shrinkwrap.json and SBOM for an install from the registry; from the archive file npm resolves them from their ranges, and web-scumm-studio-server deps says what an install resolved). Install it beside the engine of the same version (docs/en/STUDIO-SERVER.md, "From the release archives"). Two people's passes join the sheet from 4.2.1: studio-hosted (a real Clever Cloud deployment, a real Safari) and studio-collab (three people, a cut, a redeploy).

  • The shared Studio's screens (4.2.1): on a hosted Studio, a visitor sees a login page and someone who is not a member sees "access not granted", with nothing of the game; a session the identity provider has not confirmed for 15 min shows "identity provider unreachable". In the header: the exact revision the editors and reads are at, who is here and where, and your name with the spans, the members (owner) and the logout. A write refused because another member holds its span (423) or because its file changed (409) keeps the draft to copy and try again; an owner may take a span over. The owner's Members screen changes roles, removes members, ends sessions, makes invitation links and shows the audit log. A cut of the connection is shown, and what was committed meanwhile is read back from the journal; a write cut off is sent again with the same id and applies once. A note's author is the session's.

4.2.0

16 octobre 2026Release sur GitHub

"Stable World" (LOG #185–#187): 4.1.20's code, the version changed (plan 4.1.20 §12, D34), one Field Kit fix, and ship preflight-stable reading the reduced verdict below. It restores strict SemVer: from 4.2.0 a minor adds, a major removes. It ships before the people's passes (D38, the maintainer's decision): none of the thirteen (#75–#87) was made, so 4.2.0 promises as stable only what needs none, the content format and its tools, the desktop Chromium PWA and the Bridge on SQLite, and says every other surface limited or experimental (GO / NO-GO 4.2: GO on a reduced scope). The passes are made on v4.2.0 itself; promoting a surface in a 4.2.x needs its pass on that 4.2.x (D38). The tag is signed with the maintainer's SSH key (D37) and the release is immutable. npm publishing moves to a later 4.2.x: it needs the maintainer's 2FA at each step. v4.2.0-rc.1 carried the defect below; v4.2.0-rc.2 was the campaign's candidate, no pass used either.

Fixed

  • field:check and field:report work on a folder fresh from field:init (4.2.0): field:init writes the candidate's surfaces and site files beside the thirteen reports (4.1.20), and the two read them as reports, so the first check of every pass failed and field:report wrote nothing. Found trying the commands on v4.2.0-rc.1's candidate before any pass began; the test folder now holds what field:init writes.

Changes

  • The README installs from 4.2.0's release archive (4.2.0): releases/download/v4.2.0/web-scumm-4.2.0.tgz. docs:truth, quality:baseline and readme-quickstart still accept a v<version>-rc.N release for this version's archive and nothing else, for the next campaign that needs one.
  • ship preflight-stable reads "GO for 4.2 on a reduced scope" (4.2.0, D38): the maintainer's decision to ship before the passes is not a GO, so no report contradicts it; a plain GO the reports contradict still blocks, and tests/support-truth.test.ts accepts the reduced verdict only with D38 written.

4.1.20

15 octobre 2026Release sur GitHub

"Last Mile" (LOG #184): no new mechanic; the last 4.1.x release with code before 4.2. A Bridge backup is schema 4 and says whether it holds the whole store, the quota included, under a digest of its whole envelope (ADR 0023). A restore names its mode, merge or replace-store, and runs only with the store to itself under a maintenance lease (migration 0005); replace-store writes a verified safety backup first. The last two measured mutation sets, connectors-edge and reality-store-postgres, gate, in shards from one matrix. Every candidate a tag may name runs the full qualification profile on its own archives and image, and three loads per store. A people's pass is bound to the files of its surface and carried to a later candidate only when none changed, the version included (ADR 0024). A release is a draft first, then immutable; 4.2's tag signing, preflight and npm runbook are rehearsed without publishing. GO / NO-GO 4.2: NO-GO, the people's passes run on the 4.2.0-rc candidate. Dated the day its final candidate froze; the tag's own date is in the release notes.

Breaking

  • restore says its mode (4.1.20): bridge restore without --mode restores nothing. --mode=merge is 4.1.19's restore unchanged. --mode=replace-store --yes-replace-store --safety-out=<new file> makes the store the backup, after a safety backup of the store, written and checked first (docs/en/UPGRADING.md §32).

Fixed

  • A connector no longer crashes on an unexpected rejection (4.1.20): the badge and email connectors' bookkeeping of requests in flight, the IMAP poll's timer, the SSH password login and the /health handler each catch a rejection now. A pairing that throws refuses the SSH login, and a health() that throws answers 503. None of them can be reached under today's contracts.
  • A secret that is not a string is refused at start (4.1.20): an inline secret, password or bridge.token written as a JSON number or object used to be accepted, and then every request failed with a 500.
  • The IMAP client closes its socket after a greeting that is not OK (4.1.20): it used to stay open until the server hung up.
  • A stopping Telnet connector reads nothing more (4.1.20): in the second before its sockets are destroyed, bytes a client still sends are no longer read, even if the connector starts again. docs/en/CONNECTORS.md says that /health answers 503 when unhealthy and that an inline secret must be a string.

Changes

  • Dependencies of 11 October (after 4.1.19): Playwright 1.64, the MCP SDK 1.32.1, knip 6.40, cross-env 10 (a CLI here; Node 20+, which the project's 22.12 meets); the workflows' actions pinned to checkout 7.0.1, setup-node 7.1.0, upload-artifact 7.0.2, download-artifact 8.0.2 (a hash mismatch now fails, as the release wants) and attest-build-provenance 4.2.2 (a wrapper on actions/attest), each SHA checked against its tag.

  • @types/node stays on the oldest supported Node (22): Dependabot no longer proposes its majors; Node 26's types reject code that is right on 22.

  • 4.1.20 "Last Mile" planned (4.1.20): docs/dev/PLAN-4.1.20-LAST-MILE.md (French) and D34–D36 set out the last 4.1.x release with code. Its PRs plan to make the two measured mutation sets gating, have the candidate qualify the exact archives and image it publishes, give a Bridge backup its completeness and restore two explicit modes, carry a human pass only across identical artefacts, and rehearse 4.2's signature and npm publication. The field campaign moves to the 4.2.0 release candidate.

  • The Bridge's backup code is split by responsibility (4.1.20): bridge/src/backup/ holds the file's schema, its canonical form, its bounded and atomic files, the snapshot and the restore, with index.ts as the public face. No behaviour changes: a test pins the messages a refused file gets, the command line's codes and lines and a backup's canonical form, and it passes before and after. The layers test now walks the Bridge's sub-folders.

  • The Postgres adapter's mutation set gates, judged on a real Postgres (4.1.20): reality-store-postgres joins the gated sets, with 40 of its 41 mutants killed and the last named as equivalent. Its tests each get a database of their own and run one file after the other. CI's bridge-postgres job runs the set's eight files, not five.

  • Mutation in shards, from one matrix (4.1.20): a long set runs in shards (--shard=i/n), which are then merged into the set's report (--merge). The merge refuses a shard that is missing, doubled, cancelled or of other inputs. ci, the nightly, the candidate and the release run the matrix that tools/mutation-shards.ts generates, and no workflow writes its own list of sets. release.yml refuses a tag whose commit generates none.

  • Every named survivor is an exemption with its terms (4.1.20): each entry of docs/dev/mutants.json says its category, why, proof, owner and the version it is read again at. An incomplete entry or a version that has come fails the run.

  • A mutation run that cannot start says why (4.1.20): it names each failed test and its first error.

  • A Bridge backup is schema 4 (4.1.20, ADR 0023): its digest covers the whole envelope, not only its rows. It says what it holds (completeness), and it carries the speedrun quota, which no backup carried before. Every SQL table is classified, so a future table cannot slip out of a "whole store" backup. Files of schemas 1 to 3 are still restored, with --mode=merge. The digest is an integrity check, not a signature.

  • A restore has the store alone, or nothing (4.1.20, ADR 0023): from store schema 5 (migration 0005), serve and every writing command hold a maintenance lease in the store itself. restore and a migration down take the store alone. They are refused at once, naming who holds it, while anything else serves or writes. Leases run on the database's clock, and an expired lease is never in the way. On Postgres an advisory lock rides beside the lease, on a connection of its own (--lock-timeout). A serve that loses its lease holds its writes (503) and fails /readyz until it takes one again. Stop every Bridge older than 4.1.20 before the first restore (docs/en/UPGRADING.md §32).

  • Every surviving mutant of badge verification read (4.1.20): the 197 survivors of connectors-edge in connectors/src/badges/verify.ts (168) and connector.ts (29) were read one by one. 193 are killed by tests/connectors-badges-edge-mutants.test.ts, which checks each refusal's exact reason, the exact bounds of dates, counts, indexes and the cache, each shape a document may take, and what the badge form answers and rejects. The other 4 are named as equivalent in docs/dev/mutants.json: two key lookups whose forced branch gives the same null, and two fetcher options passed as undefined, which the fetcher reads the same as absent.

  • Every surviving mutant of the email connector read (4.1.20): the 116 survivors of connectors-edge in connectors/src/email/connector.ts (70), imap.ts (40) and parse.ts (6) were read one by one. 109 are killed by tests/connectors-email-edge-mutants.test.ts, which checks the configuration, each refusal's exact reason, the sender links and their bound of 10,000, the webhook's answers, the IMAP pass (sizes, flags, retention, its timer) and every bound of the IMAP client byte for byte, against an in-memory IMAP peer. The other 7 are named in docs/dev/mutants.json: 4 equivalent and 3 unreachable.

  • Every surviving mutant of the terminal connectors read (4.1.20): the 113 survivors of connectors-edge in connectors/src/ssh/connector.ts (44), terminal/shell.ts (32), telnet/connector.ts (25) and terminal/guard.ts (12) were read one by one. 111 are killed by tests/connectors-terminal-edge-mutants.test.ts, which checks the address gate and the session guard at each bound, the resume words and their bound of 10,000, every branch of the virtual shell, Telnet negotiation byte for byte, and both servers' authentication, sessions, closes and refusals, against fake sockets and fake ssh2 connections. The other 2 are named in docs/dev/mutants.json: 1 equivalent and 1 unreachable.

  • Every surviving mutant of what the connectors share read (4.1.20): the 62 survivors of connectors-edge in connectors/src/run.ts (28), http.ts (18), registry.ts (12) and config.ts (4) were read one by one. 60 are killed by tests/connectors-rest-edge-mutants.test.ts, which runs web-scumm-connector in-process (its arguments, --config=, the token from a file or inline, the manifest from a file or the Bridge, the tenant and limits, the health and metrics port and its 404s and 503), reads a body at its limit, the per-address budget at the minute and its sweep above 10,000 addresses, and builds each connector from the registry. The other 2 are named in docs/dev/mutants.json: 1 equivalent and 1 unreachable.

  • Every mutation set gates (4.1.20): connectors-edge joins the gated sets, in 4 shards, and the measured list is empty. Its 488 survivors (12 October) were read family by family: 473 killed by tests, 15 named, each with its argument or its test.

  • The candidate qualifies exactly what it publishes (4.1.20): a candidate started by hand, the only one a tag may name, runs the Bridge's full qualification profile on its own four package archives and image, for its own run. Its gate refuses anything less: the short profile, another commit or run, a step missing or failed, other archives or another image (tools/release/candidate.mjs qualification). A pull request's run keeps --short.

  • The qualification plays both restore modes under the maintenance lease (4.1.20): a restore is refused at once while the three Bridges serve, then --mode=replace-store runs with them stopped, after its safety backup, and every player's signals and the board are compared. Its report (ops-qualify.json, schema 2) says the profile, the commit, the candidate run, the four archives' SHA-256 and the command that reproduces it. Each step has a stable id, its duration and its first defect.

  • One manifest of a release's files (4.1.20, tools/release/artifacts.mjs): for each file, the job that makes it, its place in the candidate's manifest, its published name and its SBOM. release.yml checks its folder against the manifest before uploading and makes nothing the candidate made. ship verify refuses a release whose files are not exactly the manifest's. It gives back the 4.1.19 release file for file.

  • The Bridge image says how it was made (4.1.20): its identity (schema 2) adds the OS, the architecture, the Docker engine and the buildx that built it, the archive's compression and size.

  • Load figures are medians of three (4.1.20): the candidate loads each store three times. The median of the three p95 is compared with a baseline of the same runner and protocol (docs/dev/baselines/load.json). It raises an alert past 25 %, and fails only past the absolute budget (proposals under 1 s, the leaderboard under 2 s) or when all three loads are that much slower.

  • What a large backup costs in memory is measured (4.1.20, tools/backup-memory.ts, the candidate's backup-memory job): backups of 64, 128 and 256 MiB on SQLite and Postgres, measured against the qualification container's 512 MiB. It records the peak memory of the candidate Bridge's export, read and restore, and their durations, and checks that one byte and one row above the bounds are refused.

  • A release download that meets a 5xx is tried again (4.1.20): external-consumer and readme-quickstart, as ops:qualify already did. The connectors' end-to-end check draws its SSH host key until ssh2 reads it back.

  • The baseline refuses a qualification of another commit (4.1.19's pointed at one of 43dac1b) and records the medians and the memory measures. The README says "the four package archives" and which files the candidate makes and which the release makes.

  • A people's pass is bound to its surface (4.1.20, ADR 0024). docs/dev/surfaces.json says what each of the thirteen passes reads: the candidate's files (the built site file by file), the checkout's configuration (the README's install section, now between generated markers in both languages) and the passes it depends on. Each exclusion has its reason and a test. The candidate writes and attests qualification-surfaces.json, and a file in no surface fails it.

  • The candidate builds the site once, and the release publishes it (4.1.20): its archive, its assets manifest and the SHA-256 of every file it serves (site-files.json). release.yml publishes them, with the surfaces, and builds no site to publish.

  • npm run field:carry (4.1.20) carries a pass to a new candidate when no file of its surface changed, the version included. It writes a pending CarryAttestation beside the person's report, which is never rewritten. The maintainer approves it in a commit signed with their key, judged by .github/allowed_signers as the source candidate holds it. field:check and field:report then count it as passed. ci refuses a pull request that changes the trust root together with an attestation.

  • A release is made as a draft, then published (4.1.20). release.yml creates it as a draft, uploads every file, publishes it and reads it back. A run on a release already published stops and changes nothing, and a draft an earlier run left is made again. This is what immutable releases need, which the maintainer turns on from 4.1.20, with a ruleset on the release tags.

  • ship verify also asks GitHub (4.1.20): gh release verify and gh release verify-asset for each file, from 4.1.20 on, beside its own sums, attestations and list of files.

  • ship preflight-stable --candidate=<run> (4.1.20) says what 4.2 stable still needs, check by check, and signs, publishes and changes nothing:

    • the signing key and allowed_signers;
    • immutability and the tag ruleset;
    • the candidate's provenance;
    • each npm package: its archive, name, bootstrap, stage-only trusted publisher, OIDC proof and version;
    • the publishing step's exact Node and npm;
    • GO / NO-GO against the reports.
  • 4.2's tag signing, rehearsed (4.1.20): ship tag --sign signs a 4.2 tag with an SSH key, and a 4.1.x tag stays annotated. Each candidate rehearses it with an ephemeral key: a signed tag never pushed, verified, then the key destroyed. The report goes into the manifest and the release.

  • The npm runbook (docs/en/RELEASING.md, French in docs/fr/): exact pins (Node 22.23.3, npm 11.21.0), a bootstrap by staged publishing of a reservation version, stage-only trusted publishing, the two-day window, the 2FA promotion, and the dual-use classification of the connectors (not declared; staged with a 2FA promotion anyway). A nightly job that does not gate watches that the four npm names stay free.

  • The candidate's baseline reads every job, and retries (4.1.20): tools/release/run-jobs.mjs reads every page of the run's jobs (a candidate runs more than 100), retries a 5xx or a 429, and refuses a listing that does not add up to its total_count. It replaces the one bare gh api …/jobs?per_page=100 call.

  • The trust root is the maintainer's software key (4.1.20, D37): .github/allowed_signers names id_ed25519, not the hardware key the plan named. A signature proves the key was used, not that the maintainer personally approved; an approval still needs the maintainer's explicit yes, and Claude never writes one. A rotation is its own pull request touching only allowed_signers.

  • load.json reseeded where the measure holds (4.1.20): Postgres from the technical candidate's three-load medians; SQLite kept at 4.1.19's values, because one code measures 262–481 ms on these runners and the faster median would turn the next slow runner into a regression. Every replaced value stays under previous.

  • replace-store's refusals all tested (4.1.20): a tenants-only file and a file of an older SQL schema, each sealed as a Bridge would, are refused from the command line with the store unchanged, beside the schema 3 case.

4.1.19

11 octobre 2026Release sur GitHub

"Field Closure" (LOG #168): no new mechanic; it closes what 4.1.18's second reading found. A Bridge backup is one snapshot and a restore all or nothing (schema 3, ADR 0021). The Bridge and the connectors ship with their locks; the candidate builds the Bridge's image once, with its three identities, and five CycloneDX SBOMs, and the release publishes them as made, with the baseline the candidate wrote of itself. A Field Kit bundle carries its candidate's attested manifest and is checked from the bundle alone. Mutation sets are named for what they mutate; the four connectors are played from their archives; the qualification profile covers the leaderboard. The README is rewritten around three promises (create as data, the Studio, prove and ship) and its quick start is played from the candidate's archive. The DSL window (D33) admitted nothing. GO / NO-GO 4.2: NO-GO, the people's passes start on this release's candidate. The tag is not signed and nothing is published to npm (D30, D18).

Breaking

  • restore into the JSON-lines journal is refused (4.1.19): it could not be done all or nothing (--force used to delete then fail). Migrate the journal to SQLite (migrate --from=jsonl --to=sqlite), then restore.

Fixed

  • The connector SDK no longer calls an answering Bridge unreachable (4.1.19): a body null (or any JSON that is not an object) used to throw while it was read, and the proposal was retried then refused unreachable. It now reads as an empty answer. An acceptance without a valid sequence is sent again with the same key; if the Bridge never gives the sequence, the refusal is bridge. The log line is signal.malformed-answer.
  • The terminal connectors keep UTF-8 whole (4.1.19): a continuation byte that continues nothing, or a byte that never begins a character, is dropped. A backspace after one no longer erased a column of the prompt. A character split between two network chunks stays whole.
  • The live RTA after a resume starts at 0 when the run had not started (4.1.19): it used to count from the resume. Whether it had started is read from the replayed tape. Logical times, .wsrun and rulesVersion are unchanged.

Changes

  • 4.1.19 "Field Closure" planned (4.1.19): docs/dev/PLAN-4.1.19-FIELD-CLOSURE.md (French) and D33. It closes what 4.1.18's second reading found: the Bridge's backup and restore, reproducible images and SBOMs, a Field bundle that can be checked on its own, mutation sets named for what they cover, and a baseline measured on the tagged SHA. The DSL keeps every existing name and meaning; additive elements may enter through a gate before the coverage PR.

  • A Bridge backup is one snapshot; a restore is all or nothing (4.1.19, ADR 0021). backup writes schema 3: every tenant, run and daily record read in one read-only transaction, an envelope naming the backend, the Bridge and SQL schema versions, the counts and a SHA-256 of the payload, bounded (--max-rows, --max-bytes) and written to a temporary file then renamed. restore reads the file bounded, checks every row before opening the store (schemas 1, 2 and 3), then writes tenants, runs and daily records in one transaction compared with the file before it commits: a failed restore leaves the store as it was and says "nothing restored". A daily record already held is now a conflict (refused without --force, replaced with it) instead of being kept silently.

  • external-consumer checks the older engine against its release's sums and a SHA-256 it knows (4.1.19). The candidate creates a game on 4.1.17, on 4.1.16, and on the pre-release 4.1.18-rc.1, then moves it to the candidate.

  • The Bridge and the connectors ship with their lock (4.1.19): npm-shrinkwrap.json inside each archive, made from locks kept in the repository (scripts/locks/, npm run locks, every version the root lock's). An install of either archive resolves nothing. A lock that no longer fits its manifest stops the pack.

  • The Bridge's qualification image is reproducible and traceable (4.1.19):

    • npm ci from the image's own lock, with pg and OpenTelemetry's API at exact versions;
    • built once by the candidate and saved with its config digest, OCI manifest digest and archive SHA-256;
    • loaded and checked by deployment-smoke (ops:qualify --image-from=), then published by the release as it was qualified, with its identities and SBOM.
  • Five CycloneDX SBOMs, made by the candidate (4.1.19): one each for the engine, create-web-scumm, the Bridge, the connectors and the image. Each is made from what its archive holds, and the release no longer installs anything to make one. web-scumm-<tag>-sbom.cdx.json keeps its name but now describes the engine package (its dependencies as a game installs them, Vite and its tools included), no longer the repository root's five runtime dependencies. The image's SBOM names the Bridge and its base image too.

  • The qualification proxy runs unprivileged (4.1.19): Caddy runs as the operator's uid, read-only, with bounded tmpfs. The worker's image is pulled by its digest.

  • A Field Kit bundle can be checked by someone who holds only the bundle (4.1.19, schema 2). It now carries the candidate's manifest and that manifest's provenance attestation, which the candidate's gate signs from 4.1.19. npm run field:verify-bundle reads the archive itself under budgets: links, devices, .. and absolute paths refused, at most 16 MiB a file and 64 MiB in all. It checks every file and the report against its candidate, then says who made the manifest: offline against a trusted root saved once, or online by the attestation and the run read now.

  • Bundled evidence keeps the path its report names (4.1.19): 4.1.18 wrote evidence/evidence/….

  • Evidence is bounded (4.1.19): field:check says when a file or a report's evidence passes what a bundle may carry.

  • FIELD (en/fr) says the leak scan is a hygiene barrier, not a proof (4.1.19).

  • ship and the Field Kit share one provenance contract (4.1.19, tools/release/provenance.mjs): the manifest read strictly, digests, attestation (offline or online) and the run.

  • The Bridge's qualification profile covers the leaderboard too (4.1.19, npm run ops:qualify, ops/qualify/). It now checks, in the HTTPS, three-instance, Postgres topology:

    • Daily and Mystery across instances;
    • a run verified by the speedrun worker, its duplicate refused, and moderation by a token file;
    • an instance killed while its worker verifies, with the run taken by another instance and verified once;
    • one submission quota shared by the three instances;
    • the board after 20 000 runs;
    • a schema 3 backup with runs and daily records, restored after the topology and its database are destroyed;
    • a store made by the published 4.1.17 Bridge, read by this one.
  • Mutation sets named for what they mutate (4.1.19).

    • connectors becomes connectors-pure: the SDK, the MIME reader, the line editor, the virtual disk, and the badge crypto and fetch. The old name still works until 4.2.
    • Two new sets, measured every night but gating nothing yet: connectors-edge (configuration, email, badge verification, the terminal's guard and shell, SSH, Telnet) and reality-store-postgres (the Postgres adapter).
    • MUTANTS, TOOLS and ROADMAP now say exactly what each set covers.
  • The four connectors are played from their archive (4.1.19, npm run connector-archive-e2e, a candidate job):

    • email by webhook and by IMAP;
    • Telnet, with UTF-8 and an over-long line;
    • SSH, with a backspace, the virtual disk and a wrong password;
    • Open Badge, with a valid did:key credential, an invalid one and a private address refused.
  • The Vite configuration no longer loads the Studio (4.1.19). The dev server loads the Studio itself, and the demo snapshot is built in a process of its own.

    • The configuration loads 7 modules besides itself, instead of the whole Studio and engine.
    • Vite's 500 warnings about imports without a file extension are gone.
    • Where Node strips types, the native configuration loader can load it.
    • The player's bundle is the same, byte for byte.
  • Known flakes are now observed, not retried blind (4.1.19):

    • an SSH test key pair is checked whole, and kept when refused;
    • a browser error now reports its kind, stack, page and arguments;
    • Biscuit's first call is warmed up and each verdict is timed.
  • external-consumer and the new archive script share scripts/lib/archive-harness.mjs. Each step has an id and a duration, and a failure names the step and the command that reproduces it.

  • The README rewritten around three promises (4.1.19): create a game as data, shape it in the Studio, prove and ship it. The first screen is the game, the Studio and a quick start; Reality, Remix and speedrun come after.

    • Captures of this release from the real player and Studio (docs/img/readme/4119-*, npm run docs:screenshots).
    • A maturity table that agrees with SUPPORT and the GO / NO-GO (tests/readme-truth.test.ts).
    • The quick start names Python 3 with Pillow, which npm run assets needs; npx web-scumm doctor replaces an npm run doctor that a created game does not have.
  • The README's quick start is played from the candidate's archive (4.1.19, npm run readme-quickstart, a candidate job): its lines as written, in a folder outside the repository. After the tag, --published checks that the README's URL answers with the SHA-256 the release lists.

  • The release publishes the baseline its candidate wrote (4.1.19, tools/release/baseline.mjs): web-scumm-<tag>-baseline.json and .md, read from the candidate's own jobs, in its manifest and attested. A measure the candidate did not make is written as such.

4.1.18

10 octobre 2026Release sur GitHub

"Dress Rehearsal" (LOG #160): no new mechanic; the rehearsal of 4.2 "Stable World". The people's passes become reports bound to the candidate they tried (the Field Kit, npm run field:check and its siblings), each an open field-pass issue the maintainer closes once made. connectors and reality-store join the gated mutation sets (322 survivors read; an SSH backspace bug found). The candidate packs the four archives once, installs them outside the repository on Ubuntu, macOS and Windows, and the release publishes those very files, the connectors' archive included. The Bridge has a reproducible qualification profile (ops/qualify/) that the candidate kills with proposals in flight, destroys and restores. GO / NO-GO 4.2: NO-GO today, every people's pass still to make. The tag is not signed and nothing is published to npm: both are 4.2's (D30, D18).

Fixed

  • web-scumm build on Windows in a folder named in its 8.3 short form (4.1.18): a project under C:\Users\RUNNER~1\… failed (Rolldown refused the page's name, a path between the short and the long form); the CLI and the Vite config take real paths. scripts/pack.mjs on Windows: esbuild by its API, npm through the shell. Both found by installing the archives on Windows.

  • A Bridge backup carries the leaderboards and the daily challenge (4.1.18): backup wrote the tenants only, and a restore lost every run and every Daily or Mystery token; it is schema 2 now (runs and daily_kv, on a SQL store), schema 1 still restores, and a restore over existing runs needs --force.

  • Backspace in an SSH terminal erases a whole character (4.1.18): on a pty, deleting after a multi-byte UTF-8 character (é) left its lead byte behind (aé then backspace gave a�): the loop looked at the byte left in the buffer, not the one it had just removed. Found by the connectors' mutation reading.

Changes

  • The Field Kit (4.1.18, FIELD): a people's pass is a JSON report bound to the candidate run it tried (commit, run, the SHA-256 of every file it judged), with four statuses never merged (not-run, blocked, failed, passed). npm run field:init writes the thirteen passes at not-run; field:check refuses a report of another candidate, evidence whose SHA-256 changed, a passed without its time, operator, environment, scenario or evidence, two reports of one pass, a field the schema does not name, and a key, a token, an address or a bearer; field:report generates the sheet's people's passes; field:bundle packs a report and its listed evidence, logs redacted, refused on a key. The release notes count only passed as done.

  • The four archives installed as a third party installs them (4.1.18, plan §7): npm run external-consumer reads the archives (names, versions, licences, binaries, exports, no file: dependency, no checkout path), moves a game made on 4.1.17 to them (migrate, verify, build, its 4.1.17 save and a new game played to the end through web-scumm/testing), runs create-web-scumm, serves the Bridge from its archive (SQLite, and Postgres), and plays a connector scenario through it (a pairing code from the Bridge, a signed letter accepted, the same one a duplicate, a forged signature refused). Portable (Node's own APIs, npm through the shell on Windows); CI runs it on Ubuntu, macOS and Windows.

  • The candidate packs the packages once, and the release publishes those (4.1.18): candidate.yml packs the four archives, installs them outside the repository on Ubuntu (Node 22 and 24), macOS and Windows, and names each in its manifest; release.yml publishes the candidate's archives after checking their SHA-256, never packs again, and attaches web-scumm-connectors too (until now it was packed but not published).

  • connectors and reality-store are gated mutation sets (4.1.18, D32): their 322 survivors (204 and 118) read one by one, each killed by a short test (tests/connectors-*-mutants.test.ts, tests/reality-store-*-mutants.test.ts) or named in docs/dev/mutants.json with its reason. Every set is now a job of its own in ci, the nightly, the candidate and the release; release.yml asks the tag's own GATED which sets it gates.

  • The Bridge's qualification profile (4.1.18, ops/qualify/, REALITY-OPS): an HTTPS proxy (Caddy), three Bridge instances built from the web-scumm-bridge tarball, one Postgres, two tenants; every image pinned by digest, the Bridges read-only and unprivileged with their tenants mounted read-only, the database password and URL as secret files, Postgres never published. npm run ops:qualify brings it up, kills an instance during proposals, holds a stream on another, checks the tenants apart, then backs up, destroys the topology with its database and restores it (ops-qualify.json). The distributed profile stays experimental until the 24–48 h pass behind a real domain.

  • BRIDGE_STORE_FILE (4.1.18): the store's URL read from a file, so a database password is a secret file, never an environment value a process listing shows.

4.1.17

9 octobre 2026Release sur GitHub

"Stabilization" (LOG #155): no new mechanic; the release truth green again and every gate it rests on closed. A tag stands on one candidate run: candidate.yml, started by hand on the exact commit, runs its jobs in parallel (the heavy suite three times from cold, the load on SQLite and on Postgres, the five gated mutation sets, the worker's container) and its last job judges them all and writes a manifest of its artefacts with their SHA-256; ship tag --candidate=<run> checks it and release.yml publishes those artefacts after checking each digest. The leaderboard is ranked by the store before its limit (each pseudonym's best, canonical times), admission and quota hold for every instance together, a code wheel's result is computed again from its answers (D31, ADR 0020), remix, speedrun and runs are gated mutation sets, the player has a Mystery world, a run resumes in every world, the worker runs in a documented container and bridge serve moderates with a token file. The tag is not signed: deferred to 4.2 (D30).

Fixed

  • The candidate's gate fails when a job it judges failed (4.1.17): its step piped the verdict into tee under GitHub's default bash -e, without pipefail, so ✖ mutation: failure was printed and the gate passed (candidate 37998053206; the run itself stayed red, and ship tag and release.yml require a green run, so no tag could stand on it). Every step piped into tee runs under shell: bash now, and a test refuses one that does not.

  • The test of a worker writing too much asks its reason (4.1.17): a worker that exits before its SIGKILL lands leaves a cut answer, also crash, so the mutant that forgot the cause survived on some runs.

  • The nightly's SQLite load measures SQLite (4.1.17): its row set BRIDGE_STORE="" and bridge:load kept the empty string (??), so SQLite was never opened and the missing report failed nothing. Each store is now a job of its own with --store=; the report names the backend the store object is and the commit, and tools/load-report.ts fails a report that is missing, incomplete or of another backend.

  • One job per mutation set (4.1.17): the 4.1.16 nightly ran the gated sets, then reality-store in the same job, which reached the job's 90 minutes and was killed after core and reality had passed. The nightly, ci (full-ci) and the candidate run each gated set in its own job; reality-store has its own budget. The mutation reports are kept with the run (.cache/ was left out of every upload until now: hidden files).

  • upgrade-check --from=v4.1.15 downloads v4.1.15 and web-scumm-4.1.15.tgz, not vv4.1.15 and web-scumm-v4.1.15.tgz; a pre-release takes its tarball's name without the suffix (4.1.17).

  • The no-op memo keys a parked character by its room and bag, not its pixel spot (4.1.17): guests() reads every parked player, and players:<id> was valued with its position, which the search's own dimensions leave out; the memo kept one entry per spot and missed. The demo's proof runs 56 588 engine tries instead of 69 958 (136 176 without the memo: a gain of 2.41, where 4.1.15's content had brought it to 1.95 and the nightly's memo test red). Every result is the same; memoVerify: 1 checks every skip.

  • A leaderboard never loses a faster run (4.1.17): the store returned the first 10 000 runs by submission and the queue ranked those, so a faster run submitted after them was missing (reproduced with 10 001 rows). The store now ranks: each pseudonym's best, ordered by time then submission then id, equal times sharing a rank, then the limit (100 lines, &limit= up to 1 000). The memory store, SQLite and Postgres answer the same.

  • A ranked time is canonical (4.1.17): "00042" and "42" were two times; the worker's time is kept as decimal digits without leading zeros (at most 30), ordered as a decimal past Number.MAX_SAFE_INTEGER.

  • The speedrun queue's room holds for every instance together (4.1.17): an instance counted the waiting runs, then wrote; two instances seeing 99 of 100 both wrote. RunStore.admit says created, duplicate or full in one step (a transaction under the tenant's lock), and counts runs waiting or being verified.

  • The submission quota holds for every instance together (4.1.17): each process kept its own buckets, so three instances gave a client three quotas. bridge serve on a SQL store keeps one bucket per client in run_quota (migration 0004), keyed by an HMAC of the address (never the address), its secret derived from the tenant's event key; a refusal says Retry-After. A library host keeps the per-instance MemoryLimiter unless it passes a SqlLimiter.

  • A worker that writes too much is said so (4.1.17): its answer over 1 MB is killed, and the queue read the kill's signal as a timeout (inconclusive, timeout); it is inconclusive, crash, "the worker wrote too much". Found by the mutation run of the new runs set.

Changes

  • A tag stands on a candidate run (4.1.17, plan §4.2): gh workflow run candidate --ref main -f sha=<sha> runs release-check, the heavy solver suite three times on new runners, the load on SQLite and on Postgres, the four runtimes and each gated mutation set, side by side; candidate-gate judges them all (a skipped job is red) and writes candidate-manifest.json (the SHA, the run, the SHA-256 of each file for the release). ship tag … --candidate=<run id> refuses a run that is not a green candidate of that SHA and writes the run into the tag; release.yml checks that run's files against their sums and publishes them. npm run release-check:local is the part a machine can run.

  • The release carries the runs it was tested with (4.1.17, the 4.1.16 plan's §11.3): Story, Remix Fixed, Remix Random, Daily and Mystery .wsrun files from e2e:remix-speedrun --out, 4.1.15's schema 1 run, their manifest (schema, world policy, rules version, test key, commit, the verdict this engine gives), the check's report and the candidate's manifest. The Daily and Mystery tokens in them are signed with the published test key: demonstration fixtures, not attestations.

  • npm run test:heavy runs one file after the other (4.1.17): the partition test's four workers had to share the runner with eight other files, and its one 600 s budget for eight searches ran out. Each game and configuration is now a test of its own with its own budget, so an overrun names its configuration.

  • npx tsx tools/oracle-case.ts --case="<id>" --out=<file> and --diff <before> <after> (4.1.17): one case of the solver's oracle in full, and the difference of two runs by meaning (fields, first session entry, flags added, removed or reordered, reachable states, engine, Node and options). The oracle's fixture keeps digests only.

  • Bridge schema 3 (4.1.17, bridge/migrations/0003): the ranked times of 4.1.16's rows brought to the canonical form, a value that is not a decimal set aside (its time removed, its trust untouched, said in its reason), and the index the ranking reads. bridge migrate --schema=2 undoes the index only. Upgrade every instance of a tenant together: a 4.1.16 instance still running beside a migrated store would write times as they came.

  • npm run runs:load (4.1.17): the leaderboard at 100 000 runs on SQLite or Postgres: p50/p95 latency, resident memory, the SQL plan; a bound passed fails it. The candidate and the nightly run it on both stores.

  • The leaderboard says what it ranks: each pseudonym's best (the Bridge knows no player identity).

  • A minigame's result can be proved (4.1.17, D31, ADR 0020): the code wheel records its answers (by their place in the author's list, so a translation keeps them) and the wheel's hash beside its result (SessionEntry.mgt); a replay generates the wheel again from the world, judges the answers and stops on another result — the verifier says invalid-replay, minigame-transcript. A speedrun category asks for it with codeWheel.proof: 'transcript' (code-wheel-proof: the word won alone is refused). The reference chapter's new category wheel-proved wants the wheel won and proved; its other categories, and every 4.1.16 run, keep their meaning.

  • A printed wheel is not seen (4.1.17): a category with codeWheel.medium: 'physical' whose run played the wheel is valid-unranked (physical-wheel-unwitnessed) until a moderator ranks it.

  • API (4.1.17): CodeWheelTranscript, MinigameTranscript, MinigameOutcome; Presenter.minigame may resolve with { result, transcript } (widened, an implementation returning a result alone is unchanged).

  • SPEEDRUN en/fr says what replay-valid proves and what it does not (a person, a printed wheel, the RTA).

  • Bridge schema 4 (4.1.17): the table run_quota. runs.perMinute in a configuration's runs section.

  • The mutation set runs (bridge/src/runs.ts, runs-store.ts, runs-limiter.ts) is gated from 4.1.17: npm run test:mutation:runs, a job of its own in ci (full-ci), the nightly, the candidate and the release. Its survivors read one by one: tests of the queue, the worker, the routes and the stores (tests/bridge-runs-*.test.ts).

  • gh workflow run mutation -f set=<set> -f ref=<ref> (4.1.17): one mutation set measured on a runner, its report kept with the run; it gates nothing.

  • The player's Mystery world (4.1.17): the Remix menu's Mystery world entry asks the game's Bridge for a commitment and its reveal, verifies both offline with the game's daily key, builds the world and keeps both tokens beside it; a speedrun started in it carries them and its start time (valid-unranked without a server witness).

  • A run resumed keeps its world and its proof (4.1.17): e2e:remix-speedrun interrupts and resumes a run in each world (Story, Remix Fixed, Remix Random, Daily, Mystery): the same head, world, proof, verdict and board; resuming in another world or with another proof than the one sealed is refused (RunStartRefused).

  • The speedrun worker's container profile is run in CI (4.1.17): tools/speedrun/container.mjs makes its docker run arguments (no network unless --allow-network, read-only, a bounded /tmp, no capability, no new privilege, an unprivileged user, limits on processes, memory and CPU, the packages read-only, its own timeout -s KILL); npm run e2e:worker-container (CI's worker-container job, in the candidate and the nightly) runs the reference run inside it, then a hostile script refused the network, the host's files and secrets, and killed.

  • Moderation from bridge serve (4.1.17): runs.adminTokenFile (0600, 32 characters at least; serve refuses a missing, short or world-readable file); the bearer compared as hashes of equal length; every refusal audited, never the token; /healthz counts moderations accepted and refused.

  • ship checks waits for pr-gate (4.1.17): the second tier's jobs appear once check is done, so a list where every check passed could be partial (8 of 32 on #67).

  • The connectors' ssh tests draw their host key through sshKeyPair() everywhere (4.1.17): two fixtures still called ssh2's generator, which now and then writes a key its own parser refuses (the "Malformed OpenSSH private key" flake).

  • tests/bridge-fanout.test.ts removes its folder with retries (4.1.17): Windows keeps a killed instance's SQLite file busy for a moment (EBUSY in the Windows job).

  • The heavy suite's partition budget is 240 s per search: the slowest of the candidate's three cold runs on the runner (146 s) and 60 % more.

  • A Mystery speedrun starts within the minute of its reveal (4.1.17): after it the page says so instead of recording a run the verifier refuses, and a page clock behind the Bridge's never dates the start before the reveal. Resuming a Mystery run keeps its sealed proof, whatever its new start time.

  • bridge serve without runs.adminTokenFile has no moderation route (404, nothing counted or audited); the file's mode is checked on POSIX systems; the worker container has no swap beyond its memory limit.

  • The Remix and Speedrun mutation sets are gated (4.1.17, plan §9): their survivors read one by one, by file — the seed codes, the manifest's compilation and every one of its problems alone, the application of a world, the categories' worlds, the envelope's sealing, the recorder (RTA, pauses, the finish, resume refusals), the verifier's guards (each reached by a run recorded dishonestly, not a hand-edited file) — and killed by a test, or named equivalent in docs/dev/mutants.json with why. remix and speedrun join core, reality and runs in the gate: their own jobs in ci (full-ci), the nightly, the candidate and the release.

  • The verifier says a stored world's refusal by the code loadVariant gives it (the mapping of a code nothing threw is gone).

4.1.16

8 octobre 2026Release sur GitHub

"Convergence" (LOG #147): no new mechanic; the capabilities 4.1.14 and 4.1.15 published made to work together, end to end. A speedrun is bound to the world it was played in: .wsrun schema 2 carries the exact world and the Bridge's signed tokens, sealed into its proof; the verifier makes the world again from its seed, checks it against its category (worldVerdict), replays the run in it and names its leaderboard (leaderboardKey); a schema 1 run stays a Story run (breaking, with its reading rule: D29, ADR 0019). The Bridge's leaderboards and daily challenge are durable (SQLite, Postgres), shared by instances under leases, and mounted by bridge serve; a minigame's result is in the session, the replay and the story; the code wheel plays by keyboard and gamepad; Remix and Time Attack are checked together in Node, Chromium, WebKit and Firefox on every pull request (e2e:remix-speedrun); canonicalJson writes the same text in Firefox; Pages deploys only behind pr-gate; the docs are checked against the code (docs:truth). Measured against 4.1.15 in docs/dev/baselines/4.1.16.md; what this release does not do is in the LOG and the passes sheet (docs/dev/passes/4.1.16.md).

Breaking

  • A .wsrun is schema 2 (4.1.16, ADR 0019): it carries the run's generator seed (runSeed, was seed), the exact world it was played in (variant) and, for a Daily or Mystery world, the Bridge's signed tokens (worldEvidence), all sealed into h0. A schema 1 file (4.1.14, 4.1.15) is still read and verified, as a Story run; offered to a Remix category it is refused (legacy-world-missing). A tool that read envelope.seed reads runSeed on schema 2.

  • RunStore is the durable queue's (4.1.16): create (false on a duplicate key), claimNext (a lease), complete (only by the lease's holder), setTrust, queued; put and byKey are gone. A host that wrote its own store implements these (MemoryRunStore and SqlRunStore ship).

Fixed

  • The release line is a line again (4.1.16): v4.1.14 was tagged on a side branch whose only extra commit lowered the coverage floors for that tag; it is merged into main (the stricter floors kept), so v4.1.14 and v4.1.15 are both ancestors of what comes next. ship tag and release.yml now refuse a tag whose previous stable tag is not its ancestor (tools/release/ancestry.mjs).

  • Pages deploys after pr-gate, the terminal gate, instead of four of its jobs: no deployment while Firefox PWA, a proof, a mutation set or the reference game is red.

  • A speedrun in a Remix world verifies in that world (4.1.16): 4.1.15's verifier and the Bridge's worker replayed the base game, so a run where the key lay under the oranges could not verify. The verifier now checks the stored world against the game (loadVariant) and the category (worldVerdict), rebuilds it (applyVariant), replays the run there and names its leaderboard (world.leaderboardKey); the same inputs in two worlds are two runs. A world is made again from its seed and must be the one stored: a world relabelled with another seed and rehashed is refused (world-forged), as is an unknown algorithm version (world-algorithm). The recorder no longer turns a Daily or Mystery category into a random seed, refuses to start a run its category could not rank (RunStartRefused), and keeps the world, the evidence and the run's seed in its checkpoint (a resume in another world is refused).

  • canonicalJson writes the same text in Firefox (4.1.16): Firefox's normalize('NFC') turns a lone surrogate half into U+FFFD; such a half is now kept and the text around it normalized alone, as Node, Chromium and WebKit do (e2e:canonical, never run before, failed on it).

  • A Mystery reveal is signed by the Bridge (its token), so the time of the reveal is the Bridge's word.

  • Two tied runs share a rank on a Bridge leaderboard; the order after the time is the submission, then the id.

  • The daily challenge refuses what is not a day: date=, 1999-99-99, 2026-02-30 (400), a day older than retentionDays (410), a game id that is an object's property (__proto__, 404); its commit counters forget clients whose hour is over.

Changes

  • A new logo at the head of both READMEs.

  • SpeedrunCategory.world (D29): { policy: 'story' | 'fixed' | 'random' | 'daily' | 'mystery', mode, fixedSeed?, codeWheel? }; the validator refuses what it cannot give a meaning to and warns about 4.1.15's seed: 'daily' | 'mystery' without world. API: SpeedrunWorldPolicy (content); categoryWorld, runSeedPolicy, SpeedrunEnvelopeV1, SpeedrunEnvelopeV2, SpeedrunWorldEvidence (testing); SpeedrunVerifyResult.world.

  • Durable, shared speedrun leaderboards (4.1.16, plan §8): runs in the Bridge's SQLite or Postgres (migration 0002), created once per key across instances, claimed by one worker under a lease and claimed again when the worker died, kept across restarts; workers per instance; a leaderboard per verifier key (&key=, a Fixed or Daily world's board apart); a Daily run sent after its day is practice; audit lines for submissions, verdicts, moderations and deletions.

  • bridge serve mounts /v1/runs and the daily challenge from a configuration's runs and daily sections, on the SQL store, behind the same tenant, CORS and rate limits as the other routes; bridgeServer takes runs and daily. REALITY-OPS gives the worker's isolation profile (a container without network, read-only, no secret).

  • The player keeps a Daily world's signed token and a speedrun in that world carries it.

  • A minigame's result counts (4.1.16, plan §9): a minigame that says how it ended (the code wheel's mg-record) has its result recorded in the session (SessionEntry.mg, fed back on a replay like a choice) and written in the reserved flag minigame.<id> (won, passed, skipped, failed, disabled), which the story reads with { flag, eq } and the journal reports as flagChanged. In the player every minigame reports (skipped, else won); the solver and the sessions of 4.1.15 record nothing new and replay as they were. A behaviour change for existing games: a save made in the player now carries a minigame.<id> flag for each minigame played (the solver does not explore branches hung on one). Presenter.minigame may resolve with the result; a command never sets a minigame.* flag (validator).

  • The code wheel, end to end: a story wheel lost after its tries ends failed (no longer like a parody); the whole wheel by keyboard (Escape skips, a strict wheel starts focused on a turn button) and by gamepad (up and down choose an answer, A confirms, B skips); the focus returns to the game when any minigame closes; a speedrun category's world.codeWheel is enforced by the verifier (code-wheel-rule: skip: false wants it won, enabled: false wants it not played). e2e:a11y wins the wheel at the keyboard, checks the record, the flag, the focus and reduced motion, and runs axe on it.

  • Remix and Time Attack are tested together, end to end, in four runtimes (4.1.16, plan §10): npm run e2e:remix-speedrun takes each world policy of the reference (Story, Remix Fixed, Remix Random, Daily, Mystery), records the solver's route with the real recorder in Node, Chromium, WebKit and Firefox (the four .wsrun the same, byte for byte), verifies it in a new process and by a Bridge queue's isolated worker, and checks its verdict, its leaderboard key and its trust. A cross-runtime CI job runs it with e2e:canonical, e2e:remix and e2e:speedrun on every pull request (in pr-gate), and the nightly at 500 seeds.

  • The reference chapter races in Remix worlds: categories Remix Fixed (WS-0000-02DZ, its own board), Remix Random, Daily and Mystery, beside Any%, No Hints and Real Time.

  • npm run docs:truth: what the documentation states against the code (the save and .wsrun schemas, every script and tool file it cites, the capabilities it calls mounted, no release link newer than the version), in CI's check job. ARCHITECTURE and SUPPORT (en, fr) say saves schema 4 and .wsrun schema 2.

  • Mutation sets remix and speedrun (npm run test:mutation:remix, test:mutation:speedrun, and test:mutation:reality named): measured, not gated yet; their first runs left survivors to read before 4.2.

Voir toutes les versions (35)