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:7345How it works
Three pieces compose into the watch loop:
- A debounced file watcher. Wraps
node:fs.watchwith 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 singlechangenotification. Callersawait watcher.next()to pull the next event — async-pull, not callbacks — so the run loop stays sequential. - An attached bridge. Watch mode requires
--attach. The bridge stays alive across iterations; without--attachit 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. - 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.jsoncache) 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-clientOpen 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 7345Output:
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:7345The 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.yamlorflows/login.flow.md). - Every
mockNetwork[].fixturereference, 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
| Persists | Resets per iteration |
|---|---|
| The bridge socket | The flow result |
| The runtime’s React tree (no re-handshake) | Per-step matcher state |
| The simulator / device | The 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.jsonTroubleshooting
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.