Skip to content

Release notes

Which version am I running?

Open About in the application. It names the version, the build it came from and when that build was made.

Quote all three when you report something. The version alone answers "which release"; the build answers "is this the state I am looking at", and on a tool that updates by reloading a page, that is usually the question that matters.

Since 2.3

On the demo server since 15 October 2026; these changes become the next version. A workspace from 2.3 opens as it is.

The editor

  • The tree of procedures and the tree of steps stand in the main area, left of the editor, with a divider you can drag; each page keeps its own width. The sidebar holds only the navigation. See Procedures.
  • Test cases are reordered by dragging in their procedure; they run in that order, and are numbered so.
  • A procedure is renamed by clicking its name at the top, like a test case.
  • The divider between the tree and the editor is wide enough to catch.
  • "Run procedure" and "Run test case" say what the buttons do.
  • A question mark beside XPressRT in the sidebar: If you need help with anything, ask the agent, with the way to the Assistant page.
  • A sentence may hold an apostrophe — the system's state is {state} —, quotes or a backslash. Before, one such sentence made a step file that did not compile, and then no run in the workspace started, not even an example.
  • A fourth example in every new workspace: records and requests, form steps, and block steps the way an assistant writes them.

Running

  • The test runtime stays warm between runs, and the next one is prepared while you work; Run Test shows Reporting… while the last report is written, and Open report waits for it.
  • When the runtime does not come up, the message names the file, the line and the error — not only Traceback (most recent call last):.
  • A setting records its target beside what was reached. In series.json a set carries target (and target_name for a state); value is what was reached. Where the two differ, the report and the live log warn — set QCW, reached CW —; the step still passes.
  • A time in a message reads as a person writes it: within 300 ms, within 15 s — not within 0.30000000000000004 s or within 15.0 s when the blocks compute their limit.
  • Download Run Reports is the new name of Download Run Results in the run view: what it packs are the run's reports.

Live view

  • Filters fold under Filters, time and search stay open; the legend stands on the left, a click in the chart sets a cursor with every value at that moment; ⌘/Ctrl + wheel zooms, the wheel alone scrolls the page.
  • Every chart and the log can be made taller or shorter on its own, and keep their height; states are drawn in a lane of their own with their names; several moments can be marked from the log.
  • An earlier run can be loaded from what it recorded, and the Log button in a report opens it for its test case. See Live view.
  • Numbers and switches are listed in the log from the start; also list numbers and switches is ticked unless you untick it.
  • In a run loaded from what it recorded, the steps before and after the procedure show how they came out — not unfinished and running.
  • A grip stops scrolling the page once its chart or the log is as tall or as short as it can be, and stays under the mouse.

Benches and data points

  • The binding row is a table — your workspace on the left, the bench on the right —, values are chosen rather than typed, named values show their number, a scale shows the range it turns into, and fixed values are entered in their own type. See The binding.
  • A point that cannot be bound yet — a record, a list — is shown greyed out.
  • The names of a state in a driver's offering stand apart: STBY | RDY | EMIT.
  • The line between data points and record types can be dragged.

Requirements, regression, onboarding

  • The head of the Requirements page is calmer, like the regression page; checkpoints fold, and long explanations fold under their heading across the application.
  • What an onboarding adapted is recorded: a test case taken over but not word for word stands under Adapted — what, from, to, why, and whether the meaning changed; a changed meaning needs the name of who agreed. Before it hands the run in, the assistant is told which procedures it has not checked or dry-run. See Onboarding.
  • Schedule onboarding: the Onboarding page asks your assistant for an import, with the source of the test cases — a recorded one, or a new one with its link and part. See Onboarding.

History

  • A refused change leaves nothing behind. An import that was refused halfway left marks on steps that did not exist and an entry import a plan of test cases for an import that never happened; now the workspace is as it was and the history has no entry.
  • A Python environment (.venv/), node_modules/ and tool caches are not recorded as changed outside XPressRT.
  • Every change through the interface is one sentence in the history — describe step … instead of POST /steps/describe, and the same for folders, suites, tags, test cycles and settings.
  • unmarked-changes.json moves to .xpress/ when the workspace is opened — one entry in the history, moved the diagnostics file to .xpress/ —, and the history leaves it alone from then on.
  • What an assistant wrote while its ai-generated tag could not be set is tagged when the workspace is opened as a change of XPressRT — it no longer appears as changed outside XPressRT, and an assistant writing at the same moment no longer loses the tag.

Working with an assistant

  • The house rules in AGENTS.md begin with the chain of test cases, steps, data points and benches, working without a bench, every step through data points, and that an assistant may start a dry run. See The rules it is given.
  • Five minutes for every call, an hour for an import or a whole run handed in, with a message every 15 seconds that it is still working. See How long a call may take.
  • An assistant that repeats itself is stopped after the same call with the same answer three times in a row — waiting for a run or the Agent queue is not repeating. See An assistant that repeats itself.
  • An assistant creates steps through data points. A check that asks the operator, and a setup or action that asks Yes / No, are refused with what to write instead; steps from before are listed by check_procedure and about. A person in the step form is not bound by this. See Steps an assistant cannot create.
  • A reading shows a picture, like an operator question — in the step form and through picture for an assistant.
  • check_procedure and about name a step file that does not compile, with its file and line.
  • update_data_point reaches GitHub Copilot in VS Code, which left it out because of its schema.
  • The progress of a run counts the test case's own steps: a step of a preparation or clean-up is named after where it runs, the preparation steps before every test case are counted in — no more step 5 of 3.
  • explain_run no longer counts the preparation of a procedure as a test case.
  • AGENTS.md names no internal finding numbers.

Version 2.3

Released 13 October 2026. Everything that has changed since 2.2 is in this one release.

Start 2.3 with a new workspace. Opening a workspace made with 2.2 has not been tested.

