# What the agent runs asked for — 2026-10-07

The follow-ups from [the agent usability runs](https://m0serve.dev/notes/agent-usability-runs.md):
the lines each page lacked, three changes to `m0`, and the two skills.
Each stumble the runs counted became a line on a page, a change to the
tool, or a line in a skill. SPEC N27 and N52; decision D66.

## Lines on the pages

The Python quickstart's serve commands pass `--host 127.0.0.1`, here and
on the page after it, and the page says the default is every interface.
It says the binary resolves libpython from `PATH`'s `python3`, so a start
script activates the virtualenv. It says what `publish()` does with a line
break and what its return value means, and that a form a page posts meets
Django's CSRF check. The home page says, above its first fold, that
`llms.txt` indexes every page and that each page is Markdown at its URL
with `.md` in place of the trailing slash: four of five Python runs
fetched the HTML page first, through a fetch that summarises and drops
code blocks.

On the Mojo side, the overview has a table of the API pieces and the page
each is on, links `llms.txt`, and no longer says "no form library" of a
framework with `form(req)`. The views page gives `reply.problem` its four
arguments, the other `reply` helpers beside it. The host and views pages
say where a send from a view reaches. The scaffold's `AGENTS.md` names the
host and views pages by URL before it names the installed source, which is
where all five runs went first.

## Three changes to `m0`

- **`m0 new .`** writes the empty current directory and takes its name.
  Every Mojo run wrote `./NAME` and then moved the files up a level; one
  then saw `m0 doctor` report the moved files.
- **The printed next step binds `127.0.0.1`**, as do both quickstarts.
- **`--template board`** is a list every tab shares, whose sender is a
  view. Every run chose `live` and deleted about half of it, because its
  producer is a timer and the task's sender was a request.

## The board, and two facts it rests on

`POST /messages` appends to a list held in the state and publishes the
whole board through the state's `DatastarStream`: no producer, no
database. Two facts about Datastar 1.0.4 shaped the rest, each read off
the bundle rather than remembered.

- **An action's answer is applied only when its status is 200.** The
  fetch code returns after its retry decision for any other status, so a
  422 fragment never reaches the page, unlike htmx 4, which swaps every
  4xx. The board keeps an empty message in the browser with `required`,
  and answers a client that bypasses it with `reply.problem`.
- **The morph keeps a typed value unless the `value` attribute changed**:
  it compares the old element's attribute with the new one's, not the
  property. Answering the form again would not have cleared it. The
  input is bound to `$text`, and the POST answers `application/json`
  `{"text":""}`, which Datastar reads as a signal patch.

The form is a fragment of its own outside the streamed board, so a frame
replaces the board in every tab and leaves another visitor's draft alone.

Every view that touches the state is `on_loop=True`. Under
`--blocking-threads N` the host builds a state for each pool thread, and a
post answered there lands in a list and a stream nobody reads. The gate
serves the board behind a pool for that reason, and the template's own
`smoke.sh` serves it without one, so both shapes are on the wire. Each
`on_loop` reverted alone fails the gate in its own way: the post reaches
no stream, the stream is refused 409, or the document shows an empty
board.

The same rule found a defect in `live`: its `/events` view was not
`on_loop`, and its handler answered only stateless loop routes, so behind
a pool every stream was refused 409. Nothing served `live` behind one.
The gate now serves both streaming templates behind a pool.

`ViewsApp` forwards three stream hooks and not `sse_peer_frame`, so a
views application cannot deliver another worker's frame. The board stays
one process (`max_workers() -> 1`), and D66 records what would add the
hook.

## The gate

`smoke-scaffold` holds the board as it holds the others, and adds the
current directory to every template's `new` phase. `sabotage-scaffold`
gained ten rules for the board, two for `live`'s stream and two for
`m0 new`, each run once before landing.

## The skills

`skills/m0serve/SKILL.md` and `skills/m0/SKILL.md` carry what the runs
looked for and where they found it. They live in this repository, so a
change to the product changes its skill in the same pull request. The
repository is public, which makes it where a reader installs them from;
the owner's private `wtalcott` marketplace carries them as a plugin whose
upstream is this directory. The run harness gained `--plugin-dir`, which
loads a plugin into every session of a run and records its version and a
hash of its files.

## The Python track with its skill

Five more runs of the Python brief, with the `m0serve` skill loaded and
the site as it stood before this change:

| track | tool calls, median (range) | wall clock | cost |
|---|---|---|---|
| m0serve, no skill | 13 (13–15) | 88 s | $0.37 |
| m0serve, with the skill | 8 (7–10) | 74 s | $0.27 |

All five met the contract, each invoked the skill first, and each fetched
one page, the quickstart as Markdown. Every flagged result was this
machine's: three runs still chose the port the desk server holds, and
each read the bind failure in its log and moved. The skill did not change
the first port choice.

## The Mojo track after the release

Ten more runs of the Mojo brief, with `m0 0.9.0` on PyPI and the site
deployed from these pages, five of them with the `m0` skill loaded:

| track | tool calls, median (range) | wall clock | cost |
|---|---|---|---|
| m0 0.8.0, the pages before this change | 33 (24–34) | 196 s | $1.14 |
| m0 0.9.0, these pages | 14 (11–25) | 100 s | $0.46 |
| m0 0.9.0, with the skill | 8 (8–9) | 65 s | $0.29 |

All ten met the contract. Every run wrote `--template board` with `m0 new
.`, and none read the framework's source, where every earlier run had.

One stumble was left, in all five runs without the skill. Each ran
`m0 --help`, which did not list the templates, then a bare `m0 new .`
before it knew one's name. Each got `views` and replaced it with `board`
once `m0 new --help` had named it. In two, the session refused the
`rm -rf` that emptied the directory, and those are the 15- and 25-call
runs.

So the top-level help now lists the four templates, a line each, and an
`m0 new` given no template says it took the default and lists the other
three (SPEC N27). Both read one table in `new.py`, which `m0 new --help`
reads too. The next `m0` release carries it.
