mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
prerestack-081132
1496 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3908559fe2 |
fix: report real claim results from daemon stop (#1818)
* fix: report real claim results from daemon stop `daemon stop` typed `claimsReleased`/`claimsOrphaned` as the literal `[]` and every path hardcoded them, so a graceful stop that released a device claim still reported none (#1799 observation 3, #1320 acceptance). Graceful teardown now records each session's claim outcome — released after a clean teardown, orphaned when teardown left the claim in place — into the daemon shutdown report, and the CLI merges them alongside provider releases. Forced and not-running stops stay empty because they cannot know, and a report written before claim reporting still reads its provider releases. * fix: classify daemon stop claim results from the clear outcome `clearDeviceClaim` deliberately resolves without deleting when the on-disk claim is no longer the one it acquired, so the shutdown ledger's "the call resolved" test reported a successor's claim as released — a device the daemon never freed, counted as freed. `clearDeviceClaim` now returns a typed outcome (`deleted` | `absent` | `ownership-changed`) instead of nothing, and the ledger classifies from it: released only when absence is confirmed, and a new `superseded` bucket for a claim another owner had already taken over. Superseded is neither released (this daemon freed nothing) nor orphaned (no claim of ours remains to reconcile), so folding it into either would break that list's meaning; it also raises a warning so a device now owned elsewhere cannot pass silently. |
||
|
|
8d0de32ba4 |
fix(android): let a covering sibling hide only what its content covers (#1808)
* fix(android): let a sibling hide only what its content covers (#1806) pruneAndroidCoveredSubtrees credited a higher drawing-order sibling with painting its whole box as soon as it had any content anywhere inside it, or a label of its own. A full-screen DoraemonKit drag surface holding one 189px floating icon therefore condemned the entire app subtree, and an empty labelled match_parent placeholder did the same. Occlusion is now spatial. A subtree's footprint is the bounding box of what it presents (agent targets and labelled leaves); a sibling is covered when its footprint lies under a candidate's footprint. A node's own label is no longer paint evidence: a container's content-desc describes its children and an empty labelled View draws nothing. Only a touch target still hides its full box (scrims). Comparing footprint to footprint keeps stacked screens with matching margins registering as covered. Live on a Pixel 9 Pro XL API 37 emulator with a DoKit-shaped overlay added to the test app: snapshot -i went from 2 nodes + the sparse hint to the full app; helper-XML A/B across home/catalog/form/product-detail recovered every label with none lost, and non-overlay screens are byte-identical. * fix(android): measure occlusion by overlapped area, not bounding box Review on #1808: a bounding box of two corner controls spans the viewport, so a transparent overlay with a control in each corner still acquired a full-screen footprint and could prune the app beneath it. Footprints now keep their presented rects apart, and coverage is the overlapped area of the two unions (coordinate-compressed cell sweep). Scrollables count as presenting their box: they consume touches over it, which is what lets a real pushed screen (header, scrollable body, footer) still cover a drawer surface. Adds the disconnected-corner regression. * fix(android): count what a covered sibling shows, not only what it paints Fuzzing random sibling trees old-vs-new surfaced the one direction the footprint model could still regress: a container whose only painted content is small (one corner icon) but which also carries labelled containers or testID-only markers was condemned as soon as a touch surface covered that icon, since markers and container labels are not paint and never entered the footprint. Footprints now carry two rect sets. `paints` (touch targets, scrollables, labelled leaves) is what a candidate can cover with; it still excludes identifiers and container labels, or the DoKit fix would unwind. `shows` adds every labelled or identified node and is what a covered sibling must lose in full. Focusable-only nodes no longer paint their box either, matching #1733 for descendants as well as siblings. Adds the marker regression. Re-fuzzed 20k trees: new-prunes-more is down to 0.14 %, all of the class where everything the target shows lies under a higher touch/scroll surface. Live captures unchanged. * test(android): pin that focusability never paints a covering candidate's box A full-screen focusable wrapper holding one clickable icon is a covering candidate; the lower app content must survive. Fails when paintsOwnBox counts focus targets again. |
||
|
|
f843dc2df1 |
fix(scroll): keep saturated scroll gestures out of the status bar; gate Android replays from android/emulator (#1781 A1) (#1820)
* fix(scroll): keep saturated scroll gestures out of the status bar; gate Android replays from android/emulator (#1781 A1) `pnpm gate replay-android` failed 4/8 whenever it ran after the full-tier Android E2E (replays-nightly run 32107665052, job 95620294899): 05-app-lifecycle, 06-swipe-gestures and both fixture replays diverged under "A system surface covers the app". The E2E was not the cause. Reproduced on a pixel_7 / API 36 AVD with the same cutout geometry CI's `avdmanager --device pixel_7` produces (status bar 136px, not the 63px of a plain 1080x2400 skin): - `03-scroll-discovery.ad` runs `scroll up 3`. The scroll planner clamps travel to the viewport minus a 5% band, so the touch-down landed at y=120 — inside the 136px status bar — and pulled the notification shade instead of scrolling. On API 36 the app window is edge-to-edge, so the reported viewport starts at y=0 and includes that bar. - The shade then covered every replay until `04`'s `back` closed it. Native readdir order on the runner (03, 05, 06, fixture/02, fixture/01, 04, 01, 02) put four files in that window; the last green run (2026-07-30) had 04 right after 03, so the pull was masked. Fix in the product, not the lane: DEFAULT_EDGE_PADDING_FRACTION 0.05 -> 0.1 in the TS scroll planner and its Swift port. Every real Pixel has a cutout (5.7% of a Pixel 7's height) and an iPhone's Dynamic Island status bar is 6.9%, so any saturated `scroll up` opened the shade / Notification Center for real agents too. Parity vectors updated in both suites plus a Pixel 7 regression vector (1080x2400, amount 3 -> touch-down y=240 > 136). Second contamination the same order exposed once the shade was gone: `fixture/02-selector-routes-covered-diagnosis.ad` is a #1715 reproduction recipe that FAILS BY DESIGN at step 9 (covered-target refusal) and leaves the device in landscape, yet the gate enumerated `test/integration/replays/android` recursively. iOS keeps gate replays in `replays/ios/simulator` and fixture recipes in `replays/ios/fixture`; Android now mirrors that: the six Settings replays move to `replays/android/emulator`, `test:replay:android` points there, and `fixture/` stays E2E-owned (`full:fixture-replays` already runs 01 by path). android.yml and the workflow-evidence fixture follow the path; the replay-compat manifest keeps the historical paths it pins at released tags. Verified live (Pixel 7 geometry, API 36, --retries 0): control run at main head in CI order reproduces exactly CI's 4/8; with the fix, `pnpm gate replay-android` 6/6 in both native and CI order, and `03` leaves Settings on screen (scroll up 3 now touches down at y=240). * test(scroll): drive the TS and Swift scroll-plan parity vectors from one table (#1820 review) The two suites hand-mirrored the same vectors and #1820 had to edit both by hand — the drift class the repo already closes for the tap-point rule via contracts/fixtures/tap-point-policy.json. The scroll vectors (plus both planner constants, pinned behaviourally on a 1000px axis) now live in contracts/fixtures/scroll-gesture.json; scroll-gesture.test.ts and RunnerTests+ScrollGesture.swift iterate it. Verified: vitest 10/10; the four XCTests run on an iOS 26.2 simulator with the unit flag on (Executed 4 tests, 0 failures). Also: test/ci/android-workflow-evidence.json says what it guards. Follow-up for content-safe viewport bounds + discovery order: #1821. |
||
|
|
2b6d04a13e |
fix: enforce device claims for sessionless device mutations (#1809)
`boot` and `shutdown` never consulted the host-global device claim store, so a daemon in one state directory could terminate an emulator another daemon held a verified-live claim on and report success (#1799). Rather than adding a claim check to those two handlers, this makes the class unrepresentable: `CommandDescriptor` gains a REQUIRED `deviceClaimPolicy` trait (#1320's vocabulary), and the request-execution scope enforces it where the request runtime bindings create a device binding — the one seam through which any handler can obtain device operations, and already the place per-device deduplication lives. A `transient-exclusive` command acquires a command-scoped claim before operations reach the handler, refuses a foreign live claim with the existing DEVICE_IN_USE/DEVICE_CLAIM_LIVE_OWNER error, and releases in the scope's finally. Every other policy performs no claim-store I/O, so session-bound commands keep #1320's non-goal intact. |
||
|
|
4c5f693a03 | fix: declare frameworkTier on the hover descriptor (main parity gate red) (#1817) | ||
|
|
1234590a69 |
fix(ios-runner): reject CGRect.infinite in navigation frame guards (#1816)
topLeadingNavigationFallbackPoint and isTopNavigationControlFrame only validated width/height, never the origin. CGRect.infinite has a finite (if absurd, ~-9e307) origin, so it slipped past the isFinite/>0 guard: the fallback point collapsed to roughly (-9e307, -9e307), and an unresolved element frame was classified as sitting in the nav header band. Extract isUsableNavigationFrame(_:), which adds the isInfinite check TapPointPolicy.isAllowed already uses for the same rect, and share it between both helpers. Fixes #1812 |
||
|
|
142d156338 |
ci(ios): run the full XCTest suite nightly and check the PR test list (#1781 A7) (#1789)
* ci(ios): run the full XCTest suite nightly and check the PR test list (#1781 A7) * fix(ci): skip the runner server entry point in the nightly and validate both test flags * docs(ci): restate the nightly lane cost and timeout honestly * docs(ci): stop quoting XCTest counts that drift between commits * ci(ios): tighten the nightly timeout to the measured suite duration |
||
|
|
0f4f322878 |
fix: reject selector-shaped wait arguments instead of reading them as text (#1813)
`wait <condition> '<selector>' [timeoutMs]` (e.g. `wait open 'label="Open"' 25000`, `wait exists 'label="x"' 100`) and any unrecognized `key=value` token used to fall through parseWaitPositionals' text fallback and wait out the full timeout for literal text that could never appear on screen — reading as a false "element absent" instead of the caller's own argument mistake (#1035 is the sibling fix for click/press/fill/get). parseWaitPositionals now returns a typed `invalid` variant whenever a positional token is selector-shaped (a recognized key, or an unrecognized key=value) but the list doesn't form a valid selector expression, or a valid selector prefix is followed by unquoted trailing tokens. The message names the offending token, points condition words (exists/present/appears/gone/disappears) at the selector form, and always offers the explicit `wait text '<text>'` escape hatch. Bare text (single- and multi-word) and the explicit `text` keyword form are unaffected. Excluding `invalid` from the type consumed by selector-runtime's toWaitTarget makes the remaining kind-by-kind narrowing exhaustive without a runtime fallback branch. |
||
|
|
801734d433 |
feat(ai-sdk): add agent-device/ai-sdk tool set and document the MCP zero-code path (#1804)
* feat(ai-sdk): add agent-device/ai-sdk tool set and document the MCP zero-code path
Adds `createAgentDeviceTools()` under a new `agent-device/ai-sdk` subpath,
built from the same command registry the MCP server uses so both stay in
lockstep without a hand-maintained tool list. Introduces a `frameworkTier`
descriptor facet ('core' | 'extended') so the factory can default to a
curated perceive/act loop instead of handing a model dozens of tools.
`ai` is wired as an optional peer dependency, imported lazily inside the
factory rather than at module scope, so importing the subpath itself never
requires `ai` to be installed - only calling it does. The package's own
publishing gate (scripts/lib/shipped-imports.ts) is extended to recognize
peerDependencies as a valid resolution source, since this is the first
optional peer this package has shipped.
Also restructures the AI SDK doc around three tiers (zero-code via
@ai-sdk/mcp, the new typed tool set, hand-written tools) and fixes a stale
`needsApproval` reference in favor of the current `toolApproval` API.
* fix(layering): classify src/ai-sdk as a rank-4 zone
The layering guard requires every src/<folder>/ to be explicitly ranked or
unranked; the new src/ai-sdk/ subpath (added in the prior commit) was left
unclassified, failing CI's Layering Guard job. It sits at the same tier as
client/compat/daemon-server/metro/remote/sdk - a public integration surface
consuming mcp (3) and core (2), imported by nothing else in the tree.
* fix(ci): cover, exempt, and pack the new ai-sdk subpath
Fixes the remaining CI failures on the ai-sdk subpath commit:
- Coverage: src/ai-sdk/index.ts had no dedicated unit test (only manual/
integration verification), so changed-line coverage sat at 6.9% against
the 70% gate. Adds src/ai-sdk/__tests__/index.test.ts (core vs 'all' tool
filtering, session/platform pinning and schema hiding, error
normalization, toolApproval passthrough) with createCommandToolExecutor
and createAgentDeviceClient mocked the same way command-tools.test.ts
does, plus a dedicated missing-peer-dependency.test.ts that mocks `ai`
itself to throw, isolated to its own file so it doesn't affect the other
tests' use of the real, installed `ai` package. Changed-line coverage is
now 29/29 (100%).
- Fallow Code Quality: src/ai-sdk/index.ts and examples/sdk/ai-sdk-tools.ts
are entry points with no in-repo importer (reached only via package.json
exports / run directly), and the new subpath's exports are unused
internally by design - both need the same treatment src/sdk/*.ts and its
examples already have in .fallowrc.json.
- Integration Tests: test/integration/installed-package-metro.test.ts and
src/__tests__/package-exports.test.ts each hand-list every published
subpath and smoke-check it from a real packed install; added ./ai-sdk to
both so the new subpath is actually exercised, not just silently passing.
* fix(ai-sdk): hide MCP transport/config fields from the model too
createAgentDeviceTools() only removed session and mcpOutputFormat from tool
schemas. stateDir was still model-visible and reached the shared executor
as client configuration, letting a tool call redirect into a different
daemon state directory - defeating the "one pinned session" guarantee the
factory exists to provide. includeCost and responseLevel are MCP
tool-config knobs in the same category, irrelevant to this adapter.
Widens the hidden-field set to session/stateDir/mcpOutputFormat/
includeCost/responseLevel, and now strips them from the runtime input
inside execute() too (not just the schema), so the guarantee holds even if
a caller bypasses schema validation. The schema-properties filter and the
input filter now share one omitHidden() helper instead of two near-
duplicate implementations.
Addresses the P1 review comment on #1804.
|
||
|
|
20458cbcf6 |
fix(daemon): reject '.', '..' and empty session names at resolveSessionDir (#1815)
safeSessionName only rewrites characters outside [a-zA-Z0-9._-], so the names '.' and '..' survive unchanged and path.join resolves them to the sessions dir itself or its parent, the daemon state dir. A remote caller's --session .. would then land app.log / runner.log / requests/*.ndjson outside the sessions tree. SessionStore.resolveSessionDir is the one place a session name becomes a directory (AGENTS.md: session artifact paths come from session-store), so it now refuses such a name with INVALID_ARGS. Every request goes through it first thing in createRequestExecutionScope, before any artifact path is used, so this is also the admission-time rejection; every other caller passes an already admitted name. isSafeSessionSegment mirrors the predicate PR #1814 adds for its request-diagnostics route; whichever lands second takes the trivial merge. Regression tests were proven red against the pre-fix code: resolveSessionDir returned the sessions dir / state dir for '.', '..', '' and the request scope resolved runnerLogPath to <stateDir>/runner.log. |
||
|
|
04613ae8d3 |
ci: keep Bundle Size job green on transient GitHub comment failures (#1795)
* ci: keep Bundle Size job green on transient GitHub comment failures The size measurement and job summary had already succeeded on PR #1789 (run 32050847506) when the PR comment write got a 503 during a GitHub incident and failed the whole lane. --post-comment now retries 5xx / 429 / network errors (4 attempts, 1s/2s/4s backoff) on both the list and write calls. If it still fails, it prints a ::warning::, appends a note to $GITHUB_STEP_SUMMARY, and exits 0. Other 4xx (bad token, missing permissions) stay fatal. * refactor: split GitHub response classification to satisfy fallow complexity gate * fix: reconcile uncertain comment creates instead of re-POSTing; add regressions Retry now wraps the whole list -> write cycle rather than each request, so a create whose response was lost (network error / 5xx) is re-listed on the next attempt and turned into a PATCH of the marker comment instead of a duplicate POST. Splits the retry/classify helpers under the fallow complexity gate. Adds scripts/__tests__/size-report-post-comment.test.ts (unit-core): spawns the real script against a stubbed fetch and pins uncertain-create reconciliation, transient exhaustion (warn + exit 0), and fatal 4xx (nonzero, no retry). SIZE_REPORT_RETRY_BASE_MS lets the tests skip real backoff. |
||
|
|
8db36299e4 |
feat(web): add hover command for hover-gated UI (#1783) (#1786)
* feat(web): add hover command for hover-gated UI (#1783) Add a first-class `hover <x y|@ref|selector> [--settle]` verb, admitted on web only, that moves the pointer without pressing via the agent-browser backend (mouse move). It rides the existing targeted-touch pipeline (ref/selector/coordinate resolution, occlusion/off-screen guards, settle observation, response builder, recording) through a new optional Interactor/backend `hover` op that only the web provider implements. Touch platforms have no hover state: capabilities advertise it on web only and iOS/Android/Linux reject it at admission with a --platform web hint; longpress stays the mobile hold-gesture verb. Closes #1783 * fix(hover): native hoverRef route for web @ref, android coverage pin, revert skill edit Review follow-ups on #1786: - hover @ref on web now dispatches through the provider's own element handle (agent-browser `hover <ref>`) via a new backend hoverTarget, mirroring click/fill's ADR 0011 native-ref path — web ref frames carry no rects, so the coordinate route could never resolve them. The shared preflight + exact-ref dispatch is extracted into dispatchNativeRefInteraction and used by tap/fill/hover; the guarantee matrix native-ref row now lists hover. - Daemon regression test is production-faithful: rect-less web ref frame, scoped provider, asserts no coordinate dispatch. Selector→coordinate and provider hoverRef tests added. - Android emulator coverage summary pin 2/53 → 3/54. - skills/agent-device/SKILL.md reverted (out of scope, AGENTS.md rule). - Docs/help disclose that --settle with @ref on web shares click's existing limitation; use a selector or coordinates for the settled diff. * test: drive hover through the apple output guard; cover direct hover dispatch The provider-integration apple-leak guard partitions every public command into driven/skipped; hover was neither, which failed Integration Tests and took Coverage down with it. Drive it (it reaches the Apple capability refusal, which is scanned like any other error response). Also cover the direct-dispatch handleHoverCommand seam. * test(web): drive hover @ref in the provider-backed web scenario The integration-progress gate requires every public command to be referenced by a provider-backed scenario. Add hover @ref to the web desktop flow: it must reach the provider's hoverRef handle (never a coordinate) and be recorded on the session without fabricated x/y, like click @ref. |
||
|
|
8b0560a51d | fix: hide clamped descendants of offscreen iOS rows (#1811) | ||
|
|
37e9d34581 |
chore(format): ignore .claude/** so oxfmt from the main checkout can't sweep worktrees (#1803)
Running `pnpm format` in the main checkout walks into .claude/worktrees/*/, where the anchored ignore pattern scripts/maestro-conformance/corpus/** no longer matches, and rewrites every worktree's conformance oracle corpus (quotes + trailing newlines; 45 files × 15 worktrees on 2026-08-17 20:59). .claude/ is only ignored via the user's global gitignore, which oxfmt does not consult. |
||
|
|
681cad2222 |
test: assert the specific error code instead of any failure (#1781 B4) (#1790)
Converts the 20 test assertions across the repo that accepted ANY
failure (bare `expect(...).toThrow()`, bare `assert.throws(fn)`, bare
`assert.rejects(p)`) into assertions on the specific AppError `code`
each test is actually about, or — where the propagated error is
genuinely opaque (a mocked upstream failure whose identity, not its
shape, is the point) — identity assertions with a comment explaining
why.
Added a synchronous `assertThrowsAppError(fn, {code, message?})`
sibling to the existing `assertRejectsAppError` helper in
src/__tests__/test-utils/app-error.ts, exported via the test-utils
index, for the two src/ sites that needed it.
packages/provider-limrun and packages/provider-webdriver have no
test-utils dir and cannot import from src/, so those sites use
vitest's `expect(...).toThrow(expect.objectContaining({ code }))` or
an inline `assert.rejects(p, matcherFn)` instead.
Sites converted:
- packages/provider-limrun/src/app-log-runtime.test.ts:153-155
(bare `.toThrow()` x3 -> `UNSUPPORTED_OPERATION`)
- src/daemon/__tests__/app-log.test.ts:39 (bare `.toThrow()` ->
message match; plain Error, not AppError, from verified-file's
identity check)
- src/daemon/__tests__/resumable-upload-range.test.ts:13 (bare
`assert.throws(fn)` -> `INVALID_ARGS`); also fixed line 19's
`assert.throws(fn, value)`, a documented Node.js gotcha where a
string second argument is the failure message, not a matcher, so
it was equally bare in effect
- packages/provider-webdriver/src/webdriver-client.test.ts:229 (bare
`assert.rejects(p)` -> asserts the raw AbortSignal.timeout()
rejection's `name`, since the transport re-throws it unwrapped)
- src/daemon/handlers/__tests__/session-device-claims.test.ts:129,
151, 174 (bare `assert.rejects(p)` x3 -> identity assertions; each
test's point is device-claim rollback/retention around an opaque
mocked upstream failure)
- src/platforms/android/__tests__/settings.test.ts:109 (bare
`assert.rejects(p)` -> `UNSUPPORTED_OPERATION`)
- src/platforms/android/__tests__/snapshot.test.ts:1071, 1342 (bare
`assert.rejects(p)` x2 -> `COMMAND_FAILED` + message)
- src/platforms/android/__tests__/touch-helper-session.test.ts:526
(bare `assert.rejects(p)` -> `COMMAND_FAILED`, wrong-protocol
message)
- src/platforms/apple/core/__tests__/runner-command-retry.test.ts:472,
527, 550, 762, 881, 1016 (bare `assert.rejects(p)` x6 ->
`COMMAND_FAILED` with the recovery-path-specific details/message)
- src/platforms/apple/core/__tests__/runner-transport.test.ts:61
(bare `assert.rejects(p)` -> identity assertion; fetchWithTimeout
does not wrap fetch() failures into an AppError)
No repo-wide scanner/lint rule added (explicitly out of scope per
#1781); no test loosened.
|
||
|
|
ccf64f6797 |
ci: move parked device replay suites to a dispatch-only workflow (#1781 A1) (#1794)
* ci: move parked device replay suites to a dispatch-only workflow (#1781 A1) Both full-tier device jobs have failed every scheduled run since 2026-07-24: the Android suite inside full-tier scenarios that had never executed end to end, the iOS suite on varying steps. They move to .github/workflows/replays-manual.yml, which has no `schedule:`, so the schedule stops emitting a guaranteed failure while the suites stay runnable on demand. A job-level `if: github.event_name == 'workflow_dispatch'` would have looked the same and lied: `workflowLanes()` decides `qualifying` per workflow FILE and never reads job-level `if:`, so the manifest kept reporting replay-android, replay-ios, and replay-ios-device as scheduled-lane owners — the silent-owner-loss failure the manifest exists to catch. A separate file is what the file-level model already reads correctly. Those three checks now have no pull_request/schedule owner, so they are declared as MANUAL_ONLY_OWNERS rather than folded into UNPROVABLE_OWNERS, whose claim ("it runs, this loader cannot see it") is no longer true for replay-android. check:gate-manifest drops from 48 to 46 wired checks and names the three on every run. Two tests pin it: a dispatch-only lane is non-qualifying however many gates it declares, and every manual-only declaration must name a registered check that no qualifying lane owns, so a re-scheduled lane cannot keep a stale exemption. * ci: attest manual-only checks against their dispatch lane (#1781 A1) Review P1: MANUAL_ONLY_OWNERS was a negative allowlist — it proved each entry named a registered check no qualifying lane owned, but nothing tied the entry to a lane that can still run it. Deleting a parked job, or its run-gate step, would have left the manifest green and still printing the check as manual-only: parked coverage silently becoming deleted coverage. Each entry now names its dispatch lane, and a new 'manual-only' audit assertion resolves that name against the derived model: the lane must exist, must still be dispatch-only, and must still declare the gate. replay-android carries an explicit `opaque` flag because its gate sits inside the third-party emulator action's `script:` (#1429), so the job's existence is the whole attestation the model can make — and the flag says so rather than letting an unreadable lane look like a declaring one. Four regressions pin both directions: deleting a declaration reports the check as unowned; deleting the parked job fails with 'no workflow defines'; re-scheduling the lane fails until the entry is dropped; and a parked lane that loses its run-gate step fails unless the entry is opaque. * ci: make manual-only mean dispatch-only, not merely non-qualifying (#1781 A1) Review follow-up: the attestation checked `qualifying === false`, which is true of any lane that is not pull_request/schedule. Swapping `workflow_dispatch` for `push` in replays-manual.yml would have kept the audit green and the checks printed as manual-only, while the runs nobody starts by hand quietly started themselves on every push. The lane model now keeps the trigger names instead of collapsing them into that one bit, and the manual-only assertion requires `workflow_dispatch` and nothing else. Three planted regressions cover the gap the review named: a parked lane re-triggered by `push` fails, a parked lane with no trigger at all fails, and the loader test pins that trigger kinds survive into the model (a push lane reads `[push]`, the nightly reads `[schedule, workflow_dispatch]`). |
||
|
|
e3f3a2488e |
test(android): restore full-tier lifecycle and observability scenarios (#1781 A1) (#1793)
* test(android): restore full-tier lifecycle and observability scenarios (#1781 A1) The nightly Android job has failed on `click id="automation-request-microphone"` since the full tier landed: `settings permission reset microphone` runs `pm revoke`, and revoking a *granted* runtime permission kills the app process, so the round after an accept clicked into an empty launcher surface. Reproduced on the pinned CI image (android-36 google_apis_playstore, Pixel 7): pm revoke leaves pid 4259 alive when the permission is denied and kills it when it is granted, with NexusLauncher resumed afterwards. Fixing that exposed the rest of a scenario that had never executed end to end: the post-revoke readback cold-started on the tabs home instead of Automation lab, the relaunched Automation lab needed its controls revealed, the Form tab does not exist on the Automation root route, that section needs the system IME back, and the IME diagnostic sits above the bottom of the form. The observability scenario then failed the same way (reveal distances tuned for a taller device) plus an event-timeline walk whose page was smaller than the events each page read appends. Validated live against a local Pixel_7_CI emulator (API 36, same profile as the lane): the full tier now runs bootstrap -> inventory -> automation-system -> form-input -> keyboard-ime -> capture-close -> lifecycle-system -> observability-artifacts and stops only in full:fixture-replays. * test(android): repair the drifted fixture replays and pin the catalog canary (#1781 A1) Review follow-up. The nested batch regression now checks a sibling card instead of the notice that owns `dismiss-notice`: resolving a child already proves its parent is present, so the old target could not fail on its own. Confirmed on a Pixel 7 / API 36 emulator that `gesture-lab-card` and `dismiss-notice` are on screen together at the scenario's existing 0.3 reveal (both present at 0.2-0.4; the card is gone by 0.5). Getting a full-tier run to complete then required repairing what the lane had never executed: - `01-navigation-scroll.ad` clicked `label="Catalog, 0 new notifications"`. #1543 made the cart badge conditional, so the live label is `Catalog` — what the iOS twin already used. - The catalog scroll canary lives inside the scrolling content, and Android accessibility snapshots carry on-screen nodes only, so every state except the initial `top` was unobservable: `wait "Catalog scroll: down|bottom|up"` could never pass, whatever the swipe coordinates were. `stickyHeaderIndices` pins that one line, which makes all four states readable at any offset on both platforms rather than tuning the .ad around a canary that scrolls away. - `gesture-lab-android.ad` started its multi-pointer gestures at y=1040, inside the target when the file was last repaired but 90px from its top edge after #1567 moved the card (targets now span y=949-1525). The second pointer landed outside the view, which reads as "the gesture did nothing". Multi-pointer gestures now start at the target centre, and the header comment records the geometry they depend on. Evidence: the lane's own command (`AGENT_DEVICE_ANDROID_E2E_TIER=full` over smoke-android-emulator.test.ts) passes end to end on a Pixel 7 / API 36 AVD with a CI-equivalent fixture APK (cached native + head JS through the same repack the workflow runs): 9/9 scenarios, 153s. |
||
|
|
f45228ae71 |
ci: skip device lanes for root-level docs-only changes (#1781 A9) (#1791)
* ci: skip device lanes for root-level docs-only changes (#1781 A9) Add AGENTS.md, CHANGELOG.md, CONTEXT.md, CONTRIBUTING.md, LICENSE, and SECURITY.md to the pull_request paths-ignore block in ios.yml, android.yml, linux.yml, macos.yml, ci.yml, and size.yml. These root-level docs files were the only gap left after docs/**, website/**, and README.md — PRs #1568 (SECURITY.md only), #1697 (CONTEXT.md + docs/adr only), and #1722 (AGENTS.md + docs/) each still triggered a full 9-15 min macOS iOS run despite touching only prose. Why each file is safe to ignore for every one of these six workflows: - None of the four device workflows (ios/android/linux/macos) or their composite actions read any of these six files at runtime; the only hits from `grep -rln` across scripts/, src/, test/, and .github/actions/ are prose comments pointing humans at CONTEXT.md or AGENTS.md sections (e.g. scripts/layering/check.ts, scripts/wire-compat/run.ts, src/mcp/tool-ref-pins.ts) — never an `fs.readFileSync`/`readFile` of the file itself. - The check-affected selector (scripts/check-affected/model.ts) already classifies all six as pure docs: `isDocs()` matches any `.md` file plus the literal `LICENSE`, and `docsOwnership()` only special-cases `website/docs/docs/commands.md` (unrelated). So these files already select zero checks — they only ever produced `docsOnlyPaths` entries, never `SelectionReason`s. - Because they select zero checks, the gate-manifest's path-coverage category derivation (`scripts/gate/model.ts` `categories()`, which iterates `plan.reasons`) never records a category for them, so ci.yml has nothing check-manifest-only that these six files would need to keep reachable. `pnpm check:gate-manifest` and `pnpm check:gate-manifest:test` both stay green after the change (48 checks / 33 lanes, 28/28 gate tests passing). - size.yml's bundle-size job (scripts/size-report.mjs) measures the `pnpm build` dist output and startup timing only — no reference to any of these six files. (npm packs LICENSE/README.md into the publishable tarball, but that's a `pnpm check:package` node-22.12 concern in ci.yml's packaged-cli job, which is driven by `dist` contents and `package.json`, not by LICENSE/README prose — already evidenced by README.md being ignored here since before this change.) Scope disclosure: `mutation-affected.yml` uses a `paths:` allowlist (not paths-ignore) so it's structurally unaffected; `test-app-build-cache.yml` has no path filter at all. Neither was touched. actionlint and `pnpm check:gate-manifest`/`:test` pass on the changed workflows. * test: pin root-doc paths-ignore entries with a regression test Addresses review feedback on #1791 from thymikee: the docs-only classifier for AGENTS.md/CHANGELOG.md/CONTEXT.md/CONTRIBUTING.md/ LICENSE/SECURITY.md across ios.yml/android.yml/linux.yml/macos.yml/ ci.yml/size.yml had no regression pin. Neither check:gate-manifest (only proves a *registered check* is reachable) nor actionlint (only validates YAML shape) nor generic Markdown coverage would catch a single dropped entry — e.g. LICENSE reappearing in one workflow's paths-ignore list but not another's would silently put a full 9-15 min device run back on prose-only PRs. test/ci/root-docs-paths-ignore.test.ts parses the six real workflow files and asserts, using the same matchesGlob the gate-manifest model uses to decide lane triggering, that each of the six root docs is ignored by each workflow's pull_request paths-ignore. Registered in vitest.config.ts's unit-core project next to its sibling upload-agent-device-artifacts.test.ts (parse-only, no device/subprocess lane needed). Verified red on main (all 36 file x doc assertions fail — confirmed via a throwaway script reading `git show main:.github/workflows/*.yml`) and green on this branch (6/6). Full unit-core project (873 files / 6641 tests) still passes; check:gate-manifest and check:gate-manifest:test unchanged (48 checks / 33 lanes, 28/28). |
||
|
|
0d3b7413c5 |
fix: prevent private AX subtree leaks at source (#1807)
* fix: prevent private AX subtree leaks at source * fix: preserve values in settle signals * fix: normalize settle signal semantics |
||
|
|
3651047be8 | chore: restore unreleased version (#1805) | ||
|
|
6e33d2c3f7 | 0.20.10 | ||
|
|
ed1d44fa62 |
fix: unify private AX scroll visibility (#1798)
* fix: unify iOS scroll snapshot visibility * fix: preserve composed scroll hints |
||
|
|
302019563c |
ci: drop duplicate nightly-macos replay job (#1781 A2) (#1787)
The nightly-macos job in .github/workflows/replays-nightly.yml is byte-identical in substance to smoke-macos in .github/workflows/macos.yml: same setup-apple-runner-build call, same macos-helper gate, same replay-macos gate invoked with --retries 2 --report-junit test/artifacts/replays-macos.junit.xml. macos.yml already runs this gate on every pull_request and every push to main, so the nightly copy adds no additional information — 0/40 nightly runs have ever failed independently of the PR/push gate. Verified: check:gate-manifest and check:gate-manifest:test still pass against the trimmed workflow (no gate lost its CI owner), and the full check:tooling suite is green. |
||
|
|
0697915b10 | 0.20.9 v0.20.9 | ||
|
|
8b698e8efc |
fix: stabilize private AX settle snapshots (#1784)
* fix: stabilize private AX settle snapshots * fix: harden private AX settling |
||
|
|
75a4817789 | fix: install dependencies for iOS runner releases (#1785) | ||
|
|
4cfa34bd22 |
docs: reposition README around mobile app automation for AI agents (#1780)
* docs: reposition README around mobile app automation for AI agents Lead the README with the category (mobile app automation, testing, and verification for AI coding agents) and the three surfaces (CLI, MCP server, Node.js API) so humans and search engines can classify it, then keep verification and evidence as the differentiator. - Move per-platform transport/caveat sentences (HarmonyOS HDC/uitest, Vega VVD-only) out of the intro into "How it works" and add an inline guard comment plus an AGENTS.md rule so new platforms only add a name to the intro list. - Add MCP and Node.js quick starts next to the CLI walkthrough. - Add a works-with/proof line, a "What to ask your agent" prompt list, a product-ladder sentence, and two AEO-shaped FAQ entries. - Point the cloud/remote row at the remote proxy and device clouds docs. - Align npm, MCP registry, and docs-site descriptions with the same category phrasing. - Remove em dashes and evaluative filler per the humanizer skill. * docs: tighten README hero, link proof points, move install above capabilities * docs: add mobile-MCP FAQ distinction, sharpen hero support line, drop coming-soon * docs: make the build-on-top audience explicit in README * docs: trim README repetition and signposting * docs: close the session in finally in the README Node.js snippet |
||
|
|
856ff3886f |
fix(ios): preserve final-probe xcodebuild diagnostics (#1776)
* fix(ios): preserve final-probe xcodebuild diagnostics * refactor(ios): split runner startup transport |
||
|
|
f378050586 |
feat(snapshot): attach a fallback screenshot to sparse captures (#1764)
A sparse verdict already tells the caller to use a screenshot as visual truth, which made that screenshot the guaranteed next command on every unreadable screen — a second round trip to obey advice we authored. The user-facing `snapshot` dispatch now takes the shot itself and links the path in its warnings. The fallback is deliberately hung off `dispatchSnapshotViaRuntime` and skipped for internal observations: selector resolution, settle, and wait polling reach `captureSnapshot` directly, so a wait polling an unreadable screen cannot turn into a screenshot per poll. A failed shot is swallowed — the verdict's own warning still carries the manual remedy, so the fallback can never fail the snapshot that was asked for. Sparse captures also say when the screen is the app's problem. Only the `sparse-tree` reason code is evidence about the app: every backend reached the screen and it published no semantic content, which is the same emptiness assistive tech gets. `ax-rejected`, `budget`, `no-nodes` and `capture-failed` are limits of this tool and stay unattributed, so readers are not sent to file bugs against code that is not broken. |
||
|
|
13c482ce50 |
fix: preserve compact snapshot action labels (#1773)
* fix: preserve compact snapshot action labels * fix: narrow iOS implementation cell labels |
||
|
|
d8a7d03faf |
refactor: route application lifecycle through runtime facts (#1759)
* refactor: route application lifecycle through runtime facts Moves the canonical `open`, `prepare`, `close` and internal `runtime` descriptors behind package-owned lifecycle bindings admitted from device runtime facts, while daemon request/session policy and public response construction stay put. Based on main, which already carries the boot unit, the parametrized cutover gate and the apps unit. Readiness is package-owned there, so the Apple and Android bindings call ensureAppleReady/ensureAndroidReady rather than a root readiness bag; ensureAppleReady gained an onColdBootStart hook so open keeps warming the runner cache in parallel with a cold boot, and a narrow markBooted port publishes readiness' fresh observation so a flow still makes one simctl listing. Cutover rows take R24-R27, clear of the accepted catalog and the sibling install stack, and cutoverTableDefects rejects a duplicate rule id. Two defects this unit introduced are fixed here rather than shipped: `open <app> <url>` dropped the URL on a first open, and test-IME activation was first fatal on an unobtainable helper and then over-caught. Helper unavailability is a typed non-activation outcome now; fence, lock and post-record failures propagate. The duplication the unit had accumulated is gone: one runtime-admission module instead of five per-command copies, one direct-lifecycle binding factory instead of six hand-rolled packages, one transport-hint predicate, one session finalization path, and no identity-wrapper module. * fix: allocate lifecycle cutover rows after deployment * chore: preserve lifecycle union reconstruction * fix: reconcile lifecycle runtime stack * refactor: tighten lifecycle runtime topology * refactor: remove superseded runtime adapters * fix: preserve stacked runtime cutovers * test: preserve migrated runtime ownership * test: move Android deployment retry ownership * test: extract runtime hint fixtures * fix: preserve lifecycle stack invariants * fix: complete lifecycle runtime cutover * fix: remove lifecycle cutover residue |
||
|
|
66cca1a5b8 |
refactor: route install commands through platform runtime (#1758)
* refactor: route install commands through platform runtime * fix: preserve stacked runtime facts * fix: align deployment facts with shutdown runtime * refactor: simplify capability facts projection * style: format capability facts projection * fix: preserve migrated capability ownership * fix: propagate deployment artifact cancellation * refactor: move Harmony deployment mechanics into package * refactor: move Apple deployment tools into package * refactor: move Android deployment tools into package * refactor: inject deployment temporary storage * refactor: remove superseded deployment helpers * fix: preserve provider deployment transport |
||
|
|
5855dfc2e0 |
refactor: route shutdown through device runtime (#1757)
* refactor: route shutdown through device runtime * fix: cover shutdown cutover review gaps * fix: propagate shutdown cancellation * fix: move shutdown mechanics to platform owners * fix: pass device to shutdown fact fixture * test: cover shutdown facts in session state fixtures * test: simplify Android shutdown assertions * fix: preserve Apple shutdown cancellation |
||
|
|
5b1b1aad2e |
fix: fingerprint the built daemon's real import graph (#1545) (#1771)
* fix: fingerprint the built daemon's real import graph (#1545) The static-import regex in computeDaemonCodeSignature required whitespace around `from` and after `import`/`export`. The built daemon entry is a minified bundle (tsdown/rolldown `minify: true`), whose real import statements have zero whitespace (e.g. `import{o as e}from"../foo.js"`), so the regex never matched any dependency edge and the graph walk silently degraded to fingerprinting only the entry file (`graph:1`) instead of its true ~50-file runtime graph. That defeats the safety net the signature exists for: a daemon started from one build can look identical to a client running a materially different build as long as the entry file's own size/mtime happen to coincide, and the "code-signature mismatch" takeover this check drives was one of the mechanisms observed in #1545's worktree dev-daemon multiplication. * refactor: fingerprint import specifiers by shape, not by import grammar Replaces the two keyword-anchored regexes (one for static import/export/from clauses, one for dynamic import()) with a single pattern that matches any quoted, relative-path-shaped string literal. The prior approach had to track the exact whitespace/keyword shape bundlers emit around a specifier, which is exactly what broke for minified output in the immediately preceding commit — formatted source, minified builds, static imports, re-exports, and dynamic imports all put the same thing in the same place (a relative string literal), so matching on that directly is both simpler and immune to future formatting differences. A same-shaped string that isn't really an import is safe to over-match: it just fails to resolve to a file and gets dropped. Verified the file count is unchanged against this repo's real build (dist: graph:50, src: graph:741, both identical to the keyword-anchored fix). |
||
|
|
b59b4e5c96 |
fix(daemon): make request-timeout cleanup and hints platform-aware (#1751)
* fix(daemon): make request-timeout cleanup and hints platform-aware
Every local request timeout ran the Apple-runner xcodebuild pkill cleanup
and emitted Apple-specific hint wording ("Apple runner work was aborted",
"Timed-out Apple runner xcodebuild processes were terminated") regardless
of the session's actual platform, which was misleading on Android/web/
Harmony timeouts.
The client already carries the request's declared --platform on the exact
DaemonRequest object both timeout call sites hold (req.flags?.platform), so
no new plumbing or src/platforms import is needed. When the platform is
known non-Apple, the Apple pkill cleanup is skipped and the hint drops its
Apple-runner claim; when it's unknown, both stay on their historical
fail-safe behavior.
Part of #1739 (independent cleanups)
* fix(daemon): split timeout cleanup eligibility from hint evidence
Maintainer review on #1751 found a real correctness inversion in the
first pass: gating the Apple xcodebuild pkill cleanup on the request's
declared --platform flag is wrong in both directions.
- req.flags.platform is not authoritative for session-bound execution.
applyStripLockPolicy (request-lock-policy.ts) lets an existing
session's real device platform silently override a conflicting
declared selector under --session-lock strip, so a request declaring
a non-Apple platform can still legitimately execute Apple work.
Skipping cleanup on that declared flag would skip real cleanup —
the dangerous direction.
- The common session-bound request omits --platform entirely, so the
previous fail-safe (undeclared -> treat as Apple) left the hint
wrongly claiming Apple involvement on the motivating case
(Android/web/Harmony session timeouts) too.
Redesign: separate the two decisions.
- Cleanup eligibility is unconditional again for every local timeout,
matching the original pre-fix behavior: the pkill patterns are
Apple-process-name-specific, so sweeping them on a non-Apple host or
session matches nothing and costs a few no-op subprocess spawns,
never a wrong skip. There is no client-visible signal that proves a
session-bound request cannot touch an Apple runner, so eligibility
does not try to prove one.
- The hint may only name Apple-runner involvement on evidence this call
site actually has: an explicitly declared Apple platform selector, or
the cleanup itself having terminated a matching process
(appleCleanupEvidence). Anything else gets platform-neutral wording.
Adds production-seam route tests
(src/daemon/client/__tests__/daemon-client-timeout-route.test.ts) that
spy on the real exec seam and drive actual socket/HTTP timeouts through
sendRequest, covering the rebound-session and unknown-session cases a
pure formatter test cannot catch. Proved red against the prior commit
(
|
||
|
|
39cd4d346a |
refactor: route appstate through platform runtime (#1755)
* refactor: route appstate through platform runtime * test: keep appstate capability fixture below complexity limit * test: cover appstate required readiness fact * fix: align appstate facts with boot readiness * fix: keep appstate use declaration minimal * fix: close appstate parity and ownership gaps * docs: record final appstate size accounting * fix: merge neutral runtime imports * docs: align final appstate size totals * fix: remove stale runtime dependency edges * docs: correct appstate size accounting * refactor: keep runtime-use factory internal * docs: itemize runtime-use relocation * fix: move appstate queries into runtime packages * refactor: retire root foreground query paths * refactor: share Android foreground parser ownership * fix: preserve Android appstate parser precedence * docs: keep appstate evidence in review artifacts * fix: keep appstate runtime loading lazy * fix: fail closed for stale limrun appstate * fix: preserve limrun recovery and abort appstate * fix: narrow limrun exact-owner recovery * fix: allocate appstate cutover rule * fix: reconcile appstate with merged main * style: format harmony runtime test * fix: allocate appstate rule id * fix: allocate appstate layering rule * fix: remove stale app command admissions * fix: close appstate layering regressions * fix: align Harmony capability parity with runtime facts * test: cover limrun recovery-only readiness * fix: keep Limrun recovery binding app-log only * fix: parse Android app state in linear time |
||
|
|
9c22467832 |
refactor(ci): make gate ownership structural (#1429) (#1753)
* test(ci): prove every registered gate is owned and reachable (#1429) A check that silently stops running looks exactly like a green build. Two suites had already stopped: `check:tmpdir-leaks` (with its model tests) and `test:fixture-cache` are real package scripts that no workflow ran, reachable only through the `check:unit` aggregate CI never invokes. `CHECK_CATALOG` becomes the registry of every check and `pnpm gate <id>` the only way CI runs one, so finding what a lane runs is a scan for `pnpm gate` rather than an attempt to interpret shell. `pnpm check:gate-manifest` then asserts against the real workflows that every registered check is run by some qualifying lane (per unit, not per script name), that every check the real selector activates for a path is run by a lane that path would start (#1420's class), and that every Vitest project and suite script belongs to a check. The wiring that keeps those honest is asserted too: a gate id must name a registered check, an `if:` must be ruled on in GATE_CONDITIONS so `if: false` unowns what it guards, an action declared to run a gate is proven to, and a job whose steps the loader cannot open fails closed. It deliberately does not try to prove CI runs project code only through `pnpm gate`. Whether a shell block executes project code is not decidable from its text, so shell this model does not recognise earns no ownership credit — the failure direction is a check reported unowned, never one waved through. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ * test(ci): update the two suites that assert on rewired workflow text `scripts/mutation/workflow.test.ts` and `test/ci/trusted-fixture-artifact.test.mjs` read the workflow and action files and assert on their command text, so routing those steps through `pnpm gate <id>` moved what they were matching. They are the two suites the manifest cannot help with: it proves a gate is still run, not that a test asserting on how CI spells a command was updated with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ * fix(ci): credit gates by execution shape, and keep every guard Three ways the manifest could report a gate as owned when it does not run. 1. Crediting was a substring scan over `run:`, which #1429 explicitly rules out — "do not infer reachability from a command name merely appearing in workflow text". `false && pnpm gate x`, a gate inside `if false; then … fi`, one named in a heredoc, and `echo pnpm gate x` all credited it. There is a live instance: conformance-regenerate.yml's "Fail if regeneration changed anything" step names `pnpm gate maestro-regenerate` inside an error message telling a human to run it, and that credited the gate. A gate now counts only as the first command segment of a line, and a body carrying shell structure earns nothing. Reachability inside a script is not decidable, so this does not try: unrecognised shape means no credit and the check reports unowned. `VAR=$(pnpm gate x …)` is read, since the assignment form is unambiguous and the gate runs. 2. Job-level `if:` was not modelled at all, though six live jobs carry one, so a job that cannot run still credited every gate inside it. Two conditions on the mutation lanes are now declared. 3. A caller's `if:` REPLACED the guard on a nested composite-action step (`guard[0] ?? step.condition`), so an outer `always()` erased an inner `if: false`. Steps carry every guard between the lane and the step. Also corrects two source comments that still claimed project code run outside the runner fails the manifest. It does not: such a step earns no credit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ * ci: add the run-gate action that names a gate structurally The seam the ownership proof will read instead of shell. A lane says which gate it runs in `with.gate`, a typed input the manifest reads straight out of the YAML and validates against CHECK_CATALOG. Nothing here is wired yet — the ~60 call sites and the model change follow. Added first so the target of that conversion is reviewable on its own. `args` cannot select which gate runs; it is appended after the id, so the worst a wrong value does is fail the gate it already named. There is no `|| true` and no output capture: the gate's exit code is the step's exit code, so a gate cannot run without being able to fail its lane. Part of #1429. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ * merge: main (#1770) and route its three new steps through the runner #1770 landed the orphan-check fix on main, wiring `check:tmpdir-leaks`, `check:tmpdir-leaks:test` and `test:fixture-cache` into Coverage, Layering Guard and Integration Tests. This branch had wired the same three through `pnpm gate`, so the merge produced two steps per check rather than a conflict — each check ran twice. Kept main's steps, with the placement and reasoning reviewed on #1770, and changed only their `run:` line to the canonical runner. Dropped this branch's duplicates. Net effect on CI is unchanged: the same three checks, in the same three lanes, once each. Gate manifest green after the merge: 47 checks wired across 33 lanes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ * fix(ci): address review — suite detection, freerange, glob, vacuous skip-list Six review findings plus the mutation blocker. [bug] `registered` was shape-only, so a `test:*` script running `node src/bin.ts test <dir>` resolved to a `script:` leaf and was invisible. Four `test:replay:*` scripts were owned only because someone hand-registered them; `test:replay:android` was neither registered nor reported while the nightly ran the same six .ad files by inlining them. A `test:*` script is now a suite by name. `replay-android` is registered, and the nightly runs the script instead of re-listing its files so the two cannot drift. The nightly invokes it inside `reactivecircus/android-emulator-runner`'s `script:` input — shell handed to a third-party action this loader does not read — so the suite executes but cannot be credited. Recorded in UNPROVABLE_OWNERS with that exact reason rather than assumed. The fixed detector also found a second orphan the review did not name: `test:integration:progress`. That one is a reporter whose `--check` sibling is the registered gate, so it is declared in REPORTING_SCRIPTS — a declaration that itself fails when inert. [bug] `freerange` defaulted to localRunnable, so fail-open ran `fr` (a Bun binary) on the pre-push path. Now false. [suggestion] The `--run` skip-list asserted `build:android-snapshot-helper`, a name `android-helpers` no longer uses, so it could not fail. Derived from the catalog instead. [suggestion] `matchesGlob` joined `**` splits with `.*`, making the adjacent slash mandatory — GitHub's `**` matches zero directories, so `src/**/*.test.ts` did not match `src/a.test.ts`. Pinned against `packages/*/src/**/*.test.ts`. [suggestion] Deleted the unwired `run-gate` action. It had no callers, was absent from GATE_ACTIONS, and its comment described a system that had not shipped. It returns with the rewiring, not before. [suggestion] Collapsed the module headers that narrated discarded designs. Mutation: `daemon entrypoint publishes HTTP metadata and cleans up on shutdown` is the only test here that spawns a real daemon process. It takes ~1.1s alone but exceeds Vitest's 5s default inside Stryker's dry run, which aborts the sweep before a single mutant runs. Given 30s. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ * fix(mutation): order sandbox aliases longest-first so subpaths resolve Every shard of the mutation sweep aborted in Stryker's dry run with: Cannot find package '@agent-device/selectors/engine' imported from .tmp/stryker/sandbox-*/src/core/selector-pipeline.ts The alias was generated correctly; it just never won. Vite matches a STRING alias by prefix and takes the first hit, and `workspaceSpecifierTargets` emitted the bare `@agent-device/selectors` ahead of the subpath entries. The bare entry therefore captured `@agent-device/selectors/engine` and rewrote it to `…/src/index.ts/engine`, which does not exist; Node fell back to real package resolution, could not find the subpath inside the sandbox, and the dry run failed before a single mutant ran — so the shard uploaded an empty envelope instead of a report and the ratchet failed for want of one. Sorting longest specifier first makes the most specific alias win: @agent-device/selectors/engine -> packages/selectors/src/engine.ts @agent-device/selectors/ast -> packages/selectors/src/ast.ts @agent-device/selectors -> packages/selectors/src/index.ts `/ast` never tripped this because nothing in a related test set imported it; `selector-pipeline.ts` introduced the first subpath import that mattered (#1744), so the mutation lane has been unable to run since that landed. Any PR touching `scripts/mutation/**` — which fails open into the full sweep — would have hit it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ * refactor: derive gate ownership from workflow structure * fix: run gates without optional arguments * fix: resolve mutation workspace subpaths exactly --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
c7565cb1f8 |
refactor(snapshot): clean snapshot ownership (#1754)
* refactor(snapshot): clean snapshot ownership * fix(snapshot): address ownership review feedback |
||
|
|
07d528086b |
fix: harden iOS alert smoke scenario (#1767)
* fix: harden iOS alert smoke scenario * fix: address iOS alert smoke review feedback * fix: present fixture alert after React commit |
||
|
|
85aef7af75 |
fix(ios): make custom-action reads atomically single-flight (#1749)
* fix(ios): relax custom-action refusal timing assertion * fix(ios): make custom-action admission atomic |
||
|
|
15133b587f |
fix: validate macOS recording finalization (#1732)
* fix: validate macOS recording finalization * fix: preserve local macOS recording ownership * fix: resolve recording teardown session keys |
||
|
|
3eda37b0e0 |
fix(ci): run the three checks no workflow was running (#1770)
`check:tmpdir-leaks`, `check:tmpdir-leaks:test` and `test:fixture-cache` are real package scripts with real assertions that no CI lane executed. All three are reachable only through `check:unit`, an aggregate no workflow invokes, so a regression in any of them could not fail a PR — and a check that silently stops running looks exactly like a green build. Verified by transitive script reachability over every workflow and composite action on main: zero references to any of the three anywhere in .github. Placement: - `check:tmpdir-leaks` joins Coverage, the lane whose instrumented suite is what would leak a run directory. - `check:tmpdir-leaks:test` joins Layering Guard rather than sitting next to the check it covers. vitest-tmpdir-global-setup.test.ts proves the lifecycle by spawning a real nested `vitest run`, and Coverage already loses runs to worker-fork teardown errors; starting a nested Vitest beside the full instrumented suite is a contention risk with nothing to gain. - `test:fixture-cache` joins Integration Tests, beside the fixture-app fallback smoke that covers the same artifact contract. All three pass at this commit, so this wires green gates rather than red ones. `check:tmpdir-leaks` was also shown non-vacuous: planting an abandoned agent-device-test-run-* directory makes it fail and name the directory. Part of #1429, which stays open for the ownership proof that would have caught this class automatically. Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
f98814ff10 |
fix: preserve recording evidence across fault boundaries (#1734)
* fix(recording): preserve native evidence across fault boundaries * fix(android): retire evidence after active publish rollback |
||
|
|
c9e6df1d6f |
test(cli-help): gate every help example against the CLI schema (#1766)
Help is the agent-facing command contract, and agents copy its example lines verbatim, so an example the parser rejects is a broken command surface. `help react-native` advertised `open --metro-host/--metro-port` from 0.16.x until `open` gained the flags, and the same drift was still live in `help macos`, which showed `snapshot -i --platform macos --surface menubar` even though `--surface` is an `open`-only flag the session then keeps. Add a conformance test that reads every example back out of the rendered help (root usage, every topic, every command) and runs it through the production parser plus the command's CLI reader. Lines carrying placeholder syntax in an unquoted token are synopsis shapes and are skipped; everything else must parse. Fix the macOS menu-bar example and reword the two prose lines that opened with `agent-device ` but were not commands, so the rule stays total. |
||
|
|
b990271580 |
fix(ios): use current devicectl capture-screenshot syntax for physical devices (#1769)
The direct-capture fallback ran `devicectl device screenshot --device <id> <path>`, which current Xcode devicectl rejects with "Unknown option '--device'", forcing every physical-device screenshot into the slower XCTest runner path. Xcode's devicectl now requires `devicectl device capture screenshot --device <id> --destination <path>`. Fixes #1760 |
||
|
|
eabc936a0f |
refactor: route apps through request runtime (#1756)
* refactor: route apps through request runtime * test: remove stale apps adapter mock * fix: clean apps runtime replay artifacts * fix: remove stale runtime test exports * refactor: simplify apps runtime admission * test: exercise runtime use through facade * fix: close apps runtime admission gaps * fix: disambiguate doctor app inventory callback * fix: keep capability fixture below complexity limit * fix: keep HarmonyOS app inventory fail-closed * test: type HarmonyOS app admission fixture * fix: restore HarmonyOS app inventory parity * test: align HarmonyOS readiness fixture * fix: preserve HarmonyOS doctor app parity * test: type HarmonyOS doctor fixture * fix: isolate HarmonyOS doctor policy * fix: align apps cutover with shared rule catalog |
||
|
|
b13c06338e |
refactor: route boot through readiness runtime (#1747)
* refactor: route boot through readiness runtime * fix: separate boot admission from readiness * fix: register boot cutover policy * refactor(runtime): move readiness into platform owners * fix(test): tolerate provider temp cleanup races |
||
|
|
ba48146520 |
refactor(layering): one parametrized runtime-command-cutover gate (ADR 0019 §8) (#1745)
* refactor(layering): fold the four cutover policies into one parametrized gate ADR 0019 §8: the per-command cutover gates consolidate into one parametrized runtime-command-cutover gate driven by a table of migrated commands. Adding a migrated command adds a row; the mechanism carries one planted-red proof instead of one per command. Part of #1739 (wave 0) * fix(layering): scope cutover calls to lexical owners * chore: format cutover ownership model |
||
|
|
8f98d23f14 |
refactor(layering): give each colliding rule id its own number (#1750)
* refactor(layering): give each colliding rule id its own number
R11 and R13 each named two unrelated rules. report() groups violations by the
rule string and titles every annotation `Layering drift (${rule})`, so a shared
number made the guard's output ambiguous about which rule fired.
Reference counts decided which rule keeps its number. R11 package-boundaries is
named in ~30 places (CONTEXT.md, ADR 0019, testing.md, the mutation and
affected-check configs, four package source comments, its own tests) against one
for the contracts rule; R13 platform-package-substrate is the RULE in three
policy files plus CONTEXT.md, ADR 0019 and model.ts against two for the devices
cutover. Both keepers stay put and the two newest rules move up:
R11 contracts-implementation-authority -> R18
R13 device-inventory-cutover -> R17
R17/R18 follow the namespace's order-of-addition convention (R14 #1701 < R15
#1702 < R16 #1724): device-inventory-cutover landed in #1699 and
contracts-implementation-authority in #1701. #1656 took R19 for
selector-pipeline-ownership on the same reading.
The rule-map header in check.ts is renumbered and reordered back into numeric
order, and gains the R18 entry the contracts rule never had -- without it a
reader looking up an R18 violation finds nothing where they used to find the
wrong rule. deviceInventoryCutoverSummary() was also the only OK-line summary
not leading with its rule number, which is what made the number unreadable from
the success line in the first place.
Also corrects a normative ADR reference. ADR 0019's platform-package import
rules -- contracts-to-platform, sibling-platform, root/daemon, raw-process --
are R13's, as CONTEXT.md:420 already says. The R11 attribution predates
platform-package-policy (#1697, a day before #1699), when R11 was the only
package rule.
* chore(layering): retire the expired R11/R13 collision allowances
KNOWN_RULE_ID_COLLISIONS was opened for exactly the two collisions the previous
commit renames apart, and ruleIdCollisionFailures expires an allowance on
contact: once the collision is gone the entry fails as stale, because a list
still naming it would wave it back through if anyone reintroduced it.
Both entries are therefore deleted in the change that removes the collisions,
leaving the empty list that admits nothing. The namespace is now one-to-one
across R2-R19.
|
||
|
|
7f5dbd2e50 |
chore: drive unused production exports to zero (#1743)
* chore: drive unused production exports to zero `pnpm check:production-exports` has been failing on main with 21 findings. Each was investigated rather than blanket-suppressed; they split three ways. Genuinely dead, deleted: - `androidDeviceForSerial` (android/adb.ts) had zero references anywhere, tests included. - `streamAndroidLogcatWithAdb` (android/logcat.ts) had no production consumer and only a guard-clause test; its `captureAndroidLogcatWithAdb` sibling is the published SDK surface. Removed with its options type and test. Test-only aliases over live siblings, collapsed: - app-log-resource-store re-exposed four bound store methods; production used only `resolvePath`, tests used the other three. The sibling screen-recording-resource-store exports just the store, so this now matches: one export, all consumers call `appLogResourceStore.x`. - device-claims re-exported `canonicalLocalDeviceKey` for a single test, while production imports it from device-claim-paths directly. Dropped the re-export and pointed the test at the canonical module. Real consumers the analysis cannot see, exempted with the reason: - The nine remaining `src/cli/commands/*Command` handlers are reached only through `dedicatedCliCommandHandlerLoaders`, the dynamic import() table in router.ts. `deviceCommand` already carried an inline suppression for exactly this; replaced it with one config entry naming the table that enumerates all ten, matching the existing daemon route-handler entry. - `resolveVitestMaxWorkers` (vitest.config.ts), `DEVICE_CLAIM_IN_USE_SAMPLE` (bench sample producers) and the capture-kit `createAppLogLiveHandle` facade export joined the existing entries that already record their exact shape. - `**/*.fixtures.ts` is now an ignorePattern: all 15 build doubles for co-located tests, several import `vi`, and none is imported by production source. Pattern-matching them as test infrastructure also keeps this class of finding from recurring. Gate now reports zero. Unit suite, layering, fallow audit, MCP metadata, build, bundle-owner and package checks all pass. * chore: scope the fixture exemption to unused exports Review feedback on #1743: `ignorePatterns` removes a file from every Fallow mode and rule, but the false positive here is only production-unused-exports. Moved *.fixtures.ts to an ignoreExports entry so fixtures stay inside health, dead-code and cycle analysis. Kept `exports: ["*"]` rather than today's three symbols because the property is per-file — no fixture module has a production consumer — so a new fixture symbol should not reopen the finding. check:production-exports still reports zero, and a full `fallow --summary` returns identical totals (2 dead-code / 4 dupes / 123 health) with and without the change, so nothing was newly surfaced or newly hidden. * test(fallow): prove fixture policy scope |