In short:

  • Every change is recorded, with who made it, which way and why — a history per object, the Changes view over everything, and readable with git or Fork. See History.
  • From requirements to test cases. A coverage run proposes the repair — a step, rows, a whole test case, the data point it needs — you approve or reject it on the Requirements page, and your assistant writes it. Closing is checked against the test case, not taken on trust.
  • One place for everything that waits for the agent: the Agent queue on the Assistant page, numbered as your assistant takes it. Schedule coverage and Schedule regression leave a request there; every approval can be withdrawn while it waits.
  • Decisions say what waits for you, and for whom — the author, the test cases, the bench, the team — with an export of exactly that.
  • Test runs start faster: from Run to the first question about 3.5 s before, under 1 s now.
  • The regression page shows the state of now, and an adaptation can also remove a step, remove rows or add a step.
  • Time checks name what they measure from. A step marks the moment with MARK, a check measures since = AT(…) — also across a 2-second window in between, and also when an earlier check already recorded the event. See Time has a reference.
  • An event is a change after the mark. A fault that was already there before the trigger no longer passes a "within 200 ms" check with 0 ms — nor at the moment the bench reports it again.
  • The moment comes from the bench, with its clock — and without a bench, the record says whose moment it is instead.
  • The report says where every time was counted from, the mark, the step that set it and the clock.
  • A new time check without a reference does not build; existing ones keep running and are pointed out.
  • Coverage runs judge world, trigger and observation, with a new finding, TRIGGER, at the checkpoint.
  • Form steps can be measured from: every set and read step marks itself, and AT_POINT(point) measures from the newest value of a point.
  • A check names its trigger in its sentence — … within 200 ms after the operator removes the cover — and measures from that line of the test case.
  • Every source says which part of it is meant — a set, a container, a submodule and branch — with its link, on every page and in every report.
  • The data point list can be dragged wider, and keeps its width.
  • Nothing is silently reinterpreted. An unknown type, kind of step or kind of request is refused with a sentence; a number has one decimal mark, and 1.234,5 is refused instead of guessed; an approval holds for what was approved. See Groundwork.
  • Data points sit under Test-Editor, beside Procedures and Steps.
  • Two more types: on/off and text, through the catalogue, the steps, the operator's dialog, variants tables, blocks and the report. See On/off and text.
  • Requests: a third kind of data point for what has to happen — bring the unit to Ready, run a self test —, with typed parameters, an instruction for the operator, a step action Request, the block REQUEST, a dialog with Done / Failed / Not possible and a block in the report. See Requests.
  • A test case that names a value that does not exist is not run. A state the point does not hold, something other than on or off for a switch, a name a parameter of a request or a field of a record does not know — in the sentence or in the variants table, for form and block steps alike: that test case, or that one row, is left out, everything else runs, and the run list, the summary and explain_run show it as not run with a sentence that says what stands there, what exists, and that the mistake is in the test case. Until now it failed in the step, like a finding about the unit, or the whole run was refused for one misspelt name. run.json lists them in notRun (optional, still version 2); step-results.json is unchanged. A number that is not one still keeps the run from starting. See Running tests.
  • A second run does not start while one is going. start_dry_run is refused while a run is going in the window — also one a person started with Run Test and that is waiting at a dialog —, with the sentence the writing tools use, the start time in local time and a pointer to stop_run. Run Test and starting a suite are refused the same way. Before, the second start withdrew the open question of the first run and ended it, and said nothing about it. A run waiting at a dialog for more than a minute and a half no longer counts as finished. See Running tests.
  • A step file with POINT carries only the points its pattern reaches. POINT("Bench/FootPedal/{pedal}") can only become a point under Bench/FootPedal/, so the generated file holds those and no longer the whole catalogue. A new data point elsewhere no longer rewrites every generated file that has a POINT. The generated files of an existing workspace that use POINT are written once more, with shorter tables; the Python of the steps themselves is unchanged, so no approval is touched. A POINT that takes its point from a variable still carries the catalogue.
  • An assistant writes rows of a binding itself. write_binding_row writes or replaces the row of a data point on a bench, remove_binding_row removes one, and rename_data_point carries the binding along in the same change. The check runs before anything is written — a row with an error is refused —, every change names its reason and stands in the history under the assistant, and it is a new version of the binding: the bench has to be commissioned again, which only a person does. propose_binding is there for when a person should decide. An assistant cannot disable a row, write a simulation, create or commission a bench, or probe a request. See Benches.
  • One seam: form, block and Python steps ask the same place who serves a data point, every value records who did, and a step the bench aborts is aborted, not failed. See One seam.
  • Benches: a page to describe a bench, bind data points to what it offers, check the binding with place and fix, disable a row or a driver with a reason, and commission it — first with a simulated bench. A run chosen on a bench lets it serve what it binds, says before the start what the operator serves, and records the bench in run.json (now version 2). See Benches. The check reads translations in their direction (reading: every bench value means exactly one workspace value; setting: every value fits the target), commissioning calls no request that can change the safe state by itself — those are probed by hand —, and disabling a row keeps a commissioning valid. Changing a bound data point asks per bench and carries the binding along where it can; an assistant writes, removes and proposes rows of a binding, and commissioning stays with a person.
  • Negative tests: a request step can pass only when the request is refused (passes_on: ["refused"]) — accepted, it fails with a sentence.
  • Who served a value: the report, the Run Viewer and explain_run say for every value who served it — the bench with its driver, the operator, or the simulation —, by whose clock and where it came from; a request also what was sent to the bench and which values the binding fixed, the ones that affect the result set apart. series.json is now version 4. See Who served a value.
  • Live view: a window of its own that shows what passes the data points while it happens — numbers and switches as a chart, states, text, requests, marks and step boundaries as a log —, with filters, a pause and a CSV export. It shows exactly the entries the run records, whoever served them, marks values that carry no bench clock, and writes nothing. See Live view.
  • Operating without a test: on the Data points page, Read now, Set and Run per point, through the bench you choose and its binding — checked like a form step, locked while a run is going on the same bench (in any window), with a second confirmation for a request that can change the safe state, and Safe state for the whole bench. Without a bench, Read now shows the last recorded value and says where it came from. Everything is kept in the bench journal — on the bench's page, and never a test result. An assistant has no tool for it. See Operating without a test.
  • Watch on a bench: a switch beside the bench on the Data points page reads every point the bench serves once a second and shows it in the live view — without a run, not recorded, resting while a run is going there.
  • A simulated bench can move: an effect with overMs lets a reading rise or fall over a time instead of jumping.
  • Safe state is in the stream and in the run: the live view draws it, and a run that was going on that bench says in its report, at the step and at the top, that somebody called it by hand — shown, not judged. series.json keeps it under events, with the marks and step boundaries of the run; its version stays 4.
  • A value that is no state is no reading: when a bench reports a value for a data point with named states that the binding does not translate, the access fails at the bench — in a run the step is aborted by the bench, not failed —, and the check of the binding says so beforehand.
  • A changed offering asks for commissioning again, like a changed binding, and says what changed.
  • A run waits for the history: it begins only when what changed outside XPressRT has been recorded, so that nothing a run writes is ever attributed to outside.
  • The simulation is described in one place: The simulation in the manual, and the same above the Simulation box on the bench's page.
  • Record types in the catalogue: a message or a bundle is described once — a name and typed fields, also records inside records and lists — under Record types on the Data points page. A read or set point holds one, a request takes its fields as parameters, and any point can hold a list. An assistant has its own tools for them, each change with its reason. A bench cannot be bound to one yet. A workspace without record types keeps datapoints.json as it was; with one the file is version 3. See Records and lists.
  • Records and lists in blocks and in a run: a block or Python step reads one field of a record with a colon — ErrorService/Received:code —, sets a record with a bundle { code = 4711, source = "pump" }, calls a request with the fields of its type, checks a record against the fields it names, and counts or searches a list (CHECK_CONTAINS). Exactly once within 500 ms is COUNT of what COLLECT gathered from a mark; a point that reports events can have received nothing. What does not fit the type does not build, with the line. In a dry run the assistant answers records, lists and the occurrences of a message, each field checked against its type. The run records a record as an object with its type and the units of its number fields; series.json stays version 4. See Records and lists.
  • Records for the operator, in form steps and in the report: without a bench a person enters a record with one input per field, each checked by its type, a list as rows, and what a point received over time as a table with a row per occurrence and its moment — no row means nothing was received. Read data points, Set data points and Request work on a record field by field, with a parameter of the sentence wherever a single value stands, so a variants table has a column per field; create_step takes the same. The report, the Run Viewer and the live log show a record as a table of its fields, and what was collected over time as a row per occurrence. measurements.json keeps a record as an object with its type and units, a limit on one field as a reading under Point:field; its version stays 3. See Records and lists and Records in the report.
  • A message, end to end, without a bench: send it as a request with the fields of a record type, receive its confirmation as an event, check a field and exactly once within 500 ms, with the operator serving both — and a new message is a new row of the variants table, with no change to the catalogue. See A message, end to end. The operator's dialog grows with the fields of a record and shows the moment first; the instruction of a request names a record parameter as its fields, never as JSON.
  • Renaming a field of a record type, or a named value of one, carries along the block files and instructions that name it, in one write, and lists beforehand what it will touch. Python written by hand is listed, never rewritten. A renamed value also goes along in the test cases whose sentence or variants table gives it to that field, and checking a procedure names a test case that gives a field a value it does not know.
  • A stated course counts from the mark: where a check measures since = AT(…) and nobody pushes samples, the values a person or the assistant states are stamped from that mark — numbers as well as records. Before, they were stamped backwards from the moment of the answer, and a short window after a trigger was empty as soon as answering took longer than the window.
  • Renaming a data point carries its field usages along — and a point that is called like a field of some record is no longer mistaken for that field.
  • A number in a record may have no unit, on purpose: an error code or a quantity is ticked no unit instead of getting an invented one. Data points and parameters keep their unit.
  • The window in the background: a browser slows a background tab down, and a change through the assistant took many seconds there or ran out of time although it was carried out. XPressRT no longer waits on the page's timers where something waits for an answer, opens the workspace once per request instead of once per file, and reads what the recorded runs hold once per run instead of for every data point. A call that still runs out of time says that the window is in the background. See The window in the background.
  • A tool answers when its change is in the history: what an assistant changed is recorded under its name by the time the tool says so, and a change that was left between writing and recording is recorded first when the workspace is opened again — it no longer ends up in the next person's entry.
  • A name cannot contain a colon any more, for a data point or a folder: the colon reads a field of a record.
  • The events of a run are in the order they happened: a safe state called by hand from another window no longer stands behind the end of its test case in series.json.
  • Two windows and the assistant: when a second window took the assistant's connection and then leaves, the first one takes it back by itself within seconds.
  • A value set from a block or Python step keeps its unit in series.json, as the catalogue named it at that moment — like one set from a form step.
  • A simulated bench hangs together: a command can set a value that is read afterwards (effects) — set 5 V, read 5 V back. See The simulation.
  • The local runner speaks a new version of its protocol (7). If a runner is running when you update, XPressRT brings local/xpress_local.py up to date and says so; stop the runner and start it again. Until then a block or Python step that needs it fails with that sentence — it does not run half.
  • The plan report by bench: every run stands under the bench it recorded itself, with its tester and test place, and two benches that fix a value differently are pointed out. See The plan report.
  • A rename keeps approvals: an approved block step stays approved when a data point, a folder or the name of a state is renamed, and the history says the approval was carried over.
  • No internet needed, licences included: the Python runtime and its packages come from the application's own server. About lists each package with its version and its licence, and the licence texts are delivered with it.

