* fix(device): state when nothing is claimed and reap dead claims
Two defects found while asking which agent held a connected iPhone.
device status printed only "21 stale claims hidden" and no verdict: the
"No local advisory device claims found" line was gated on there being zero
stale claims too, so the one case where a user most needs to hear that nothing
holds the device is exactly the case that never said it. It now reports the
empty live set alongside the hidden-stale notice.
Nothing ever reaped claims whose owner died abruptly. Claims are released on
session close and daemon shutdown, but a killed process leaves its file
behind, and a real store had accumulated 21 of them spanning two weeks, every
owner dead. Daemon startup now prunes them, next to the existing web-browser
orphan cleanup.
Pruning is deliberately narrower than the CLI's stale filter: it removes only
owner-process-dead claims. owner-state-dir-gone describes a LIVE process whose
state dir vanished, and deleting that claim could hand its device to a second
session.
* fix(device): prune under the claim lock and record its diagnostics
Two review findings on the startup prune.
The scan classified a claim dead and then unlinked it, but claim paths are
derived from the device key: a concurrent daemon can prune the same dead claim
while a new session writes its live successor to that exact path, and the
unlink would take the successor with it. Liveness and owner token are now
re-checked while holding the per-device claim lock, matching what
clearAdvisoryDeviceClaim already does, and a file whose name is not the
canonical path for the key it contains is left alone.
Daemon startup runs outside any diagnostics scope, where emitDiagnostic returns
without recording, so neither a successful prune nor a failure produced the
promised event. The prune now opens its own scope and flushes, the way
emitFatalDiagnostic does.
* fix(device): prune after daemon info is published so its log survives
publishDaemonInfo truncates daemon.log, so the prune's diagnostic was written
and then wiped: every successful startup left no device_claim_prune event in
the log users are pointed at. The prune now runs after publication.
The regression starts the real runtime and reads the resulting daemon.log,
because the ordering is the bug — a test around the prune alone passes either
way.
usbmuxd answers Connect with a result code, and every non-zero code collapsed
into one 'Failed to connect' error carrying a cable hint. Probing the daemon on
this host for the two codes that matter: an unknown DeviceID answers 2, and a
closed port on an attached device answers 3.
Result 2 means the device went away between ListDevices and Connect. It now
raises the same unattached verdict as a missing ListDevices entry, so a
CoreDevice device falls back to its network tunnel instead of failing with a
cable hint while Wi-Fi is available — the gap #1517 left open.
Result 3 means the device is reachable and only the runner port is not bound
yet, which is the normal state while the runner starts. Telling the user to
check the cable was wrong; it now says so.
Adds hermetic multi-device coverage for #1521: selection follows the UDID
rather than list position, and a UDID sharing a prefix with another device
never matches.
* feat(ios): reach physical devices through usbmux first, tunnel as fallback
Physical iOS runner commands now resolve to usbmux whenever the device is
attached by cable, and fall back to the CoreDevice tunnel route only when
usbmuxd reports it unattached (#1403).
Measured on an iPhone 17 Pro: steady-state is a wash between the two routes,
but past the tunnel cache's 30s TTL the network route pays ~4.5s of devicectl
re-probe plus session re-establish on the next command, where the usbmux
session stays hot at ~440ms. Cabled devices now never pay that tax, because
the tunnel lookup, its cache, and the cache invalidation only run on the
fallback path.
Wi-Fi-only devices keep working: modern CoreDevice Wi-Fi runs over remoted and
never appears in usbmuxd, so the unattached verdict routes them to the tunnel.
That verdict is carried by usbmuxDeviceAttached:false and answered inside the
same connect attempt rather than by burning a retry, so an XCTest-backed
device — which has no tunnel — now surfaces its cable/trust/unlock hint
instead of retrying for the full budget.
Replaces the AGENT_DEVICE_IOS_RUNNER_ROUTE experiment override from #1510.
* fix(ios): make the unattached usbmux verdict terminal for xctest devices
waitForRunner recorded the unattached verdict as a generic connect failure and
retried it for the whole budget, so readiness preflight and read-only commands
on an XCTest device still burned 2x45s and lost the cable/trust/unlock hint —
the exact hang #1510 measured, which this PR claimed to fix but only fixed on
the sendRunnerCommandOnce path.
Retrying cannot attach a cable and an XCTest device has no tunnel to fall back
to, so the typed verdict is now thrown from the attempt, excluded from the
connect retry policy, and passed through waitForRunner unwrapped. The predicate
moved to runner-contract.ts because the retry policy needs it and importing the
transport there would close an import cycle.
`emitReplayTestRetryProgress` publishes `hint` on every retried failure
(session-test-attempt.ts), and nothing asserted it. The existing hint coverage
coming out of #1505 pins only the final-attempt emit site, so deleting the
retry line left the entire suite green.
The retry fixture's first-attempt error carried no hint, so no assertion could
have caught it even in principle. It now carries one, and the retry result
value asserts it.
Counterfactual, run rather than assumed — with `hint: attempt.outcome.error.hint`
deleted from the retry emit site:
✓ a reporter sees the shipped suite-start, skip, and test-start values
× reporter step and result sessions track the running attempt, not the start value
✓ a failing suite reaches the reporter with the failure message, hint fields, and exit code
✓ sharded runs give the reporter shard-scoped sessions and device identity
- "hint": "retry hint from the failed attempt",
+ "hint": undefined,
Only the new pin fails; the final-path hint test keeps passing, which is the
gap. Production code is unchanged — `git diff` on session-test-attempt.ts is
empty.
This lands before P3b relocates session-test-attempt.ts into
packages/replay-test, so the move cannot drop the field silently.
Refs #1478
Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8
Co-authored-by: Claude <noreply@anthropic.com>
A content verdict (system-window-only, content-poor-app-window,
empty-helper-output) means the capture mechanism worked but sampled the
screen mid-transition. That state resolves itself within a frame or two,
yet a single sample turned it into a hard command failure: the CI alert
dismiss in #1510's run failed because one capture landed while no
application window was attached.
Only polling waits rode these verdicts out (isUnreadableCaptureContentError);
every other command failed on the first mistimed sample. Re-capture a bounded
number of times before reporting the verdict, so the loud failure is preserved
for a screen that stays unreadable while a single mistimed sample no longer
fails a command.
`assertElementTextAfterScrolling` waited 1000ms per attempt. The wait's whole
budget goes to its first capture, so that budget has to cover one snapshot of
the current surface; on a loaded simulator it does not, and every attempt fails
with `wait_capture_stalled` instead of reporting the element as off-screen.
This broke the iOS Smoke Tests lane on main. #1484 moved the smoke automation
scenario onto this helper for `automation-press` and `automation-longpress`;
before that it was only reached from the full lane, so the tight budget went
unnoticed. Every sibling wait in the same scenario already budgets 2500ms or
more (`label="Settings"` 10000, `alert wait` 5000, `wait text` 2500).
Two changes:
- Raise the per-attempt budget to 2500ms, matching the nearest sibling.
- Stop spending a scroll attempt on a stalled capture. A stall means the
snapshot never came back, so the surface was never read — it is not evidence
the element is off-screen, and scrolling on it moves the surface for an
unrelated reason. Two stall retries absorb a slow runner; a genuine absence
still consumes attempts and still fails.
The final assertion now carries the last wait's JSON, so a future failure says
whether it stalled or genuinely never found the element.
Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8
Co-authored-by: Claude <noreply@anthropic.com>
* refactor(replay-test): neutralize the values crossing the scheduler seam
#1478 P3, part 1 of 2. Prepares the replay-test extraction by removing every
non-neutral value that crosses the scheduler seam, in place under `src/`, so the
physical move to `packages/replay-test` is a file move rather than a redesign.
`DaemonResponse` no longer crosses the seam. `session-test-types.ts` typed
`runReplay`/`finalizeAttempt` as returning a daemon response and the scheduler read
`.error.code`, `.error.details`, and `.data.replayed/.healed/.warnings/
.snapshotDiagnostics` off it throughout. That is invisible to R10 today only
because `checkDaemonTypesImporters` skips `src/daemon/`; once the files live in a
package they become external `daemon/types.ts` importers, which the ratchet only
lets shrink. Attempts now resolve as tagged `ReplayTestAttemptOutcome` values
carrying exactly what the scheduler consumes, including an `infrastructure` tag —
classifying an environmental failure needs platform boot-diagnostic vocabulary the
scheduler must not import, so the host decides and the scheduler reads the verdict.
`session-test-outcome.ts` is the one place a daemon response becomes an outcome.
Step events get a narrow per-attempt port. They were emitted from
`session-replay-runtime.ts` and `session-replay-maestro-observer.ts`, both reading
a request-global `AsyncLocalStorage` seeded per attempt. The scheduler now hands
each attempt an `onStep` sink, threaded the way `tracePath` already is; both
engines call it and `withReplayTestActionProgress`/`readReplayTestActionProgress`
are gone. A direct `replay` simply has no sink.
ADR 0012 divergence becomes a neutral leaf. `src/replay/divergence.ts` depended
only on kernel contracts and redaction, yet Maestro constructs divergences too and
CLI/MCP both render them, so P5 could not have moved it into `packages/ad-replay`.
It is now `@agent-device/contracts/divergence`; the renderer's output text is
unchanged.
The progress wire vocabulary moves to `@agent-device/contracts/progress`. It is
serialized by `request-progress-protocol.ts` and reconstructed by the CLI reporter
path, so it belongs below both; `src/request/progress.ts` keeps only the sink and
its AsyncLocalStorage binding.
Together these clear all four of replay-test's recorded R10 migration imports, so
the rule now enforces unconditionally for that module.
Behavior is unchanged. The shipped reporter contract — export spellings,
object/factory loading, hook names, timing/order, value fields, the synchronous
live-hook rule, awaited suite completion, error handling, exit codes — is
untouched, and `session-test-reporter-values.test.ts` passes unmodified. The
`--shard-all` `total`/`runnable` asymmetry is preserved as characterized.
Refs #1478
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8
* test(replay-test): pin the Maestro reporter step path against the onStep port
Review finding on #1509: the native `.ad` reporter ratchet exercises only one of
the two `onStep` forwarding chains, so deleting a link in the Maestro chain would
silently stop `onTestStep` for every `test --maestro` run while every existing
reporter test stayed green. Same defect class as the dropped diagnosticId/logPath
(#1501) and the dropped reporter `hint` (#1505).
Maestro is one of P3's two required real adapters and its chain shares no links
with the native one below `runReplayScriptFile`:
scheduler sink -> runReplayScriptFile -> runTypedMaestroReplayFile
-> createMaestroReplayObserver({ onStep }) -> actionStarted -> onStep
Adds a Maestro scenario driving `test --maestro` through the real session handler
and the real reporter registry. It asserts the step payload the engine produces
(`stepIndex`/`stepTotal`/`stepCommand`/`stepValue`, including that a value-less
command stays value-less) together with the attempt/session identity the scheduler
supplies, since that half of the event came from request-global AsyncLocalStorage
before P3. A second case drives a retry so step events must carry attempt-1's
session and then attempt-2's. The flow `name` also pins the reporter `title`, a
value only the Maestro path can produce.
New file rather than an addition to session-test-reporter-values.test.ts: that file
is the pinned characterization and must keep passing unmodified, and Maestro needs
its own vi.mock of core/dispatch for device resolution.
Counterfactual run, both links, each restored after:
- dropping `onStep` from createMaestroReplayObserver in
session-replay-maestro-runtime.ts
- dropping the emitMaestroStep call from actionStarted in
session-replay-maestro-observer.ts
Each dropped both onTestStep events ("expected [ 'onSuiteStart', 'onTestStart',
…(2) ] to deeply equal [ 'onSuiteStart', 'onTestStart', …(4) ]") and failed both
new cases, while session-test-reporter-values.test.ts passed all 4 — exactly the
hole the reviewer identified.
Test-only; no production change. Bundle output is byte-identical to b339c640f.
Refs #1478
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat(ios): usbmux runner route override + #1403 transport experiment evidence
Adds AGENT_DEVICE_IOS_RUNNER_ROUTE=usbmux, an experimental override that
routes coredevice-backend physical devices' runner commands through usbmux,
and simplifies the xctest branch that awaited a no-op resolveRunnerTransport.
Documents the live #1403 experiment (iPhone 17 Pro, USB + Wi-Fi legs):
steady-state is a wash, but the >30s-idle tax drops from ~4.5s (tunnel
re-probe + session re-establish) to ~440ms because the usbmux session stays
hot; CoreDevice Wi-Fi devices never appear in usbmuxd, so the verdict is
usbmux-primary with network fallback rather than tunnel-code deletion.
Also records the cable-out failure gap (2x45s retry hang swallowing the
usbmux DEVICE_NOT_FOUND hint), which affects today's xctest backend too.
* fix(ios): document daemon scoping of the usbmux route override
The env is re-read per resolve but from the daemon's environment, which is
captured at daemon launch — a later CLI invocation cannot flip the route on
a running daemon. Correct the source comment and experiment doc, and lock
the read-point semantics with a regression test.
android-emulator-runner runs the script with /usr/bin/sh, which is dash on
ubuntu runners and rejects `-o pipefail`:
/usr/bin/sh: 1: set: Illegal option -o pipefail
The Android full emulator suite has therefore failed on every nightly since
it landed in #1484, aborting before the first command. The sibling emulator
scripts in android.yml and perf-nightly.yml already use `set -eu`/`set -e`;
match them. The serial assignment stays guarded by `test -n".
Preparatory slice for P4a (#1478). No aggregate, no transaction type, no
publication-writer migration — those land separately.
Two things:
1. Name the session-script session states in the shared test factories
(`makeAuthoringSession`, `makeRepairArmedSession`,
`makeRepairCompleteSession`) and route 40 inline session literals across
11 test files through them. The `saveScript*` fields are not independent
— recording without a boundary is ordinary authoring, a boundary without
`saveScriptComplete` is an ARMED-but-uncommittable repair, and only the
COMPLETE combination publishes — so re-deriving the combination per test
buried the distinction each test was actually about. Pure refactor: the
factories write today's fields and no assertion was weakened.
2. Delete `saveScriptDefaultedHealedPath`. It had zero production readers:
the writer's refuse-on-exist guard has been uniform since #1235, so the
flag was written in three places and never consulted. Removing it takes
its R7 owner row with it and lowers the R10 baseline from 30/42 to 29/40
(the ratchet fails on a drop too, so this cannot be deferred).
Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8
Co-authored-by: Claude <noreply@anthropic.com>
The capability matrix admitted `viewport` on every Apple simulator and
device while no Apple interactor implements `setViewport`, so the command
was routed to the device and only rejected inside dispatch with the
backend-shaped "viewport is not supported by this backend". Nothing can
serve it there: Apple screen geometry is fixed by the selected device type
and neither simctl nor XCTest exposes a resize primitive, so viewport is a
web-surface contract (its CLI summary and docs already say so).
Deny it in the descriptor's capability facet, matching the adjacent Android
denial, and give the Apple family an unsupported hint that names the surface
that does support it. Admission now fails before the request reaches the
device: "viewport is not supported on this device" with supportedOn: web.
The iOS simulator coverage manifest's known-gap row becomes a
capability-denial row owned by the static coverage test, which retires the
whole known-gap level and the `full:known-gaps` live scenario that existed
only to pin this failure.
Closes#1407
* test: characterize replay-test reporter contract and extend R10
P3 of #1478 moves the replay-test scheduler into `packages/replay-test` and
makes attempt identity scheduler-owned. Before production code moves, pin what
a shipped custom reporter actually observes today, and close the import
boundary the extraction has to end up satisfying.
Reporter values (all pinned as shipped, none proposed):
- the `RequestProgressEvent` -> reporter-value projection field by field,
including key presence for absent optionals and the dropped `command` events;
- `session` provenance across the seam: the start value is always `attempt-1`
(it is built before any attempt runs), step values track the running attempt
through the per-attempt AsyncLocalStorage context, and result values carry the
attempt that produced them, so a retried case reports three different
sessions to one reporter;
- the shard-scoped session prefix and device identity a sharded run reports;
- module export spelling precedence, the six optional hook names, the live-hook
vs final-hook error asymmetry, and exit-code recommendation semantics.
Late-timeout finalization/cleanup:
- finalization always runs before cleanup, and the timing trace records that
order (`finalize_start/stop` then `cleanup_start/stop`);
- a replay settling inside the 2s grace window cleans up once and is not marked
`timeout_cleanup_pending`, and the raced TIMEOUT response still wins;
- a replay that misses the window defers its second cleanup until the abandoned
replay settles, and that late cleanup's failure is swallowed.
R10 now rejects replay-test imports from `src/request/**` and engine internals
(`src/replay/`, `src/compat/`, `src/maestro/`, `src/ad-replay/`), with the four
imports that exist today recorded as shrink-only migration entries so the rule
enforces immediately and the extraction must delete them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8
* test(daemon): assert reporter hint on the real onTestResult value
The failing-suite characterization supplied `hint` as input but never
asserted it on the hook value, so dropping `hint: error.hint` from the
scheduler path left all reporter tests green. Assert it on the real
reporter value so the ratchet catches a shipped field going missing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(daemon): reject unsupported raw --save-script flags at request ingress
`flags.saveScript` arms session-script publication the moment a successful
handler records the request's action, so any recordable command reaching a
handler with the raw flag set could arm publication and write a `.ad`
artifact — `record stop` immediately through record-only cleanup, `trace` and
the interaction commands later on close. The CLI/Node/MCP surfaces only emit
the flag for its released owners, so that arming path was reachable only by a
hand-built wire request or a free-form `batch` step.
Close it at the daemon request seam (`createRequestHandler`), the single entry
point both the socket and HTTP transports funnel through, and ahead of
admission, session lookup, device resolution, and handler dispatch. Ownership
is declared on the command descriptor (`saveScriptFlagOwner`) rather than
re-stated as a string set, so `open`, `close`, and `replay` keep the flag and
everything else is rejected with an ADR 0010 shaped `INVALID_ARGS` carrying a
hint, diagnosticId, and logPath.
With that path pinned closed, a record-only session can no longer carry
`recordSession`, so `record stop`'s immediate `writeSessionLog` could only ever
be a no-op; delete it.
Refs #1478.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8
* test(daemon): assert ADR 0010 diagnostics on save-script rejection
The HTTP test helper projected only code/message/hint, so `diagnosticId`
and `logPath` could regress while the transport tests stayed green.
Preserve both in the helper and assert on each transport that
`diagnosticId` is present, `logPath` is present and exists, and the log
records `save_script_flag_rejected`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8
---------
Co-authored-by: Claude <noreply@anthropic.com>
* refactor: pnpm workspace + @agent-device/kernel pilot (#1490 W0)
Extend the workspace with packages/* and move the kernel behind an
enforced public API: packages/kernel with nine consumer-earned subpath
exports (errors, device, snapshot, contracts, collections, rect,
redaction, daemon-error, bounds — the last absorbed from utils as Rect
vocabulary). Every kernel import repo-wide becomes the
@agent-device/kernel/<sub> specifier; kernel tests move to
src/__tests__/kernel/ and exercise the package surface. The root
declares the package in devDependencies (workspace:*), tsdown bundles
it (noExternal) so the published artifact and its runtime dependency
manifest are unchanged.
Gate rewiring in the same change, per the W0 brief:
- R1 kernel-sink retires (physically subsumed); new R11
package-boundaries guards no-root-back-imports, relative tunnelling
past exports maps, undeclared workspace deps, and non-exported
subpaths, with runtime resolution pins via import.meta.resolve.
- resolveImportEdges and mutation ownership follow workspace
specifiers through exports maps, keeping R4 cycle checks, depgraph,
and derived test ownership connected across the seam (kernel-errors
still owns 495 tests). listSourceFiles includes packages/*/src.
- kernel becomes an unranked zone; mutation registry, stryker mutate
globs, and the mutation-affected workflow path filter move to
packages/kernel/src/errors.ts.
- check:affected gains packages/ ownership (manifests fail open);
vitest and coverage include packages/*/src; fallow ignores
packages/** (its resolver cannot follow workspace specifiers).
- The affected-selector CI job installs dependencies: its closure now
crosses workspace specifiers, and the R8 relative exception is
unsafe for production src files (Node ESM does not realpath, so dual
specifier/relative loads would instantiate modules twice). The R8
zero-dep set is pinned empty with that rationale.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep
* fix: address W0 review — mutation sandbox, exports-map resolution, tsc -b
Review findings on #1494, all five:
1. contracts-schema-public.test.ts reads the kernel source at its
packages/ path (fs access invisible to the codemod and typecheck).
2. Mutation lane: Stryker sandboxes the tree but pnpm's node_modules
symlink resolves @agent-device/* back to the real repo, so mutants
in the sandbox never load and vitest.related finds no tests.
vitest.mutation.config.ts now aliases each EXPORTED specifier to
its source (derived from exports maps, never a wildcard), keeping
resolution inside the mutated tree. Validated: kernel-errors module
runs end to end (dry run 3,984 tests, mutants killed, exit 0).
3. Layering/depgraph resolve workspace specifiers through the
exports-derived map (workspaceSpecifierTargets) instead of
reconstructing paths, so '.'-facade packages resolve; the
positional fallback remains only for map-less fixtures (P0 pin).
4. Per-package project references implemented: packages/kernel is
composite (emitDeclarationOnly -> dist-types, gitignored), the root
references it, and typecheck becomes tsc -b — probed to catch type
errors on both sides under TypeScript 7 native.
5. R11's relative-route exception now requires membership in an actual
R8 zero-dep job closure (zeroDepClosureFiles walks entries), not
mere scripts/ placement — closing the dual-instantiation bypass.
Also from review discussion: daemon-error moves out of the kernel
package to src/client/ — its consumers (cli, client facade) rehydrate
wire DaemonErrors client-side; the daemon only produces them. Kernel
drops to 8 exported subpaths before any of them ship.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep
* refactor: one exports-map reader for mutation alias and ownership
Fallow flagged workspaceExportAliases (cognitive 15, CRAP 90). The
manifest-reading logic already exists as workspaceSpecifierTargets in
scripts/layering/package-boundaries.ts, so both the Stryker sandbox
alias table and the mutation ownership walker now consume it instead
of carrying near-clones. Behavior unchanged; mutation suite 45/45 and
changed-code fallow green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep
* fix: composite kernel without a root references edge
FreeRange runs plain `tsc -p tsconfig.json`, and a root `references`
entry makes non-build-mode TypeScript demand the referenced project's
built declarations (TS6305) — a standing "build first" tax on every
plain -p consumer (fr, editors). Keep the per-package composite
project and build it in typecheck (`tsc -b packages/kernel` before the
root and examples/sdk passes), but drop the root references edge: root
consumption resolves through exports to source, identical to runtime
and to the bundler. Probed: plain -p green with no prebuilt output;
kernel-side type errors still caught by its own build.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep
* fix: R11 uses the layering parser; mutation config is a fallow entry
Review blockers on #1494:
- R11's private single-quote regex could miss a double-quoted or
re-export route into packages/*/src. specifierSites now delegates to
the layering model's parseImports (both quote styles, side-effect
imports, re-exports, dynamic imports), with direct regressions for
each formerly-invisible form.
- vitest.mutation.config.ts becomes a declared fallow entry instead of
a tolerated unused-file finding: the full-repo audit now reports it
reachable (unused files 2 -> 1; the remainder predates this PR).
FreeRange clean-checkout evidence: with packages/kernel/dist-types and
every *.tsbuildinfo deleted, `pnpm check:freerange` reports 0 findings
on this head — the TS6305 topology died with the root references edge
in the previous commit; check:freerange has no build precondition.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: publish replay refs after response projection
* fix: retain replay device lock through finalization
* fix: verify Apple text wait misses with snapshot
* fix: lock replay device before open effects
* fix: lock replay device before open preparation
W-pre of #1490: kernel's file set becomes its public API before it
moves to packages/. Three files drifted into the sink because R1
pushes shared code down, not because they are foundational:
- audio-probe-support -> src/contracts/ (capability policy consumed by
platform plugins and interactor registration)
- input-validation -> src/contracts/ (lowest-rank consumer is
contracts/gesture-input)
- observability-redaction -> src/commands/observability/runtime/
(colocated with its only consumer, diagnostics-format)
Pure moves; no behavior change. Kernel retains errors, device,
snapshot, contracts, collections, rect, redaction, daemon-error.
Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep
Co-authored-by: Claude <noreply@anthropic.com>
* test: add scroll-edge-state suite, close mutation gap (#1455)
scroll-edge-state.ts had no dedicated test file and scored 28.75%
mutation (92/320) — the worst of the five tracked decision kernels.
Adds src/utils/scroll-edge-state.test.ts covering every exported
function's decision branches (captureScrollEdgeState,
runScrollEdgePasses, formatScrollEdgeMessage) plus the private
container-selection/scope/signature logic they exercise. Raises the
scoped mutation score to 95.31% (305/320); the 15 remaining survivors
are named, justified-equivalent mutants (documented in the PR).
* refactor(test): split scroll-edge-state suite, drop signature test-gaming
Splits the 1,323-line scroll-edge-state.test.ts into three behavior-focused
sibling suites (container selection, scope naming, pass orchestration) plus a
shared fixtures module, per AGENTS.md's per-file LOC tripwire and sibling-test
convention.
Also removes the dead `signature`/buildScrollStateSignature/collectSubtreeNodes/
hasAncestor production code: it had zero consumers anywhere in the repo, and
the tests pinning its private string format were the mutation-score-gaming
flagged in review — with it gone, there's nothing left to game.
Addresses review feedback on PR #1474 (#1455).
* refactor(test): split scroll-edge-state suite further, tighten unreachable branch
Splits container-selection.test.ts (562 LOC, over the 500-LOC extract
tripwire) into four narrower suites by durable question: container
existence, hidden-edge decisions, target resolution, and broad selection.
Adds a scrollNode() fixture builder to cut inline node-literal repetition
across the suite.
Removes the "captureNodes resolves to undefined" test: the real contract
(both production call sites, and captureScrollEdgeState's own captureNodes
type) always resolves to a real array, so this exercised an unreachable
branch behind a type-unsound cast. Tightens analyzeScrollEdgeState's
parameter type to drop the now-provably-unreachable `| undefined`.
Simplifies buildScrollContainerScope to filter out non-string
identifier/label fields instead of mapping them to an empty-string
placeholder — behaviorally identical (the placeholder always failed
isUsefulScope's length check anyway) but removes a StringLiteral mutation
surface that a synthetic fallback could never legitimately kill without
coupling a test to Stryker's specific replacement text.
Addresses further review feedback on PR #1474 (#1455).
* test(ci): single-retry policy for enumerated contention-flaky files
* fix: satisfy fallow
* test(ci): read failures through a lane reporter so timeouts stay distinguishable
* test(ci): cover the lane reporter and drop its duplicated boilerplate
* chore(fallow): own the retry lane's tool-loaded export seams
* test(ci): block retries on non-test failures and classify timeouts structurally
* test(ci): decide retry eligibility from runner metadata and route gate verdicts through blockers
* refactor(ci): name the retry policy's rules in code instead of comments
* test(ci): mark runner-aborted timeouts inside the runner instead of inferring them
* test(ci): make timeout provenance a per-run secret, not a writable flag
Cover direct task.meta mutation in the real child-Vitest fixture gate.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* test(ci): retry the failed files in the first run's project and coverage modes
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix: drop deleted repo-health file from the retry list after #1480
Rebase onto main post-#1480: the SkillGym/repo-health descope deleted
scripts/repo-health/run.test.ts, whose CONTENTION_RETRY_FILES entry
would now fail this PR's own missing-file check, and inlined the
slow-test budgets into the reporter, resolving the budgets-module
import. Envelope comments now point at scripts/lib/lane-envelope.ts
instead of the closed#1430.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep
---------
Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
* chore: drop SkillGym and the repo-health aggregator (#1412 descope)
Remove the SkillGym harness (test/skillgym/), its check-affected lane,
package scripts, and devDependency — the help-conformance bench is now
the single non-gating small-model oracle. skills/ markdown classifies
as docs in the affected-check selector instead of failing open.
Remove scripts/repo-health: its only gating assertion duplicated the
Layering Guard job, its case-count metric imported the deleted SkillGym
suite, and its sole planned consumer (#1424 / PR #1477) was closed with
the Track C descope on #1412.
Verified: check-affected node --test suites, oxfmt, oxlint, tsc,
check:layering, fallow audit vs origin/main, and the full unit suite
(unit-core + subprocess-stub) all pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep
* fix(scripts): fold slow-test budgets into the reporter for production-exports
The Fallow production-exports gate flagged all three budget exports:
their in-file consumer (SLOW_TEST_RATCHET) and the repo-health entry
point that kept the module reachable were both removed in the descope,
leaving the config-loaded reporter as the only consumer — invisible to
--production analysis. The data-only module's second consumer is gone,
so per the boundaries-are-earned norm the constants move into the
reporter instead of gaining a suppression.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep
* docs: align skills/ format policy and purge last SkillGym mention
Address both P2 review findings on #1480: the testing-matrix row and
the selector's formatGate both still claimed oxfmt covers skills/,
while selectChecks classifies skills/*.md docs-only (oxfmt ignores
**/*.md, so the claim was a no-op even before). The matrix now states
the docs-only policy and formatGate drops the dead underSkills fact.
The merged examples/README.md index (from #1469) loses its skillgym
mention.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat(examples): add runnable Node.js SDK examples under examples/sdk/
examples/test-app is a fixture and the repo's only prior examples/
content; the real SDK usage patterns lived only in
website/docs/docs/client-api.md with no runnable script anywhere.
Adds four standalone, typechecked examples covering the minimum surface
from #1463: root client session (create -> open -> snapshot/tap ->
close with typed error handling), agent-device/metro
(normalizeBaseUrl/resolveRuntimeTransport), agent-device/contracts
(centerOfRect on a snapshot node), and agent-device/batch (runBatch for
a custom transport). Each imports the published `agent-device/...`
subpaths rather than relative src/ paths.
examples/sdk/tsconfig.json path-maps those subpaths to src/sdk/ so
`pnpm typecheck` (now also run against this tsconfig) checks the
examples in CI without a prior build, workspace link, or publish step.
Running an example for real still resolves `agent-device` as a
self-referencing package after `pnpm build`.
src/__tests__/client-api-examples-drift.test.ts guards the examples
against drifting from client-api.md's subpath API manifest in both
directions, picked up automatically by the existing unit-core vitest
project (no new script or workflow needed).
examples/README.md indexes the new examples and notes that test-app/
remains a fixture, not an example; it is not renamed or moved.
Refs #1463
* fix: address Fallow findings on the new SDK examples
Fallow flagged the four examples/sdk/*.ts files as unused files (not
reachable from any entry point) and three functions as high complexity.
- Register the examples as manual entry points in .fallowrc.json,
matching how other standalone scripts (scripts/patch-xcuitest-runner-icon.ts,
scripts/runner-request-count/run.ts) are already declared.
- Reduce complexity in client-session.ts and contracts-result.ts by
extracting device-resolution/error-reporting and rect-assertion
helpers out of main().
- Reduce complexity in the drift guard's parseSubpathManifest by
splitting bullet-matching and backtick-name extraction into their
own functions.
Verified: pnpm check:fallow --base <PR base sha> now reports no issues,
and pnpm check:tooling / pnpm test:unit stay green.
Refs #1463
* fix: compile client-api.md's actual code snippets, not just its symbol manifest
Addresses review feedback on #1463's drift guard: the existing guard only
parsed the doc's "Public subpath API" bullet manifest and compared imported
symbol names, so a fenced ```ts snippet could drift or stop compiling
without the guard noticing.
Added test/integration/client-api-doc-snippets.test.ts, which extracts every
fenced ```ts block from client-api.md and typechecks it against the real
agent-device/* sources (reusing examples/sdk/tsconfig.json's existing paths
mapping, read via `tsc --showConfig` so there's one source of truth). Free
identifiers that continue a `client`/`snapshot` from an earlier snippet are
stubbed — typed against the real SDK return type, not `any`, so continuation
snippets still get real checking. Lives in the Node integration lane
(test/integration/*.test.ts), not vitest's unit-core: it spawns a real tsc
Program, well past the unit suite's 2.5s budget.
Running this check against the existing doc surfaced real, pre-existing
snippet bugs (unrelated to the new examples), fixed here:
- "sessions.artifacts": `result.cloudArtifacts` accessed without narrowing
the `CloudArtifactsResult | DaemonArtifactsResult` union first.
- "Device cloud sessions": `platform`/`device` were passed into the client
constructor config, which doesn't accept them; moved to the `apps.open()`
call where those fields actually belong.
- "Android ADB providers": the inline `exec` handler had no parameter types,
so it failed under strict/noImplicitAny; annotated with the real
`AndroidAdbExecutorOptions` type.
Two further gaps the check surfaced are pre-existing product/API-surface
questions out of scope for this PR (not the new examples), so they're
allowlisted in KNOWN_DOC_GAPS with comments rather than silently patched:
- "Remote Metro helpers" documents prepareRemoteMetro/reloadRemoteMetro/
stopMetroTunnel/resolveRemoteConfigProfile as public, but none of them are
exported from agent-device/metro or agent-device/remote-config today.
- "Web sessions"/audio probe pass `platform` to `observability.network()`/
`.audio()`, but NetworkOptions/AudioOptions have no `platform` field even
though the CLI's network/audio commands accept `--platform`.
Refs #1463
* fix: close the doc-snippet compiler's stubbing hole and the two suppressed gaps
Addresses the second round of review feedback on #1463's drift guard:
1. stubFreeNamesAndRecompile auto-stubbed every "Cannot find name" as `any`,
so a typo like `cliet.apps.open()` would silently pass on the second
compile. It now only stubs identifiers in an explicit allowlist
(KNOWN_FREE_NAME_STUB_TYPES) — the real SDK-derived continuations
(`client`, `androidClient`, `snapshot`) plus the doc's own invented
host-glue names, each typed precisely rather than loosely. Anything else
is left as a real compile failure. Added a regression test that feeds a
`cliet` typo through the guard and asserts it fails.
2. KNOWN_DOC_GAPS filtered six real compiler errors out of the final
assertion while the test claimed every snippet compiles. Investigated
both and fixed the actual contracts instead of suppressing them:
- `prepareMetroRuntime`/`reloadMetro` (src/metro/client-metro.ts) and
`stopMetroTunnel` (src/metro/metro.ts) already existed and matched the
doc's described workflow almost exactly (same result shape) but were
never re-exported from `agent-device/metro`; same for
`resolveRemoteConfigProfile` and `agent-device/remote-config`. Added
the four exports and fixed the doc's stale function names
(`prepareRemoteMetro`/`reloadRemoteMetro`) and one stale field name
(`profileKey` -> `companionProfileKey` on the prepare call) to match.
- `NetworkOptions`/`AudioOptions` (src/contracts/client-observability.ts)
had no `platform` field even though the CLI's `network`/`audio`
commands accept `--platform` for the same use case, and the client
methods already forward the options object to the daemon generically
(`executeCommand('network'|'audio', options)`) — so this was a type
gap, not a runtime one. Switched both from AgentDeviceRequestOverrides
to DeviceCommandBaseOptions (matching PerfOptions' existing pattern),
closing the gap for real instead of stripping `platform` from the doc.
- The "Android installFromSource()" snippet was missing its
`createAgentDeviceClient` import outright; added it.
KNOWN_DOC_GAPS is gone — every fenced snippet now compiles for real, and
the test's assertion matches what it claims.
3. Switched the raw `execFileSync` calls to `runCmdSync` from
src/utils/exec.ts, per AGENTS.md's process-execution invariant (this is
a .ts integration test, not a packaging fixture that needs to stay
dependency-free).
Refs #1463
* docs: fix stale reloadRemoteMetro() prose reference to reloadMetro()
The prose right after the Remote Metro helpers snippet still named the old
function; the compile guard only checks the fenced snippet, not surrounding
prose, so it didn't catch this leftover from the prior rename.
Refs #1463
---------
Co-authored-by: Claude <noreply@anthropic.com>
* ci: fold single-grep jobs into steps, call named pnpm scripts
- Merge ios-runner-swift-compat and no-test-di-seams (each just
checkout + one rg assertion) into steps of a new static-checks job,
keeping each step's own failure message. Removes two job-scheduling/
checkout overheads and two PR status-check lines.
- Replace the layering-guard job's inlined copies of check:layering and
depgraph:test with the named pnpm scripts, removing the silent-drift
risk between the workflow and package.json.
- Fix the same drift in conformance-regenerate.yml, which inlined
maestro:conformance:regenerate byte-for-byte.
- Leave affected-selector's inline node invocation as-is: R8's zero-dep
closure check (scripts/layering/zero-dep-jobs.ts) finds a job's entry
scripts by matching literal paths in the run: block, so switching to
`pnpm check:affected:test` would zero out its entries and make R8
fail closed. Documented inline why this one stays inlined.
- Leave publish-mcp-registry.yml's sync-mcp-metadata --check alone: that
job never runs the setup-node-pnpm action, so pnpm isn't provisioned
there at all.
Refs #1462
* ci: teach R8 to resolve pnpm script names, drop affected-selector's inline copy
R8's zero-dep-job entry scan matched literal script paths in a run: block,
so a bare `pnpm <script>` invocation found zero entries and R8 failed
closed — the reason affected-selector kept an inline node command instead
of calling pnpm check:affected:test (#1462). zeroDepJobs now also resolves
a pnpm script name against package.json and scans the resolved command for
entry paths, so affected-selector can call the named script like every
other job.
Also replaced the other workflows' inlined copies of named package.json
scripts (test:replay:*, perf, perf:android, maestro:conformance:differential,
check:mcp-metadata, size) with their pnpm names, keeping each job's
CI-specific trailing flags — found via a repo-wide sweep for any run: block
whose text duplicates a scripts entry.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L67kDSTAJwaoLCwANEJFRM
* fix: revert publish-mcp-registry pnpm regression, recurse R8 alias resolution
publish-mcp-registry.yml's job only provisions Node via actions/setup-node,
never the repo's setup-node-pnpm action, so pnpm is never installed there —
the earlier sweep's `pnpm check:mcp-metadata` would have broken the release
path. Reverted to the direct node invocation with a comment explaining why,
matching the PR's own stated rationale for leaving it alone.
zeroDepJobs' pnpm-alias resolution only expanded one level: a resolved
script that itself invoked another named pnpm script had its entries
silently dropped from R8's closure. resolveRunEntries now recurses through
chained aliases with a per-chain visited set, so a nested alias's entries
are found and a cycle stops re-expanding a repeated name instead of
recursing forever. Added coverage for both the chained and cyclic cases.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L67kDSTAJwaoLCwANEJFRM
---------
Co-authored-by: Claude <noreply@anthropic.com>
* obs: add repo-health snapshot command aggregating existing analyzers into one JSON
Adds `pnpm repo-health [--json]`, an offline command that aggregates the
signals this repo already computes into one deterministic JSON snapshot by
reusing each analyzer (depgraph, layering ratchets, coverage json-summary,
size-report, fallow baselines, slow-test ratchet, bench/skillgym registries)
rather than reimplementing any metric.
Carries mandatory v1 provenance (schemaVersion, commit, per-analyzer content
hashes, input provenance) so #1424 can persist history. Component metrics
(instability/abstractness/main-sequence distance) are observatory-only. The
only gating behaviour is the depgraph-vs-layering R6 consistency assertion,
already wired into the Layering Guard job via scripts/depgraph/model.test.ts.
Closes#1423
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* repo-health: hash read artifacts and flag staleness in provenance
Coverage and size are artifacts repo-health reads but does not produce, so
they carry no producing-commit stamp. Previously provenance recorded only
their paths and the current HEAD, so a snapshot taken after a source edit
without rerunning those producers would pair prior metrics with the new SHA —
#1424 would persist a false commit-indexed history entry.
Now each read artifact is bound via artifactProvenance() to its content hash
(so history keys on the bytes the metrics came from, not a commit they may
predate) and an explicit `stale` flag (true when a production source file is
newer than the artifact). The coverage/size analyzer-config hashes are added
to provenance.tool, and the human summary marks stale artifacts. Adds a pure
stale-artifact regression test.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* repo-health: bind artifact freshness to the whole producer input set
The stale check derived freshness only from listSourceFiles() (src/*.ts) and
applied it to both coverage and size. Coverage also depends on tests and vitest
config; size-report.mjs reads package.json and runs npm pack — so a change to a
non-src producer input could leave an old artifact marked stale:false while
provenance recorded the current HEAD, a commit the metrics predate.
Each read artifact now declares its full producer input set (PRODUCER_INPUTS)
and is flagged stale when ANY tracked input in that set is newer than the
artifact (via git ls-files mtimes). Freshness that cannot be proven — an empty
observable input set — is reported stale, never falsely fresh. The input set is
recorded as provenance.inputs.*.producerInputs. Adds isArtifactStale() with a
regression covering a non-src input newer than the artifact and the unprovable
case.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* repo-health: prove artifact freshness from a stamped producer commit; route git through runCmdSync
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---------
Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* test(kernel): cover all six errors.ts exports with table-driven cases (#1456)
Extends errors.test.ts beyond normalizeError/toAppErrorCode:
- AppError construction and NormalizedError/AppError instanceof contract
- asAppError identity/wrap/duck-typing behavior (cross-realm errors,
non-Error throws, fallback code precedence)
- isAgentDeviceError real-instance-only contract
- retriableForErrorCode and defaultHintForCode, table-driven over
KNOWN_APP_ERROR_CODES (the module's own source of truth) rather than a
hand-copied code subset
- normalizeAgentDeviceError delegation to normalizeError
- a cross-cutting composition test asserting the retriable/hint contract
request-router.ts's enrichDaemonError and the CLI's printHumanError
actually rely on
No production changes; mutation re-run and PR to follow.
* test(kernel): pin maybeEnrichCommandFailedMessage gating and regex boundaries (#1456)
Adds targeted cases for branches the table-driven/contract tests didn't
reach: the COMMAND_FAILED/processExitError/string-stderr gating in
maybeEnrichCommandFailedMessage, GENERIC_EXIT_MESSAGE's `-?\d+$` boundary
(negative code, trailing text, non-numeric code, whitespace in the tool
token), and STDERR_NOISE_PREFIX's zero-or-more-whitespace semantics for
both the adb/xcrun/simctl and "error:" groups.
* test(kernel): close remaining mutation gaps in firstStderrLine/detail helpers (#1456)
Each addition here was verified against a hand-applied mutation (matching
Stryker's actual survivor diffs) before landing, confirming it fails on
the mutant and passes on the original:
- firstStderrLine: line-trim-before-prefix-match (a real skip-pattern
line ahead of the leading-whitespace one is required, since
redactDiagnosticData already .trim()s the whole stderr string once
before firstStderrLine ever sees it), and the >200-char truncation
branch (both sides of the boundary: 200 exact vs 201+).
- stringDetail/booleanDetail: reject a wrong-typed diagnosticId/logPath/
hint/retriable instead of surfacing it.
- stripDiagnosticMeta: details drops to undefined once stripping the
known meta keys leaves nothing behind.
* style(kernel): apply oxfmt to errors.test.ts
Fixes the Lint & Format CI failure on PR #1456 — format:check flagged
this file, oxfmt --write resolves it with no behavior change.
* test(kernel): split errors.test.ts into focused sibling suites (#1456)
Per review feedback: the growing errors.test.ts hit 601 lines, past
AGENTS.md's extract-before-500 threshold (tests are not exempt). Splits
along the reviewer's requested seams, moving tests verbatim with no
assertion changes:
- errors-message.test.ts (234 lines) — stderr/message normalization:
maybeEnrichCommandFailedMessage, firstStderrLine, the
GENERIC_EXIT_MESSAGE/STDERR_NOISE_PREFIX regex boundaries.
- errors-metadata.test.ts (157 lines) — hint/diagnosticId/logPath/
retriable/supportedOn lifting and stripping, divergence passthrough,
the stringDetail/booleanDetail/stripDiagnosticMeta type-guard
contracts, and the normalizeError/retriableForErrorCode/
defaultHintForCode cross-cutting contract.
- errors-code-policy.test.ts (228 lines) — AppError construction,
asAppError/isAgentDeviceError duck-typing contract, toAppErrorCode,
the retriableForErrorCode/defaultHintForCode table-driven policy
tests, and normalizeAgentDeviceError.
Also repoints scripts/mutation/ownership.test.ts's two references to
the deleted errors.test.ts path at errors-code-policy.test.ts — the
mutation ownership deriver reads the file from disk to follow its
import graph, so a stale path silently resolves to "owns nothing"
instead of failing loudly.
---------
Co-authored-by: Claude <noreply@anthropic.com>
* test(apple): split apple/core index.test.ts into per-module files (#1459)
apple/core/__tests__/index.test.ts had grown to 3,487 lines / 100 test()
call sites across nine unrelated production modules with zero describe()
blocks, mirroring the aggregation disease android already fixed with its
per-concern split. Cut along the existing import boundaries: apps.test.ts,
interactions.test.ts, simulator.test.ts, physical-device-screenshot.test.ts,
screenshot.test.ts, screenshot-status-bar.test.ts, devicectl.test.ts.
config.ts and runner/runner-client.ts have no dedicated file since no test's
primary subject is one of their own exports (their symbols are only
referenced as constants/mocked dependencies inside other modules' tests).
Move-only: every test body is byte-identical to the original apart from
relocated shared setup. Device fixtures and the xcrun-stub harness used by
2+ files moved to a new apple-core-stub-helpers.ts; each split file keeps
only the vi.mock() blocks its own tests actually exercise (narrowed from
the original's blanket five-module mock, verified safe since the dropped
mocks always defaulted to real-passthrough behavior anyway).
Verified 100 test() call sites total across the new files (matches
original), and vitest.config.ts's SUBPROCESS_STUB_TESTS glob updated to
the new file list. Per repo convention, these process-spawn suites are
timeout-flaky under CPU contention; a timeout on an unrelated file is not
a regression, only assertion failures are.
* style: run oxfmt on apps.test.ts
CI's Lint & Format check caught a multi-line import in the new
apps.test.ts header that oxfmt wants collapsed onto one line.
* feat(scripts): blast-radius query over the depgraph model (#1425)
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(scripts): keep --limit bound to its value in depgraph affected
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---------
Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>