Skip to content

[agent-cli] status drops --explain: every run carries the sources that changed, the OTA verdict, and the EAS answer - #71

Merged
Kudo merged 12 commits into
kudo/status-2-ota-cachefrom
kudo/status-3-no-explain
Oct 19, 2026
Merged

Kudo merged 12 commits into
kudo/status-2-ota-cachefrom
kudo/status-3-no-explain

Conversation

@expo-tuft

@expo-tuft expo-tuft Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Stack 3/4 of the plan to remove status --explain. Based on #70 (which is based on #69).

Why

--explain gated three things — the per-source change list, the OTA verdict, the EAS build lookup — because two of them cost a second or more. "That's not friendly to use." #69 and #70 made those costs rememberable, so the flag has nothing left to gate.

How

  • --explain is removed with no alias (an unknown option, exit 1), the way --cloud was in [agent-cli] One flag for EAS: --cloud is --eas on the device commands #34.
  • Every run carries changedSources, ota, and the EAS lookup. --build <id> no longer needs --explain.
  • The two per-platform fingerprints of a linked project start at the top of collectStatusReportAsync, beside the probe's own, and the lookup joins them through the in-process memo (LLP 0023 Layer 1). A cold run of a linked project costs the wall clock one fingerprint:generate instead of two. An unlinked project starts none.
  • BuildsStatus.askedEas now means "this run called EAS for at least one platform" (false when every platform was answered from the record or gated: signed out, not linked, no fingerprint). Same key, same type; the cli:status event's easBuildsAsked follows.
  • The eas build line prints whenever EAS has answered (found or none) or was called; it stays out when every platform was gated, because the freshness rows carry that reason once.
  • The change list prints platforms with identical source lists once: changed ios, android: 3 sources.
  • --no-fingerprint-cache help text now says what it refuses: fingerprints, the evaluated app config, the remembered EAS answer.
  • Prose that named status --explain: help, AGENTS.md content, workflow help, the --assert refusal, the change-ota-unsafe follow-up, .expo/README.md, code comments. LLP 0004 §Status, 0011, 0016, 0022, 0023, 0027 updated.

Test Plan

  • Unit: statusAsync-test "what every run carries" (classification, OTA + EAS on every run, refused cache threaded to both, section isolation); easBuilds-test without lookUp (asks both platforms, answers from the record per platform, askedEas false when nothing was asked, asks again over a stale key, remembers nothing without a project hash); resolveOptions-test for --build alone; format-test / sections-test strings.
  • E2E status-test.ts: the former --explain cases run as plain runs; --explain exits 1; --build build-1 alone is taken and echoed; a linked plain run asks EAS twice and prints eas build … ios: none; an unlinked plain run prints no eas build line and names eas init; the impact headline carries changedSources; three fingerprints on a linked run, one on an unlinked one.
  • help-test, eas-runner-test, eas-shim-test, e2e-live/live-eas-test updated for the missing flag.
  • Ran: bun run typecheck, bun run lint, bun run test (4228 pass), bun run test:e2e -- status-test eas-runner-test eas-shim-test help-test (all pass).

expo-tuft Bot added a commit that referenced this pull request Sep 15, 2026
Co-authored-by: kudo <46429+kudo@users.noreply.github.com>
@Kudo
Kudo added this pull request to stack #78 September 16, 2026 06:07
expo-tuft Bot added a commit that referenced this pull request Sep 16, 2026
Co-authored-by: kudo <46429+kudo@users.noreply.github.com>
@expo-tuft
expo-tuft Bot force-pushed the kudo/status-3-no-explain branch from d9499dc to d67d0cd Compare September 16, 2026 19:25
expo-tuft Bot added a commit that referenced this pull request Sep 16, 2026
Co-authored-by: kudo <46429+kudo@users.noreply.github.com>
@expo-tuft
expo-tuft Bot force-pushed the kudo/status-3-no-explain branch from d67d0cd to f3011d4 Compare September 16, 2026 19:39
expo-tuft Bot added a commit that referenced this pull request Sep 24, 2026
Co-authored-by: kudo <46429+kudo@users.noreply.github.com>
@expo-tuft
expo-tuft Bot force-pushed the kudo/status-3-no-explain branch from f3011d4 to 90d3db0 Compare September 24, 2026 14:29
@expo-tuft
expo-tuft Bot force-pushed the kudo/status-3-no-explain branch from 9e5e093 to 3ad9485 Compare October 15, 2026 12:42
@expo-tuft
expo-tuft Bot force-pushed the kudo/status-3-no-explain branch from 3ad9485 to 4820939 Compare October 15, 2026 15:03
Comment thread packages/@expo/agent-cli/src/status/easBuilds.ts Outdated
Comment thread packages/@expo/agent-cli/src/status/statusAsync.ts
@expo-tuft
expo-tuft Bot force-pushed the kudo/status-3-no-explain branch from 4820939 to 2c3c75e Compare October 15, 2026 19:01