One seam

Every read, set and request now goes through one place that decides who serves it: the operator, the simulation in a dry run, and — once one is connected — a bench, for every kind of step at once. For the operator nothing changes: the dialogs are the same.

Every value in series.json and measurements.json (both now version 3) says who served it and what the seam answered — every sample of a course too. An access the seam did not answer ok stands in series.json with its status and no value, and only values answered ok count in a mean. A set from a block or a Python step is applied by nobody without a bench, and the report says not applied — no bench instead of showing it as set.

When the seam does not answer ok, the step is aborted by the bench: not a failed check, no defect report, the rest of the test case does not run. The run list counts it as Aborted, a test plan as not run, to be repeated, and step-results.json (version 2) keeps it with its cause. A request's own failed or refused stays a result; only a timeout aborts it.

Requests

A data point is now read, set — or a request: something that has to happen, once per call, with its parameters given together, answering ok, failed or refused. Without a bench the operator carries it out from the instruction the request carries; the dialog has three buttons and no text field. Every call is in series.json (now version 2) with its parameters, status and both stamps, and in the report as a block under its step. A coverage run proposes a missing request like a missing data point, and every data point proposal says why its kind. datapoints.json now carries a version number; a file written by a newer XPressRT is refused instead of read half.

On/off and text

A data point now holds numbers, named values, on/off or text. On/off is written on or off (true and false too) everywhere — a step, a variants cell, an answer — and nothing else is taken; the run records true or false, the report shows on or off. A text point may set a longest length and a form, a regular expression; a reading is held against a text as is, contains or matches the form, upper and lower case counting.

The operator gets a choice for on/off and a text field for a text, and can confirm only what fits. The step form offers on/off as a choice and a text as a text field, without limits. A variants cell is checked against what its column feeds when the test case is written — in the editor, which offers a choice, and through an assistant. Blocks are checked against the catalogue when they are built: WRITE on a point that is read, a value the point cannot hold, and an order or a sum on on/off or text do not build. measurements.json is version 2: a reading can hold true/false or texts, and the plan report's mean takes numbers only.

Groundwork: nothing silently reinterpreted

Before request points, new types and a bench page come, the ground they stand on is made firm:

  • What is not known is refused, not filed as the nearest thing. A data point of an unknown type, a step that does something this version does not know, values on a point that is not an enum — refused with the reason, also in an imported plan with the place. A request of a kind this version does not know is said to be one, never described as a coverage run. A fixed set value that is not a number is no longer dropped, which turned the step silently into a question to the operator.
  • One rule for numbers. A point or a comma is the decimal mark — 5.0 and 5,0 are the same five in the form, the dialog, a variants cell and a block step. There is no thousands separator: 1.234,5 is refused with that reason. A limit from a variants row that is no number fails the step; it no longer becomes no limit at all.
  • An approval holds for a content. A data point approved for the bench is open again when a later run proposes it differently, and its request is withdrawn; closing the request compares what was created with what was approved.
  • The catalogue file is read, not taken. A datapoints.json that cannot be read is refused with line and column, and nothing is written over it; points that break a rule are listed. The type of a point can change through the assistant too, while no run has recorded it.
  • Faults fixed: the plan report averaged the numbers of named values; a rename left a point's path standing in test cases that name it as a value; a block variable called last built and failed at the bench; READ on a named point was taken for a text; a dry run of a test case that does not exist ran over nothing and reported Nothing failed; the bench server was released after a local run instead of after a run at the bench.
  • One source for a step's defaults: a step an assistant creates gets the same error code as one made in the form (E601 for a question).

Time checks measure from a mark

A test case like

When the operator presses the start key
Then for 2 s the device does not report RUNNING
And  within 200 ms the device reports fault F12

used to measure the 200 ms from the start of the third step — that is, from second 2. A fault that came after 1.9 s and was still there passed with 0 ms.

Now the action marks itself, MARK("Trigger"), and the checks measure since = AT("Trigger"). WAIT_UNTIL, COLLECT, TIME_OF, WINDOW, LAST and ATTACH_CHART take since, and TIME_SINCE takes AT(…) as its reference. What was recorded since the mark counts: the order of the checks no longer decides what is measured. Whether a state already stood is decided by its value at the moment of the mark, which the bench hands over with the course; where nobody knows it, the result says state at the reference unknown. A chain works the same way — a check that finds answer A marks it, and "B within 30 ms after A" measures B from A's stamp, however long the runner took in between. AT() without a name is the newest mark, and a check whose mark nobody set fails with No reference "Trigger" was marked before this check instead of measuring from somewhere else.

There is no default reference and no "last action": which step is the trigger is a question for the requirement, and a block says it or there is none.

Measured from a form step, or from a point

Most triggers are form steps — Set data points, Read data points. Each of them now marks itself in every run, without anything to fill in: under its line as the test case writes it and under its sentence with braces. A check after it measures since = AT("the user opens the {door}") — the newest occurrence, whatever the value —, or names one line by filling in a value of its own sentence. AT() without a name does not see these marks; it would otherwise land on the last setup step. Form and block marks are one list per test case, so a block finds a form step's mark before it and the other way round, and the report names the form step: after step 4 "the user brings the system to Ready state" (clock entered).

AT_POINT(Folder/Name) measures from the newest value of a point — set, read or measured, from a block or a form step — for a check that knows its trigger's point.

A mark on an observed event counts from after it: "the same request again within 5000 ms" finds the next request, not the marked one. And a data point can be marked as one that reports events (Kind: Event): every report after the mark counts, also one that says the same as the last; a state counts only a change.

A check names its trigger

