* feat(screenshot): add crop-on geometry core and cropTarget selector rows
* feat(screenshot): declare crop-on flag, script round-trip, and snapshot runtime plan
* feat(screenshot): run the crop leaf after the platform write and before scale
* feat(screenshot): expose --crop-on in the CLI and surface crop warnings
* chore(gates): declare crop-on capture-kit subpaths and scope the crop scenario exemption
* refactor(screenshot): split crop target/policy module and trim redundant coverage
Address review comments at 570da2c417:
- Split the 328-line screenshot-crop.ts leaf: the target acceptance matrix,
classifier, and pre-device argument policy move to screenshot-crop-target.ts,
so both implementation modules meet the 300-line target.
- Reuse kernel isPositiveFiniteRect/rectArea in the rect-projection module
instead of redefining them locally.
- Drop the crop-on CLI forwarding case (redundant with screenshot-options
flag-mapping coverage + the generic dispatcher) and the transport-based
warnings case, replacing the latter with a focused screenshot-result unit
test. This also returns the two legacy aggregate test files to their
merge-base length for the test-file size ratchet.
* refactor(screenshot): extract macOS crop-target decision to keep classifier under the complexity budget
classifyAppleCropTarget inlined the macOS surface decision, pushing its
cyclomatic complexity to the fallow threshold. Move it back out to a
small helper so the target classifier stays within budget.
* refactor(screenshot): dedupe the meaningful-signal predicate and polish png-crop
- Hoist isMeaningfulSignal into @agent-device/contracts/snapshot (next to
normalizeType/isMeaningfulLabel) so the ref overlay and the crop
rect-projection share one copy instead of each carrying an identical
private predicate. Behavior is unchanged.
- png-crop: isCropBox was a no-op 'box is Rect' predicate (input already
Rect) — make it a plain boolean, and tighten the doc to the contract.
* refactor(screenshot): drop the dead crop outcome flag and cover the projection seams
- ScreenshotCropOutcome.cropped was a constant true that no caller read;
the crop either returns (success) or throws, so the outcome reduces to
the partialIntersection observation.
- resolveScreenshotRectSpace and resolveSnapshotBounds were the only
projection exports without coverage: pin the accepted-backend map, the
unaccepted-backend typed refusal, and the viewport-root / union / empty
bounds branches.
* fix(remote): materialize test suite artifacts against a remote daemon (#2246)
`agent-device test` crashed with ENOENT against a remote daemon because the
scheduler resolved `--artifacts-dir` against the caller's `cwd`, sent over the
wire, on the daemon's own filesystem. Mirrors #1802's read-side fix for the
same command: the CLI now redirects `--artifacts-dir` to a temp directory the
daemon owns before the suite runs, and the daemon rewrites every artifact path
in its response back to the caller-local root and registers the suite
directory as one downloadable artifact through the existing screenshot/record
transport, extended here to also support directories via the codebase's
existing safe archive extractor (the archive comes from a remote daemon, a
different trust domain, so a raw `tar` invocation was not enough).
* fix(remote): publish test artifacts atomically
* perf(cli): keep artifact downloads lazy
* docs: trim the CONTEXT.md glossary within the guidance byte budget
CONTEXT.md sat at 11,992 of its 12,000-byte guidance budget, so no new domain term could be added
without first paying for it.
- Condense eighteen definitions that had grown past one line (platform leaf, command surface,
runtime use, runner command traits, interactor, coordinate-first resolved element activation,
parent-owned touch point, guarantee cell, delegation-on-error, ref frame, snapshot producer,
snapshot policy facet, capture hint, regular presented-depth frontier, clip fold,
AX-unavailable target invalidation, Maestro program, Maestro observation generation). The
definitions keep their meaning; only the elaboration is gone.
- Move the five test-harness terms of 'Providers and tests' (provider-backed integration
scenario, provider transcript, scenario transcript, in-process provider scenario harness, HTTP
contract test) to docs/agents/domain.md, which AGENTS.md already routes to for domain
vocabulary. None of them names a concept a command or a wire shape carries, and none appears in
a test name.
CONTEXT.md is 10,517 bytes after this pass.
* feat(runtime): add the managed-local owner kind, device-claim rule, and managed binding fence
ADR 0021 foundations, unit 1. Nothing registers a managed local owner yet, so every arm below is
reached from tests only; the point of the unit is that the arms exist and fail closed.
- `RuntimeOwnerRef` gains `{ kind: 'managed-local'; instance }` with `managedLocalRuntimeOwner`:
one owner per allocator instance, family-agnostic because the device carries its family. Every
owner-kind discrimination becomes an exhaustive switch, so a fourth kind is a type error at each
site: the owner key, the unavailable-facts provider mode, the durable envelope decode, and the
gateway's provider-mode acceptance and exact-owner selection.
- `deviceClaimRuleForOwner` ('ordinary' | 'allocator-held' | 'none') in the new leaf
src/daemon/device-claim-rule.ts replaces the boolean `isLocalDeviceClaimTarget`. Both claim
gates switch on it, and the admission gate now evaluates it under every device-claim policy: the
`transient-exclusive` condition moved inside the ordinary arm, so a managed owner is verified
where an ordinary owner would never have touched the store.
- `requireAllocatorHeldDeviceClaim` (src/daemon/device-claim-allocator.ts) is the one read-only
verifier both gates consult. It never acquires, never locks and never clears; in this unit it
can only answer `binding-invalid`, `missing`, or `conflict`, because no allocator-held claim
kind exists until unit 2. `allocatorHeldAdmissionError` answers each outcome with its own
refusal through an exhaustive switch, so an outcome the verifier learns to produce is a
compile error until it is answered.
- A missing allocator-held claim refuses with COMMAND_FAILED / `allocator-claim-missing`,
`retriable: false`. It is deliberately not a `DeviceClaimConflictReason`: replay retries every
conflict reason as infrastructure, and a managed identity no allocator activated is permanent.
- `managedBindingFence` / `decodeManagedBindingFence` encode `[requesterId, identityIncarnationId]`
as the fence token and the request generation as its generation, so two requesters on one
identity incarnation never share a fence. The ids are fenced verbatim, and the decoder accepts a
token only if it re-encodes to itself.
- Claim admission now receives the binding intent the gateway bound, so an exact-owner fence
reaches the gate unchanged. Session open still binds ordinarily and passes an ordinary intent:
a managed local owner is therefore refused there structurally, and the Host open route replaces
that intent when it lands.
- CONTEXT.md: managed local owner, device-claim rule, managed binding fence, request generation,
identity incarnation.
* fix(daemon): decide allocator-held admission totally instead of by an optional error
`allocatorHeldAdmissionError` returned `AppError | undefined`, so its switch without a default
was never exhaustiveness-checked: TS2366 fires only when the return type excludes `undefined`,
`noImplicitReturns` is off, and oxlint has no exhaustiveness rule. A verifier outcome nobody
answered would therefore fall out as `undefined`, which both gates read as an admission — claim
admission throws nothing and session open proceeds to open the session on a device it never
verified.
Replace it with `decideAllocatorHeldAdmission`, returning
`{ admitted: true } | { admitted: false; error }`. The return type excludes `undefined`, so
dropping an arm is now a compile error at the switch, and a gate asks whether the outcome was
admitted rather than whether an error happened to come back. `buildAllocatorHeldRefusal` and the
admission gate are projections of that one decision.
* docs: restore the meaning five CONTEXT.md definitions lost in the trim
The condensing pass shortened these five past the point where they still said what they meant:
- Capture hint said 'presented depth' where the term is 'regular presented depth', which is what
Regular presented-depth frontier is measured against; the short form read as a different axis.
- Clip fold lost both that the interpreter runs inside presentation for every backend and that a
platform difference may not enter as a backend exception. Those are the whole rule.
- Snapshot policy facet lost the process boundary that makes it host-side at all: runner-side
Swift presentation stays separate.
- Runner command traits lost 'independently of the public command surface', which is what
distinguishes them from the command surface.
- Delegation-on-error said 'settles', and Settled observation makes 'settle' a term of its own.
CONTEXT.md is 11,674 of its 12,000-byte budget.
* docs(daemon): correct the claim-gate and managed-owner comments
- The claim-gate docstring claimed there is no other way to obtain device operations. That is
true of command handlers, but two daemon-owned recovery paths bind outside the seam:
application-lifecycle-recovery.ts (ordinary intent, daemon shutdown) and
durable-capture-runtime-recovery.ts (exact-owner intent read back from a durable envelope,
which this unit makes able to carry a managed local owner). Name them instead of claiming
coverage the seam does not have.
- The open path's comment described a session executing under an allocator-held claim, a state
this route cannot produce. Say what the `{ kind: 'ordinary' }` literal actually is: the truth
of a route that binds ordinarily, which the Host open route replaces with the request's exact
intent when it lands.
- Name U3 as the unit that fills the exact-owner selection arm, rather than the whole ADR.
* fix(runtime): accept transport-composed facts for a managed owner
providerModeMatchesOwner's managed-local arm accepted mode === 'local' only, but
selectExactOwner's managed-local arm loads the device's local family owner through the same
loadLocal a local-family owner uses, so it inherits that owner's provider modes verbatim. A
managed binding over a transport-composed local device (e.g. a remote ADB or web-provider
transport) would fail bindingContractFailure's facts check and be rejected as an owner/facts
mismatch. Accept the same local-family modes the local-family arm already does; still
unreachable until U3 registers the exact-only owner, which is where the binding regression
test that pins this lives.
* feat(runtime): register the managed local owner as an exact-only wrapper and add the neutral allocator port (#2259)
* feat(runtime): register the managed local owner as an exact-only wrapper and add the neutral allocator port
ADR 0021 foundations, unit 3. Unit 1 added the `managed-local` owner kind and left the gateway's
exact-owner arm for it failing closed; this unit gives that arm a registry and the owner it selects.
Nothing in production registers a managed owner yet, so both are reached from tests only.
- `createComposedPlatformRuntimeGateway` gains a `managedOwners` list that only the `managed-local`
arm of `selectExactOwner` reads. `selectOrdinaryProvider`, `inspectFacts` and the ordinary `bind`
arm never see it, and `providerModules` pairs one provider-runtime owner with one
`ProviderDeviceRuntime`, so ordinary selection cannot reach a managed owner by construction
rather than by a check. A duplicate instance is refused at composition.
- The wrapper (src/platform-runtime-managed-owner.ts, root zone, no platform imports) binds only
under an exact-owner intent naming itself, loads the device's own family owner through the
gateway's loader, delegates with an ordinary intent — a family owner refuses a foreign exact
owner — and republishes the binding under the managed owner. It does not read the fence: what a
managed binding fence proves is the device-claim gate's business. `ownsDevice` returns false.
- Twenty cells are withheld as `owner-capability-missing`, enumerated by mechanics rather than by
catalog group: the four device-lifecycle cells, the four application cells that boot or shut the
device down (`prepareApplicationOpen`, `prepareAppleRunner`, `closeApplication`,
`finalizeApplicationClose`), and the twelve durable-capture cells, which a managed binding could
never reattach because the family runtime stamps envelopes with its own local owner. The
operations are then filtered by those facts, so an operation cannot outlive its own fact.
- `@agent-device/contracts/managed-device-allocation` is agent-device's own allocator port: lease
request, lookup, supersession, cancellation, renewal, release, activation confirmation, identity
status, removal acknowledgement, and the typed environment projection. Types only, named to match
the allocator's published contract so the two sides cannot drift, with no dependency on any
allocator package. Its only implementation is a scripted fake under `*.fixtures.ts`.
- Budgets: the new contracts entry surface is a one-module closure; the `src/platform-runtime.ts`
hub moves 47 -> 48 for the wrapper, whose own value imports were already in that closure.
* fix(runtime): withhold the deployment cells from a managed binding and trim the allocator port
Review findings on the managed local owner.
- `deployApp` and `deployMaterializedApp` join the lifecycle group. Both family deployment runtimes
ensure device readiness before installing, and `deployAppUse` requires `deployApp` alone — so
`install` on a managed binding would have booted the allocator's device with nothing to refuse
it. Twenty withheld cells become twenty-two, and the refused-uses test covers `deployAppUse`.
- The wrapper's doc comment no longer implies that withholding cells is a complete lifecycle
exclusion: several retained Apple cells (screenshot capture, settings, clipboard, application
launch) boot the simulator lazily inside the family runtime, where cell selection cannot reach.
That is the same class as the pre-binding readiness path, and closing it is a family-runtime
change.
- `readLeaseEnvironment` leaves the allocator port. It was beyond the vocabulary the contract
fixes, and it made the scripted fake carry a real parser whose only test passed with every
production line reverted. `ManagedLeaseEnvironment`, `ManagedLeaseEnvironmentKey` and
`LeaseEnvironmentError` stay as types; the reader that produces them lands with the unit that
first turns a grant into a device.
- CONTEXT.md drops an operation enumeration that was already incomplete.
* fix(runtime): withhold the lazily-booting Apple system and screenshot cells
Screenshot capture, settings, clipboard and application launch were retained on a managed
binding even though their Apple family-runtime implementations can boot the simulator lazily
below cell-selection granularity (screenshot's shutdown-failure retry boot; settings, clipboard
and application launch each resolve a local interactor the same way). That preserves rather than
blocks the exact bypass ADR-0021 section 3's hard boundary names: managed lifecycle/readiness
belongs to the allocator, and no handler path may fall back to direct lifecycle tooling.
Withhold captureScreenshot, setSetting, readClipboard, writeClipboard and openApplication
alongside the existing withheld groups. The wrapper's doc comment now names the pre-binding
readiness gap explicitly as the same class of follow-up, rather than folding it into a retained-
cells caveat that no longer applies. MANAGED_RETAINED_OPERATION moves to tapPoint, the cell the
fixture-based regression tests now use to prove something survives the wrapper.
* chore: retrigger CI (stale synchronize event after rebase)
* fix(runtime): lazy-load the managed owner wrapper to satisfy the eager-closure no-growth gate
Main's eager-closure budget gate (the merge-base ratchet) replaced the hand-tracked
HUB_BUDGETS map with an automatic no-growth-vs-merge-base check: src/platform-runtime.ts
is a hub with no growth allowed at all, not a number bumped by hand with a justifying
comment. The static import of createManagedLocalRuntimeOwner in platform-runtime-gateway.ts
added one module to that hub's closure (47 -> 48), which now fails
scripts/__tests__/eager-closure-budgets.test.ts outright rather than needing a manual bump.
Move the value import into loadManaged's dynamic `await import`, matching how the rest of
this file's owner loaders defer their leaf modules. Only the managed-local arm reaches this
path, so an ordinary bind never pays for it, same as before -- the wrapper module itself was
simply the wrong side of the eager/lazy line.
* test(platform): extract shared fixtures from the Apple perf and Android runtime suites
Move the duplicated host-command routing, app-bundle writers, and ps
handlers behind packages/platform-apple/src/core/__tests__/perf.fixtures.ts,
and the Android runtime host, device, and ordinary-bind builders behind
packages/platform-android/src/runtime.fixtures.ts. Every test keeps its
title and its own assertions; test and assertion counts are unchanged.
* chore(gates): exclude .fixtures.ts modules from changed-line coverage
runtime.fixtures.ts (packages/platform-android/src) sits outside any
__tests__ dir, so vitest's coverage.include picked it up as production
source and the changed-line gate scored its 20 unreachable construction
lines directly, failing at 39.39% against the 70% threshold.
Add '**/*.fixtures.ts' to coverage.exclude (repo-wide convention:
40+ fixture modules, all test support, never production). The file
disappears from lcov and scripts/coverage-changed/model.ts's existing
excluded-path fallback reports it non-gating instead, with no
duplicate classifier needed there.
Planted red (targeted run against the real diff/model, not committed):
BEFORE (file present in lcov, all added lines uncovered): totalLines=110
coveredLines=0 pct=0 passed=false. AFTER (file absent from lcov post-fix):
totalLines=0 excludedTotal=99 excludedReason=excluded-path passed=true.
Confirmed against a real `vitest run --coverage` + `check:coverage-changed`
pass: runtime.fixtures.ts absent from coverage/lcov.info, gate PASS,
99 lines reported under excluded/excluded-path.
* test(android-e2e): make an alert-dismiss failure self-explaining
The Android smoke scenario asserted the post-alert canary text with a
screen-wide `wait text`, whose timeout report is a top-6-label surface
dump that may not include the element in question at all. Assert the
canary through the specific automation-alert-result element instead, so
a failure states its actual current value directly.
Also record the tapped alert button's coordinates alongside its label
(already recorded) in the Android alert-handled result, and capture a
screenshot artifact when an e2e `wait` step times out, so a flake has
more to go on than the surface dump.
* review: extract shared alert-test fixtures, trim narrated docblock
- Extract node/text/button RawSnapshotNode builders (near-duplicated
between alert.test.ts and alert-detection.test.ts) into a sibling
alert-fixtures.ts, reconciling the two button() signatures by keeping
the optional `permission` param. Both suites now import from it.
Planted red: bumped the shared button() rect width and reran both
suites — 2 of 7 tests failed on the changed tap coordinates
(alert.test.ts's dismiss/accept cases), confirming the fixtures are
live-wired; reverted, suite back to 7/7 green.
- Trim captureWaitTimeoutScreenshot's docblock in runtime.ts to the
caller-facing contract only ("never throws, returns undefined on a
failed capture"); the surface-dump rationale already lives in the PR
body's Summary.
* fix(android): apply settings airplane through the connectivity service
settings airplane wrote airplane_mode_on and then broadcast
android.intent.action.AIRPLANE_MODE, which Android refuses for non-system
callers. The write landed, the broadcast failed, and the device reported
airplane mode with the radios still up.
The connectivity service now owns the change: it is read to prove the build
supports airplane mode before anything is written, driven with
cmd connectivity airplane-mode enable|disable, and read again so the response
reports the mode connectivity holds rather than the one requested. Builds
without that command are refused unmutated with UNSUPPORTED_OPERATION.
Closes#2223
* test(android): pin the mechanics eager closure at 178 modules
Splitting the airplane owner out of settings.ts adds one module to the
mechanics facet, which is implementation-eager by design. The row moves to the
measured number in the PR that grows it.
* fix(android): report only capability absence as unsupported airplane mode
An unrecognized nonzero probe — a permission denial, a connectivity-service
error — was answered with "requires Android 11; use a newer device". Only the
prose adb prints when a build ships no shell implementation for the command
now selects UNSUPPORTED_OPERATION; every other failed read stays
COMMAND_FAILED with its classified hint, and the write is unreachable from
both.
The predicate that reads that prose already existed for the clipboard service
and is now named for the question it answers, so airplane mode reuses it
instead of adding a second message sniff.
* fix(apple): switch to manual code signing when a provisioning profile is set
CODE_SIGN_STYLE was hardcoded to Automatic even when
AGENT_DEVICE_IOS_PROVISIONING_PROFILE was configured, so xcodebuild rejected
the resulting PROVISIONING_PROFILE_SPECIFIER + CODE_SIGN_STYLE=Automatic
combination with "conflicting provisioning settings" on physical-device runs.
Fixes#2153
* fix: satisfy formatting and the test-file size ratchet
- oxfmt: wrap the long array literal in the new manual-signing test.
- runner-client.test.ts was already pinned at the 1000-line tripwire
(1577 lines); adding a test grew it past the pin, which the ratchet
test rejects by design ("extract instead of adding to a file over
the tripwire"). Extract the pure runner-cache-metadata.ts build-
settings tests (signing, bundle, performance, sandbox args) into a
new runner-cache-metadata.test.ts, shrinking runner-client.test.ts
to 1441 lines and lowering its pin to match.
* 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).
* feat(apple): reclaim retained runners under device-claim authority
Implement the #1320 retained-runner rule: a daemon holding the host-global
device claim may stop and replace a warm XCTest runner whose owner no longer
holds that claim. Previously worktree B's open failed with an unstructured
COMMAND_FAILED (IOS_RUNNER_OWNED_BY_OTHER_DAEMON) for up to five minutes
after worktree A closed, until A's daemon idled out.
- Runner leases now record deviceClaimProtocol: 1; takeover is gated on the
lease declaring claim arbitration, so owners from pre-claims builds are
never preempted.
- The claim-authority probe is daemon-bound through the existing runner-owner
seam and answers from the claim store by process identity; unbound embedders
answer false and keep today's refusal.
- Disposal now skips device-wide runner container-app termination when the
on-disk lease is owned by someone else, so the losing daemon's idle stop or
shutdown cannot kill the successor's runner on the shared simulator.
- Help topics updated: the live-owner runner rejection now names claim
arbitration instead of being unconditional.
Live-validated with two daemons sharing one claim store against a throwaway
simulator: live-owner open still rejects with structured DEVICE_IN_USE;
after close, the contender opens in ~8s with the lease re-owned while the
loser daemon is still alive; stopping the loser afterwards leaves the
winner's runner healthy.
* fix(apple): make disposal ownership check and device-wide teardown one lease-locked operation
Review P1 on #2160: cleanupRunnerSessionResources read the on-disk lease
token and then terminated the device-global runner container bundles without
holding the runner-lease lock, so a loser could pass its check while a
successor's reclaim was in flight and then kill the successor's runner.
The ownership check, the container-app termination, and the lease release now
run as one operation under the runner-lease lock — the same lock a successor
holds for its entire reclaim-and-publish window — on every disposal path.
Callers already inside the lock (startup abort, lease-publish failure,
stopIosRunnerSession) declare leaseLockHeld instead of re-acquiring the
non-reentrant lock; if the lock cannot be acquired, all device-wide teardown
is skipped and an own unreleased lease turns stale on process exit.
The two-owner regression starts loser disposal inside a held successor
window, completes the takeover, and proves the loser neither terminates the
successor's runner nor touches its lease. Observed red against the pre-fix
code. Live-revalidated twice with overlapping daemon stop and takeover open
on a real simulator.
* fix(apple): match runner takeover authority by canonical device identity
Review P1 on #2160: the authority probe matched any active claim with the
same raw device id, while claims are canonically scoped by platform family +
Apple OS + id — an Android claim whose serial equals an Apple runner id could
authorize destructive takeover of a live Apple runner (and the prior probe
regression used an Android fixture, blessing exactly that).
The probe now receives the full DeviceInfo through the runner-owner seam and
the daemon answers with an exact canonical-local-device-key lookup: family,
Apple OS, and id must all match the held claim, which also replaces the store
scan with a direct keyed read. prepareRunnerLeaseForStartup takes the device
rather than a bare id so the lease seam can never degrade the match.
Adversarial regressions: a same-id Android claim grants no authority for the
Apple device (and the true Apple claim does) at the probe, and the lease-seam
test pins that the probe receives full family/OS identity, never a bare id.
* refactor: sink package-closed src modules into existing packages
Move closed modules into contracts, kernel, capture-kit, and ad-script,
and declare DaemonCommandDescriptor in core so R6/R9 can pin the remaining
provider-webdriver type cycle.
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor: keep contracts and capture-kit off generic sinks
Move interaction-outcome, snapshot warning rendering, and inventory ALS
behind focused owners, and plant R18/R70 domain-shape gates so they
cannot return as package export-map growth.
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor: drop moved implementation comments from owner modules
Names, types, and tests already carry those invariants; the relocated
files should not keep review-history or control-flow narration.
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor: drop the empty snapshot-quality layering zone
W1 moved the verdict into capture-kit and this PR moved warning rendering
into snapshot-presentation, so the ranked zone no longer has production files.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* 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>