Skip to Content

Watch mode

klera run --watch re-runs the flow on every save. Edit prose, hit save, see it pass — no command re-run, no cold bridge handshake, no device boot per iteration. Iteration latency drops by an order of magnitude versus the cold-run-per-edit loop.

klera run flows/login.flow.md --watch --attach ws://127.0.0.1:7345

How it works

Three pieces compose into the watch loop:

  1. A debounced file watcher. Wraps node:fs.watch with two guarantees the raw API doesn’t provide. Most editors fire 2–3 fs events per save (truncate, write, close); the watcher coalesces events within a 200 ms window into a single change notification. Callers await watcher.next() to pull the next event — async-pull, not callbacks — so the run loop stays sequential.
  2. An attached bridge. Watch mode requires --attach. The bridge stays alive across iterations; without --attach it would tear down each save and the runtime would have to re-handshake every time. The cold-run cost was the whole reason to add the flag.
  3. A reload-and-rerun inner loop. After the cold run, the loop re-loads the flow on every save (re-parses YAML or re-reads the .flow.json cache) and dispatches against the existing client. A parse error mid-save prints to stderr and waits for the next save instead of ending the loop. SIGINT exits cleanly.

A typical session

The conventional layout is two terminal panes — one runs the bridge, the other iterates against it.

Boot the device and start Metro

In your app’s directory:

npx expo start --dev-client

Open the simulator, let the app load, and verify the welcome screen renders. The runtime registers itself as soon as the <KleraRuntimeProvider> mounts.

Pane 1 — start the bridge

klera serve --port 7345

Output:

bridge listening on ws://127.0.0.1:7345 press Ctrl-C to stop.

The bridge logs each runtime connection. Leave this terminal running.

Pane 2 — run with --watch --attach

klera run flows/login.flow.md --watch --attach ws://127.0.0.1:7345

The first iteration is the cold run: load the flow, attach to the bridge, execute the steps, render the result. Subsequent iterations print a line on each save:

status: PASSED (4123ms) ✓ [0] tap — passed ✓ [1] type — passed ✓ [2] assert — passed watching flows/login.flow.md for changes (Ctrl-C to exit) ↻ flows/login.flow.md changed; re-running status: PASSED (3987ms) ...

Edit, save, repeat

Open flows/login.flow.md in your editor, change a sentence, save. The watch loop fires within 200 ms. Edit the wrong selector, and the re-run fails — fix the prose, save again, the loop runs the corrected version against the same bridge.

Exit

Ctrl-C in pane 2 stops the watcher and exits cleanly. Ctrl-C in pane 1 stops the bridge.

What gets watched

The watch set is the flow file itself plus every fixture the flow references. Concretely:

  • The flow file (flows/login.flow.yaml or flows/login.flow.md).
  • Every mockNetwork[].fixture reference, resolved relative to the flow file’s directory.

If a flow references fixtures/login-response.json via mockNetwork, the watcher subscribes to both files. Editing the mock JSON re-triggers the flow under the new stub data — same fast feedback you get editing the YAML itself.

# flows/login.flow.yaml steps: - mockNetwork: - url: https://api.example.com/login fixture: ../fixtures/login-response.json # ← also watched - tap: Sign In - assert: { visible: Welcome back }

The startup banner reflects the watched count:

watching 2 files for changes (Ctrl-C to exit)

Flag combinations

--watch requires --attach. Without an existing bridge, watch mode would have to spin up and tear down a fresh bridge every iteration — that’s exactly the cost watch mode exists to avoid. Run klera serve in another terminal first, then attach.

--watch is also mutually exclusive with --parallel. Multi-platform parallel needs one bridge per platform; the bridge-management story across watch iterations isn’t built yet. If you need to watch a parallel flow, run a single platform with --watch and switch platforms by restarting the run command.

The CLI rejects both invalid combinations with a clear message:

$ klera run flows/login.flow.md --watch --watch requires --attach: watch mode reuses the same bridge across iterations; without --attach the bridge would tear down each save. Run `klera serve` in another terminal first.

What you keep across iterations

PersistsResets per iteration
The bridge socketThe flow result
The runtime’s React tree (no re-handshake)Per-step matcher state
The simulator / deviceThe redaction cache (rebuilt from .env)
Loaded fixtures (until the file changes)Visual-snapshot baselines comparison

The runtime state on the device is not reset between iterations. A flow that taps a button and navigates to screen B leaves the device on screen B; the next iteration starts there. For flows that need a clean app state per iteration, add a relaunch step at the top, or write the flow to no-op when it lands on the right screen.

Reports + watch

Watch mode honours --report <path>: each iteration writes a fresh JSON report at the same path, overwriting the previous. Pair with reports and klera report --html to keep an auto-refreshing HTML view open in a browser tab.

klera run flows/login.flow.md \ --watch \ --attach ws://127.0.0.1:7345 \ --report .klera/reports/login.json

Troubleshooting

Edits don’t trigger the watcher. Check that your editor isn’t writing to a temp file and renaming over the original (some editors do “atomic save”). The watcher subscribes to the original path; an atomic save replaces the inode and the subscription detaches. Configure the editor to write in place, or disable atomic save.

The flow re-runs twice on every save. The 200 ms debounce should coalesce editor multi-events. If you’re still seeing double runs, your editor is probably saving twice (e.g. “format on save” + “save” both fire writes). Either disable one or file a debounce-window adjustment.

SIGINT doesn’t exit immediately. The watcher waits for the in-flight iteration to finish before it unwinds. A flow that’s wedged on a missing element (--wait-timeout defaults to 30 s) will block SIGINT until the timeout fires. Lower the timeout or terminate the flow with a tap that resets the screen.

Last updated on