A check that serves many test cases cannot know which line its trigger is. It now says so in its sentence, as a placeholder, … after {trigger} with since = AT(trigger), and the test case writes the line out: the interlock goes LOW within 200 ms after the operator removes the cover. Every step marks itself under its line when it is done: form steps, block and Python steps, and a question to the operator with the moment of the answer. The check therefore finds the line wherever it stands before it, in the steps, the background or the preparation. The last occurrence counts, and (1) picks an earlier one. A step that only reads — a form read, a READ or LAST in a block — is a trigger too: the check counts strictly from after the value it read.

A trigger that is no line before the check, a line that only comes after it, or a line that leaves no moment is pointed out when the test case is written, in the editor and to an assistant. It is pointed out, not refused, and the run fails with the sentence. The editor shows ⏱ beside such a check, with the step it measures from, and offers the lines before it as you type. Renaming a line carries its triggers along. A coverage run reports a time check without a trigger as TRIGGER time check names no trigger.

The moment comes from whoever knew it

A value that is set is stamped with the moment it took effect, by whoever knows it — a bench with its clock, as soon as one answers. Without a bench it is the moment the operator confirmed the setting (entered) or the moment XPressRT sent it (runner); in a dry run an assistant answers, simulated. Every one of these is kept with its clock in series.json, and a time measured from it says so.

The report says where every time was counted from

A RESULT of a time shows under its name after "Trigger" = step 4 "the operator presses the start key" (clock bench-1/io). A check that fails over a time names it: 312 is outside 0 … 200 (measured: 312 ms after "Trigger" …). Two clocks, or no bench clock at all — not measured on a bench clock — are said beside the number; such a time never passes silently. See Times and what they are measured from.

Building asks for a reference

A WAIT_UNTIL, COLLECT or TIME_OF in a check without since gets the warning needs a reference. If the check is new or changed since the last build — a renamed sentence counts as new — it does not build, and the step window says why. Checks that were there before keep building and running as they did, and their result says measured from the start of this step — no reference, until they are changed in the workspace. A renamed value of a data point does not count as a change. Nothing is rewritten for you.

Building also warns when a mark and the event it is measured against are stamped by two clocks, or when the marked point has no bench clock; and check_procedure reports an AT("x") that no earlier step of the test case marks.

Coverage runs judge world, trigger and observation

The assistant's ways of working now say how a test case is written — what is established before (the world), the one event the requirement says causes the reaction (the trigger), and what the device then does (the observation), in that order — and that every time check names its reference. Where a requirement leaves open which event is the trigger, the assistant asks you; in a batch run it builds its best reading, says so in the step's purpose and records an UNCLEAR finding for the author.

A coverage run that finds an existing test case getting that order wrong records a TRIGGER finding with one of four subtypes — world after the trigger, trigger not last before its checks, one condition from two triggers, sentence and definition differ. It stands at the checkpoint under Why, for the test cases, with the proposal that corrects it; Trigger: on the Decisions view filters by it, and the export carries it in a column of its own (subtype, after type). An import brings tests in as the source has them and says where the order is wrong, rather than rearranging them. See Decisions.

Every source says which part of it is meant

A source is hardly ever the whole system: it is one set in an ALM, one area path, one module, one submodule on one branch. A source now carries that part as its scope, and a new one is not recorded without it; your assistant asks you for it. Wherever a source is named — Requirements, Regression, Onboarding, the reports and the findings export — it reads the same way: its short name, its name as a link, and its scope. A run keeps its sources as they were when it was handed in, and says so when a scope has changed since. A source recorded before shows scope not recorded until somebody says. See Where they come from.

Every change is recorded, with its reason

A workspace opened from a folder now keeps a history: every change to a test case, procedure, plan, suite, data point, block file, step, requirement decision or request — who made it, which way, and why. You do not have to do anything for it; it starts the first time you open the folder.

Who. XPressRT asks your name every time you open a workspace, prefilled with the last one (Working as in the header, where you can also change it). A change without a name is not made. Changes an assistant makes carry the model it says it is and the platform it runs on, with its version — Claude Opus 5.5 via claude-code 2.1.286 — and what XPressRT does by itself — building blocks, recording what a test run wrote — carries XPressRT.

Which way, and why. Every entry says whether it came from the interface or from an assistant, and through which action. A change made from a decision carries the decision; a change an assistant makes carries its reason. An assistant has to give one — the request it carries out, the run it is doing, or what you asked in the conversation — and a change without a reason is refused. So a test case changed on an approved coverage proposal reads: approved by you, on this date, carried out by the assistant on that request.

Changes from outside are not lost. When a file changes while XPressRT is not looking — an editor, git pull, a copied folder — the history notices it when you open the workspace, when you come back to the window, and after a test run, and records it as changed outside XPressRT. Who did it is not known, and it says so instead of guessing. A file that was only rewritten with the same content is not a change.

Where you see it:

  • Under every test case, procedure, plan, suite, data point and block file a History section: newest first, with name and role, the way it took, the reason, and what changed in plain terms — Step 3 (Check): "within 2 s" → "between 2 s and 2.5 s", Variant row 4 added, unit: "°C" → "K", Added test case "Heating" in Oven. A test case shows only what changed it, not its neighbour in the same file; when only the preparation of its procedure changed, that stands there as a note. A renamed or moved test case keeps its history, a copy starts its own.
  • Changes in the sidebar: every change at once, filtered by people, agents, XPressRT or outside, by kind of object, by date and by text. Export as CSV or HTML; every export names the last recorded change and its time, so it can be shown later that an export belongs to exactly that state.
  • The History of the Requirements page (see below) now links every entry to the change that recorded it, names the way it took, and also shows changes to the coverage state that have no entry of their own — a finding edited by hand, for example.
  • Your assistant reads it with read_history — every change, or the history of one test case, procedure, plan, suite, data point, block file or requirement — so you can ask it who changed step 3 and why? or what did the agent change yesterday without a request?. about says since when the workspace keeps a history.

Where it lives. In .xpress/history.git inside the workspace folder — an ordinary git repository with the workspace as its work tree, so git --git-dir=.xpress/history.git --work-tree=. log reads it, and a clone of it opens in Fork or any other git client. Read it, never write into it: XPressRT keeps it. See History. .xpress/ is listed in the workspace's .gitignore, so the history does not travel into your team's repository.

What changes in your files. Findings, decisions and approvals of a coverage run, and requests to an assistant, are now kept one file per entry (agents/coverage/findings/, decisions/, approvals/, agents/requests/) instead of one large file each. XPressRT converts an existing workspace the first time it opens it. If you keep the workspace in git, expect that commit.

What it does not do — yet:

  • It cannot restore an earlier state. It shows what was; going back is a separate step that is not built.
  • It is kept per workspace folder on one machine. Two people working on copies of the same workspace have two histories.
  • A workspace that lives in the browser has none. The History section says so.
  • A test case renamed outside XPressRT appears as deleted and created anew; only a rename through XPressRT carries its history along.

From requirements to test cases, and everything that waits for the agent

From the test runs of 02. to 05.10.

