Working with an assistant¶
An AI assistant — Claude, Copilot in VS Code, Cursor — can work in XPressRT: list the steps and test cases, write new ones, create data points, read a run and explain what went wrong. It does this through the XPressRT window you have open, over the Model Context Protocol (MCP), and not beside it.
That is the whole design, and it is what makes it safe to use. The assistant can do what you can do in that window, in the workspace that is open there, and nothing else. What it creates appears in the interface at once. The assistant has its own page, Assistant in the sidebar (marked beta): that is where you connect it, and where What the assistant did lists its latest requests, newest first. The question mark beside XPressRT at the top of the sidebar leads there too: If you need help with anything, ask the agent.
The page is marked beta because the way works but still moves: which tools there are, what their descriptions say, how a change is marked.
What an assistant can and cannot do¶
| It can | It cannot |
|---|---|
| list procedures, test cases, steps, data points, suites, plans and runs | change a result — a record that can be edited afterwards is no record |
| read a test case and change it | reach a workspace other than the one open in the window |
| create procedures, test cases, steps and data points — an operator question or a reading with a picture, too | start a real run: a run it starts is always a dry run |
| set the preparation and clean-up steps of a procedure | approve a step built from blocks — there is no tool for it, and an approval is a person's signature |
| write the body of a step as blocks, and build them | |
| check a procedure for what will not run | |
| explain why a run failed | |
| try a test out — start a dry run and answer its prompts itself |
While an assistant is connected, the header shows a pink Assistant badge. It reads Assistant working while a request is being handled.
The rules it is given¶
Every workspace carries its house rules for assistants in AGENTS.md, written
by XPressRT and renewed when they change. At the top, before anything about
files and benches:
- The chain. Test cases, steps, data points, benches: each layer knows only
the next one, by its name, and none changes because another one is
implemented differently. To automate a test case, the assistant changes what
its existing steps do (
change_step_action) — it does not write new sentences or change the test case. A sentence belongs to every test case that uses it. - Without a bench. A workspace is always runnable. Data points and steps come from what the test needs, not from what a piece of equipment offers; until a bench binds a point, the operator serves it — in a dry run the assistant does. A bench comes last, and binding it changes no test case.
- Every step goes through data points. A check reads a point and XPressRT judges; a bare yes/no question to the operator ("Is this expected result true?") records nothing and is not what an assistant creates.
- Runs. The assistant may start a dry run and answer its prompts; a real run is started by a person at the bench.
The same sentences open the assistant's skills for onboarding, coverage and regression.
Steps an assistant cannot create¶
The rule on data points is not only written down; create_step,
change_step_action and import_plan hold to it. What decides is the shape
of the step — its kind and what it does —, never its words:
| Kind | Refused for an assistant | Allowed |
|---|---|---|
| Check | every operator question, a wait, a serial number, a set step | read, request, Python |
| Setup, Action | an operator question with Yes / No | everything else — an instruction the operator confirms, a typed answer, set, request, read, a serial number, a wait, Python |
A refused call says which step it was and what to write instead, and nothing
is written; import_plan names every such step of the plan at once. A person
in the step form is not bound by this. Steps an assistant created before are
not refused but counted: check_procedure and about list them, and
change_step_action switches each one on the sentence it has — the test cases
stay as they are.
Preparation and clean-up¶
Steps that belong to no single test case — once before the procedure, once
after it, the Preparation steps before every test case, and before or after
one test case — the assistant sets with set_preparation. The same rules hold
as on the procedure page, and a few more, because these steps run outside the
variants table:
- what it passes replaces that place; an empty list clears it
- every step has to be an existing sentence under its kind, with the value
written in — no
<column>and no{braces} - a step that does not resolve, or a test case that does not exist, is refused, and nothing is changed
A picture with an operator question¶
create_step and change_step_action take picture for a step of kind
ask or read: the path of a PNG, JPG, GIF or WEBP file on the machine the assistant
works on, up to 5 MB. It is put into the workspace the way the step form puts
it there, and the operator sees it in the same dialog. Left out, the picture
the step has stays; an empty string removes it, file and all. read_step
names the picture a step shows.
Checking a procedure¶
check_procedure finds what will not run before anybody is standing at the
bench: a sentence no step is defined for, a line written as the wrong kind,
braces left in a test case or in the text the operator reads, a column the
table does not have or no step uses, a data point that was deleted, a data
point a step uses the wrong way round — reading one that is set, or setting
one that is read — and the warnings on the blocks of the steps it uses. The
preparation and clean-up steps are checked like any other.
It matters because none of these faults announce themselves in a run: a step that is not found does nothing and does not fail, so a test case carrying one comes back green with nothing checked. What it cannot check is judgement — whether a limit is right.
Writing a step body¶
The body of a Python step is written as blocks, not as Python: the assistant
writes a file in local/blocks/, then calls build_blocks, which builds the
Python under local/steps/ and names every line that did not build. If no
block does what a step needs, it says which block is missing. See
Blocks.
When the assistant is running a test¶
An assistant can start a dry run to find out whether what it wrote actually runs and whether its checks fire. It decides when it starts the run who answers the prompts: itself, or a person at the bench, in the ordinary dialog. A real run — the one that is evidence about a unit — is started by a person.
While a dry run goes, a window sits in front of the page saying so, with a Cancel the run button. That window is also the lock: nobody can start a second run while it stands, because two runs in one workspace write into the same place.
When the prompts go to the assistant, the run stands until it answers — which looks like a hang and is not; the window says what is being waited for. Two things you can do from where you are sitting:
- Cancel it. Nothing is sent to the assistant at that moment. It finds out the next time it asks what the run is waiting for: the answer then says plainly that the run was stopped before it finished, that no further question will come, and that what came after the stopping point did not run.
- Answer one yourself. The assistant can hand a single prompt over — a question that needs eyes on the unit, say — and then the ordinary dialog appears, with its picture, its readings and its forms. The prompts after it go back to the assistant.
A step that reads a record or a list asks for it as a whole:
pending_question names the type and its fields, and the assistant answers
with an object, a field with named values by its name. A check that collects
what a point received over time is answered with one entry per occurrence
and its moment — and for a point that reports events an empty list is an
answer: nothing was received. Every field is checked against its type; what
does not fit is refused with the field, what was expected and what came, and
the question stays open. Such a question can be handed to a person like any
other: they get the dialog with one input per field, or the table of
occurrences.
create_step writes form steps on such points too. A read step says per field
what it checks — "fields": {"code": {"min": 4000, "max": 5000},
"detail:severity": {"expect": "{sev}"}} — or, on a list point, the entry it
has to contain ("contains"); a set step gives the record as target, a
request a record parameter as an object. A parameter of the sentence stands
wherever a single value stands, never inside a list. read_step says it back
field by field.
A dry run is a run like any other: same record, same report — except that it never counts towards a test plan. What it proves is that the test runs, not anything about the unit — and where the assistant supplied the values, the record says so:
- in
series.json, each value it typed carries the originassistant - in
measurements.json, its readings carry the sourceassistant - in
step-results.json, a value typed into a step carriesenteredBy - a serial number it entered shows as set by the assistant in the report and
when the assistant reads the run back with
explain_run
What an assistant changes is marked¶
A procedure, test case, step or plan an assistant creates or changes carries the tag ai-generated afterwards. Every change it makes sets the tag again. Taking it off is the review: a person has read what the assistant wrote.
Marking is a second write after the change itself, and it can fail — most often when the page cannot load a part of itself any more. Then:
- the change still applies
- the tool's answer to the assistant carries a NOTE saying what was written but not marked
.xpress/unmarked-changes.jsonkeeps each open entry, with what it touched, so it survives a reload (a workspace that still has the file at its root gets it moved there once, when it is opened)- the next time marking works, XPressRT marks the open entries one by one and removes each that it could mark; an entry that was too big to keep with its change stays in the file and has to be marked by hand
aboutlists what is still open
This is not the same as a request that did nothing. If the assistant reports that the XPressRT page could not load a part of itself and asks you to reload the page, nothing was done: reload the page, and let it try again.
The tools¶
The assistant sees these tools. You do not call them yourself, but knowing them tells you what to ask for.
| For | Tools |
|---|---|
| Finding your way | about (versions, and what is still unmarked), describe_bench (who answers a reading, and what nobody here can know) |
| Procedures | list_test_cases, create_procedure, update_procedure, copy_procedure, move_procedure, delete_procedure, check_procedure, set_preparation |
| Test cases | read_test_case, read_test_cases (several at once), create_test_case, update_test_case, move_test_case, delete_test_case |
| Steps | list_steps, read_step, similar_steps, create_step, change_step_action, change_step_kind, describe_step, rename_step, move_step, delete_step |
| Blocks | list_blocks, build_blocks |
| Data points | list_data_points, create_data_point, update_data_point, rename_data_point, rename_data_point_value, delete_data_point |
| Record types | list_record_types, create_record_type, update_record_type, rename_record_type, rename_record_type_field, rename_record_type_value, delete_record_type — messages and bundles, see Records and lists. Renaming a field or one of its named values rewrites the block files and instructions that name it in one write — and, for a value, the test cases whose sentence or variants table gives it to that field; what_uses with record_type and field lists all of them beforehand, and Python written by hand is reported, not rewritten. check_procedure names a test case that gives a field a value it does not know. |
| Folders | create_folder, rename_folder, delete_folder |
| Benches | list_benches, read_binding, write_binding_row, remove_binding_row, propose_binding — rows of a binding only; commissioning stays with a person, see What an assistant does on a bench |
| Tags and suites | list_tags, set_tag_colour, list_suites, create_suite, update_suite, delete_suite, set_suite_status |
| Plans | list_plans, create_plan, update_plan, delete_plan, start_plan_cycle, close_plan_cycle, plan_progress |
| Dry runs | start_dry_run, pending_question, answer_question, answer_questions (several from one table), hand_to_operator, stop_run |
| Reading runs | list_runs, explain_run, history |
| What reaches what | what_uses, what_uses_many, changed_since |
| Agent runs | save_agent_run, validate_agent_run (the same checks, nothing written), read_agent_run, list_agent_runs, import_plan (a whole set at once), confirm_decomposition |
| Requests from a page | open_requests, close_request, adapt_step |
| Sources | read_sources, propose_source, withdraw_source |
| How to work here | skill |
Six prompts come with it. An assistant client shows them as commands you pick, not as something the assistant calls:
- what_can_you_do — looks at what is in the workspace and says what could be done with it
- explain_the_last_run — reads the newest run and explains what failed and why
- build_a_procedure — walks from what is to be tested to the data points, the steps and the test cases, and shows you each part before writing it
- bring_in_my_tests — reads the tests you already have and writes them here, with where each one came from; see Onboarding
- show_me_coverage — works out what your test cases cover against the requirements; see Requirements
- plan_a_regression — works out what a change affects; see Intelligent Regression
Some answers carry fields as well as words. pending_question,
read_test_case, read_test_cases, list_test_cases and read_agent_run
answer with the same thing twice: the text a person reads, and the fields an
assistant would otherwise have to read back out of that text. It matters for one
thing you might notice: an assistant that answers a dry run no longer has to
guess what a dialog accepts, so fewer runs fail on a "yes" where the step wanted
the word "confirm".
A dry run says how far it has got. While it runs, pending_question 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 — a procedure with three rows is
three runs, not one. The total is read from your file before the run starts; if
it cannot be read, the answer says how much is done and not how much is left,
rather than naming a number nobody counted.
A run can be checked before it is handed in. validate_agent_run runs
exactly the checks save_agent_run runs and writes nothing: the answer says
whether it would be taken and, if not, every reason. It is the same checks and
not a second set, so a run this calls clean is one that goes in. What it cannot
say is what comes about when a coverage run is laid onto the state — which
decision stands, what comes back open. That happens there.
A wrong source can be taken back out. withdraw_source removes one on your
word, with your name — a wrong address, a container nobody meant, one that was
named twice. What was already handed in stays as it was: a run names the source
it read, and that record is what it was at the time.
A request you leave on one of those pages — start coverage for these
requirements, carry out this adaptation, write the missing test case — waits
until an assistant calls XPressRT next. It is named above every answer the
assistant gets, and open_requests has the whole of it. The page cannot reach
the assistant; the line runs the other way.
The Agent queue¶
Before you tell your assistant Work through the open requests in XPressRT, you can see everything it will work through: the Agent queue, on the Assistant page under What it cannot do. The sidebar entry Assistant carries the count — Assistant · 3 — so you see that something is waiting.
It lists every open request from every page — an approved proposal or adaptation, a data point to create, a question for a proposal, a test case to write, a coverage run or a regression you scheduled. A kind of request that is new to this page still shows, under its own name. Each row says:
- its number, in the order your assistant takes them — oldest first. The
numbers are the ones
open_requestsgives: 1 of 5 in the conversation is row 1 here; - what kind of work it is, and a link to where it came from — the checkpoint, the data point, the regression run, the test case;
- what exactly is to be done — the line your assistant gets, such as add "Check …" after step 2;
- who asked and when, and the note they gave with it;
- a warning when it no longer fits as things are now: the step is already in the test case, the test case changed or is gone, the data point exists. Your assistant sees the same warning before it starts;
- the agent is working on this while your assistant makes calls under it.
Every row can be taken back, with a reason: an approval with Withdraw approval — what it approved stands to be decided again — and anything else with Withdraw. Both are kept under your name and show in the History. Schedule coverage, Schedule regression and Schedule onboarding — on the Requirements, Regression and Onboarding pages — each leave a request here; a scheduled onboarding names the source of the test cases, and only an onboarding run that read from that source closes it. A request withdrawn while the assistant was on it is not counted as done when it closes it; the assistant is told so.
The list follows what happens without a reload — also in another tab: a request the assistant closes disappears at once. Recently done, folded away, shows what was closed in the last seven days, with the assistant's answer.
The queue only shows and withdraws. The order is the one requests were made in; it cannot be changed here.
Connecting — workspace in a folder¶
Nothing to set up. Every workspace carries its own server,
local/xpress_mcp.py, and the entries that tell Claude Code (.mcp.json) and
VS Code (.vscode/mcp.json) to start it. XPressRT writes them, and keeps them
current, whenever the workspace is opened.
- Open the workspace folder in Claude Code or VS Code. The assistant starts the server by itself; Claude Code asks once whether it may.
- In XPressRT, with the same workspace open, go to Assistant in the sidebar. Settings has an Open Assistant button that leads to the same page. A line says a server is waiting for this workspace. Press Connect.
That is the only time you press it. From then on the window connects on its own whenever the server is there, until you press Disconnect.
What it needs is Python 3.9 or newer — the same as a Python step. The server uses Python's standard library only, so there is nothing to install, and an update of XPressRT brings the new server along the next time the workspace is opened.
Why there is still a secret
The server listens on a port on your computer, and any web page in any tab
could reach that port. So it leaves its address and a secret in
.xpress/mcp.json inside the workspace, and the window reads them from
there. A web page cannot read a file in your workspace folder — and nobody
has to type anything.
Claude Desktop¶
Claude Desktop does not open folders, so it needs one entry — in Settings › Developer › Edit Config, with the path to your workspace:
{
"mcpServers": {
"xpressrt": {
"command": "python3",
"args": ["/path/to/your/workspace/local/xpress_mcp.py"]
}
}
}
On Windows, write python instead of python3.
More than one assistant¶
Several assistants can work in the same workspace at once — Claude Code and VS Code side by side, for example. Each starts its own copy of the server; the first one holds the connection to the window, and every later one asks the window through it. There is nothing to set up for this.
More than one workspace¶
Each workspace has its own server, and the server belongs to the folder it sits in. Open two workspaces in two assistants, and each talks to the XPressRT window that has its workspace open.
A window with a folder open connects only to the server whose handshake lies in that folder — not to whichever it was connected to before. Switch the window to another folder and it connects by itself to the server waiting there; if none waits yet, the Assistant page says so, names the folder the last server works for, and the window connects as soon as one starts. A server whose handshake was copied along with a folder does not take the copy over: it starts its own.
The window in the background¶
The assistant works through the XPressRT window, so the window has to be open — but it does not have to be in front. You work in your editor or with the assistant, and the window sits in a tab behind it.
A browser slows a background tab down: its timers fire once a second, and it gets less of the machine. XPressRT does not wait on those timers where something waits for the answer, and it reads what the recorded runs hold only once per run rather than for every data point — so a change through the assistant takes about as long in the background as in front.
Should a call still run out of time, the answer says whether the window was in the background — The XPressRT window is in the background, and the browser slows a background tab down — bring it to the front — and that nobody knows whether the change was carried out. The assistant then reads the state before it repeats anything. A window of its own beside the editor, rather than a tab behind other tabs, is never slowed.
A tool that changes something answers when its change is in the workspace history, not before: what the assistant did is recorded under its name by the time it says so.
How long a call may take¶
A call waits up to five minutes for a window that is there and working.
Bringing in or handing in a whole run at once — import_plan,
save_agent_run, validate_agent_run — may take up to an hour: an
import of hundreds of test cases takes time, and nobody has to sit beside it.
While such a call works, XPressRT tells the assistant's program every 15
seconds that it is still working, so the program does not give up on it. An
answer that took longer than 20 seconds says how long, and whether the window
was in the background. A window that goes away — reloaded, closed — is still
reported after about 20 seconds, not after the deadline.
An assistant that repeats itself¶
An assistant that calls the same tool with the same arguments three times in a row, and gets the same answer each time, is stopped: the answer will not change, and it is told to ask you instead. A change asked for the same way a third time is not carried out. Asking again for something that changes with time — an open question of a run, the Agent queue, the list of runs — is waiting, not repeating, and is never stopped. The same holds for a change refused because a run is going: asked again once the run is over, it is carried out.
When the assistant cannot do something¶
A change that is refused leaves nothing behind: what the request had written
up to that point is undone, the workspace is as it was, and the history has no
entry for it. The answer says why. check_procedure and about name a step
file that does not compile, with its file and line — such a file stops every
run in the workspace.
Connecting — workspace in the browser¶
Not possible yet. An assistant needs the workspace as a folder on your computer: it opens the folder and starts its server there, and a workspace kept inside the browser has no folder. A download for such workspaces is planned. Until then, keep a workspace you want to work on with an assistant in a folder.
An assistant with access to the folder¶
Some assistants — Claude Code, for one — can also read and write files
directly. Pointed at a workspace folder, such an assistant could change
everything in it: the procedures, the generated definitions, the results of
finished runs. XPressRT cannot take that access away. What it does is say
where the line is, in a file every such assistant reads first: AGENTS.md
at the root of the workspace (and CLAUDE.md, which points to it).
The rule it states: local/ is the assistant's, everything else belongs to
the interface — and even in local/ a few files are XPressRT's.
- In
local/go the data, the images and the helper modules the steps need. Nobody overwrites them. - A step body is not one of them. It is written as blocks, in
local/blocks/<name>.blocks, and built withbuild_blocks. If no block fits, the assistant says which block is missing — never Python beside the blocks. - XPressRT's files in
local/are not to be written: the runner library, the MCP server and the READMEs, which are replaced on every open; the Python built from a block file; andlocal/blocks/approvals.json, which records who approved which step.
Everything else — procedures, preparation and clean-up included — goes through the tools above. A result is read, never written.
Both files are written and kept current by XPressRT whenever the workspace is opened. Do not edit them — your change would be replaced.