Skip to content

✨ Slice E of #848: run and reconstruct one XMD entry in a REPL - #853

Merged
taras merged 1 commit into
mainfrom
agent/issue-848-journey
Sep 28, 2026
Merged

taras merged 1 commit into
mainfrom
agent/issue-848-journey

Conversation

@taras

@taras taras commented Sep 27, 2026

Copy link
Copy Markdown
Owner

Closes #848.

Why

Issue #848 asks for a REPL that runs one XMD entry and reconstructs it from a
URL. It was delivered as five slices; this is Slice E, the one-entry journey,
and the only one that makes xmd repl exist. Slices A–D (#849, #850, #851, #852)
built the durable projection, the execution, the keyed tree and the terminal
platform. This assembles them and does not redesign any of them.

What changes

Before: the kernels exist and nothing uses them. There is no command.

After: xmd repl opens an empty draft in a full-screen terminal. Type or paste
one XMD entry and submit it, and the REPL runs it for real — the scopes it
admits, the bindings its eval blocks publish, a generated fragment's source
before that fragment is admitted, and each line it renders. When the entry asks
a question, the question's drawer opens; a valid answer typed into it settles
the ordinary elicit event, and what the document renders after that changes
because of the stored answer. Pause holds expansion at its next gate; History
freezes the view at an earlier position, read only; Continue releases the hold.

The screen always shows its canonical xmd://repl/... location, in full. Pass
that location back and a second process reconstructs the same view from the URL
and the retained events alone — reading no component source, compiling no eval
block and asking nobody anything.

How it works

DurableEvents -> frozen ReplModel -> resolved immutable view
  -> keyed Freedom tree -> semantic layout -> tty frame
  -> typed action back to the root

application.ts is the one state boundary. Every action a component can ask for
is a closed union; every navigating one builds a candidate route, resolves it
against the model, and is adopted whole or not at all. Selecting a history
position is a reprojection rather than a filter: the route is adopted, the model
reprojected there, and the view verified at that reading — with the standing
route restored untouched when it does not resolve.

program.ts is the command as one scope. It draws when something a frame shows
has changed — a reprojection, the overlay, a resize, an action — and never on a
clock. The order is snapshot, commit, lay out, render, present, retain the map,
then acknowledge. One burst of input is one frame.

Components receive detached view data and hold no events, no session, no
repository and no host operation. How wide a row may be is a question the layout
answers: surfaceWidth() and drawerWidth() say how much room a size really
gives.

Review guide

Start with: specs/repl-spec.md — the product, for a reader deciding how to
use it.

Then review:

  1. packages/cli/src/repl/application.ts — the view, the reducer, the surface
  2. packages/cli/src/repl/program.ts — the one scope and the frame order
  3. packages/cli/src/cli.ts — xmd repl [location] and its pre-installer refusals
  4. packages/cli/src/repl/storage.ts, repl-assembly.ts, the four *-repl.ts

Look carefully at:

  • the frame order in paint(): acknowledged only after the bytes are presented
  • reproject() and the verify-or-restore around it
  • the draft clears when the entry exists, not when submission is requested

What must stay true

  • Components hold no authority — checked in the source, not in behaviour, by
    repl-boundaries.test.ts: a component that happens not to touch the Journal is
    not a component that cannot.
  • Describing or refusing a command reaches no host — checked by a counting
    ReplHostInstaller driven through runXmd(), entered zero times for program
    help, repl --help and all three parser refusals, and once when the command
    really runs.
  • A cold open performs no completed work — checked by counting reads, compiles
    and provider calls at the seams where performing them happens.

How to verify it

  • The complete journey drives raw bytes end to end: paste, submit, hold at a real
    expansion gate (proving the journal and screen do not move across turns),
    inspect an earlier prefix, return live, Continue, answer, settle — then capture
    the on-screen URL and cold-open it in a fresh host, comparing topology, complete
    binding value, complete schema and answer, ordered History markers and output.
  • Negative rows cover a preflight refusal, a second entry, a corrupt history, a
    malformed location, a control chord, a too-small terminal and an invalid
    navigation that must leave route, focus and pointer target untouched.
  • Documentation tests compare the spec's command examples to the real parser, its
    route grammar to the real codec, its Elicit statement to the real reader, and
    Freedom's provenance to its manifest.

Scope

Included

  • xmd repl [location], its help, and its refusals
  • application components, the state boundary and the command loop
  • the retained per-user data root and the four runtime assemblies
  • specs/repl-spec.md and the final architecture inventory

Intentionally unchanged

  • one entry per execution: no second entry, catalog, fork, agent or snapshot
  • no new durable record type: each history is ordinary DurableEvents
  • concurrent writers remain unsupported; no lease protocol is added

Generated or mechanical changes

  • test-weights.json comes from the Measure test weights workflow, run
    36350297300 against this branch's production commit. It is committed
    unchanged as a separate commit on this branch once the run finishes.

Risks and limitations

  • Two processes writing one execution is unsupported and unguarded.
  • The pinned @bomb.sh/tty 0.9.0 returns at most 128 events per scan and never
    resolves a lone Escape; both are handled at the normalization boundary and
    documented at the call site.

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded.
  • Generated or mechanical changes are clearly identified.
  • The description matches the final diff and test results.

@taras
taras force-pushed the agent/issue-848-journey branch 2 times, most recently from 7669750 to fe6b59d Compare September 28, 2026 00:13
Slice E of #848: the one-entry journey, and the `xmd repl` command. The four
accepted kernels — the durable projection, the execution, the keyed tree and the
terminal platform — are assembled here and not redesigned. Closes nothing on its
own: this is the slice that makes them a product.

The screen always shows the canonical `xmd://repl/...` location it is at, in
full — as many rows as it takes, above whatever surface is being shown, because a
prefix of a location is no use to somebody trying to come back to a view. How
wide a row may be is a question the layout answers: `surfaceWidth()` and
`drawerWidth()` say how much room a size really gives, and a row written to a
guessed width is reflowed into rows the layout never allocated. A drawer's rows
reach its own edges for the same kind of reason: a renderer writes what changed,
so a modal that wrote only its own text would let the transcript show through
from where that text ended — that
is the one thing a person copies out of it, so it is on the screen rather than
only printed when the command ends, and it changes as the draft and the route do.

`xmd repl` opens an empty draft in a full-screen terminal. Type or paste one XMD
entry, submit it, and watch its real durable execution: the scopes it admits, the
bindings its eval blocks publish, a generated fragment's source before that
fragment is admitted, and each line it renders. When it asks a question, the
question's drawer opens — a blocked document is the interaction, not something to
go hunting for — and a valid answer typed into it settles the ordinary `elicit`
event, after which what the document renders changes because of the *stored*
answer. The screen always shows a canonical `xmd://repl/...` location, and the
command prints the one it ended at.

Pause is a *hold*, not a request: the journey asks for one before the document
can reach its question — the provider's outstanding request is work that keeps a
walk from ever becoming satisfied, so a pause asked for then never becomes a
hold — and waits for the controller to report exactly `paused` at a later
expansion gate. Held, neither the journal nor the screen moves across turns; one
Continue releases it and the same execution goes on to the question.

`[history]` opens the positions the file holds; selecting one freezes the whole
view there, read only, with nothing of the present in it — no live output, no
waiting question, no pause capability — and `[live]` returns to the head. A
position is a different *reading* of the same file rather than a filter over the
head, so selecting one reprojects and then verifies there; a route that does not
resolve at the position it names leaves the standing one exactly as it was.

Pass that location back and a second process reconstructs the same view from the
URL and the retained events alone: the same selected topology, the same binding
values, the same transcript, the same recorded answer, the same drawer. It reads
no component source, compiles no eval block and asks nobody anything — proved by
counting at the seams where that work happens, not at the durable operations that
replay enters.

`application.ts` is the one state boundary. Every action a component can ask for
is a closed union; every navigating one builds a candidate route, resolves it
against the model, and is adopted whole or not at all — a navigation that selects
nothing leaves the route, the selection and the draft exactly as they were and
says why. A refusal of one *action* appears in the footer beside the control that
was refused; only a view that cannot exist at all replaces the screen. Components
receive detached view data and hold no events, no session, no repository and no
host operation, which `repl-boundaries.test.ts` checks in the source rather than
in behaviour — because a component that happens not to touch the Journal is not a
component that cannot.

`program.ts` is the command as one scope. It draws when something that a frame
shows has changed — a reprojection, the overlay, a resize, an action — and never
on a clock: the frame subscription is taken for the frame it is about to draw and
released after it, so demand and drawing are the same fact. The order is snapshot,
commit, lay out, render, present, retain the map, then acknowledge. One burst of
input is one frame, because a paste arrives as hundreds of text events and a loop
that drew after each would make a person watch their document appear a character
at a time.

Four things the integration found in the kernels beneath it, each fixed here:

- The tty scanner returns at most **128 events per call** and buffers the rest, so
  a paste longer than 128 characters was silently losing everything past it.
  `input.ts` now drains until the buffer stops producing.
- A focus claim in a description is re-read at every commit, and this screen
  commits every frame — so a standing `focus: true` dragged focus back after every
  Tab and traversal could never move. A claim is where focus *starts*.
- A draft cleared at submission lost somebody's document to a preflight refusal.
  It clears when the entry exists, which is the only moment it is finished.
- A corrupt history raised out of the command instead of mounting the refusal.
- A focus marker read after the commit that settles it is a frame behind, so a
  person reaching for a control was acting on the one after it. A frame whose
  focus moved is drawn once more with where focus actually is.
- A one-line summary longer than its column was reflowed into rows the layout
  never allocated. A line is a line; what was retained is in the drawer, whole —
  a recorded question's schema and answer are shown in full there rather than
  summarized.
- Selecting a history position never reprojected at it, so every such route was
  refused. A position is a different reading of the file: the route is adopted,
  the model reprojected there, and the view verified at that reading — with the
  standing route restored untouched when it does not resolve.
- A derived focus marker is a frame behind the tree that owns focus, so a person
  reaching for the marked control was acting on the next one.

The focused control is marked, because a person has to be able to see where their
next keystroke goes. The marker is derived from the mounted tree and is therefore
one frame behind it, which is what focus being the tree's answer means.

`specs/repl-spec.md` describes the product for a reader deciding how to use it,
and `architecture.md` gains the final inventory and the one data flow. The
documentation tests compare its command examples to the real parser, its route
grammar to the real codec, its Elicit statement to the real reader, and Freedom's
provenance to its manifest.

The command's grammar is one optional location and no options. A stray option, a
second positional and a malformed location are all refused before a per-user
directory is formed, a file is created or the terminal's modes are touched.

Six defects review found in this assembly, each fixed here:

- A cold location was resolved *after* the session opened, and opening one starts
  or resumes the execution. A URL naming a scope the file never held could
  therefore replay, ask its question again and append — and still answer that the
  URL was wrong. Everything a route names comes from records already on disk, so
  the route is resolved against the retained history first, where being wrong
  costs nothing.
- `paint()` swallowed a refused subscription, reconcile or render and handed back
  the previous frame. The command stayed in raw mode on the alternate screen
  showing a picture it could no longer update. They raise now: the scope that owns
  the terminal is the only thing that puts its modes back.
- Nothing observed the overlay's output. A document that only prints reprojects
  nothing, asks nothing and expands nothing, so its text waited for an unrelated
  event or for the root to settle. The session announces its output on its own
  stream and the command watches it.
- The terminal adapters installed unconditionally and Node discovered the absence
  of a tty when `setRawMode` failed — several steps after an empty history file
  existed. Whether a person is at the terminal is now part of the terminal Api,
  answered by each runtime from its own descriptors, and asked before a path is
  formed or a file created.
- An accepted answer left `+elicit` in the route and the typed text in the field.
  The question was over and the drawer was gone, so the printed location named a
  drawer nothing mounts. `answer()`'s outcome is read, and an accepted one closes
  the route it was typed into.
- `[continue]` was mounted whenever a controller existed, including while
  expansion was playing or still *taking* a pause — where activating it would
  cancel the pause somebody had just asked for. It exists only while a
  continuation is held, and the reduction refuses one that is not.

A seventh, found reviewing the first six: `+elicit` resolved against any live
head, whatever the Journal held. A URL kept from while a question was up
therefore passed the new preflight, replayed the whole run, mounted no drawer
because no live question exists, and reported success.

Resolution now takes the one fact a model cannot hold: whether this process is
asking. A waiting question is precisely what a Journal never records — it records
answers — so nothing in a history distinguishes one that is about to ask from one
that never will. `settled` looked like that fact and is not: an execution whose
question is answered and whose root is merely still open is unsettled, asks
nobody anything on reopening, and would have passed. The fact defaults to *not*
asking, so cold resolution refuses a live drawer outright and only the process
holding the question can open one.
@taras
taras force-pushed the agent/issue-848-journey branch from fe6b59d to bc909c7 Compare September 28, 2026 00:22
@taras
taras merged commit 8adc104 into main Sep 28, 2026
43 of 44 checks passed
@taras
taras deleted the agent/issue-848-journey branch September 28, 2026 00:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Run and reconstruct one XMD entry in the REPL

1 participant