Everything that waits for your assistant is in one place. The Agent queue on the Assistant page lists every open request from every page, numbered in the order the assistant takes them — the same numbers open_requests gives — with what is to be done, who asked, and a warning when it no longer fits. Every row can be withdrawn with a reason; what the assistant closes disappears at once, also in another tab. The sidebar entry Assistant carries the count. See The Agent queue.

  • Schedule coverage and Schedule regression under the title of each page leave a request for the assistant: coverage over all requirements, the ones whose test cases changed, or what the filter shows; a regression with what changed, which is required.
  • An approval can be withdrawn while its request is open — a proposal, a data point, an adaptation on the regression page — with a reason, under your name. What it approved stands to be decided again. Anything else waiting for the agent is withdrawn as a request, also with a reason; a withdrawn request the agent closes afterwards is not counted as done.
  • Two steps approved behind the same step both count as written. The one written second moved the first down, and the first read as not done — and could be approved a second time. Now a step that already stands behind its step cannot be approved again, and the assistant is not let to write it twice.
  • Decisions shows what waits for you, in sections: Look again (a decision whose evidence moved, with what changed, step 11 → 12), To review, Waiting for the agent, the answered ones, Marked outdated and Handled at the checkpoint. The log of every decision moved to History → Decisions only.
  • Every finding says for whom it is — the author, the test cases, the bench or the team. You can change it and mark a finding outdated, both with your name; filter by it and export exactly what is filtered as CSV or HTML. The HTML report has the questions for the requirement author in a section of their own. A gap or partial coverage at a checkpoint is no longer a finding to decide: it is answered by the proposal there.
  • A finding shows its repair: the proposal the agent made for it, or the ones at the same checkpoints.
  • What the source links stands next to what the runs checked: a relationship in the ALM or a requirement tag on the test case, as a dot in Traceability, with a filter for where the two disagree and a column in the CSV.
  • Data points first, then the tests. A proposal that needs a data point the bench does not have brings it along. Data points to decide comes first; approving answers every open question the source left, and the tests waiting on it become approvable once it exists.
  • A note for the agent can go with an approval; if the assistant deviates because of it, it says how, and the card reads done, deviating as the note asked.
  • Verified otherwise is done only with its evidence. Record evidence on the checkpoint, kept with your name; Withdraw evidence takes it back, and an older entry does not come back by itself.
  • Every id is a link, the place you jump to is marked, and Back returns to where you were. Quotes match whole words and say when their place is unclear.
  • A step added with new sentences names them and closing checks they were created as approved; a change only in form — spacing, punctuation — no longer reopens a checkpoint. A decision whose finding is gone is archived; a proposal whose test case was deleted reads gone, with nothing to approve.
  • A block step's approval can be withdrawn, with name and reason; the step list filters block steps by approved and not approved.
  • Buttons say they are working. A button that waits on the workspace shows it and cannot be pressed twice — before, nothing seemed to happen, and a second click asked twice.
  • A large run handed in by an assistant gets its answer instead of a timeout, and every change in the history carries its own title.

Test runs start faster

  • A run only mounts what it needs. Before every run, the whole workspace folder was copied into the runtime — including earlier results and, with the history, the history repository — and copied back afterwards. Now a run sees your procedures, steps, data and configuration and your own folders, but not earlier runs, the coverage state or XPressRT's own files. On a workspace with 1,500 files, mounting went from about 1.4 s to 0.1 s, and the report button appears 0.6 s sooner. Files that change on disk while a run is going are no longer at risk of being removed when the run writes back.
  • The blocks are built in a tenth of the time. Before every run XPressRT rebuilds the Python of your block files. It opened the workspace again for every file it read and read the same files again for every step — on a workspace with nine block files, almost two seconds before every run. Now about 0.1 s.
  • The first question of a run comes a second sooner. The operator channel waited one second on every run for a connection it did not need in the browser.

Together, from pressing Run to the first question, on a workspace of that size and with the runtime already loaded: about 3.5 s before, under 1 s now.

Also in this release

The step types that addressed an instrument directly are gone — measure, power supply, switch or relay, communicate, program a device. Nothing served them: they stood greyed out in the picker and explained nothing, and a step built from one could not run. What comes back will come back through the data point, so the step that names one stays as it is. A step from an older workspace that carries such a type opens as Python; its body is untouched.

The regression page shows the state of now, not the finding of yesterday.

  • Every adaptation says what the test case reads now — ✓ step 3 now reads … against still reads … — not changed yet — without opening the chain, and read from the test case rather than from what the agent reported.
  • A test case the agent wrote on request moves up to the affected ones and drops out of "changes without a test case". For that the agent names it; a name that points at nothing is refused.
  • A line in the header counts what is still open, down to ✓ Nothing open, and the same count warns at the test plan: it can be made, and these test cases will change while it stands.
  • An adaptation can now also remove a step, remove rows of a variants table or add a step — the three most common real ones, which had to be written as free text before.

A whole set comes in at once. An assistant writes a plan as a file in your workspace and imports it in one call — all of it or none of it, through the same checks as one at a time. See Onboarding. The first real import took 292 calls for 37 test cases; that is what this is for.

Braces that are not a name are data. A test case line may read rmCAN sends {"x":10, "y":0} to PatientPositioning and stays as it is. For software and module tests that is the normal case; before, every message had to be moved into a variants column.

