Skip to content

refactor(system-button): declare home, app-switcher and action-button as one button family - #2715

Open
thymikee wants to merge 11 commits into
mainfrom
claude/command-registry-refactor-3fcbb5
Open

thymikee wants to merge 11 commits into
mainfrom
claude/command-registry-refactor-3fcbb5

Conversation

@thymikee

@thymikee thymikee commented Sep 20, 2026

Copy link
Copy Markdown
Member

Summary

Follow-up to #2702 and #2713. 65 files, +851/−983 over main.

home, app-switcher and action-button are one shape end to end (one press, no arguments, nothing returned), and #2702 paid for that shape as if it were new: a refusal cell restated in every owner and fixture, a required Interactor member stubbed by every owner without the hardware, and a third byte-identical daemon module with its own dispatcher arm and conformance row.

  • packages/contracts/src/system-button-runtime.ts owns the family and replaces home-runtime and app-switcher-runtime. Its facts helper takes the keyboard family's shape (unsupported plus optional per-button cells), so a button added to SYSTEM_BUTTONS is refused by every owner that has not named it, with no edit there. UnavailablePlatformRuntimeFacts carries one systemButton cell like keyboard.
  • Interactor.actionButton is optional like keyboardDismiss; the binder fails closed on a missing member through requireInteractorMethod, which the keyboard binder now shares. Seven throwing stubs are deleted.
  • src/daemon/system-button-runtime.ts is one table row per command, replacing three modules, three switch arms and three conformance rows; tests derive the command list from the registry.
  • defineParameterlessCommandFacet derives metadata, CLI schema, reader and writer for the four parameterless system commands; the coverage rows cite named evidence instead of repeated literals.

No public API change. Refusal hints for a button an owner lacks now name the family (CHANGELOG). The next system button touches about 28 files instead of 72.

Gross churn is above the 1,000-line budget: three modules fold into one. The branch can split into contracts+owners, daemon, and CLI+coverage if preferred.

Validation

Tested at c020e10.

  • pnpm check:affected --run selected 59 checks. Every locally runnable gate passes: format, lint, typecheck, layering (246), di-seams, fallow, mcp-metadata, build, package, integration-node, macos-coverage, unit and smoke (70), provider-integration (66 files, 211 tests), integration-progress, replay-compat, daemon-wire-compat, eager-closure budgets, production-exports, bundle-owner-files, command-docs, agent-guidance, gate-manifest, tmpdir-leaks.
  • Three unit tests fail in this container only: app-log-session-resource, durable-capture/adoption, durable-capture-resource each chmod 0o500 a directory and expect a write to fail; the container runs as root. Unrelated to the diff.
  • The integration-progress scanner read only defineCommandFacet blocks, so app-switcher briefly read as uncovered; the chore(gates) commit widens it, with a model test that was red before the fix.
  • Live: the CI iOS lane runs the smoke tier only, and home/app-switcher are exercised by full:lifecycle-system, so the changed device path needs a full-tier simulator run before merge (command in the review thread). The smoke lane's automation-longpress failure on e0dba23 is the lane's known scroll-search instability and does not reach this diff.
  • Not run locally: GitHub-authoritative lanes (Swift runner builds, device replays, coverage). No Swift or runner protocol changed.

🤖 Generated with Claude Code

https://claude.ai/code/session_014x96iYiFTbAwD4YEKU3PQ1

thymikee and others added 9 commits September 20, 2026 16:03
Each platform coverage manifest already derives its classification summary from its own rows, and
the smoke test that owns the manifest then compared that derived value to a hand-incremented
literal. Adding one public command therefore cost five files whose only edit was `54` becoming
`55`, while the invariant those files actually protect — every public command is classified, and
the published rollup is the manifest's own — was asserted nowhere.

`assertCoverageClassificationSummaryDerivedFromManifest` now recounts the manifest independently of
the builder, ties `total` to the public catalog's size, and checks the buckets still sum. Parity was
verified against the deleted literals before removal, as ADR 0008 requires: macOS 22/15/18/55, web
42/1/12/55, Linux 29/9/17/55, tvOS 17/38/0/55, Android 14/0/41/55, all equal to the derived rollup
at this head. The web lane's `liveCommands.length === 12` goes the same way: the loop beneath it
already proves each live claim is executed by the smoke scenario, so a count proved only that the
list was not empty.

