Skip to content

Flows: follow named flows through a build, with named checkpoints that two runs pair up by - #74

Draft
brentvatne wants to merge 3 commits into
mainfrom
@brent/capture-strategies
Draft

brentvatne wants to merge 3 commits into
mainfrom
@brent/capture-strategies

Conversation

@brentvatne

@brentvatne brentvatne commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Someone who wants to look at particular paths through an app (signing in, or opening a pattern) can now name those flows and the checkpoints along each one. The same flows in two runs (two builds, or an iPhone and an Android phone) line up checkpoint by checkpoint. This is the first capture strategy beyond model exploration and --replay, and the comparison builds on it.

npx @expo/explore explore:binary ./my-app.app --flows flows.json
{ "flows": [
  { "name": "Open a pattern", "steps": [
    { "action": "tap", "label": "Patterns" },
    { "checkpoint": "Patterns" },
    { "do": "Open the first pattern in the list" },
    { "checkpoint": "Pattern" }
  ] }
] }

How it works

  • Steps. A step is one of three kinds:
    • an exact action, in the --replay format;
    • a step described in words (do);
    • a named checkpoint.
  • Described steps. A model carries out each described step on its own (src/binary/flows.ts). It gets only observe and act, plus step_done and step_blocked, so the explorer records every action and refuses the same controls it always does. A step gets 40 tool calls, counted by the runner because a Codex session has no turn limit of its own. A flows file with only exact steps needs no model.
  • Flow tools. For a flows run, the explorer offers start_flow, checkpoint and end_flow (src/binary/explorer.ts).
    • Every flow starts from a relaunched app: open <app> --relaunch, which ends the process and keeps the app's data. The run's first open relaunches too, so on explore:session the first flow doesn't start wherever the app was left.
    • A checkpoint is always recorded. The explorer takes the picture itself, and reuses the one it took on arriving at the screen when it hasn't looked at the app since.
  • Failures. A failed step or relaunch, or a checkpoint whose screen can't be read, ends its flow with the reason, and the next flow still runs. After two relaunches fail in a row, the run stops. A launch-URL or relaunch error is redacted like any other controller error.
  • Checkpoints without a picture. A screen that withholds its picture (a secret on it, a blocked screenshot) leaves its checkpoint recorded without one, with why, and the flow goes on. The run's summary lists such checkpoints.
  • Every planned flow and checkpoint is in the report, in the file's order, including those the run never reached: each one without a picture says why. The flow record exists before the relaunch. When the run ends, the explorer asks the strategy which checkpoints it never attempted, including one that a budget stopped mid-call. A run blocked at launch records every flow as failed.
  • Output, with no schema change:
    • each flow becomes one or more journeys of the map (journeyIds), with ids derived from the flow's name, so they are the same in every run. When the app moved without a recordable transition, the walk goes on as another journey;
    • exploration.json gains an optional exploration.flows, holding each flow's status and its checkpoints in order (ExplorationFlow in src/contracts/standalone-report.ts).
  • Checks before any device starts: flow and checkpoint names, unknown fields, the screen, action and turn budgets, {{secret:NAME}} inputs in do steps, and --provider/--model/--hints when there is no described step. A described step or a secret limited by platforms to another platform needs no model or value, and a flow with no checkpoint on the platform is skipped. On explore:session without --platform, secrets are checked once the session reports its platform.

Tried on euxy

Three runs of a three-flow file on a euxy e2e build (main 07d5aab), each on a local iPhone 17 Pro simulator, with gpt-6.1-sol for the described steps:

  • Every run: all 3 flows completed in about 2 minutes.
  • Captures: 6, down from 10 before the arrival picture was reused.
  • Pairing runs 2 and 3: all 5 checkpoints paired by name.
    • 4 were pixel-identical.
    • The fifth differed only in the status bar, which Explore's review comparison already ignores.

Not in this PR

  • Comparing two flow runs, using the review's image comparison, and a step-by-step flow view in the Viewer.
  • Flows in PR reviews, and a non-LLM crawler. The crawler needs a stricter action policy in the explorer first.

Tests

  • test/node/binary-flows.test.mjs (23 tests) drives the real explorer with a scripted device and a scripted model. test/helpers/flows-host.ts gives the tests the explorer's own CiError, so a device error is recognised as one.
  • test/node/binary-local.test.mjs adds 3 tests through runBinaryExploration with a fake local controller:
    • the first open relaunches, and a failed relaunch is recorded with its secrets removed;
    • a launch-URL error is wrapped and redacted;
    • a run blocked at launch records every flow.
  • Two multi-model reviews of this PR, and an adversarial review of the first round's fixes, found the defects fixed in e66fc6c and d944e7d. Each fix was then broken on purpose, and every break fails a test. One test runs a described step through the real runBoundedSession, which rejects with AGENT_CANCELLED when step_done ends the step.
  • npm test passes (1732 tests), tsc is clean, and lint shows only the 6 existing Viewer warnings.