Smaller things, each from something that happened:

  • A source can be taken back out (withdraw_source), on your word and with your name. A wrong address stayed before, and the next coverage run read two sources for the requirements where there is one.
  • The decomposition can be confirmed in the conversation, with your name and the date; the coverage report says who instead of unconfirmed.
  • An operator question that asks for something this workspace can measure is named when it is created, and about counts how many there are. In the first real workspace 17 of 18 steps asked a person while 16 data points lay unused.
  • Test cases that say where they came from and that no onboarding run records are named too — at three, ten, twenty-five of them.
  • A column of a variants table that no step uses is reported, unless its name starts with #: #Objective is a note for whoever reads the table.
  • The runtime is fetched while you are still looking for your procedure, and leaving a view no longer throws it away.
  • A finding of a regression run can quote its source (quote); the quote stays in the source's own language, everything the agent writes itself is English.
  • The answers an assistant gets carry fields as well as words, and it can read several test cases, ask about several names and answer a dry run's prompts from one table — one call instead of one per prompt.
  • A dry run over a whole procedure says that it is an execution and not a static regression. The two were confused once, and half a set ran with made-up answers before anybody noticed.
  • "Bring my requirements in" now lands in the right place. It is the same run that judges coverage — requirements arrive by being read, split and judged — and an assistant says so instead of writing test cases by its own lights. It asks one question first: do tests for these requirements already exist somewhere? If they do, they are brought in first, so you do not get proposals for test cases you already have.
  • The Coverage page is called Requirements. It is the same page and the same measurement — but a page named after a measurement can only carry the measurement, and this one is where requirements are judged and, before long, where test cases are asked for. The old address still leads there.
  • A coverage run can propose the repair, and you approve it on the Requirements page. Rows for a variants table, a step, or a whole new test case — each in a box under its checkpoint, with the sentences it would add to the pool listed before you decide. An approval carries your name and becomes a request for your assistant; a rejection carries a reason. If a later run proposes something else for the same checkpoint, the approval does not carry over: both are shown, and a request not carried out yet is withdrawn.
  • A link whose step has changed no longer counts. Before, it was named in a box on the side while the number, the bar and the checkpoint still said covered. Now the checkpoint reads unjudged — was covered until a coverage run has looked again, and the report counts it the same way. A run can also name the variants rows a verdict rests on; striking one of them has the same effect.
  • A limited coverage run no longer drops findings outside its scope, even when they name a test case it brought along. It also keeps the title, summary and order of the requirements page instead of replacing them with its own.
  • The approved proposal is the one that gets written, values included. The box, the request to the assistant and read_agent_run show the steps and the variants table; closing checks the table. A proposal for a second test case on the same checkpoint no longer replaces an approved one, and a test case written on request stays visible at its checkpoint until a run has judged it.
  • "Done" is checked, not taken on trust. When your assistant closes a request as done, XPressRT looks at the test case first — rows, steps, their place and keyword, the preparation, and for a new test case every approved step in its order and the new sentences as approved. If it is not there, the request stays open and the assistant is told what is missing. On the regression page an adaptation reported as applied that the test case does not show can be sent back to the assistant.
  • A proposal card says what became of its request: waiting for the agent, written, reported done but not in the test case, declined with the agent's reason, or withdrawn. Two changes to the same test case no longer replace each other, and deciding again withdraws the earlier request at once. The card also names the test case and the steps around the change, and a step for the preparation says how many test cases it reaches.
  • Your assistant asks before every coverage run whether to bring proposals — all of them, only the missing ones, or none — and records your answer with your name. Nothing in between. With all or missing, every open checkpoint comes with a proposal or a reason why there is none: the bench lacks a data point, a procedure is missing, or the author has to answer a question. The Requirements page says what you answered, counts the reasons, and lets you ask for a proposal at a single checkpoint later.
  • The Requirements page carries its numbers in the head, cut by the source you picked, and the column with runs, sources and export folds away. Proposals fold per checkpoint, earlier ones per proposal, and every requirement shows how many proposals it has and how many are unresolved.
  • A rejected proposal stays rejected. With "missing" it counts as there, so the repair you turned down does not come back by itself; Ask for a new proposal on the rejected box asks for a different one, and a run that brings the same repair under a new name is told so. Asking at one checkpoint asks about that checkpoint only; the rest of the requirement stays as it is.
  • A source is "fetched" only when a run fetched it. A run limited to a few requirements no longer moves the date unless it says it read the source. The source shows the date of its note, how many requirements the state holds from it and when a full run last read it; the source of the test cases says so.
  • About names the code that is running. On a development server that is the current commit and whether it has uncommitted changes, not the one the server started with.
  • The Requirements page reads by the work, not by the letters. Each requirement shows its title from the source, its text as Description with the quoted places marked, and its checkpoints grouped: to decide, waiting on someone else, gaps, covered (folded). Status is a pill with dot and word; a proposal is a box of its own with the difference it makes. New views: Proposals (by what became of them), Traceability (requirements × test cases, with CSV export) and a matrix per requirement. Runs, freshness and sources moved from the side column into the head, folded to one line.
  • Intelligent Regression reads in the same way. The run's head carries its quality as a coloured tag and three tiles that lead to their part of the page; Regression planning stands open under it with the plan button. The parts fold as cards in the order they are worked through — What changed, To adapt, Run as it is, Changes without a test case, Hints — and an adaptation shows its difference as on the Requirements page.
  • The approve button says what it does — the agent writes it — and once a proposal is approved, or a new one asked for, the page offers the sentence to hand your assistant, to copy, as the regression page does.
  • Proposals that need you are all in the Proposals view. Besides To decide, the groups Agent says done — test case not and Back with you — declined or withdrawn list what an agent handed back, including approvals whose proposal a later run no longer brings; Send back works for them too.
  • A request for a proposal stays on the page. Asking at a checkpoint shows who asked and that the agent has not answered yet, with the sentence to copy, after a reload too; the agent's refusal stands there with its note and Ask again. You can ask at any open checkpoint without a proposal, not only where a run gave a reason.
  • One test case for several checkpoints. A proposal names the other checkpoints it checks (alsoCovers); they are no gaps, the matrix marks them, and if the proposal is rejected they say which one they relied on.
  • A new test case that repeats its procedure's preparation is refused, and a finding that concerns nothing is refused; a run no longer removes a finding of another tranche, and a later tranche may name it.
  • An operator question that runs out of time is not answered. Reading and setting data points fail through their If it fails setting instead of taking whatever stood in the dialog — a setting step with no target used to pass with nothing recorded. The failure reads as a sentence, not a Python traceback.
  • Numbers in a data point step are checked while you write it. A value that is not a number is red where it was typed and the step cannot be saved; an empty Set to says at the field that the operator will be asked.
  • A requirement keeps its structure. A transition table or a list in the source stays one on the Requirements page instead of running together into a line of words; a quote inside a table cell is marked in that cell. Nothing in the text is drawn as HTML.
  • "No proposals" keeps what was already there. A run answered none brings a checkpoint back with the reason it had; only a reason the run writes itself is refused.
  • A run is told what it would otherwise lose unnoticed, without being refused: newSentences next to change instead of inside it, an id shortened from the one the source prints, a requirement whose address the state already holds under another id, a quote that is not in the text.
  • A checkpoint quotes its requirement. One without a quote is refused, unless it is not testable because there is nothing to quote.
  • The open work adds up. A checkpoint that is not split and not testable was counted twice. Handing in a run now says testable checkpoints, with the total and the not testable ones beside it.
  • Traceability shows the requirement, not its id twice: the title or the start of the text, and the id underneath. The CSV carries every open proposal as its own row. Compare with … names runs the way the run list does.
  • "Run as it is" is one line per test case. Reclassify… opens on request, Deciding as is said once beside the name, and nothing sticks over the cards while scrolling. Empty parts start folded, the chosen run stays in the address, and table rows in evidence show as cells instead of pipes.
  • Freshness compares content. A procedure changed and set back no longer marks the state out of date. Limited runs that together read a whole source count as a full read, and the source says when.
  • A folder chosen again is the same workspace, with the same id, and the list calls it by the name in its xpress.json. The Assistant page no longer says "no reconnect" beside the Reconnect button.
  • describe_bench says what WAIT_UNTIL measures. It told agents a bench could not time milliseconds, and they reported every timing check as a finding.
  • A checkpoint can be verified otherwise. Inspection, analysis and demonstration stand beside the coverage by test cases, with the evidence they name — not as a gap, and no longer as not testable that blocks the whole requirement.
  • Steps show their kind, not "And". Differences, the state of an adaptation and new test cases say Setup, Action or Check for every line.
  • The same requirement under a new id is refused. A run that gives an address the state holds for one requirement a different id — a shortened one, say — no longer lays a second copy beside it. A real rename says so with source.stableId; a requirement that is gone goes into meta.retired. Many requirements sharing one document address are not affected.
  • Where a requirement's text serves as its title, only its first paragraph shows — no table or list as raw text. Table rows in evidence show as cells on the Requirements page too.
  • After a rejected proposal that also covered a checkpoint, that checkpoint no longer says it was "not asked", and counts no proposal. The Proposals tab counts what its groups count.
  • A checkpoint verified otherwise names its evidence in its row. A new test case that repeats its procedure's preparation is told whether that runs before the procedure or before every test case. A run that leaves out a finding only it concerns is told so, instead of "there is no finding".
  • The regression page forgets fold states from older builds, so its parts start the way they are meant to.
  • History, for the audit. The Requirements page has a History view: who decided what, when and why — runs, decisions on findings including replaced ones, approvals and rejections, questions to the agent, closed and withdrawn requests — with the file each entry is recorded in. Per requirement from its card, as CSV, and in the HTML export. A withdrawn request now records when and by whom.
  • The Agents section in the sidebar stays open on the Requirements page too.
  • A limited run keeps what reaches beyond it. A finding that also concerns requirements outside the run stays, with the links that name it; the answer names such findings, and names every finding or proposal a run does remove.
  • "None" keeps the proposals that are there. A run asked for no proposals brings none and leaves the earlier ones as they are, with their decisions. With "all", a proposal that is rethought keeps its id; one that would vanish is pointed out before the run is handed in.
  • A checkpoint opens again only when its sentence changes -- the sentence its quote stands in, or a step that checks it. A sentence added elsewhere in the requirement, or a link that only touches, no longer opens every checkpoint.
  • A changed step must be a sentence that exists. A replacement whose step would read as a new sentence names it in newSentences, as a new test case does, in coverage and in regression alike.
  • The freshness note names the procedures that changed, and counts only those with test cases of the state. The workspace button shows the name the workspace gives itself, and the Assistant page offers Reconnect for this workspace when the window moved to another one.
  • Requirements a full run did not bring are no longer counted. They wait, marked, for a person; a run that knows they are gone archives them with the reason.
  • Steps on the agent pages read Setup, Action and Check, as everywhere else. A step added "after step 2" is found behind its neighbour, even when another change moved the numbers.
  • An assistant does not follow the window into another workspace. Its server is started for one workspace; if somebody opens a different one in the window, writing is refused and every answer says so, until the window is back. Before, every call quietly worked on the other workspace -- an assistant told to stay in one project had no way of noticing it was changing another.
  • A dry run says how far it has got. pending_question now also answers with how many test case runs are done of how many, which test case it is in and which step it last reported. A row of a variants table counts as a run of its own, because that is how it is executed. Before, between two prompts there was only "a run is going", and an assistant either asked again too early or waited too long.
  • A run can be checked before it is handed in (validate_agent_run): the same checks as handing it in, nothing written, and every reason it would be refused. A refusal at the moment of handing in means the whole run goes back, and a run is not small.
  • A step's sentence may contain +, ( or . and the test case that writes it still finds it. A sentence became a pattern without its special characters being escaped, so TC-021 +3V3 is between {u_min} … demanded a space the line does not have and matched nothing: the step was in the list and writing the test case was refused with has no step. Specifications for electronics are full of +3V3, +5V, ±2 %. A full stop in a sentence is now a full stop as well — at J27.10 used to match at J27x10.
  • The language your operator is spoken to in can be set per workspace (operatorLanguage in xpress.json). Everything an assistant writes stays English; the texts a person reads in a dialog are written in that language, and the note about German text is not raised for them. See Your workspace.
  • Asking for a state that nobody has written yet is answered with a sentence instead of an error. A fresh workspace has no coverage state, and that is the first thing an assistant asks about; it used to read back GET /agents/state/coverage → 404 and had to guess whether something was broken.