@Kudo Kudo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

askedEas now flips when eas build:list is about to run, so a call that returns unknown counts as asked, and a fingerprint failure or an all-record run does not. Per-platform fingerprints start only after auth, and only for platforms the lookup will hash, so signed-out, unlinked, and record-hit runs no longer leave a fingerprint CLI running. The eas build row comment in format.ts still describes a default run that asked nobody, and the cli:status installed field in events.ts still says a default run reads no device.

Issue counts by severity

  • bugs: 0
  • suggestions: 2
  • nits: 0

Issues outside the diff

Issue 1 -- Severity: suggestion

  • File: packages/@expo/agent-cli/src/status/format.ts:62
  • Description: The comment above the eas build row still says a default run "asked nobody" and that ios: unknown · android: unknown (EAS was not asked) is a non-fact to leave off the report. That was the old --explain gate. hasBuildsToReport now prints the row when askedEas is true or any platform is not unknown, including an EAS none. The JSDoc on hasBuildsToReport was updated. This call-site comment was not.
  • Suggestion: Rewrite it to match that JSDoc. The row stays off only when every platform was gated before the call, or came back unknown without a call, because the freshness rows already carry that reason.
  • Status: open

Issue 2 -- Severity: suggestion

  • File: packages/@expo/agent-cli/src/events.ts:248
  • Description: The cli:status field installed is still documented as "Null on a default run, which reads no device". This PR reads the installed app on every run unless AGENT_CLI_NO_DEVICE is set. printStatusAsync copies report.installed?.outcome, and llp/0004 now says the check is on every run. The nearby easBuilds comments were updated. This one was not.
  • Suggestion: Say the field is null when the check is off (AGENT_CLI_NO_DEVICE) or produced no outcome, not on a default run.
  • Status: open

@expo-tuft
expo-tuft Bot force-pushed the kudo/status-3-no-explain branch from 2c3c75e to 237d106 Compare October 15, 2026 19:34
@expo-tuft

expo-tuft Bot commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

Both review items outside the diff are fixed in 237d106:

  • format.ts: the comment on the eas build row now matches hasBuildsToReport. The row is left off only when every platform was stopped before the call, because the freshness rows already carry that reason.
  • events.ts: cli:status installed is now documented as null when the check is off (AGENT_CLI_NO_DEVICE) or produced no outcome, not on a default run.

expo-tuft Bot and others added 2 commits October 19, 2026 16:59
…t changed, the OTA verdict, and the EAS answer

`status --explain` was the deep dive — the per-source change list, the OTA
verdict, and the EAS build lookup — behind one flag, because two of the three
cost a second or more. Kudo: "that's not friendly to use" [2026-09-15]. The two
PRs under this one made those costs rememberable (an EAS `none` for five
minutes, a static app config read as a file, a dynamic one evaluated once and
remembered), so the flag has nothing left to gate.

Removed with no alias, the way `--cloud` was: `--explain` is an unknown option.
`--build <id>` stands on its own — naming a build is the ask. The three
fingerprints of a linked project start together, beside the probe's own, so a
cold run costs the wall clock one instead of two. `askedEas` now means "this
run called EAS for at least one platform". The `eas build` line prints whenever
EAS answered, including `none`, and stays out when every platform was gated
before the call. Platforms whose source lists agree print once.