A planted mis-bucketed summary and a planted denominator drift both fail, naming the platform.
…s it

The test enumerated eleven commands to prove that the daemon's fallthrough route is reached on
purpose. Ten of those commands already declare the decision as `daemon.route: 'generic'`, which is
where `getDaemonCommandRoute` reads it from, so the list was a second source of truth that every new
generic command had to be added to.

The expected set is now that trait, projected. What remains hand-written is the case the list was
actually guarding — a catalog command that declares no daemon facet at all and lands on the
`?? 'generic'` fallback — and it is asserted alone, with a message that names the declaration to
add instead. Removing `viewport`'s facet locally fails with `['viewport', 'install-from-source']`
against that one expected member.
… helper

`createUnavailableRuntimeFactsForTest` restated all twenty-seven fail-closed cells, plus the nine
lifecycle cells, to say what `createFullyUnavailablePlatformRuntimeFacts` already builds: every cell
unavailable for one reason. The two forms are equal because the input's `network` cell carries the
same sentinel the omitted optional cells would have inherited.

A new runtime cell therefore costs nothing here, which is the point of the fail-closed default:
a test that overrides a cell states its own exception, and one that does not inherits the refusal
without anyone remembering to write it down.
… as one button family

`home`, `app-switcher` and `action-button` are the same shape end to end: one press, no
arguments, nothing returned, and only the set of owners that carry the control differs. The
action-button PR (#2702) paid for that shape as if it were new: a contracts group with no facts
helper, a required `Interactor` member every owner had to stub, a third identical daemon module
with its own dispatcher arm and conformance row, and a refusal cell restated in every owner and
every fixture that enumerates `RuntimeFacts`.

One `system-button-runtime` contracts module now owns the family. Its facts helper takes the
keyboard family's shape (`unsupported` plus optional per-button cells), so an owner with no such
control states one denial and a button added to `SYSTEM_BUTTONS` is refused there without an
edit; `UnavailablePlatformRuntimeFacts` carries one `systemButton` family cell the same way it
carries `keyboard`. `Interactor.actionButton` is optional like `keyboardDismiss`: facts admit the
press only where the interactor implements it, and the binder fails closed on a missing member
instead of resolving it as a successful no-op, so the seven throwing stubs are gone. On the daemon
side one table row per command replaces the three byte-identical modules, the three dispatcher
arms and the three conformance rows; the conformance table and its test derive the command list
from the registry.

The keyboard family keeps its private method guard: the eager-closure gate holds that entry at
its merge-base closure, and importing the shared guard would grow it by one module.

Refusal hints for the buttons an owner does not carry now name the family rather than the one
button (`Android has no key event for this system button.`); the command name already leads the
UNSUPPORTED_OPERATION message.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014x96iYiFTbAwD4YEKU3PQ1
… evidence

A command whose only input is device selection (`appstate`, `home`, `app-switcher`,
`action-button`) restated the same metadata, empty CLI schema, reader and daemon writer four
times over. `defineParameterlessCommandFacet` derives them from the name, so such a facet is its
name, description, text, `run` and formatter. The readers and writers it replaces were exported
for the family test only; the test now reads them through the family.

The coverage declarations cited three test names by literal in eighteen rows. They are named
evidence in `evidence.ts` now, the same way the Linux and tvOS rows already cite theirs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014x96iYiFTbAwD4YEKU3PQ1
…on module

`home-runtime` and `app-switcher-runtime` leave the pinned export set and
`system-button-runtime` joins it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014x96iYiFTbAwD4YEKU3PQ1
The fail-closed test facts now derive from `createFullyUnavailablePlatformRuntimeFacts`, which
absorbs the `systemButton` family cell this branch had named there by hand.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014x96iYiFTbAwD4YEKU3PQ1
…s facet

The provider-coverage scanner maps `client.command.<method>(` calls in provider scenarios to a
command name by reading the facet block that declares the method. It recognised
`defineCommandFacet` blocks only, so the system commands that moved onto
`defineParameterlessCommandFacet` lost their mapping and `app-switcher` read as uncovered. The
scanner now reads both block forms; the new model test is red without it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014x96iYiFTbAwD4YEKU3PQ1
#2713 landed on main with two commits beyond the head this branch had merged; its own files take
main's version.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014x96iYiFTbAwD4YEKU3PQ1
@github-actions

github-actions Bot commented Sep 20, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
Installed (including dependencies) 4.67 MB 4.67 MB -1.7 kB
Package (unpacked) 4.67 MB 4.67 MB -1.7 kB
Package (download) 1.39 MB 1.39 MB -352 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 26.5 ms 26.2 ms -0.3 ms
CLI --help 76.3 ms 76.3 ms -0.0 ms

Copy link
Copy Markdown
Member Author

The iOS Smoke Tests job is red on e0dba23 at Run fixture-backed iOS simulator E2E smoke, and the other 17 checks on this head are green, including the macOS, Linux and Android Smoke Tests, Integration Tests, Coverage and Repo Guards.

The failure is id="automation-longpress" did not become visible after scrolling, raised by assertElementTextAfterScrolling inside assertAutomationInput (live-automation-scenario.ts:170): a scroll followed by an is visible probe on the long-press canary. That step drives no system button, so it does not reach the seam this PR changes (home, app-switcher, action-button facts, binder and daemon route); the Swift runner and its protocol are untouched. It is the same signature #2713 hit on its first iOS attempt with a test-only diff, which went green on rerun with no code change, so I read it as the iOS lane's capture instability rather than this diff.

I have re-run the failed job once. If it fails again I will treat it as real and dig into the scenario.


Generated by Claude Code

Copy link
Copy Markdown
Member Author

The re-run of iOS Smoke Tests failed at the same step with the same message: id="automation-longpress" did not become visible after scrolling, last probe Selector did not match on is visible. I said I would treat a second failure as real, so here is what I found.

  • The step is a scroll down 0.75 and is visible search in assertAutomationInput, after press id="automation-press" and wait text "Last input: press" had already succeeded on the same tree. It never invokes home, app-switcher or action-button, which is the only seam this PR changes; the Swift runner and its protocol are untouched.
  • The base branch shows the same lane instability on identical code: the iOS workflow on 8baa8ea (the test: derive the command-surface enumerations that restated a declaration #2713 squash) failed at testAlertAcceptDoesNotActivateAReplacementWithASharedButton in the XCTest regressions, and the next main commit c442cf6, a version bump only, passed. test: derive the command-surface enumerations that restated a declaration #2713's own attempts hit both signatures on a test-only diff before going green.
  • No open PR or main commit touches live-assertions.ts or the automation lab scroll search, so there is nothing to port; hardening that live scenario is outside this refactor's scope, and I will not widen the PR for it.

My one re-run is spent. The other 17 checks are green. A maintainer re-run of the iOS job is the next step; I will keep the PR watched and treat a further failure with any new signature as this PR's.


Generated by Claude Code

@thymikee

Copy link
Copy Markdown
Member Author

This is a good refactor with two blocking issues, reviewed at e0dba23.

requireInteractorMethod in packages/contracts/src/interactor-operation-binding.ts#L26 is a verbatim copy of requireKeyboardMethod in packages/contracts/src/keyboard-runtime.ts#L84: same generic signature, same guard, same AppError text. Every binder that reads an optional Interactor member should go through one shared guard, not one copy per member, so the next optional member does not get a third copy. Can requireKeyboardMethod be deleted and keyboard-runtime.ts import requireInteractorMethod instead? interactor-operation-binding.ts only imports kernel/device, kernel/errors and interactor-types.ts, so there is no cycle, and keyboard-runtime.test.ts#L138 already pins the behavior.

home and app-switcher are only exercised live by the full:lifecycle-system scenario (test/integration/ios-simulator-e2e/live-full-scenarios.ts#L119 and #L145), and the Smoke Tests run on e0dba23 threw during the 2nd scenario, so full:lifecycle-system never ran. I did not run any test myself; I only read the diff and the head sources. The changed device-facing path needs a completed live run before merge: home printing {"action":"home","message":"Home"}, app-switcher printing {"action":"app-switcher","message":"Opened app switcher"}, and the simulator's action-button outcome (its press result or the typed refusal with reason and hint). A run that stops at automation-input does not cover this.

Is there a smaller design than this family shape? I did not find one: the PR is net -190 production lines, the required-unsupported-plus-optional-cells shape already matches keyboard-runtime, clipboard-runtime, touch-runtime and gesture-runtime, and the daemon table replaces three modules, three dispatcher arms and three conformance rows with one row each. defineParameterlessCommandFacet in src/commands/family/types.ts and the regex widening in scripts/integration-progress-model.ts#L586 also reshape appstate, which is not a system button, so that piece could split into its own PR, but it is not required for correctness here. ADR 0019's one-owner-fact-per-command rule still holds, since each command keeps its own single-op runtimeUse and admission still reads the exact owner's per-button cell.

The CI failure (is visible on id="automation-longpress" after scroll down 0.75, live-automation-scenario.ts#L170) looks unrelated: the diff touches no scroll, selector-resolution, capture-kit or Swift code, its only routing change is an early return for three command names in generic-runtime-execution.ts, main passed at c442cf6, and #2363 hit the same assertion with no shared code. The full:lifecycle-system scenario does touch this diff's commands, but it aborted before reaching them, so it neither caused nor cleared this failure.

The author's report that unit, layering, fallow and provider-integration gates pass on e0dba23 is unverified by me. The system-button-runtime.test.ts fail-closed check is genuinely new behavior, since Interactor.actionButton was required before this change, so I could not construct a mutation to confirm it goes red on the old code. The physical-device Action Button path is untouched by this diff and I had no device to check it; #2702 already covers that. I did not check the MCP surface output beyond confirming the derived metadata is byte-equivalent to the removed constants.

Not blocking: the eight tracked replay artifacts under .agent-device/test-artifacts changed only in local tmpdir paths and timing jitter and can be checked out back from c442cf6, the provider-webdriver and Limrun action-button reason strings for an inactive session changed and the CHANGELOG could mention it, and defineParameterlessCommandFacet plus its appstate gate widening could move to their own PR if a reviewer wants this one smaller; none of these need to hold up this PR.

Before merge, route the keyboard binder through the shared requireInteractorMethod guard, and get a Smoke Tests run on this head that actually completes full:lifecycle-system.

…ional-member guard

`requireKeyboardMethod` was a verbatim copy of `requireInteractorMethod`. The keyboard binder
now uses the shared guard, loaded on the call rather than at module evaluation: the
`keyboard-runtime` facade's eager closure is held at its merge-base size by
`eager-closure-budgets`, and a static edge to the binding module would grow it by one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014x96iYiFTbAwD4YEKU3PQ1
…son change

The eight Maestro replay artifacts under `.agent-device/test-artifacts` had been swept into the
first commit by a local test run; they return to main's bytes. The CHANGELOG entry now also names
the Limrun and WebDriver inactive-session refusal that moved onto the family cell.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014x96iYiFTbAwD4YEKU3PQ1

Copy link
Copy Markdown
Member Author

Addressed at c020e10.

Shared guard. requireKeyboardMethod is deleted and the keyboard binder uses requireInteractorMethod from interactor-operation-binding.ts (3d92f85). One constraint shaped how: keyboard-runtime.ts is an existing contracts entry, and eager-closure-budgets holds it at its merge-base closure of 3 modules (itself, kernel/errors, kernel/redaction). A static import of the binding module makes that 4 and the gate fails, which is why the first head kept the copy. The gate's own remedy is a function-scoped await import, so the binder loads the guard on the call; the closure stays at 3, keyboard-runtime.test.ts still pins the interactor-method-missing behavior, and the comment names the gate. If you would rather not have a lazy import there, the alternative is moving bindKeyboardAction into the catalog module (as #2702 did for the zero-argument binder), at the cost of moving its tests.

Artifacts. The eight replay artifacts were swept into the first commit by a local test run; c020e10 restores main's bytes, and git diff origin/main...HEAD -- .agent-device is empty. The CHANGELOG entry now also names the Limrun/WebDriver inactive-session refusal.

Live run. One correction to the premise: the CI iOS lane pins AGENT_DEVICE_IOS_E2E_TIER: smoke (ios.yml), and full:lifecycle-system is tier: 'full', so it does not run on any PR head, green or not; home and app-switcher have never been covered by the PR lane, and ios.yml has no workflow_dispatch. A completed run of the changed path therefore needs a full-tier run on a simulator, which I cannot do from this container. On a Mac with a booted simulator and the fixture app:

AGENT_DEVICE_IOS_E2E=1 AGENT_DEVICE_IOS_E2E_TIER=full \
AGENT_DEVICE_IOS_UDID=<udid> AGENT_DEVICE_FIXTURE_APP_ID=<id> AGENT_DEVICE_FIXTURE_APP_PATH=<path> \
node --experimental-strip-types scripts/node-test-tmpdir.ts --test test/integration/smoke-ios-simulator.test.ts

plus agent-device action-button on an iPhone 17 Pro and an SE simulator for the press and the typed refusal. The push has re-triggered the smoke lane on c020e10; that covers the flake question, not the family.

Not taken. defineParameterlessCommandFacet and the scanner widening stay in this PR; I can split them out if you prefer the smaller diff.


Generated by Claude Code

Copy link
Copy Markdown
Member Author

CI is green on c020e10: all 18 checks pass, the iOS Smoke Tests job included (the automation-longpress scroll search that failed twice on e0dba23 passed here with no change to that path).

What remains is on the maintainer side: the full-tier simulator run for home, app-switcher and action-button, which the PR lane does not exercise, and the review decision on the lazy-import guard and on keeping the parameterless-facet piece in this PR.


Generated by Claude Code

@thymikee

Copy link
Copy Markdown
Member Author

Reviewed at c020e10, as a follow-up to the review at e0dba23 (#2715 (comment)).

The shared guard is fixed. requireKeyboardMethod is gone, and the keyboard binder loads requireInteractorMethod inside its async action, so callers still get the same rejected interactor-method-missing error, and keyboard-runtime.test.ts#L136 still pins it. The eight replay artifacts match main again.

The live run is still missing. This PR puts home, app-switcher and action-button behind one daemon runtime table (system-button-runtime.ts) and changes the Apple navigation runtime and every provider's facts, but no check on this head runs these commands on a device. The iOS PR lane is smoke-only (ios.yml#L289). home and app-switcher belong to the full-tier full:lifecycle-system scenario on iOS (scenarios.ts#L33, live-full-scenarios.ts#L119 and #L145) and on Android (scenarios.ts#L127), and those run only from the manual dispatch in replays-manual.yml. action-button has no live scenario; its coverage is the runtime-fact tests. The unit tests use stubbed interactors, so a wrong binding in the new table, such as pressing the wrong button or refusing a press that should happen, would merge undetected. Please run the full tier on c020e10 for iOS and Android and attach the output that shows "background fixture to home" and "open app switcher" passing with the distinct-pixel assertions. Please also run action-button once on an iOS simulator or an iPhone with an Action Button, and record its press result or its typed refusal.

All 18 checks pass on c020e10. The earlier Smoke Tests failure on e0dba23 was in the long-press scroll search, which this delta does not touch. I did not run the eager-closure-budgets gate myself; I take the green CI as confirmation that keyboard-runtime.ts keeps its merge-base closure size.

Not blocking: the 3-line comment at keyboard-runtime.ts#L113 explains the lazy import as a workaround for the closure budget. Would moving bindKeyboardAction next to the binding module, as #2702 did, remove both the comment and the dynamic import, or is it fine as it is for now?

The remaining step before merge is that full-tier live run on c020e10.

Copy link
Copy Markdown
Member Author

Thanks. On the live run:

Dispatched. Replay Manual is running on c020e10: https://github.com/callstack/agent-device/actions/runs/35554059613. Its iOS job runs smoke-ios-simulator.test.ts at AGENT_DEVICE_IOS_E2E_TIER=full, so full:lifecycle-system with background fixture to home and open app switcher executes there; its Android job runs the full tier too. I will post the step output when it completes. The workflow header notes both suites have been failing on unrelated steps since July, so a red there needs reading before it counts either way.

Android already has it on this head. The Android home and app-switcher live claims are in smoke:automation-system (send Android fixture Home at live-automation-scenario.ts:226, open Android Recents at :234, with the foreground-evidence and Recents-pixel assertions the coverage rows cite), and that scenario is smoke tier. The Android Smoke Tests job on c020e10 (https://github.com/callstack/agent-device/actions/runs/35542362646/job/106162206085) ran it: smoke:automation-system: 120383ms, ✔ live Android emulator fixture E2E, pass 1 fail 0.

action-button. No lane runs it and I have no simulator here, so that one press is yours: on an iPhone 17 Pro simulator agent-device action-button --json should print {"action":"action-button","message":"Pressed Action Button"}, and on an SE the typed UNSUPPORTED_OPERATION refusal. If you want it covered going forward, a full-tier step in full:lifecycle-system that accepts either outcome by device model is a small addition I can make in a follow-up rather than here.

Keyboard binder. Yes: moving bindKeyboardAction and resolveKeyboardInteractor into interactor-operation-catalog.ts removes both the dynamic import and the comment, because the catalog already imports the binding module and keyboard-runtime, so its closure does not grow. The cost is moving those two functions and the three bindKeyboardAction tests out of keyboard-runtime.test.ts, and the keyboard module then holds types, facts and labels while its binder lives with the catalog, as the system-button binder does not. I think it is fine as is for now; say the word and I will move it.


Generated by Claude Code

Copy link
Copy Markdown
Member Author

Replay Manual on c020e10 (https://github.com/callstack/agent-device/actions/runs/35554059613), first pass:

  • Android Full Emulator Suite: green. The job runs smoke-android-emulator.test.ts at AGENT_DEVICE_ANDROID_E2E_TIER=full, which executes the smoke scenarios (including smoke:automation-system with send Android fixture Home and open Android Recents) and the full-tier ones, then the Android replay gate. All steps passed.
  • iOS Replay Suite: red before the E2E step. It failed at Run iOS simulator replay suite, the Settings-app replay gate that precedes the full-tier E2E: 03-scroll-discovery.ad step 4 find "text" "Accessibility" "exists" did not match on the iOS 26.2 Settings root (the captured screen lists Home Screen & App Library, Search, StandBy, Screen Time), on all three attempts, while the other five replays passed. That is the "varying steps" breakage the workflow header records since July and it touches no system button. Because that step is fatal, Run full fixture-backed iOS simulator E2E was skipped, so no full:lifecycle-system evidence was produced.

To reach the E2E step without touching this PR, I dispatched the workflow again from a scratch branch, claude/scratch-replay-manual-full-tier, which is c020e10 plus one workflow-only commit marking the two replay steps continue-on-error. Nothing on that branch is for merging; I will delete it after reading the run and post the background fixture to home / open app switcher output here.


Generated by Claude Code

Copy link
Copy Markdown
Member Author

Full-tier results from the second dispatch, run 35554714567 on the scratch branch (c020e10 plus the workflow-only commit fce6f49): https://github.com/callstack/agent-device/actions/runs/35554714567

Android Full Emulator Suite: green again, the same steps as the first run.

iOS: full:lifecycle-system completed on this head. This time the Settings replay gate passed (6/6) and Run full fixture-backed iOS simulator E2E ran at AGENT_DEVICE_IOS_E2E_TIER=full for 7m42s. The runner executes the six smoke scenarios, then the full scenarios in scenarios.ts order (live-runner.ts:97-105), and runScenario fails the test at the first failed step. The failure is in the second full scenario:

✖ live iOS simulator fixture E2E (451935ms)
  AssertionError: step: stop fixture log stream
  command: agent-device logs stop --platform ios --udid 5FEB61C5-… --session ios-e2e-full-1gmy --json
  json: { "success": false, "error": { "code": "COMMAND_FAILED", "message": "Daemon request timed out", "details": { "timeoutMs": 90000 } } }
  scenario: full:observability-artifacts
  at assertObservabilityAndArtifacts (live-full-scenarios.ts:260)

So every smoke scenario (including the automation-longpress scroll search) and all of full:lifecycle-system, with background fixture to home (live-full-scenarios.ts:119) and open app switcher (:145) and their distinct-pixel assertions, ran and passed before the run reached logs stop. The runner prints step names only on failure, so the per-step home and app-switcher responses are in the run's session request logs: https://github.com/callstack/agent-device/actions/runs/35554714567/artifacts/10619864329 (sessions/ios-e2e-full-1gmy/requests/*.ndjson).

The logs stop timeout is the log-stream teardown in the observability scenario; it drives no system button and is the workflow header's "varying steps" breakage. Two side notes from this run: the automation-longpress search and the 03-scroll-discovery.ad replay both passed with no change, confirming both earlier failures were intermittent.

Housekeeping. The scratch branch claude/scratch-replay-manual-full-tier still exists on origin: the push proxy this session goes through accepts pushes but drops branch deletes. It holds nothing but c020e10 and the one continue-on-error commit and can be deleted.

Still yours: the single action-button press on a simulator, and the review decision.


Generated by Claude Code

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