Smaller things

  • Data points: the list on the left can be dragged wider for long names in deep folders, and keeps its width in your browser.
  • History: recording a source said record source () for every source; it now names the source, its scope and its address.

Version 2.2

Released 24 September 2026. What is new:

Agents: what an assistant works out, kept as a state you decide about. A new section in the sidebar with three pages, each the record of a kind of work, and each checked by XPressRT before it is accepted.

  • Onboarding — the tests you already have, brought in from a document, a spreadsheet or a requirements tool. Every import stays as its own event, with what was written, what was refused and why, and the source each test case came from.
  • Coverage — what the requirements ask and what your test cases check, statement by statement, with the evidence for every link. Findings go into a queue you work through; a decision belongs to the text it was made against, and changing that text suspends it rather than keeping an answer to a different question.
  • Intelligent Regression — a change happened: which of my test cases does it affect, and why? The change is split into points, each point carries where it stands and what it was before, and every affected test case carries its evidence. The page says how much of your test base the change needs, and — the other half — how many changes nothing checks at all. You reclassify, decide, approve an adaptation, ask for a missing test case, start a limited coverage run, and make a test plan out of it. It is static: no run history, no pass rates, no bench minutes.

What you decide is kept beside the run under your name and the time, and it travels into the HTML report. XPressRT refuses a run that claims more than its input carries, and it never changes a test case on its own: an approved adaptation becomes a request your agent carries out, one line at a time.

Blind spots, as a preview. A fourth page says what is coming: code, requirement and test case held against each other, and above all the test that was written from the code rather than from the requirement — green, and proof of nothing. Nothing runs there yet, and it carries no numbers.

Named values for data points. A data point can hold a list of names with their numbers — up is 0, down is 1. Steps, blocks, reports and courses read and write the name; only the middle keeps the number. Renaming a name carries every place that means it along, and a recorded number keeps its name.

The test runtime says what it is doing. The first run of a session loads Python and its packages into the browser. That used to look like nothing happening; now the stage and the progress stand on the page, Run Test and Stop are greyed out until it is ready, and a failed start says so and offers another attempt. Pressing Stop while it loads no longer throws the half-finished download away.

A stopped run leaves its report. What it got through is recorded like any other run is now true wherever a run is stopped: from the page, by leaving the view, or with stop_run. (Reloading the page is still something else: it takes the run away before there is a report to write.) The run winds down — the rest is skipped, the clean-up runs, the report is written — and the page and the assistant both say that it is winding down. stop_run stops a run whoever started it; it used to see only its own and answered "no run is going" while one was going.

The workspace stays as it is while a run goes. Steps, blocks, data points, procedures and test cases are refused while a run is going, with a sentence naming the run; reading stays open. A run has to carry out what it says it does.

A step's description can be changed. It was written once and fixed for good after that. describe_step changes it, and for a step built from blocks it writes the PURPOSE in the block file as well — one text in two places instead of two texts. Where they differ, building says so.

Everything an assistant writes is in English — every text field of every tool, not only what it hands in with a run. What looks like another language is named, not refused, and about counts what is still there: descriptions of steps, data points and plans, the sources, the titles of agent runs, and the block files.

Fixed.

  • describe_step wrote the description of a different step when one sentence was the beginning of another, and reported success for the one it was given. The decorator is matched on the whole sentence now, and what was written is read back before anything is called done.
  • A coverage run that brought a few requirements took every finding of the state with it — including the ones somebody had decided on, whose decisions then pointed at nothing. A finding stays as long as what it points at stays, and a decided finding is never dropped in silence.
  • history listed every test case in a folder as never run, two lines under its own results, and the same for every test case with variants. A test case that ran is no longer reported as never run — which is what makes the list worth reading.
  • Renaming a step left its action-type settings behind under the old name; workspaces collected entries for steps that no longer existed. They move with the step now, and what is left over from before is cleared away.
  • Deleting a data point left its name in the generated step files until the next build. They are written again with the deletion, and it says which.
  • An agent that could not carry out a request had its reason thrown away, and the page showed the button again as if nobody had ever asked.

Pictures in the operator's dialogs. The blocks SHOW, CONFIRM, ASK_TEXT, READ and READ_SERIES can show a picture from local/assets, and building checks that the file is there. An assistant can give an operator question a picture as well. See Pictures in a dialog.

Pictures as part of an answer. The operator can attach photographs to a confirmation, a yes/no question, a typed answer or a reading. They land in the report under the step. See What the operator sees.

Every dialog names its step. Dialogs from block steps run in the browser, and the defect and proof forms, now say which procedure, test case and step they belong to, like every other dialog. A dialog taller than the window scrolls.

A step's timeout counts its own work. Time a block or Python step spends waiting at an operator dialog no longer counts against its timeout; each dialog waits up to five minutes for an answer. A step with several questions no longer fails at 60 s while somebody answers. An assistant can change the timeout of a Python step. See Time limits.

An assistant is told what will not work. Every tool an assistant has was gone through with wrong names, impossible values and names that point out of the workspace. What used to be written anyway is now refused with a sentence saying why, and nothing is written:

  • A test case may only name sentences that exist, under the kind they are defined as, with the values filled in and its own columns — the same check preparation and clean-up already made.
  • A variants table needs one value per column. A row with too few used to make the whole procedure unreadable, and every tool then refused it, including its other test cases.
  • A name with .., a leading / or a backslash is refused. Such a name put a procedure or a step body outside the folder it belongs to, where no list showed it and no tool could remove it.
  • Limits that no reading could pass (min above max, or limits outside what the data point can hold), a target outside the point's range, a question without text, a negative wait and a timeout of zero.
  • In a dry run: an answer the dialog has no button for, a value the bench could not deliver, and a reading that is not a number. They used to be taken and the step failed later, which reads like a fault of the unit.
  • Suites and plans over procedures, test cases or suites that do not exist. A plan over nothing counted nothing and looked like a plan that had not run yet.
  • Tags with a space in them. Gherkin reads them as two tags, and behave reads the second one not at all.

