Symptom

Post-PIN, no-servers FTUE landed users on a dead-end avatar speech bubble (“I’m not seeing any local Krill Servers…”). Nothing actionable behind it; the only recovery was knowing to tap the avatar and pick KrillApp.Server from the menu, which the speech-bubble copy didn’t surface. Phase 1 (#256) had hidden the childless KrillApp.Client from the swarm graph and reserved MenuCommand.KeepBuildingSwarm for a follow-up wire that this phase delivers.

Root cause

The empty-state UI was a single screenCore.announceDialog(text) call with no affordance behind the text. Locally-runnable walkthroughs (Project / Journal / TaskList / DataPoint) hadn’t been authored yet, and the FTUE pipeline assumed “the user will find a server” was the only happy path. There was no engine-shaped place to plug a “what would you like to do?” chooser into, so the FTUE state machine deadended whenever beacon discovery completed without finding anything.

Fix

Introduced a JSON-driven walkthrough engine in shared/.../flow/:

Composables under composeApp/.../app/walkthrough/:

Wire-up:

Ride-along: RequiresServerFlagTest was unblocked. Commit 622fbe7e0 (“ftue spec”) flipped KrillApp.DataPoint.json to requiresServer=false ahead of its Phase 5 task without bumping the test ratchet. Added KrillApp.DataPoint.json to expectedServerFreeFilenames, bumped counts (7→8 free, 30→29 required), and added a comment pointing at the Phase 5 task that owns the flip.

Prevention

Three rules, each pointing at a place a future regression would slip in:

  1. Auto-resolve logical-only steps in the engine, not the renderer. A flow that interleaves user prompts and side effects (Input → CreateNode → OpenEditor) must not require an extra “Continue” tap on the side-effect step. New FlowStep variants that are pure side effects should extend autoResolveLogicalSteps so the renderer never has to special-case them.
  2. Lock JSON shape with literal-snippet tests for every step variant. FlowStepTest parses each variant from a hardcoded JSON string so renaming a discriminator or @SerialName surfaces as a test failure, not a silent runtime parse swallowed by flowJson.ignoreUnknownKeys. Add the matching test alongside any new FlowStep subclass.
  3. A requiresServer flip without a test ratchet bump breaks PR-Verify CI for everyone. RequiresServerFlagTest is intentionally a hard ratchet (one of #241’s locks). When relaxing a node type’s requiresServer flag (Phases 3-5 of local-first-onboarding/tasks.md will each do exactly this), update expectedServerFreeFilenames and the partition counts in the same commit — never split JSON flip and test bump across PRs.