Commit Graph

161 Commits

Author SHA1 Message Date
Michał Pierzchała b2b084d2e1 docs: fix phantom specifiers, the duplicate ADR 0019, and the Node floor (#2533)
AGENTS.md routed request cancellation/progress and diagnostics to
`@agent-device/capture-kit` subpaths that no package exports; both live in
`@agent-device/host-kit/request` and `@agent-device/host-kit/diagnostics`. It also
named `@agent-device/contracts` as an importable seam although that package
publishes no root export, and claimed `src/daemon/handlers/session.ts` was over
budget after that extraction already landed at 242 lines.

Two ADRs carried number 0019. The hop trace has its own claims to make, so it now
numbers 0023, joins the index, and keeps the links from ADR 0019 and ADR 0022.

The Node floor split was undocumented: `engines.node` stays at 22.12 because CI
installs the published tarball on that floor, while contributors need 22.13 for the
pinned pnpm. CONTRIBUTING now says so, and installation.md names the 22.12 floor and
the web backend's Node 24 requirement.

Extend the agent-guidance contract to resolve every `@agent-device/*` specifier
AGENTS.md names against the owning package's `exports`, root included, so neither a
phantom subpath nor a phantom package root can route an agent to a module that does
not exist.
2026-09-13 10:15:57 +02:00
Michał Pierzchała e6f288012b fix(daemon): let admitted work keep the lease it is working on (#2517)
* fix(daemon): let admitted work keep the lease it is working on

A lease renewed only at admission, so a command slower than its own inactivity TTL
expired the lease that was paying for the device it was using. Expiry then tore the
provider session down underneath a client still waiting for that same command's
result, and every later command on the session reported a lease that was no longer
active. The session's own work was the thing that killed it.

Admitted work now preserves its lease the way a human-control hold does: while the
request is still wanted it defers expiry, and finishing while still wanted renews
the lease for its existing TTL from the moment the work ended. Work whose client
hung up preserves nothing — it neither defers expiry past that cancellation nor
renews the lease when it finally lands — so a handler that ignores its cancellation
cannot hold a rented device open.

Found while investigating #2509. Not its reported mechanism: a cloud WebDriver
connection profile asks for a ten-minute lease, so a one-minute hang cannot starve
it. This reaches leases on the daemon's one-minute default.

* fix(daemon): drop a released lease's work claims instead of leaving them empty

A completed pass emptied its set but left the key behind, and only the expiry sweep
removed keys — which never reads a released lease again. Every connect-and-close that
ran a command on a leased device left another permanent entry in the daemon.

Releasing the last pass now removes its lease's entry, and releasing the lease drops
its claims outright and disarms the passes still running on them, so work that
outlives its own lease renews nothing.

* docs(daemon): state the lease invariant without borrowing #2509's cause

Four comments told the report's story as though it were this mechanism. A cloud
WebDriver connection profile asks for a ten-minute lease, so the reported one-minute
hang cannot have expired anything. The invariant stands on its own; where it came from
and which leases it reaches belong in ADR 0007 and the commit, not in each test.
2026-09-13 09:23:52 +02:00
Michał Pierzchała de8703b6a0 fix(selectors): resolve a wrapper chain's control for uniqueness reads (#2501)
A control reported through its own accessibility wrapper answers a selector
twice, and a regular iOS snapshot omits unverified hittability, so the ladder
that relates a wrapper to its control cannot fire. #2482 collapsed that chain for
mutating resolution only: `press` tapped the toolbar button while `is visible`
and `get attrs` reported "Selector did not match" and `screenshot --crop-on`
refused the same screen as two nodes.

Export the collapse beside the classification that asks for it and apply it where
a read row's answer was a refusal. Rows that resolve before any refusal are
untouched, and a candidate set the rule does not recognize as one control - a
cell and the button inside it, or matches in distinct subtrees - still refuses.

Replay verifies a recorded target by resolving its recorded selector again under
the same row's refusal rules, so a step whose screen had not changed verified as
an identity mismatch on its first replay. Verification names the collapsed control
too, which is the node dispatch acted on and the node the recorded identity
carries.
2026-09-12 07:44:44 +02:00
Michał Pierzchała 0feb4e26a0 feat(ios): drive ASWebAuthenticationSession sign-in sheets in place (#2438) (#2448)
* 0.21.1

* feat(ios): drive ASWebAuthenticationSession sign-in sheets in place (#2438)

iOS apps that sign in via ASWebAuthenticationSession present the identity
provider in com.apple.SafariViewService, out of the app's process. Two facts,
both verified live on the iOS 26.2 Simulator, made these flows unautomatable:
activating or launching the host cancels the auth session, and the host AX
bridge cannot see the sheet because the app stays the AX primaryApp.

Serve and drive the sheet in place. A closed registry names the host (shared by
the TypeScript and Swift sides under a parity test); the runner reads and drives
it without activation and never adopts it as the session target; and the
Simulator route detects a running host with a cheap device-scoped ps probe and
takes the runner path, since the bridge would serve the occluded app tree as if
healthy. open refuses to launch a registered host, and captures carry a
system-surface disclosure.

Presence is foreground state, not tree content: a torn-down host serves a richer
tree than a live one, so content heuristics cannot tell them apart. The
never-activate guard is what keeps the foreground predicate sound, which also
makes the stale-tree failure mode unrepresentable for this flow.

Closes #2438

* chore(gates): register contracts/ios-system-surface in the export snapshot

* fix(ios): close the system-surface correctness gaps from review

Presence probe: absence and probe failure are no longer reported as "no
surface". The probe returns present/absent/unknown and the route takes the
runner for anything but a proven absent, so a sheet opened between two captures,
or a probe that cannot answer, can no longer fall through to a bridge capture
that would answer confidently from the occluded app tree. Only a positive
observation is memoized. The probe now matches with pgrep and reads only a
matched pid's environment, which is ~3x cheaper than the previous full
process-environment dump and stops copying every process's environment.

Open guard: the refusal moved to every resolved-host launch and terminate, so
the URL, deep-link and launch-args branches that returned before the old check
can no longer launch the host. Terminating a host is refused too, since that
cancels the presented session just as launching it does.

Comparison: the surface identity now reaches SnapshotState, and tap-failure
corroboration refuses outright when a baseline and a post-action capture
disagree about it, instead of letting app and sheet captures meet in legacy
same-presentation matching. Selector routes disclose an iOS system surface
through the shared disclosure seam rather than reading only the Android field.

The contracts import in the launch path is deferred so the app-lifecycle
facade's eager closure stays flat, and the runner's comment prose is trimmed
because apple/runner ships to npm as uncompiled source.

* fix(ios): route the system-surface probe through the Apple tool provider

The probe shelled out with runCmd, so every eligible capture spawned a real
process even in provider-backed tests that stub the Apple tool seam — 17 real
spawns in one scenario file, which is both wasted work and added latency on
timing-sensitive settle paths. It now goes through runAppleToolCommand like the
sibling ps probe, so a stubbed provider answers instead of spawning.

* fix(ios): disclose a skipped bridge when the surface probe cannot answer

Routing an unprovable probe to the runner is right, but the early return also
skipped runFallback, so the response lost its warning and kept an identity that
could still be compared against a bridge publication. An unknown probe now falls
back through the same disclosed path as a bridge failure, with its own reason.

* fix(ios): keep surface identity through comparison, find, and probe scope

A ps read that carries no SIMULATOR_UDID at all was reported as absence, so an
unreadable or truncated environment could route a live sheet to the occluded app
tree. Only a scope naming a different device is a real negative now; a missing
one stays unknown.

The shared post-gesture comparison token used comparisonKey or the backend
alone, so an app capture and a sheet capture — both XCTest — compared equal and
a sheet appearing or dismissing read as a stable surface. The token now carries
the surface, which covers stabilization, verify and settle through the one path
they share.

Mutating find rebuilt its capture without iosSystemSurfaceBundleId, so the
shared disclosure helper could not report the sheet on either outcome. It is
preserved now.

Each fix has a regression that fails without it.

* fix(ios): keep surface identity in verify and settle comparisons

`--verify` compared node digests and `--settle` diffed node-only baselines, so an app
baseline and an in-place system-surface capture (a web sign-in sheet) were treated as one
presentation: a meaningless changed verdict, and a whole-surface replacement presented as an
in-surface diff with refs.

The pre-action baseline now travels with the surface its capture described, from the resolution
and the session frame through to the settled capture, and one module owns the comparison for
both routes. Across a surface change no same-surface claim is made: evidence reports the
transition instead of a digest comparison, the settled diff and its refs are withheld, and both
payloads disclose the transition.

* refactor(test): move the cross-surface settle tests onto their source mirror

The #2438 cross-surface cases were appended to `settle.test.ts`, taking it over the
test-file size ratchet (2528 lines, 2359 at the merge-base). They assert the
comparison `post-action-surface.ts` owns, so they move to that module's mirror test
file, and the device double plus the trees both files drive move to a sibling
fixtures module under `__tests__/` rather than being duplicated.

Pure move: every test and every assertion is unchanged, and `settle.test.ts` is back
under its merge-base length.

* test(daemon): cover the cross-surface settle refusal on the generic route

`scroll --settle` and `back --settle` plumb the baseline's surface identity
through `baselineSurfaceBundleId`, but nothing asserted it: the generic route
had zero coverage of the #2438 refusal, so a regression there would have been
silent while the element-targeted route stayed green.

Assert the same contract the targeted route guarantees, in both directions and
for both commands: no diff is attached across an app/sheet boundary — therefore
no tail and no `refsGeneration` — the transition is disclosed, and the settle
observation still reports its own verdict alongside that disclosure.

Each direction falsifies a different half of the plumbing, so both are needed:
dropping the baseline's surface identity fails only the sheet-to-app tests (an
app baseline has no surface id to lose), and dropping the settled capture's
fails only the app-to-sheet tests. No production change: the plumbing was
correct, only untested.

* refactor(ios): inline the single-caller surface disclosure wrapper

iosSystemSurfaceDisclosure() only mapped provenance-or-nothing onto the shared
constant for one caller, so the caller now reads the constant directly and the
wrapper is gone. Its test becomes a test of the transition disclosure, which is
the function that still earns its place (the "sheet is gone" sentence).

readAppleSnapshotResult also called readSystemSurfaceProvenance twice inside one
spread; it is bound to a local and read once.

* docs(adr): state that a presented surface outranks a requested bundle id

prepareActiveCommandContext checks for a presented system surface before it
resolves or activates command.appBundleId, so a command naming a different app is
still served the sheet. That is intended, but the code does not read that way;
the amendment now says it plainly.

* refactor(ios): carry the system surface in the capture's comparison lineage

A capture of an in-place system surface (a web sign-in sheet) describes a
different presentation than a capture of the app, so it must never compare
equal to one. The `present` branch of the iOS snapshot route returned a bare
fallback, so that capture carried no comparison identity at all, and two
comparison sites hand-rolled the distinction from `iosSystemSurfaceBundleId`
instead.

The probe now reports which host it matched, and the `present` branch goes
through `runFallback` like the `unknown` branch beside it, lineaged to
`<device>:<host bundle>`. The comparison key then differs from an app
capture's by construction, so the surface branch in `hasMatchingPresentation`
and the surface concatenation in `snapshotComparisonKey` are gone: both sites
are plain key equality again, and neither knows that system surfaces exist.
Two captures of the same surface still share a lineage, so they stay
comparable with each other.

A presented surface is not a bridge failure, so it gets its own warning
wording: the bridge is inapplicable here, not unavailable.

* refactor(interaction): carry the pre-action baseline as one surface-scoped value

The same pre-action tree travelled as a flattened nodes/surface pair at every
boundary, and each boundary rebuilt it with a conditional spread. Carry
SurfaceScopedNodes itself instead:

- ResolvedInteractionTarget gets preAction?: SurfaceScopedNodes, replacing the
  preActionNodes/preActionSurfaceBundleId pair and the PreActionBaselineFields
  intersection on all three arms of the union.
- SettleObservationCommandOptions gets baseline: SurfaceScopedNodes, replacing
  baselineNodes/baselineSurfaceBundleId.
- RefResolution carries tree: SurfaceScopedNodes instead of nodes plus a loose
  surfaceBundleId.

That retires preActionBaselineFields(), preActionBaseline(), evidenceBaseline(),
the local SettleBaseline type, the split-then-reassemble in
settleObservationCommand, and the 'preActionNodes' in resolved narrowing tests.
SurfaceScopedNodes moves to contracts, where ResolvedInteractionTarget can name
it; only two sites now mint one from a SnapshotState.

Behaviour is unchanged: the cross-surface guarantees keep their existing tests.

* fix(ios): identify a surface capture by what the runner served

The `present` path stamped the capture's comparison lineage from the host-side
presence probe. That probe answers about a host PROCESS and deliberately stays
positive while a dismissed host lingers, so during that window the runner
truthfully returned APP content while the route lineaged it to the HOST: the
sheet capture before the dismissal and the app capture after it compared equal,
and a post-gesture poll could read the transition as a stable surface.

Derive the identity from the returned capture's `systemSurface` instead - the
runner stamps the surface it actually served - and say which of the two the
capture holds in the warning. The probe's host is now evidence only: it names
the matched host in a route diagnostic so a lingering window is legible in the
daemon log. Other reasons keep their lineage and wording byte for byte.

Captures that bypass the route's planning (a pinned backend, a custom-actions
read) also reach the runner, and the runner serves the sheet there too. They
carried no comparison key at all, so a sheet and app content fell through to
legacy presentation matching as one presentation and could corroborate a tap
across the two. The capture owner now gives those a surface-scoped identity as
well, with no fallback-source residue: nothing fell back. An app capture off
the route is untouched.

* fix(ios): derive a served surface identity at the one stamping point

A runner fallback's comparison identity was decided per call site. The
`present` path and the off-route path read the runner's `systemSurface`
stamp, but the plain `runFallback` path did not: it stamped the app
lineage the route had planned, whatever the runner returned.

The probe and the capture are separate observations, so a sheet can
appear in the gap between them. With the bridge circuit already disabled
for the generation, an app capture and a later sheet capture both
received the same app-generation key, so tap corroboration could treat
two different surfaces as comparable.

`stampFallback` now owns the decision for every runner fallback: the
surface the runner served outranks the app lineage the route planned.
The reason the bridge was skipped survives either way, and
app-generation evidence leaves with the app lineage it describes, so two
captures of the same sheet still compare equal. `runSurfaceFallback`
keeps only the reason, which is the one thing that path decides.
2026-09-11 17:37:35 +02:00
Michał Pierzchała 47b1cae548 fix(ios): refuse Simulator bridge trees that end at a web view's remote content (#2484) (#2486)
* 0.21.1

* fix(ios): refuse Simulator bridge trees that end at a web view's remote content (#2484)

Since 0.21.0 the host AX bridge is the snapshot source for local iOS
Simulators. It reads one process, and a WebKit page lives in another:
Safari and WKWebView screens were published as chrome plus empty webview
nodes, with no ref reaching the page.

The decoder now counts AXRemoteElement leaves that sit under a WebView
ancestor and reach the viewport, and the source refuses such a tree as
remote-content-boundary. The existing route fallback serves XCTest, which
resolves remote elements, for the rest of the app generation and discloses
the switch in the snapshot warning. Frameless leaves are refused; zero-area
and off-screen ones are published.

Adds a fixture-backed smoke scenario that drives the WebView lab through the
default route, amends ADR 0004 and the bridge README, and shares the e2e
snapshotNodes helper.
2026-09-11 13:12:57 +02:00
Michał Pierzchała bd42b2602f fix(ios): serve regular --depth from every snapshot backend (#2431)
* fix(ios): serve regular --depth from every snapshot backend

A regular depth-capped request was refused on every runner backend but the
recursive tree: the query sweep past depth 1 and private AX at any depth
returned no capture, so a plan pinned or deferred to private AX (custom
actions, a private AX verdict on the session, the XCTest channel penalty)
fell through to the synthetic sparse root, which the daemon then rejected as
"regular iOS snapshot presentation requires a valid viewport".

Presentation already applies the presented-depth cut to whatever hierarchy a
backend acquired, and a depth-capped regular capture is a subset of the
unscoped one from the same backend, so the refusal protected nothing the
unscoped answer did not already disclose through truncated/effectiveDepth.
Delete the gate, declare private AX as regular-depth=presentation-cut, and
record the rule in ADR 0004.

Closes #2403

* test(ios): prove a private-AX-pinned plan serves regular --depth through acquisition

The presentation-package test passes with the old backend depth gate restored,
because it calls presentation directly. This runner-bundle test pins private
AX, asks for regular depth 1 against the launched host app, and requires the
plan to reach acquisition and presentation: a private-ax verdict that is not
sparse, more than one node, a real root rect, and a payload no larger than the
unscoped capture from the same backend. With the gate restored the plan logs
SNAPSHOT_BACKEND_DEPTH_UNSUPPORTED and returns the zero-rect sparse root, and
the test fails.
2026-09-10 11:21:01 +02:00
Michał Pierzchała 6ca66c9fad fix(android): ease controlled scrolls within the requested duration (#2393) 2026-09-10 10:15:34 +02:00
Michał Pierzchała 52420d77e0 perf(ios): learn generation-scoped native depth hints in the host AX source (#2427)
The host source remembers the native levels a finished recovery accepted,
keyed by resolved target id, app generation, and producer, and sends them as
nativeLevelsHint so the guest's first request skips the known rejection. The
hint changes request strategy only: delivered depth, node bounds, and
completeness rules are unchanged. It is learned only after the delivered tree
validated, expires after eight hinted captures, is never renewed by a hinted
success, and never crosses apps, generations, or producers; explicit raw-depth
requests neither use nor teach it. The route's generation circuit stays the
only lifecycle owner.

The guest reports its request accounting (requests, rejected, continuations,
accepted levels) as recovery, which the host emits as the
ios_snapshot_source_recovery diagnostic; the source version moves to v1.5.5.
The shared recovery fixture gains the host column of every hint case and the
hinted recovery cases, replayed through the source adapter itself. The test
app gains a deep-tree screen that reproduces the native depth rejection for
paired benchmarks.
2026-09-09 18:24:17 +02:00
Michał Pierzchała 75c3185a02 test(ios): run the shared AX recovery fixture through the runner and characterize its depth memory (#2428)
Replay every recovery case of contracts/fixtures/ios-ax-recovery-conformance.json
through the runner's real private AX bridge, depth ladder, frontier extension,
and completeness verdict, asserting the delivered tree's canonical signature
and node count alongside outcome and request accounting. To make that possible
RunnerAXSnapshotBridge gains a resolved client/target capture seam and the
ladder loop becomes a platform-neutral function; behaviour is unchanged.

Add the fixture's hint cases as the runner's accepted-depth memory
characterization: each step is a real ladder capture against a client that
rejects above the accepted rung, bounded by node budget when the step is not
complete and rejected at every depth when it fails, whose observed rejections,
accepted rung, and boundedness are asserted before the runner's own learning
step records or skips it.
2026-09-09 18:24:16 +02:00
Michał Pierzchała 4d9e7ffe8f test(ios): add a shared AX recovery conformance fixture with a host adapter (#2425)
Add contracts/fixtures/ios-ax-recovery-conformance.json, a shared executable
recovery contract for the host AX bridge and the XCTest runner's private AX
bridge. Every expectation names the outcome, the native request accounting,
and the delivered tree as a canonical preorder signature with its retained
node count, so a producer that drops, duplicates, reorders, or re-parents
nodes cannot pass as complete. The fixture records per-producer expectations
and documents the intentional differences (depth vocabulary, ladders,
frontier evidence, budgets, ownership and deadlines, hint lifetime).

The host adapter is an Objective-C driver over captureSnapshotTree, compiled
and run per case by native-runtime.test.ts. The runner adapter and the
accepted-depth memory characterization follow in a stacked PR.
2026-09-09 18:24:16 +02:00
Michał Pierzchała 0dfd65f6a2 perf(ios): speed up deep snapshots and keep first taps reliable (#2414)
* perf(ios): recover deep snapshots and isolate optional tap probes

* chore(gates): enforce snapshot assets and optional probe lifecycle

* fix(ios): preserve capture bounds and local probe recovery

* chore(gates): validate base package assets with its own policy

* chore(gates): verify recovery failures respect launch observation policy

* fix(ios): fail closed on unknown snapshot frontier completeness
2026-09-09 18:24:15 +02:00
Michał Pierzchała 342e98cff9 refactor(daemon): separate open-target policy from platform mechanics (#2416)
* refactor(daemon): separate open-target policy from Android mechanics

Move resolveAndroidPackageForOpen/inferAndroidPackageAfterOpen behind
the Android owning seam in packages/platform-android. resolveSessionAppBundleIdForTarget
now lazily reaches Android mechanics itself instead of taking an
injected resolver function, so open-prepare and selector-dispatch
import only the neutral open plan/result surface from
platform-runtime-open-target.ts. Reclassifies the two R74 inventory
edges to daemon-policy-essential and updates ADR 0022.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0167UVzrdzVMCZqXgxtzWoTD

* refactor: address adversarial review findings on open-target seam

Restore try/catch around the Android-mechanics lazy load so a module
load failure still resolves to undefined instead of throwing. Rename
the unrelated private resolveAndroidPackageForOpen in app-lifecycle.ts
to requireAndroidPackageForOpen to remove the naming collision with
the new exported function. Add a planted-violation regression test
for reintroducing Android mechanics on the selector-dispatch edge.
Tighten ADR/inventory wording that overstated which files consume the
neutral resolver.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0167UVzrdzVMCZqXgxtzWoTD

* fix(platform-android): keep the mechanics facade lazy for the new open-target exports

resolveAndroidPackageForOpen/inferAndroidPackageAfterOpen were re-exported
statically from mechanics.ts, which eagerly evaluates open-target-resolution.ts
on import and tripped the eager-closure-budgets gate (177 -> 178 modules).
Wrap them as lazy async functions, matching the existing pattern used for
listAndroidAppsWithAdb/captureAndroidLogcatWithAdb in the same file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0167UVzrdzVMCZqXgxtzWoTD

* fix(android-tools): keep inferOpenedAppBundleId best-effort on a mechanics load failure

Loading Android mechanics moved from the near-infallible root
platform-runtime-open-target.ts to the real adb-backed mechanics
module, but the wrapper call stayed unguarded. A loader failure now
throws instead of leaving the app-bundle identity unset, even for a
targetless open that never needed the loaded module. Wrap the load
and delegate in try/catch so it degrades to the current bundle id,
matching the pre-refactor behavior, and add a regression test with
the loader rejecting on a targetless open.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0167UVzrdzVMCZqXgxtzWoTD

* perf(android-tools): skip loading Android mechanics when app-bundle identity is known

inferOpenedAppBundleId always loaded Android mechanics before
delegating, even when currentAppBundleId already made the delegate's
own fast-return a no-op. Check it first so the load is skipped
entirely once the identity is already known, and add a regression
test asserting the loader is never called in that case.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0167UVzrdzVMCZqXgxtzWoTD

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-09-09 17:35:12 +02:00
Michał Pierzchała 871ce1b755 refactor(daemon): normalize lifecycle participation of platform resource owners (#2415)
* refactor(daemon): typed lifecycle participation for platform resource owners

daemon-runtime.ts no longer imports the Apple runner owner, the Android
snapshot-helper/Web orphan cleanups, or the app-log legacy marker recovery
directly. Those are now behind PlatformOwnerLifecycle, a typed
startup/shutdown surface owned by the daemon; the root composition module
platform-runtime-daemon-lifecycle.ts is the sole place that wires the
concrete platform owners into it.

Reclassifies the R76 daemon-platform-runtime-inventory edges accordingly:
the leaked apple-runner-owner and operation-host edges are removed, and
resource-cleanup/daemon-lifecycle stay as composition-essential.

Closes #2333

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017oNQ8THHYFcK7NX1s78kW5

* style: apply oxfmt formatting to R76 inventory test fixtures

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017oNQ8THHYFcK7NX1s78kW5

* test(daemon): update source-ordering assertion for the renamed call site

daemon-runtime.ts now calls platformDaemonLifecycleOwners.recoverLegacyAppLogMarkers
instead of the old recoverLegacyAppLogMarkersAfterDaemonLock dynamic import; the
text-based ordering guard needs to look for the new call site.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017oNQ8THHYFcK7NX1s78kW5

* docs(adr): record #2333 as landed in ADR 0022

The registry (DAEMON_PLATFORM_RUNTIME_EDGES) already reflects the
retired apple-runner-owner/operation-host edges; update the ADR prose
to match instead of leaving #2333 listed as outstanding work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017oNQ8THHYFcK7NX1s78kW5

* fix(daemon): configure the Apple runner owner only after the daemon lock is held

configureForDaemonLock ran before acquireDaemonLock, which meant a
process that lost the lock race briefly published a global runner-owner
state dir and claim-authority probe it didn't own. Move the call inside
the post-lock try block (verified nothing reads the runner-owner state
before request time — runner-host.ts only captures getter closures) and
drop the now-unreachable clear call on the lock-failure branch. Adds a
source-order regression test pinning the new sequence.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017oNQ8THHYFcK7NX1s78kW5

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-09-09 15:54:43 +02:00
Ahmad Al-Faqih ef5a459294 refactor(daemon): isolate Apple session observations (#2405)
* refactor(daemon): consume a semantic Apple session observation port

* chore(gates): retire direct daemon observation imports

---------

Co-authored-by: PLASMA-FR <173463847+PLASMA-FR@users.noreply.github.com>
2026-09-09 08:28:06 +02:00
Michał Pierzchała 22a46d12d2 refactor(move): move the remaining package-ready modules out of src/core (#2401)
* refactor(move): move remaining package-ready modules out of src/core

- validation.ts            -> @agent-device/kernel/validation
- android-system-surface-disclosure.ts -> @agent-device/contracts/android-system-surface-disclosure
- project-runtime.ts       -> @agent-device/host-kit/project-runtime
- runtime-transport-hints(.test).ts -> @agent-device/host-kit/runtime-transport-hints
- app-events.ts (+tests)   -> src/daemon/app-events.ts
- dispatch-payload.ts (+test), payload-input.ts -> src/daemon/
- fill-backend-result.ts (+test) -> src/daemon/

interaction-outcome.ts stays in core: it correlates ResolvedInteractionTarget
with error objects across the commands(rank3)/daemon(rank4) boundary, and
contracts explicitly refuses mutable interaction-outcome lifecycle (R18).

* chore(gates): pin the exports and boundary snapshots for the moved core modules
2026-09-08 21:50:14 +02:00
Michał Pierzchała 31b01a2fe8 docs(adr): record the #2278 coupling audit and re-trace entry-to-platform hops (#2355)
* docs(adr): record the #2278 coupling audit and re-trace entry-to-platform hops (ADR 0022)

* docs(adr): re-trace hop tables with spine side-calls excluded from hop counts

* docs(adr): renumber R74 references to R76 after the main merge
2026-09-08 13:21:29 +02:00
Michał Pierzchała 1f9d940bff refactor(capture-kit): complete ADR 0019 end state — relocate snapshot and recording zones (#2385)
* refactor(capture-kit): relocate snapshot and recording zones into capture-kit

Move the ADR 0019 end-state capture zones into @agent-device/capture-kit:

- src/snapshot/** -> packages/capture-kit/src/snapshot/** (presentation,
  freshness, scroll-edge-state, ios-snapshot-runtime, android occlusion)
- src/recording/** -> packages/capture-kit/src/recording/**
- src/core/snapshot-{chrome,state,tree-ingestion,node-lookup}.ts ->
  packages/capture-kit/src/
- src/snapshot-quality/ test -> capture-kit presentation tree (directory
  retires with its last file)

Pure renames: import re-pointing and gate updates follow in the next commit.
The snapshot-desktop-surface test parks in src/__tests__/ because it pins
the root eager-import-closure walker.

* refactor(capture-kit): re-point capture and recording consumers to the new subpaths

Rewires every consumer of the relocated snapshot/recording modules to the new @agent-device/capture-kit subpath exports, adds the 23 subpath entries to the capture-kit exports map, fixes the moved recording-scripts test's __dirname-relative paths for the deeper location, and records the completed migration in ADR 0019's end state.

* chore(gates): align layering, mutation, fallow and CI gates with the capture-kit relocation

Moves the executable-policy roots, presentation-owner constant, zone ranks, authority fixture, mutation sharding globs, stryker aliases, fallow baselines and the iOS workflow's android-owned paths-ignore entry onto the new packages/capture-kit paths, and extends the planted-red coverage to the new presentation-owner subpath.

* chore: point capture-domain source-of-truth comments at the relocated capture-kit modules

* test: point shutdown recording mock at capture-kit and cover interactor acquisition presentation

* test(capture-kit): update upstream presentation test imports

* chore(gates): follow relocated snapshot assembly in R74

* test(daemon): freeze prewarm deadline assertion clocks
2026-09-08 12:41:39 +02:00
Michał Pierzchała 233a34d138 refactor(daemon): extract the session event journal into a workspace package (#2361)
* refactor(daemon): extract the session event journal into a workspace package

`src/daemon/session-event-*.ts` (6 modules) and `src/core/keyboard-actions.ts` move as
git renames into a new private package `@agent-device/session-journal`. One subpath per
moved module points straight at the moved file; no `index.ts`, no re-export at any old
path. Every consumer switches to the owning specifier.

The journal's request-shaped inputs now name `DaemonRequest`/`DaemonResponse`/
`DaemonResponseData` from `@agent-device/kernel/contracts` instead of the daemon's own
`daemon-request.ts`, which the package may not reach (R11) and which carries `internal`
with its `SessionState` callbacks and admitted `DeviceLease`. The response types were
already re-exports of the kernel ones, so no shape changes; the request type narrows to
the four fields the journal reads.

A type-level test reads every request-shaped parameter off the real signatures and
asserts the reachable type graph declares no `internal` key, holds nothing shaped like a
live session record or a `DeviceLease`, and carries no callback.

The daemon reaches the journal only by workspace specifier now, so the code-signature
walk gets the same pin the descriptor registry got: a walk stopping at the package
boundary would report an unchanged signature after an entry-shape or retention-window
edit, and a client would keep reusing a daemon writing the superseded journal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUhCzFDpMU92EM8o3tjRhy

* chore(gates): rank the session-journal package on the layering spine

R6's drift guard requires every production zone to be a deliberate ranked-or-unranked
decision. `session-journal` is vocabulary the daemon reads a dispatched request through,
so it takes rank 1 beside `command-registry` and `contracts` rather than the unranked
kit treatment: its only ranked edges are to same-rank zones, which is not a back-edge.

No `APPROVED_OVER_CEILING` row and no fallow baseline edit: rename detection carries all
seven moved entries' merge-base closures, so each falls under the no-growth rule, and no
baseline entry was keyed on the old paths.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUhCzFDpMU92EM8o3tjRhy

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 07:52:27 +02:00
Michał Pierzchała 51ed6217cc refactor(daemon): relocate the daemon client out of src/daemon (#2360)
* refactor(daemon): extract the repair-tombstone reader below store and client

`findUnrecoveredRepairCommitFailure` reads session artifacts off disk and is
reached from the daemon client, which had to import `session-store.ts` — the
daemon's largest server module — for it. Move the tombstone shape, its file
reader and the unrecovered-commit scan into `session-repair-tombstone.ts`, a
leaf below both, and give the tombstone file name a single owner.

No behavior change; both consumers keep their existing tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZkJjeEhLmyGpGtcwY8pqc

* refactor(daemon): relocate the daemon client out of src/daemon

`src/daemon/client/` is the daemon's client, not the daemon: no daemon file
imports it, and its consumers are the CLI, the Node client, the proxy command
and the injected dispatch type. Move it to `src/daemon-client/` as renames so
`src/daemon` is server code plus the shared kernel the client still needs —
`config.ts`, `daemon-process.ts`, `request-progress-protocol.ts`,
`daemon-request.ts` and the extracted `session-repair-tombstone.ts`.

Zone name and rank are unchanged (`daemon-client`, 5); the zone now falls out
of the folder instead of a `src/daemon/client/` prefix. Tests move unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZkJjeEhLmyGpGtcwY8pqc

* refactor(daemon): move the session artifact path helpers out of session-store

`src/cli.ts` and `src/remote/remote-request-diagnostics.ts` reach into
`session-store.ts` for one pure path function, `resolveRemoteRequestDiagnosticsPath`,
which made every CLI process eagerly evaluate the daemon's session store and
its whole subtree — the script writer, the event log, the action recorder and
the replay transaction vocabulary.

The four artifact path helpers name files; they hold no store state. Move them
to `src/daemon/session-artifact-paths.ts`, a leaf over `session-paths.ts`, and
point all ten consumers at it. `src/cli.ts`'s eager closure drops from 379
modules to 365 and no longer contains `session-store.ts`; the store itself is
464 -> 341 lines. AGENTS.md's declaration-site pointer follows.

No behavior change: the helpers are unmodified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZkJjeEhLmyGpGtcwY8pqc

* chore(gates): re-key the daemon-client gate paths onto src/daemon-client

Path-keyed enforcement follows the relocated files: the fallow health baseline
entries, the oxlint per-file override, the wire-compat surface/ledger/mutation
paths, and the layering zone derivation (the `src/daemon/client/` prefix is
dead now that the folder itself names the zone).

R10's external daemon request/session-state importer list gains the five client
modules. The edges are unchanged by this PR — the client has always built
`DaemonRequest` and read `DaemonResponse`; it sat inside `src/daemon/` and so
fell under the prefix skip. Naming the files keeps the dependency enumerated
and shrink-only, so a new `src/daemon-client/` module reaching `session-state`
still fails. Its size assertion now reads the recorded list instead of a
literal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZkJjeEhLmyGpGtcwY8pqc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-09-07 07:49:19 +02:00
Michał Pierzchała 0bebbe8dc2 feat: admit managed requests within lease authority (#2319)
* feat(daemon): admit managed requests within lease authority

* test: verify managed request authority and activation boundaries

* test: run managed request admission in provider integration
2026-09-06 08:10:59 +02:00
Michał Pierzchała 6b0e5d6c1c feat: add fenced managed lease admission (#2308)
* feat(daemon): add fenced managed lease admission

* refactor: derive managed horizons from canonical budgets

* fix: carry managed admission through operation dispatch
2026-09-06 08:10:58 +02:00
Michał Pierzchała cf83afb9c9 feat(ios): route Simulator snapshots through AX bridge (#2279)
* feat(ios): route simulator snapshots through AX bridge

* fix(ios): preserve snapshot fallback lineage

* fix(ios): keep regular depth in presentation

* perf(ios): reuse process-verified snapshot targets

* fix(ios): refuse snapshots beneath another foreground owner

* test(ios): bound native setup and isolate runner reset

* test(ios): synchronize helper crashes with request dispatch

* test(ios): exercise foreground guards through native capture

* chore(gates): run native snapshot ownership regression on iOS CI
2026-09-05 23:05:58 +02:00
Michał Pierzchała f1d4efe059 refactor(daemon): inline the touch sub-switch into the interaction dispatcher (#2302)
* refactor(daemon): inline the touch sub-switch into the interaction dispatcher

interaction-touch.ts was a 31-line pass-through re-switching on
press/click/longpress/hover/fill only to call the same five handlers
handleInteractionCommands already imports transitively. Inline the five
cases into the existing switch and delete the file, dropping one hop
from the press/click/longpress/hover/fill dispatch path.

Point the response-construction-guard sentinel at
interaction-touch-press.ts (a file that still exists) so it keeps
watching for a hand-rolled responseData branch across the touch
handler set.

* fix(daemon): close ADR-0011 guard gap and refresh the stale hop trace

PR review findings on #2302:

- The ADR-0011 responseData guard scanned files by an
  interaction-touch*.ts name prefix, which stopped covering the touch
  dispatch switch once it moved into interaction.ts. Added a targeted
  second check that extracts the press/click/longpress/hover/fill case
  bodies from interaction.ts and scans them for hand-rolled
  responseData, without widening the whole-file scan onto
  interaction.ts (which also hosts the unrelated `type` command's
  legitimate local responseData construction). Planted red: reverted
  one switch case to a hand-rolled responseData construction twice
  (press, then click) and reran
  `npx vitest run src/daemon/interaction/internal/__tests__/interaction-response-construction-guard.test.ts`;
  both times the new test failed with the expected
  "Hand-rolled interaction responseData found in the touch dispatch
  switch" message, then passed clean again after reverting.

- docs/adr/0019-end-state-hop-trace.md pinned itself to a commit and
  claimed the press/Android route was untouched by this PR, but the
  PR's whole point is merging two of that table's traced files
  (interaction.ts, interaction-touch.ts). Merged the corresponding
  table rows, renumbered the remaining hops, updated the file/class
  summary counts and every downstream hop-range reference in the same
  doc, and did the same for the one line in
  docs/adr/0019-request-bound-platform-runtime.md that also cited the
  now-stale 24-hop figure.

* test(daemon): hold the touch dispatch switch to structural delegation

The ADR-0011 guard extracted each touch case body with a regex that stopped
at the first nested case/default, so a hand-rolled responseData placed after
a nested switch was invisible. Parse interaction.ts with oxc-parser instead
and require every press/click/longpress/hover/fill case to be exactly one
return await <handler>(...) where the handler is imported from an
interaction-touch*.ts module. Inline planted-red cases cover the nested-switch
shape, an inline literal, a foreign callee, and a missing command.

* style(daemon): oxfmt the interaction response construction guard

* test(daemon): fold the guard's delegation check under the complexity threshold
2026-09-05 22:41:20 +02:00
Michał Pierzchała ba6c818d81 spike(daemon): give the ADR-0014 ref frame private ownership (#2296)
* refactor(daemon): make the ADR 0014 ref frame one owned value

The four `refFrame*` fields on `SessionState` were policed only by the R7
ownership table: any daemon module could write them, and only a full-graph AST
scan could say whose write it was. They are now one `RefFrame` value whose brand
key is private to `src/daemon/ref-frame.ts`, so a module outside that file cannot
construct one and cannot edit the one a session holds; the transitions replace it
whole. Every transition, rejection reason and epoch rule is unchanged.

Readers moved to the accessors ref-frame.ts exports (`refFrameState`,
`refFrameScope`, `refFrameEpoch`, plus a new `refFrameTree` and `refFrame`).
`internal-observation.ts` drops its four-field lineage copy and its field-by-field
comparison: frame identity is now one `===`.

Seen red: with the empty-result early return removed from
`markSessionPartialRefsIssued`, the new frame-identity assertion in
session-snapshot.test.ts fails; restored, it passes. A planted foreign writer
module was rejected by tsc (TS2741 missing brand, TS2540 read-only property)
before deletion.

* docs(depgraph): note the ref frame outgrew its R7 row

* refactor(daemon): make the ref frame nominal, not symbol-branded

A symbol brand on a plain object type stops construction from nothing, but not
`{ ...refFrame(session), state: 'active' }`: object spread copies the symbol key,
so any daemon module could mint an incoherent frame (active state, stale tree)
out of a coherent one and it type-checked. Proven before the fix with a throwaway
module doing exactly that write: tsc reported nothing.

The frame is now a class with `#`-private fields behind getters. That makes the
type nominal, so no object literal is assignable to it — the same probe now fails
with TS2739 (`missing #fields, scope, generation, expired`). Construction stays
inside ref-frame.ts, and the four claim sites (ADR 0014, the SessionState field
doc, and the two in the R7 owner table) now say what the type does and does not
judge: it cannot see a whole frame moved unchanged, which is why the R7 row stays.

Expiry is idempotent by identity again. `expired()` returns THIS frame when the
frame is already expired, rather than an equal copy, which is what the lineage
check in internal-observation.ts compares with `===`. Seen red: with that early
return removed, the tightened ref-frame test fails with "Values have same
structure but are not reference-equal"; green with it.

Also: the ADR 0014 stale-ref help sample seeds its epoch through a real frame
activation again, instead of leaning on the pre-frame snapshotGeneration
fallback, and a find test drops a `?? []` that can no longer be reached.

Behavior is unchanged: same frame contents, same transitions, same admission.

* chore(gates): collapse the four ADR 0014 R7 rows into the owned refFrame value

R7's owner table listed `refFrameState`, `refFrameScope`, `refFrameTree` and
`refFrameGeneration` as four fields that had to be written together by one
module; the code now carries them as one nominal value, so the table carries one
row. R10 follows: 19 writer-owned fields to 16, 22 owner claims to 19.

The row itself stays. The type stops construction, editing and spread-derivation
of a frame outside ref-frame.ts, but it cannot judge a whole frame moved
unchanged — clearing the field, or assigning another session's frame — and the
table can. The comments say that rather than claiming full enforcement.

Seen red: a planted `session.refFrame = undefined` in snapshot-session.ts fails
R7 with "owned by src/daemon/ref-frame.ts"; green once reverted.

* style: apply oxfmt

* refactor(daemon): keep ref-frame expiry module-private

`RefFrame` exposed a public `expired()` method, so any module holding a
frame could derive a new valid one and install it through a reconstructed
session record, past the R7 field scan. Expiry is now a static on the
unexported class, reachable only inside ref-frame.ts; the frame's surface is
four getters. A type-level regression pins that no outside module can
construct, spread, edit, or derive a frame (tsc covers src tests, so a
directive that stops erroring fails typecheck).

* test(daemon): hold the three accessor migrations within the size ratchet

Each file grew by exactly its new ref-frame import; one blank line between
mock blocks goes so the files stay at their merge-base length.
2026-09-05 22:32:57 +02:00
Michał Pierzchała 09c1caeb7b feat(daemon): add managed allocation operation journal (#2284)
* feat(daemon): add managed allocation operation journal

* fix: harden allocation journal recovery

* refactor: share durable file publication seam

* fix: preserve host-kit import locality

* fix: keep directory sync helper private
2026-09-05 18:35:12 +02:00
Michał Pierzchała 0c3d369567 docs(adr): ADR-0019 end-state numbers for the daemon composition root (#2242)
* docs(adr): add ADR-0019 end-state numbers for the daemon composition root

Record the measured daemon top-level file count (199) and entry-to-platform
hop counts (press/Android 38, snapshot/iOS 29) at e624ef9d3f, propose targets
(<=60 files, <=14 hops), and name the src/ zones still awaiting a package
owner, as inputs to a maintainer decision.

* docs(adr): correct ADR-0019 end-state capture-kit inventory and hop counts

PR #2242 review: the capture-kit inventory sentence claimed the package held
only ios-snapshot-engine, but it already owns recording, screenshot/diff,
snapshot quality/occlusion, audio/app-log, and iOS acquisition mechanics per
ADR §1's amendment; replace it with a file-verified inventory of the root
mechanics that actually still lack a package equivalent.

The 38/29 hop counts had no ordered chain, counting definition, or artifact.
Add docs/adr/0019-end-state-hop-trace.md with both routes re-traced
file-by-file at HEAD (24 hops each, matching the independent
maintainability-review-2026-09-02 trace to within one file) and link it from
the ADR. Relabel the <=14 target "proposed, unverified" rather than force a
derivation the snapshot/iOS runner-protocol hops don't clearly support.

* docs(adr): keep hop evidence self-contained
2026-09-03 15:06:15 +02:00
Michał Pierzchała a4f625c774 feat: add strict wait absent polling (#2236) (#2264)
* feat: add strict wait absent polling

* fix: keep wait absent coverage gates green

* fix: preserve wait absent restart diagnostics
2026-09-03 14:01:15 +02:00
Michał Pierzchała 2371ba9bff feat: add strict native absence assertion (#2245)
* feat: add strict native absence assertion

* fix: address absence assertion review feedback
2026-09-03 08:02:09 +02:00
Michał Pierzchała db08548026 refactor: enforce src/utils retirement (#2149) (#2229) 2026-09-02 07:59:22 +02:00
Michał Pierzchała 947582a3cc refactor(daemon): move interaction and find routes behind facade (#2178) (#2228) 2026-09-02 07:59:06 +02:00
Michał Pierzchała 04fb10dde5 docs: adopt Simlock-backed Host device allocation (#2220)
* docs: adopt Simlock-backed Host device allocation

* docs: link ADR 0021 implementation tracker

* docs: define terminal allocation failures
2026-09-01 21:38:41 +02:00
Michał Pierzchała 110c08c947 refactor(transport): move shared host mechanics (#2221) 2026-09-01 19:45:28 +02:00
Michał Pierzchała 1826b2e68b refactor(ios): integrate runner with snapshot engine (#2214)
* refactor(ios): integrate runner with snapshot engine

* fix(ios): preserve macOS runner snapshots

* refactor(ios): keep runner presentation device-aware

* fix(ios): validate runner scroll presentation

* fix(ios): close presenter package boundaries

* fix(ios): preserve snapshot source lineage

* test(ios): colocate snapshot engine coverage

* fix(ios): settle post-merge audit checks

* test(ios): fix manifest parity lint

* refactor(ios): simplify runner source walk

* test(ios): cover shared package source fixture

* fix(ios): close post-merge audit gaps

* perf(ios): avoid bundling acquired snapshot path
2026-09-01 18:36:09 +02:00
Michał Pierzchała 1522126f1f refactor: move close lifecycle behind session facade (#2212) 2026-09-01 10:25:59 +02:00
Michał Pierzchała 3afd154c80 perf: retain iOS runner across physical relaunch (#2200) 2026-08-31 21:41:30 +02:00
Michał Pierzchała 34e8cbb7a2 docs+ux: make device ownership discoverable end to end (#2165)
* docs+ux: make device ownership discoverable end to end

Complete the #1320 agent experience so 'busy? -> inspect -> choose or
release' is discoverable from every surface an agent actually reads:

- devices now projects the blocking claim owner per row (claimedBy with
  session and workspace, observe-policy projection; provably dead owners are
  excluded because the next open replaces them automatically), so an agent
  told a device is busy can pick a free one from the same listing.
- help debugging gains a 'Device busy and ownership' section separating the
  two DEVICE_IN_USE flavors and their exact recoveries.
- AGENTS.md documents both flavors; docs/agents/device-verification.md
  retires the last ps/kill recovery guidance in favor of device status,
  daemon stop --state-dir, and device release --stale (Stage 5 of #1320).
- ADR-0010 no longer calls DEVICE_IN_USE 'the only retriable code' without
  naming the claim path's non-retriable override.
- The rendered cross-worktree claim error gains a help-conformance quiz case
  binding (sample-output-device-claim-inspects-owner).
- README points at device status / device release --stale.

Part of #1320.

* fix: key ownership projection by canonical device identity end to end

Review findings on #2165:

- blockingClaimOwnersByDevice keyed claims and inventory rows by bare
  device.id, so a live Android claim could project claimedBy onto an
  unrelated same-id Apple/Harmony/Vega row, with scan order picking the
  displayed owner. Both sides now use the canonical local device key
  (claim.deviceKey against canonicalLocalDeviceKey of the row's claim
  identity). The cross-family same-id regression was observed red against
  the bare-id keying.
- The projection is now asserted across every hop the PR promises: client
  normalization preserves well-formed claimedBy and drops malformed ones,
  and the devices CLI formatter carries it through JSON data and renders
  the text line (MCP shares the same serialization).
2026-08-31 14:32:29 +02:00
Michał Pierzchała caa3dc23f9 refactor: dissolve caller-side src/replay into command and CLI owners (#2151)
* refactor: dissolve caller-side replay ownership

* fix: remove replay test-only export

* fix: restore replay loader promise boundary
2026-08-31 10:01:16 +02:00
Michał Pierzchała ed26b31c94 refactor: contract Apple platform surface (#2125)
* refactor: contract Apple platform surface

* refactor: use Apple plugin seam in tests

* test: ratchet snapshot handler size
2026-08-29 13:10:48 +02:00
Michał Pierzchała a6232e51cf refactor: prune platform split residue (#2123) 2026-08-29 13:10:47 +02:00
Michał Pierzchała ab20d5c2af refactor: retire platforms source seam (#2119) 2026-08-29 13:10:47 +02:00
Szymon Dziedzic d9677301f4 feat: add human takeover controls (#2078)
* feat: add human takeover controls

* fix: harden human takeover controls

* fix: align host XCTest selection count

* fix: address takeover readiness feedback

* fix: handle macos runner permission prompt in smoke tests

* fix: detect background macos permission dialog

* fix: dismiss inaccessible macos privacy sheet visually

* fix: map macos privacy prompt without accessibility

* refactor: own human-control holds in lease registry

* fix: cancel pending human takeover on disconnect
2026-08-29 13:09:54 +02:00
Michał Pierzchała 9abcd7fe03 refactor: move Apple platform family into package (#2118)
* refactor: move Apple platform family into package

* fix: preserve Apple facade sync contracts

* fix: complete Apple W4 rebase review fixes
2026-08-28 15:25:44 +02:00
Michał Pierzchała c7f42ccedc refactor: move Android family behind package exports (#2117)
* refactor: move Android family behind package exports

* fix: address Android W5 review feedback

* fix: update relocated routing fixture assertion
2026-08-28 13:02:28 +02:00
Michał Pierzchała 0ecfef1c17 refactor(platforms): sink the shared src/platforms root files into their substrate homes (#2100)
* refactor(platforms): sink the shared src/platforms root files into their substrate homes (#2082 W3)

The shared files left directly under src/platforms move onto a declared
owner: provisioning mechanics (install-source family, toolchain probing,
boot-failure classification, app-resolution caching) form
@agent-device/provision-kit above capture-kit; host mechanics resolve to
host-kit's seams; kernel takes the pure numeric helpers; contracts keeps
vocabulary only.

Settings parsing, command-attempt rendering, and the unsupported-interactor
factory stay with their families rather than pooling in a substrate
package: android and apple settings each own their parsing, and the
unsupported-interactor factory lives in root core with a vega-local copy.

A platforms-root-shape rule rejects any new shared file or directory
appearing directly under src/platforms, and the provision-kit direction
gates (no platform imports in, no capture-kit importer) are planted red.

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

* test: cover the family-owned parsers and the unsupported-interactor factories

The settings parsers and the unsupported-interactor factory arrived without
owning tests, so their branches rode on callers. Each now has one: the
appearance/state parsers over every accepted spelling and their rejections,
the attempt summarizer over its arg join and stderr budget, and both
interactor factories over the whole operation surface and the per-instance
label.

Also drops the duplicate ./snapshot-desktop-projection export key that a
rebase left in capture-kit's manifest, where JSON silently keeps the last,
and the root-shape docblock the violation message already states.

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

* docs: keep tool directives only in the touched files

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-28 07:46:50 +02:00
Michał Pierzchała e832325e87 refactor(substrate): split host mechanics into @agent-device/host-kit capability ports (#2088)
* refactor: split generic host mechanics into @agent-device/host-kit (#2082 W1)

The shared src/utils closure that blocked the platform-family moves lands
on declared owners: generic host mechanics form a new private
@agent-device/host-kit package between kernel and capture-kit, and
capture-kit keeps capture, snapshot, and recording behavior, depending on
host-kit for the mechanics it needs. tar-stream and yauzl move with the
archive code.

Every seam's exported subpaths are pinned in package-boundaries.test.ts,
the layering model ranks the new zone, R13's allow-list names it, and each
seam carries an exact eager-closure row. ADR-0019's substrate amendment
describes the layout.

Tests that mocked two of the moved modules separately became duplicate
same-seam vi.mock factories, where the second silently replaced the first;
those are merged, and the mocks that production code reaches past are
pinned at their injection points instead.

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

* refactor(host-kit): one narrow capability port per export

The four technical barrels (exec/fs/values/request) grouped by category
rather than by capability, so a consumer needing one mechanic evaluated
unrelated ones. Each export is now a single capability over the host
machine: command, process, diagnostics, retry, archive, file, request,
version. A port re-exports only what a consumer of that capability uses,
and every port carries its own eager-closure row.

Most of the old values barrel was never host mechanics. Pure record
readers, config-source values, result text, memoization, async scoping,
coordinate validation, and device-scope parsing touch no process, file, or
environment, so they join kernel's other primitives instead.

Closures fall accordingly: capture-kit's png-worker-client from 20 to 10,
png-resize from 28 to 18, session-teardown from 79 to 68, and the CLI from
386 to 380.

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

* chore: drop the migration inventories and trim the touched comments

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

* docs: trim the touched host-kit and mutation-lane comments

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

* docs: keep tool directives only in the touched files

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

* docs: keep tool directives only across the touched tree

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

* fix: point the Swift parity comment at the real TS twin and test

The W1 move rewrote this citation to packages/contracts/src/mobile-snapshot-semantics.ts,
which does not exist: the module went to capture-kit while isTapPointInsideViewport itself
went to packages/contracts/src/snapshot-visibility.ts. The TS test line was left pointing at
the pre-move path. Both now resolve.

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

* fix: repoint comment citations at the homes this refactor moved them to

The W1 move left ~20 comment citations pointing at src/utils/*.ts and
src/request/*.ts paths that no longer exist. Each now names the capability
port that owns the symbol, which survives further file moves:

  exec -> host-kit/command          host-process, owner-identity -> host-kit/process
  diagnostics -> host-kit/diagnostics   atomic-file, process-lock -> host-kit/file
  retry -> host-kit/retry           request progress/cancel -> host-kit/request
  version -> host-kit/version       ttl-memo, source-value, parsing, device-isolation,
                                    keyed-lock, success-text -> kernel subpaths

Comment-only; no closure, budget, or behavior change. ADR citations are left
as written, being dated records of the decision rather than live references.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-28 07:46:48 +02:00
Michał Pierzchała 7b48531d3b refactor: retire ADR-0019 cutover scaffolding (#2081) 2026-08-27 15:11:14 +02:00
Michał Pierzchała 03f0f408c2 refactor: move platform provider composition out of daemon (#2070) 2026-08-27 13:11:00 +02:00
Michał Pierzchała 71214e11da refactor(runtime): close residue execution units (#2054)
* refactor(runtime): close residue execution units

* refactor(runtime): address residue ownership review

* fix(runtime): preserve viewport diagnostic log path
2026-08-27 07:46:25 +02:00
Michał Pierzchała 8af2660332 refactor(android): extract the adb executor and IME cluster into packages/platform-android (#2041) (#2044)
* refactor(android): extract adb executor and IME cluster into packages/platform-android (#2041)

Implementation moves behind an injected adb host port (R13-clean); shared
vocabulary moves to contracts (android-touch-plan, android-helper-artifacts)
and kernel (keyed-lock); root keeps thin re-export shims plus the composition
wiring that binds the port. session.ts/session-observability.ts no longer
import platform transport types (opaque unknown, R62 pattern).

* refactor(android): trim shims to consumed surface, break helper-install cycle, add gate coverage

Fallow-clean: shim re-exports carry only names root still consumes;
helper-package-install imports the package subpath directly (no cycle) and
reuses the contracts decision types; transitional R13 table gets planted-red
tests; ADR-0019 records the transitional exception; eager-closure rows added
for the new entry surfaces.

* review: restore maxBuffer on spawn options, scope the transitional test allowance

Adversarial review round 1: AndroidAdbSpawnOptions keeps maxBuffer for spawn-
signature width parity with the pre-move ExecBackgroundOptions; the R13 test
allowance narrows from any test file to the cluster's own src/platforms/android
__tests__ directory, with planted-red coverage for a foreign test file; ADR
text records the scoped allowance.

* review: keep an unbound adb host port loud in listAndroidAdbSerialsQuick

* style: format policy test

* refactor(android): split the extracted adb and IME modules into focused owners

Review: the moved adb-executor.ts (652 lines) and ime-lifecycle.ts (487 lines)
carried three concerns each across the package boundary. The entry subpaths are
now thin surfaces over focused modules — adb: transport vocabulary, failure
classification+enrichment, provider normalization, provider scope/routing,
port-reverse ownership, pull/install transfers; IME: ownership state, on-device
settings record, activation transaction, restore+orphan recovery — every module
under 250 lines, each with colocated tests over an in-memory host-port stub and
a shared fake IME device. R31's fence invariant repoints to ime-activation.ts;
eager-closure pins move to the split shape.

* rebase: reconcile the transitional android table with the #2050 mechanics-facet model

The R13 declaration check now composes both enumerated subpath sets on the
exact-list model #2050 introduced; the policy test fixture declares android's
transitional subpaths the same way it declares apple's runner facet.
2026-08-26 16:40:32 +02:00
Michał Pierzchała 72cae2bc72 refactor(apple): colocate the XCUITest runner client into packages/platform-apple (#2040) (#2050)
* refactor(apple): colocate the XCUITest runner client into packages/platform-apple (#2040)

Moves src/platforms/apple/core/runner/ (34 modules + apple-runner-platform.ts and
the 30 runner test suites) into packages/platform-apple/src/runner/ — Apple
mechanics live in the Apple package. Host capabilities (exec, diagnostics,
retry, process probes, locks, Apple tooling, physical-device control) enter
through the package-owned AppleRunnerHost port; the root composition module
src/platforms/apple/core/runner-client.ts constructs the client exactly once
and re-exposes the bound operations under their historical names.

R13 admits the transitional state deliberately: the family exports its root
façade plus exactly the enumerated ./runner, ./runner/client, and
./runner/test-host subpaths; the ./runner façade subpath is the recorded #1983
seam for unmigrated root consumers; ./runner/client has one composition root
and ./runner/test-host one vitest installer; the runner subtree may own its
cache files and sockets while raw process primitives stay banned. When #1983
completes, the subpaths and every subtree exemption are deleted and the family
returns to a single implementation-lazy façade export.

* docs(adr): model the runner subtree as a durable platform-owned facet

Review correction on #2050: the sunset story attributed the runner-consumer
migration to #1983, which owns snapshot/presentation vocabulary — not the
runner's daemon/root consumers — so that event cannot delete the ./runner
subpaths or the subtree exemptions. Reword ADR-0019, R13, and the gate
comments: the facet is the intended ownership model, its seam is enumerated
and pinned (exact export list, one client composition root, one test-host
installer, raw-process ban, eager-closure pins), and the seam narrows only
if a real runner-consumer migration retires the direct consumers. The
declaration mechanism stays apple-specific until another family needs a
mechanics facet. No behavior change; identifiers and comments only.
2026-08-26 15:53:01 +02:00