What an assistant says it did, it did. update_test_case, read_test_case, update_suite, set_suite_status and delete_suite used to report success for a name that does not exist. They now say there is no such thing. Renaming a step to a sentence that exists, a run that is not there and an answer to a question nobody asked are answered as refusals rather than as faults of the application — the browser console stays for real ones.

Renaming and deleting leave nothing behind. A data point that others are computed from cannot be deleted while they need it, and renaming one carries its sources along, in the catalogue and in the blocks that read it. A refused rename leaves the step bodies untouched. Deleting a procedure or a test case takes its preparation and clean-up with it, so a new one of the same name does not inherit them. A step file with no step left in it goes when the step does.

What a step is told about itself. read_step finds a sentence with its values filled in instead of warning that it does not exist, check_procedure names blocks that cannot be built and a time span measured across two clocks, and list_runs gives the newest 20 with the full path of what ran, filtered by procedure if you ask. Progress is in words, not in the field names of an answer. A data point name with an umlaut keeps it: Größe becomes Groesse, not Gr_sse.

In the interface. The banner over a test case counts every line that cannot run, not only the ones without a definition. The test case page shows the tags it inherits from its procedure. A new suite has no status and no last run time until something runs.

Fixed. Changing what a step does through the assistant kept its operator question only when the step had been saved in XPressRT before; a step that came with the workspace or was written by hand lost it. Renaming a step made of blocks left the old sentence in the generated body, so the step was not found and the run reported the local runner as missing. A course a dry run's assistant typed was recorded as read by the operator. A copied or moved procedure kept the old title. A moved test case lost its preparation and clean-up and was renamed without saying so.

Version 2.1

Released 16 September 2026. What is new:

Blocks. A step body written as a short list of blocks instead of code — read a data point, wait, check it lies between two limits, attach the chart. The person who approves a step reads it as sentences, in English or German, and a flow diagram shows what runs in which order. XPressRT points out what would run but check nothing: a check with no verdict, limits that only reach the report, a value read and never used. A step is approved as a state: change a block and it has to be approved again. A workspace can add blocks of its own, through the same approval. Blocks run in the browser, with the rest of the run — no program to start. See Blocks.

Python steps. For what no block offers: a step whose body you write yourself, in Python, in your own editor. It runs on your machine, next to its code, while the procedure runs in the browser as always — set a breakpoint, run the procedure, and it stops there. The report shows what the step established, and every value typed for it. See Python steps.

Courses over time. A data point can say whose clock stamps it and what that clock can tell apart, and whether it is computed from other points. A step can collect a course, wait until a value crosses a limit, and measure the time between two events. Every value lands in series.json with its stamp, whose clock it was, and where it came from. Without a test stand, the operator is asked for the course instead.

An assistant over MCP. Claude, Copilot or Cursor can list, write, change and explain the tests in a workspace, through the window you have open — steps, test cases, data points, preparation and clean-up, suites, plans and tags. It checks a procedure for what would run and test nothing, builds blocks, and can start a dry run whose questions it answers itself or hands to you. The server lives in the workspace; open Assistant in the sidebar and press Connect once. Several assistants can work at the same time. See Working with an assistant.

What an assistant touched says so. Everything it changes carries the tag ai-generated until a person removes it. A value it typed during a dry run is recorded as entered by the assistant, and a serial number it gave shows "set by the assistant" in the report — never as something a person read at the bench.

Rules for assistants in the workspace. Every workspace carries an AGENTS.md that tells an assistant with file access what is its to write — data, images and helper modules in local/, step bodies as blocks — and what belongs to the interface.

Step by step. A run can stop before every step and ask whether to run it, or to stop the run there.

Tags on steps and plans, next to the tags on procedures and test cases.

Changed

  • A plan report says which runs no longer count. "No usable result" means the run was made against a version of the test case that has changed since — not that a newer run replaced it. The list of runs folds away.
  • A single run says it is one, instead of drawing an empty trend.
  • A dry run never counts towards a test plan. Ticking Dry run ticks Does not count towards test plans with it, and a dry run an assistant starts does not count either.
  • Stopping a run leaves a report of what got through, also when a question was open. A step whose question nobody answers in time fails with a timeout, and what was typed before that is kept.
  • The report says where a serial number came from: entered by the operator, set by the assistant, or given when the run was started.
  • Numbers written into a text by a block are rounded the way the report writes them: 11.4, not 11.399999999999999.
  • A step whose list could not be loaded says so, instead of claiming "No matching step" for steps that exist.

Fixed in this release

  • Trying a step out from an And line no longer breaks the step file. It wrote the step back as @and, which behave does not know, and every step in that file went missing. A step is always Setup, Action or Check, and one sentence cannot exist under two of them.
  • Python and block steps with the word Action or Check ran. Before, only Setup steps were found.
  • A procedure file that cannot be read says what is missing and can be repaired, and a deleted procedure no longer stays in the editor.
  • A note written beside a value reaches the record with the value.
  • Saving the preparation of a procedure no longer removes the preparation of its test cases.
  • A change an assistant made is no longer reported as failed when only marking it failed.

Version 2.0

XPressRT now runs entirely in the browser. This is the change the release is about, and everything else follows from it.

Nothing is installed. There is no server to set up, no account to create and no data leaving the machine you test on. Open the address, and the application — including the part that runs your tests — is in the page. On a bench machine that is deliberately kept off the network, that is the difference between usable and not.

What that changed for you:

  • A workspace can be a folder on your own machine. Your editor sees it, your backup sees it, your version control sees it. Or keep it inside the browser and start with no setup at all.
  • A run needs no network. Once the page has loaded, testing works offline.
  • Nothing to maintain. No installation on the bench PC, and no version of the application to keep in step across benches.

What is new besides that

Data points. A named quantity — its unit, its direction, what it can plausibly hold — that a step reads or sets. The step says what, the bench says with what. Today the operator types the value in; when an instrument takes over later, the test case does not change.

Preparation and clean-up around procedures and test cases. Four moments you can fill: before and after the procedure, before and after each test case. They appear in the report where they ran, they can ask the operator, and they can fail. A test case with variants gets them per row, so the second row does not start on what the first one left behind.

Defect and proof reports from the bench. A step can ask the operator to write down what went wrong when it fails, or what was observed when it passes. What they write lands in the report under the step it belongs to, with the pictures they attached, and as a page of its own to hand on.

Test plans with cycles. A plan says what a release has to cover and tracks how far it has got. Starting a cycle says from here; closing one freezes the plan so its result can be quoted. Not run is a state of its own — it says nobody has tested this yet, which is not the same as a failure.

The record. A report per procedure with everything hanging at the step it came from: the question asked and the answer given, the readings with the limits they were judged against, the charts, what the operator wrote. Beside it the machine-readable output, so results can be counted and archived without anybody reading them off a screen.

Fixed in this release

  • A workspace in a folder on your machine gets everything it needs. Setting one up stopped part-way on a file the browser refuses to write, and the run afterwards failed on the missing file rather than on the cause. A workspace that is missing something now fills it in when you open it, and setting one up that cannot be finished says so instead of leaving a half-made folder in the list.
  • A defect from a variants row stands under its step. It used to be listed at the foot of the report, away from the row it belonged to — the account was never lost, but it was nowhere near what caused it.
  • A failed preparation no longer lets the test cases run. They are reported as not run, and the run is not called complete.
  • The run summary counts a failed preparation as a failure, and names the step, the test case and the reason.

Reporting something

Tell us the version and the build from About, what you did, and what you saw instead of what you expected. A run's downloaded results say more than a description: they carry the report, the readings and the run's own record.

Where a run produced nothing at all, the browser's own console usually holds the sentence that explains it.