🤖 Generated with Claude Code

…t two runs pair up by

explore:binary and explore:session take --flows FILE: named flows, each a list of exact actions
(the --replay format), steps described in words ({"do": …}) and named checkpoints. Each flow after
the first starts from a relaunched app. A model carries out each described step on its own, with
only observe and act, so the explorer still records and blocks what it always does.

The explorer offers start_flow, checkpoint and end_flow to a flows run. A checkpoint is always
recorded, reusing the picture taken on arrival when nothing has happened since. A flow becomes a
journey of the map, named after the flow, and the exploration report lists each flow with its
status and its checkpoints in order (exploration.flows, optional). A failed step ends its flow
and names the checkpoints it did not reach; the next flow still runs.
@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Viewer preview of d944e7d · reports from brentvatne/euxy#163

Report Changes Open
Android · sdk_gphone64_x86_64 15.0 5 Map · Screens · Compare
iPhone 17 · iPhone 17 26.5 · light + dark 11 Map · Screens · Compare
iPhone Duo (closed) · iPhone Duo 27.1 · light + dark 34 Map · Screens · Compare
iPhone Duo (open) · iPhone Duo 27.1 · light + dark 34 Map · Screens · Compare

Going live: this link can take up to 5 minutes to show d944e7d; until then it shows the previous push, or nothing on a new pull request. This push's deployment is live now.

The link stays the same on every push. Deploy run

…nch left stale

From a review of #74 and an adversarial review of its fixes:

- A checkpoint reused the picture taken on arriving at a screen even after a relaunch, or after
  the model looked again. The arrival picture now lasts only until the next look at the app.
- Flows and checkpoints went missing from exploration.flows: a flow whose relaunch failed, a
  checkpoint whose observation threw (its flow still said completed), and on a budget stop,
  everything not reached. start_flow now creates the record before relaunching; a checkpoint
  that throws is recorded as missing and fails its flow; and when the run ends, the explorer
  asks the strategy which checkpoints it never attempted and records them as missing. A run
  blocked at launch records every flow as failed.
- explore:session's first flow started wherever the app was. The first open relaunches when
  the run follows flows, before the check that the app stays open.
- A flow's journey stopped at the first screen change that was not a transition. The walk now
  goes on as another journey of the flow (journeyIds).
- A launch-URL error, at the start or during a relaunch, was a plain error that ended the run
  without its secrets removed. Both are wrapped through failureText. After two relaunches fail
  in a row, the flows stop.
- A described step or a secret limited to another platform no longer needs a model or a value.
- assertFlowsFit counts the tool calls the flows take, as assertReplayFits does.
brentvatne added a commit that referenced this pull request Oct 24, 2026
… access by platform, counts turns, and hands the explorer the checkpoints a flows run never attempted
…alls, and skip flows with nothing to compare

From a second review of #74:

- A checkpoint was marked attempted before the explorer answered, so a screen, turn or time
  budget that stopped the run during the call dropped it from exploration.flows. It is now
  marked once the call returns, and recorded as "The run stopped before it." otherwise.
- A described step's 40-call limit relied on the session's maxTurns, which a Codex session
  does not have: one step could spend the whole run's budgets. runGoal now counts the calls
  and fails the step at the limit.
- A checkpoint on a screen that withholds its picture (a secret on it, a blocked screenshot)
  is recorded without one and its flow goes on, as the code did; the README said it ended the
  flow. The docs now say so, and the run's summary lists such checkpoints.
- A flow with no checkpoint on the platform being run is skipped, and its steps need no model
  or secrets.
- explore:session --flows without --platform required every platform's secrets. It now checks
  them once the session reports its platform, still before any device work.
- A test runs a described step through runBoundedSession, which rejects with AGENT_CANCELLED
  when step_done ends the step, as the real sessions do.
brentvatne added a commit that referenced this pull request Oct 24, 2026
…nputs wait for the session's platform on a flows run

runSessionExploration now chooses its strategy before it knows the platform, so the #74 change
is ported onto strategyInputs: a flows run without --platform reads its secrets once the
session reports its platform.

This branch has not been deployed

No deployments
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.

1 participant