Help, the AGENTS.md text, the workflow help, the `--assert` refusal, the
`change-ota-unsafe` follow-up, and llp/0004, 0011, 0016, 0022, 0023, 0027
follow. The e2e `--explain` cases are the same cases as plain runs now, plus:
`--explain` exits 1, `--build` alone echoes its id, a linked plain run asks
EAS about both platforms and prints the line, an unlinked one prints nothing.

Co-authored-by: kudo <46429+kudo@users.noreply.github.com>
Co-authored-by: kudo <46429+kudo@users.noreply.github.com>
expo-tuft Bot and others added 8 commits October 19, 2026 16:59
…nds on its own

#59 put the installed-app check behind --explain, which this branch removes.
The check runs on every run now, bounded by its own deadline and turned off
by AGENT_CLI_NO_DEVICE as before; a physical phone is still launched only
when --device names it, which is the consent, and --device / --device-timeout
no longer need a second flag beside them.

Co-authored-by: kudo <46429+kudo@users.noreply.github.com>
Co-authored-by: kudo <46429+kudo@users.noreply.github.com>
…d --explain

Co-authored-by: kudo <46429+kudo@users.noreply.github.com>
…m fingerprints only for a lookup that will run

Co-authored-by: kudo <46429+kudo@users.noreply.github.com>
…as status now behaves

Co-authored-by: kudo <46429+kudo@users.noreply.github.com>
@Kudo
Kudo force-pushed the kudo/status-3-no-explain branch from f9065cc to 5c5340b Compare October 19, 2026 09:05
})
: async () => null,
// On the budget of status's own lookup (llp/0035 §Decisions).
lookUpEasBuild: (root, platform) =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[🏗️ design] In a project whose last dev was --eas, every status makes an extra EAS call. The plan asks "is there a finished development-simulator build for this fingerprint?" (lookUpEasSimulatorBuildAsync). Before PR 71, only --explain made this call. Now every run does, and the call:

  • runs on its own, before the final parallel block, so its time is added to every run: about 1.5–5.5 s per call, with a 45 s limit;
  • is never saved, so it asks EAS again on every run;
  • has no gate, so it also runs on a signed-out machine or for an unlinked project, and fails every time.

The builds section already asks EAS almost the same question, and it has all three protections.

The solution. Suggestion: let the plan reuse the builds section's answer. A simulator build is a build of some profile, so:

  • The builds section says none: the plan's answer is also none, with no EAS call.
  • It says found with profile development-simulator: the plan uses that build, with no EAS call.
  • It says found with another profile: only then does the plan ask EAS itself.
  • It says unknown (signed out, unlinked, or a failed call): the plan treats it as no build, with no EAS call. That matches what the plan does today when its own call fails.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in ecbf4ca. status now makes one EAS lookup per run, shared by the plan and the builds section. The plan reads the builds answer the way you described: none and unknown mean no build, a found of development-simulator is reused, and only a found of another profile makes the plan ask EAS itself. So the plan gets the builds section's record, auth gate and link gate. The mapping is simulatorBuildFromBuilds in easBuilds.ts. It has unit tests for each case, and three statusAsync tests cover the reuse.

return true;
}
const age = ageOf(cached.checkedAt);
return !cached.build && !(age != null && age <= EAS_NONE_CACHE_TTL_MS);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 suggestion] Remove the EAS-record check from platformsToFingerprintEarly, so it starts both fingerprints whenever the project is linked and the machine is signed in.

Reasoning through a sequence:

  1. Earlier: status asks EAS and saves a found for iOS in .expo/agent-cli-eas-builds.json, under project hash A.
  2. You change native code. For example, you add a native package. The project hash is now B.
  3. You run status. After the auth check, the early-start filter reads the record. It sees a found for iOS and treats the platform as answered. It cannot see that the entry is for hash A, because hash B is still being computed. So it does not start the iOS fingerprint.
  4. The rest of the parallel block finishes, including the project hash, now B.
  5. The EAS lookup runs. It compares the saved entry's hash A with the current hash B. They differ, so the saved found is not used, and the lookup must ask EAS again.
  6. To ask EAS, it needs the iOS fingerprint. Nothing started it, so it computes it now (~1.2 s), and only then runs eas build:list.

