Repository navigation
[agent-cli] status drops --explain: every run carries the sources that changed, the OTA verdict, and the EAS answer - #71
Conversation
Co-authored-by: kudo <46429+kudo@users.noreply.github.com>
Co-authored-by: kudo <46429+kudo@users.noreply.github.com>
d9499dc to
d67d0cd
Compare
Co-authored-by: kudo <46429+kudo@users.noreply.github.com>
d67d0cd to
f3011d4
Compare
Co-authored-by: kudo <46429+kudo@users.noreply.github.com>
f3011d4 to
90d3db0
Compare
9e5e093 to
3ad9485
Compare
3ad9485 to
4820939
Compare
4820939 to
2c3c75e
Compare
Kudo
left a comment
There was a problem hiding this comment.
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 buildrow still says a default run "asked nobody" and thatios: unknown · android: unknown (EAS was not asked)is a non-fact to leave off the report. That was the old--explaingate.hasBuildsToReportnow prints the row whenaskedEasis true or any platform is notunknown, including an EASnone. The JSDoc onhasBuildsToReportwas 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
unknownwithout 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:statusfieldinstalledis still documented as "Null on a default run, which reads no device". This PR reads the installed app on every run unlessAGENT_CLI_NO_DEVICEis set.printStatusAsynccopiesreport.installed?.outcome, and llp/0004 now says the check is on every run. The nearbyeasBuildscomments 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
2c3c75e to
237d106
Compare
|
Both review items outside the diff are fixed in 237d106:
|
237d106 to
a92f22e
Compare
a92f22e to
f9065cc
Compare
…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>
…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>
…che after dropping --explain
f9065cc to
5c5340b
Compare
| }) | ||
| : async () => null, | ||
| // On the budget of status's own lookup (llp/0035 §Decisions). | ||
| lookUpEasBuild: (root, platform) => |
There was a problem hiding this comment.
[🏗️ 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 alsonone, with no EAS call. - It says
foundwith profiledevelopment-simulator: the plan uses that build, with no EAS call. - It says
foundwith 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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
[💡 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:
- Earlier:
statusasks EAS and saves afoundfor iOS in.expo/agent-cli-eas-builds.json, under project hash A. - You change native code. For example, you add a native package. The project hash is now B.
- You run
status. After the auth check, the early-start filter reads the record. It sees afoundfor 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. - The rest of the parallel block finishes, including the project hash, now B.
- The EAS lookup runs. It compares the saved entry's hash A with the current hash B. They differ, so the saved
foundis not used, and the lookup must ask EAS again. - 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.
There was a problem hiding this comment.
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]]). |
There was a problem hiding this comment.
[🐧 nit] Some text still describes the old contract, and the top of the stack (#77) still has it:
status --explainin llp/0005:806, llp/0015:116 and llp/0023:158.- "
statuspromises 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.
There was a problem hiding this comment.
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.
…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.
… 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>
#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>
Stack 3/4 of the plan to remove
status --explain. Based on #70 (which is based on #69).Why
--explaingated 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
--explainis removed with no alias (an unknown option, exit 1), the way--cloudwas in [agent-cli] One flag for EAS: --cloud is --eas on the device commands #34.changedSources,ota, and the EAS lookup.--build <id>no longer needs--explain.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 onefingerprint:generateinstead of two. An unlinked project starts none.BuildsStatus.askedEasnow 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; thecli:statusevent'seasBuildsAskedfollows.eas buildline prints whenever EAS has answered (foundornone) or was called; it stays out when every platform was gated, because the freshness rows carry that reason once.changed ios, android: 3 sources.--no-fingerprint-cachehelp text now says what it refuses: fingerprints, the evaluated app config, the remembered EAS answer.status --explain: help,AGENTS.mdcontent, workflow help, the--assertrefusal, thechange-ota-unsafefollow-up,.expo/README.md, code comments. LLP 0004 §Status, 0011, 0016, 0022, 0023, 0027 updated.Test Plan
statusAsync-test"what every run carries" (classification, OTA + EAS on every run, refused cache threaded to both, section isolation);easBuilds-testwithoutlookUp(asks both platforms, answers from the record per platform,askedEasfalse when nothing was asked, asks again over a stale key, remembers nothing without a project hash);resolveOptions-testfor--buildalone;format-test/sections-teststrings.status-test.ts: the former--explaincases run as plain runs;--explainexits 1;--build build-1alone is taken and echoed; a linked plain run asks EAS twice and printseas build … ios: none; an unlinked plain run prints noeas buildline and nameseas init; the impact headline carrieschangedSources; three fingerprints on a linked run, one on an unlinked one.help-test,eas-runner-test,eas-shim-test,e2e-live/live-eas-testupdated for the missing flag.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).