A model works a task out once. The run compiles to a typed, reviewable capability. Replay is plain code with no model in the loop. When a check fails, a ladder is climbed, and every rung only proposes.
Six layers. A caller hands a capability and inputs to the engine. The engine talks to one surface through six verbs and never sees the application directly. Solid boxes are built and tested. Dashed boxes are planned. Click any box.
From capabilities/plumbline-place-hold.json, written by hand, eleven steps in total. This is the part that picks the right Hold link out of a table with one link per share.
// targets.hold_link { "role": "link", "name": { "eq": "Hold" }, 1 "supports": { "invoke": [] }, 2 "scope": [ { "role": "frame", "name": "work" }, { "role": "row", "name": "{{input.share_id}}" } ], 3 "ladder": [ { "rung": "role_name", "cost": 100 }, 4 { "rung": "native", "cost": 10000000, "surface": "browser", "value": "a[href$=\"share={{input.share_id}}\"]" } ] } // steps[6] { "id": "open_hold", "intent": "Open the hold form for this share", "screen": "member", 5 "op": { "verb": "invoke" }, "target": "hold_link", "effect": { "kind": "navigate" }, 6 "settle": { "kind": "screen_is", "screen": "hold_form" }, 7 "expect": { "screen_in": ["hold_form"] } }
The demo run on PLUMBLINE: thirteen rows, one per step plus start and finish. Each row says what the engine decided, what went into the ledger, what the browser did, and which screen was seen afterwards. Press Play to watch it in order.
Result: SUCCESS, confirmation HX-341248, llm_calls=0, classifier_calls=0, 11 of 11 controls on their first-choice rung, 1.8 seconds.
| Step | Engine | Ledger | Browser surface | Screen seen |
|---|---|---|---|---|
| start | validate 5 inputs, register password as secret | open {{env.BASE_URL}}/signon | signon | |
| enter_operator stage | screen is signon? resolve operator_field | dispatched, observed | label rung: cell left of the input says Operator ID. fill | signon |
| enter_password stage | resolve password_field | dispatched, observed | label rung. fill (value never logged) | signon |
| sign_on navigate | resolve sign_on_button; settle: screen_is desk | role_name rung. click, wait for the frameset to load | desk | |
| enter_member stage | resolve search_field in frame work | dispatched, observed | label rung: cell says Value. fill 400118 | desk |
| search navigate | expect inquiry_results or no_match | click F5=Search, wait for the work frame to navigate | inquiry_results | |
| open_member navigate | resolve member_link named {{input.member_number}} | role_name rung, name bound to 400118. click | member | |
| open_hold navigate | resolve hold_link in row {{input.share_id}} | scope narrows to the row; role_name rung finds one Hold. click | hold_form | |
| choose_reason stage | resolve reason_list | dispatched, observed | label rung: cell says Reason. select LEGAL | hold_form |
| review navigate | resolve review_button | click F8=Review | hold_review | |
| post_hold commit | not already done? not in doubt? then resolve post_button | dispatched, observed | click F10=Post Hold; settle when HOLD RESULT text is present | posted |
| read_result read | resolve result_line; read | native css rung. inner text: HOLD POSTED CONFIRMATION HX-… | posted | |
| finish | final screen posted is a SUCCESS outcome; extract confirmation | close the browser | RunResult |
What happens inside the engine's "resolve" for the Hold link. The member page has two shares, so two links called Hold. Choose what the page looks like today.
Pick a situation.
The rule the whole project is built around: a commit written as dispatched and never observed is never pressed again. Try to make the bank post the hold twice.
The same journal file is reused across runs with handrail replay --journal.
The book is not a recipe. "Number 7, then number 3" has to become "the Search button on the inquiry screen, then the member's link". The compiler does that, and refuses by line if the book has a hole.
Then the card is cooked three times for three different members, and after each plate the judge asks the record store whether the hold really exists. Only then is the card marked verified.
// from the book, turn 10 and turn 12 act 8 invoke link "400118" // the member's own number act 3 invoke link "Hold" row "400118-S0001" // the share's own id // in the card, plumbline-place-hold.authored.json "link_input_member_number_work": { "name": { "eq": "{{input.member_number}}" }, 1 "scope": [ { "role": "frame", "name": "work" } ], "ladder": [ role_name, text ], "fingerprint": { "over": ["role", "supports", "ancestor_roles"] } } 2 "link_hold_work_input_share_id": { "name": { "eq": "Hold" }, "scope": [ { "role": "frame", "name": "work" }, { "role": "row", "name": "{{input.share_id}}" } ], 3 "ladder": [ role_name, text ], "fingerprint": { "over": ["role", "name", "supports", "ancestor_roles"] } }
| Plate | Member | Share | Reason | Run | Record store says |
|---|---|---|---|---|---|
| 1 | 400118 | 400118-S0005 | LEGAL | SUCCESS, llm_calls 0 | HOLD |
| 2 | 400337 | 400337-S0001 | LEGAL | SUCCESS, llm_calls 0 | HOLD |
| 3 | 400445 | 400445-S0001 | FRAUD_REVIEW | SUCCESS, llm_calls 0 | HOLD |
Card lifecycle: draft → verified, reliability 3 of 3. approved still needs a named person's signature; no file in the repository can grant it.
On 2026-10-08 the mock bank was changed under the verified card in two ways, and the card was replayed with a referee on call. Pick a change and follow what the engine did. Every number is from the live run.
Exact match, then the furniture, then the referee, and the referee's call is re-checked against the furniture before it is believed.
| Confidence | Verdicts | Held | Rate |
|---|---|---|---|
| 0.0 to 0.8 | 0 | 0 | - |
| 0.8 to 1.0 | 6 | 6 | 1.00 |
Six rows from one banner-drift replay, all at 0.95, all held. Too few to tune a threshold on; enough to show the loop is closed: ask, record, settle, bin. "Held" means the run ended in SUCCESS or a business outcome.
When the referee says none of these, two rungs remain. A scout, which is a model seated at the three-line card with commit struck off and a four-turn leash, may go and look. If the scout names nothing the furniture agrees with, the belt stops and the telephone rings: a person takes the baton, does what needs doing in the same live browser, and hands back naming the screen.
Neither is believed on their word alone. The scout's name must be a screen the run was hoping for and the furniture must overlap. The person's name must be one the run was hoping for too, and if the page matches a different declared screen exactly, that match wins.
| Scenario | Rung 1, referee | Rung 2, scout | Rung 3, person | Result |
|---|---|---|---|---|
| Result line reworded. Laya on-device as referee, Claude scout. | none_of_these at 0.93, refused | Said "Hold Result" in one turn, two model calls. Furniture 0.8. Accepted. | SUCCESS, llm_calls 2 | |
| Result page rewritten entirely. Claude referee, scout, console. | none_of_these at 0.95 | Wandered eight turns back to the hold form. Named nothing. | Handed back "Hold Result" while the hold form was showing. | Refused: the furniture said Place Account Hold |
| Same, no scout. | none_of_these at 0.95 | Read the stuck page and the record store (HOLD), handed back "Hold Result". | SUCCESS, escalated_to_human |
The product claim, end to end. An agent that wants a hold placed never sees a screen, a selector or a password. It sees one line on a menu, fills in the card's inputs, and reads a receipt. Both orders below ran on 2026-10-08 through the MCP server, with the mock bank live.
| Order | Tool calls | Receipt | What the agent said |
|---|---|---|---|
| A LEGAL hold on share 400226-S0002, which is already under hold | 1 | BUSINESS_OUTCOME, ALREADY_PROCESSED, retryable false | "already has a hold in place; no further retry is warranted since this is a definitive business outcome" |
| A LEGAL hold on share 400118-S0005, free | 1 | SUCCESS, confirmation HX-975249 | reported the confirmation; the record store says HOLD |
Also pending: the control rung, which asks the referee "which menu row is this control" and re-checks with the fingerprint; and recording a person's clicks during HUMAN_CONTROL, not just the screen before and after.
Place a hold, release a hold, update an address. Each is a few screens of a system nobody will replace this decade. Someone shows the task once; after that it runs as a reviewed capability with no model, with evidence for the auditor.
The same engine and the same capability schema drive a 3270 session. A screen is a fixed buffer of fields, so signatures are exact. This is the "computer use, not just browser" claim, and the legacy-banking wedge.
An AI agent should not drive the bank's screens itself. It calls one typed tool per approved capability through an MCP server and gets a typed answer: posted, already held, or not authorised, and whether a retry is safe.
| Piece | File | Lines | Status |
|---|---|---|---|
| Capability artifact and validators | schema/capability.py | 227 | built |
| Target, ladder, fingerprint shape | schema/target.py | 98 | built |
| Effect classes | schema/effects.py | 45 | built |
| Bindings, including inside targets | schema/bindings.py | 78 | built |
| Errors and results | schema/errors.py, schema/results.py | 165 | built |
| Replay engine | replay/engine.py | 288 | built |
| Journal | replay/journal.py | 121 | built |
| Input validation | replay/validate.py | 83 | built |
| Evidence recorder | kernel/evidence.py | 130 | built |
| Surface protocol, null surface | surface/base.py, surface/null_surface.py | 213 | built |
| Operations table | surface/browser/operations.py | 115 | built |
| Locator ladder, probed | surface/browser/locators.py | 125 | built |
| Fingerprint | surface/browser/fingerprint.py | 75 | built |
| Page walk and signature | surface/browser/walk.py | 139 | built |
| Playwright queries | surface/browser/queries.py | 47 | built |
| Browser surface | surface/browser/surface.py | 244 | built |
| CLI | cli.py | 128 | built |
| Hand-written capability, approved | capabilities/plumbline-place-hold.json | 11 steps | built |
| Model-authored capability, verified | capabilities/plumbline-place-hold.authored.json | 11 steps | built |
| One line of the book | schema/trace.py | 39 | built |
| Three-line card | author/tools.py | 175 | built |
| Maître d' | author/middleware.py | 171 | built |
| The guest's evening | author/agent.py | 130 | built |
| The book on disk | author/recorder.py | 82 | built |
| author and verify commands | author/run.py | 197 | built |
| Recipe writer | compile/compiler.py | 235 | built |
| The tasting | compile/verify.py | 131 | built |
| Terminal surface | surface/terminal/ | milestone 4, waits on a 3270 host | |
| Screen signatures, tiered | kernel/signature.py | 86 | built |
| The closed card | escalate/questions.py | 131 | built |
| The port | escalate/classifier.py | 52 | built |
| Referees | escalate/backends/claude.py, laya.py, jev.py | 186 | built, Jev unverified |
| The notebook | kernel/episodes.py | 127 | built |
| The three rungs | replay/rungs/one.py, two.py, three.py | 293 | built |
| The baton | kernel/control.py | 121 | built |
| The scout's port and report | escalate/bridge.py | 44 | built |
| The scout | author/bridge.py | 115 | built |
| The console | serve/console.py | 134 | built |
| The signature on the menu | compile/approve.py | 47 | built |
| The counter other agents order at | serve/mcp.py | 197 | built |
| An agent that reads the receipt | examples/agent_caller.py | 72 | built |
| Naming a screen, preparing a run | replay/screens.py, replay/prepare.py | 131 | built |
Tests: 332, of which 17 drive a real headless Chromium against PLUMBLINE, and one runs the whole evening with a scripted guest. The invariant tests in tests/invariants/ read the source and refuse any model client in the core.
The analogies the code is written in, with annotated illustrations and playable diagrams, live on their own page: Handrail in Five Pictures.