Result: this status is about 1.2 s slower than it could be, on the first run after a native change. The next run is fast again, because step 5 saved a new answer under hash B.

The answer is correct either way. Only the time is affected. With the suggested change, step 3 starts the fingerprint anyway, so at step 6 it is already done or nearly done.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ecbf4ca, but a bit differently. I kept a record check. A fingerprint run that nobody awaits still holds the process open, so starting both fingerprints on every warm run would make a record hit wait about a second for a hash nobody reads.

Instead, the early start now waits for the project hash as well as auth. It skips a platform only when its record entry is for that same hash. In your sequence, step 3 now sees hash B, the iOS entry for A does not count, and the iOS fingerprint starts right away. The lookup and the early start now share one recordAnswers helper, so they cannot drift apart.

This answers the question an agent asks after every edit: do I need to build again? And the one it should ask before every release: can this be shipped over the air? Those are two questions about two different systems. A tool answering both must never derive one from the other.

There is no `@expo/agent-cli impact` command. The classifier is the engine under `status`. The status UI, the `--explain` / `--assert` flags, and the exit codes of the gate live in [[0004-smart-start-and-project-state]].
There is no `@expo/agent-cli impact` command. The classifier is the engine under `status`. The status UI, the `--assert` / `--build` flags, and the exit codes of the gate live in [[0004-smart-start-and-project-state]]. Every run carries the per-source list, the OTA verdict and the EAS lookup, cached under `.expo` (§What `status` remembers of the answer, [[0023-fingerprint-caching]]).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[🐧 nit] Some text still describes the old contract, and the top of the stack (#77) still has it:

  • status --explain in llp/0005:806, llp/0015:116 and llp/0023:158.
  • "status promises to be instant and small" in this file at line 80.
  • The same "instant" claim in code comments: needsHuman/preflight.ts (38, 98, 131), project/localDiff.ts:7, status/sections.ts:505, status/statusAsync.ts:386/913.

Now that a plain run can call EAS and read devices, change these to the cache-backed claim, or remove them.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d407b28 for this PR. That covers the status --explain lines in llp/0005, 0015 and 0023, and the "instant" claim in llp/0011 and in the code comments you listed. I also fixed cloudSimulator.ts, subprocess.ts and three test notes. Each one now says what holds: every section has a short budget, and the slow answers come from the .expo records while they still hold.

The llp/0023 numbers stay, marked as measured before --explain was removed. Dated [observed …] notes that name status --explain stay too, since they record what was run. #159 had one more test note, fixed in e61727f.

Kudo added 2 commits October 19, 2026 18:52
…ds section, and start platform fingerprints once the project hash is known

The plan reuses the builds answer and asks EAS itself only for a found build of another profile.
The early fingerprints now skip only a record entry for the current project hash.
@Kudo
Kudo requested a review from vonovak October 19, 2026 10:57
@Kudo
Kudo merged commit 097ff06 into main Oct 19, 2026
15 checks passed
@Kudo
Kudo deleted the kudo/status-3-no-explain branch October 19, 2026 14:47
Kudo added a commit that referenced this pull request Oct 19, 2026
… it for a project with no app config (#70)

Stack 2/4 of the plan to remove `status --explain`. Based on #69.

# Why

The OTA verdict runs `expo config --json --type public` to learn the
`runtimeVersion`. Once `--explain` goes away (#71), every `status` run
pays for that call.

We tried reading a static `app.json` as a file and caching a dynamic
config's answer. Review showed that this is not safe:
- A dynamic config can read env vars and `.env` files, and no file stamp
sees them. A cache can report an old `runtimeVersion`.
- A static `app.json` can list config plugins, and `expo config --type
public` runs them. A plugin can set `runtimeVersion`, so the file alone
is not the answer.

So this PR keeps `expo config` as the source on every run, with no
cache. An env-aware cache can come later in its own PR.

# How

- `resolveRuntimeVersionAsync` still runs `expo config --json --type
public` first, for static and dynamic configs.
- A project with no app config file is not evaluated, because `npx expo`
could fetch a package for it. It reports `source: null`.
- When the subprocess fails, the answer is `source: null` and the
verdict is `unknown`. The static file is not a fallback, for the same
reasons as above.
- LLP 0011 says why the answer is not cached. LLP 0023 says
`--no-fingerprint-cache` also refuses the EAS answer record.

# Test Plan

- Unit `src/impact/__tests__/runtimeVersion-test.ts`: `expo config`
answers for static and dynamic configs with the pinned argv; a static
`app.json` is evaluated too; no app config file spawns nothing; `source:
null` when the subprocess fails (even beside a static `runtimeVersion`),
prints something unparsable, or fails on a dynamic config.
- Ran: typecheck, `bun run test` (all pass), `bun run test:e2e --
status-test` (96 pass).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: expo-tuft[bot] <288127324+expo-tuft[bot]@users.noreply.github.com>
Co-authored-by: kudo <46429+kudo@users.noreply.github.com>
Co-authored-by: Kudo <kudo@expo.dev>
Kudo added a commit that referenced this pull request Oct 19, 2026
#72)

Stack 4/4 of the plan to remove `status --explain`. Based on #71. Tests
only; no CHANGELOG entry.

## Why

Kudo asked for e2e coverage of every `status` case. This is the audit of
`status` against `e2e/__tests__/status-test.ts` after the flag went, and
the cases it was missing.

## What is covered now

- **`--build <id>` end to end.** The shared stub `eas`
(`e2e/stubs/eas.js`) answers `fingerprint:compare`
(`STUB_EAS_COMPARE_JSON` / `STUB_EAS_COMPARE_EXIT`) and `build:view`
(`STUB_EAS_BUILD_VIEW_PLATFORM` / `STUB_EAS_BUILD_VIEW_EXIT`). The
shared `installEnvFingerprintStubAsync` helper, with `diff: true`, gives
a stub `fingerprint` that answers `fingerprint:diff` by diffing the two
files, the way the real CLI does. Cases: a matching build (eas axis
fresh on the build's platform, "not compared" on the other, local axis
untouched); a differing build and what it costs (`needs-native-build`,
the added source listed); the text naming `vs EAS build <id>`; `--assert
js-only --build` judging the eas axis (20 / 0); a build whose platform
EAS cannot name (comparison attributed to none, both axes "not
compared", `--assert` exits 22); a failing comparison (exit 0 in text
and JSON, `freshness note` printed, and `comparison` still names the
build: F66).
- **Not an Expo app.** `next.command` is `npx @expo/agent-cli new
my-app`, and `not-expo-app` is the only follow-up (llp/0004 §Not an Expo
app). `project-shapes-test.ts` already covers the rest.
- **The change detail at scale.** Eight rows, `… and 2 more, in --json`,
and platforms whose lists agree printed once (`ios, android: 10
sources`). The class ladder's head in the follow-ups beside the OTA
verdict.
- **The file-level view** under a committed tree: nothing changed
(`total: 0`), a JavaScript edit (`files 1 changed · 1 js · 0 config · 0
native`, class `js-only`), `metro.config.js` raising the class to
`dev-client-compatible` with the `change-restart-metro` follow-up.
- **Follow-ups**: `runtime-errors` with an app connected,
`install-dev-client` for a project Expo Go cannot run and that has no
dev client, `skills-sync` when discovered skills are not linked.
- **Discovery ladder step 2**: a dev server the project's `start.log`
names, found with no URL given, `source: log`.

## Still uncovered, on purpose

The device section's `present` / `absent` / `unknown` and the
cloud-session `next` variants depend on `xcrun simctl` and `adb` on the
runner. They need stubbed platform tools on `PATH` / `ANDROID_HOME` and
are a separate change.

## Test Plan

- `bun run typecheck`, `bun run test:e2e -- status-test` (119 pass, 17
new).

---------

Co-authored-by: expo-tuft[bot] <288127324+expo-tuft[bot]@users.noreply.github.com>
Co-authored-by: kudo <46429+kudo@users.noreply.github.com>
Co-authored-by: Kudo Chien <kudo@expo.dev>
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.

2 participants