Commit Graph

91 Commits

Author SHA1 Message Date
Michał Pierzchała 56b72c5cf7 refactor(boundaries): put shared contracts below their consumers, gate the result (#1405)
* refactor(boundaries): move shared contracts below their consumers

Acts on the depgraph findings: type-only edges are invisible to R5, so
vocabulary that everything depends on had drifted above the zones that use it.

- contracts/: the four platform-plugin facet tags (LogBackend,
  RecordingBackendTag, PerfMetricsSamplerTag, PlatformGatedProviderResolverKey)
  now live beside the plugin contract itself, which also moves out of core/;
  NetworkEntry moves next to the command surface that renders it; and the
  click-button, recording-export-quality, interactor-types and
  runner-lease-context vocabularies move down out of core/.
- (root) drops from 29 files to 13: the internal *-contract/output/annotation
  modules move into contracts/, kernel/ (daemon-error, observability-redaction
  beside kernel/redaction), core/ (batch-policy, an ADR 0008 projection),
  commands/ (cli-command-aliases) and remote/ (upload-progress, upload-stream).
  What remains is entrypoints and the composition roots that R2 requires to
  sit outside the spine.
- utils/ joins the ranked spine at rank 1 after its only two upward files move
  to the zones they were reaching for (cli/resolve-cli-options,
  cli-schema/cli-config), putting ~336 value edges under the gate.
- Internal imports that routed types through the client-types re-export hub now
  name their real source.

Type-only spine inversions drop from 61 to 35; the remainder is two clusters
(client/client-types.ts and the ADR 0003 daemon facet). No behaviour change:
4470 unit tests and the layering gate pass.

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

* style: merge the duplicate contract imports the tag moves created

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

* refactor(imports): name the declaring module, share find's argument rules

Two follow-ups from re-measuring the graph after the boundary moves.

1. 89 type imports across 79 files routed through a re-export hub in another
   zone: `CliFlags` reached through commands/cli-grammar/flag-types.ts (52) when
   it is declared in contracts/cli-flags.ts, the replay suite result types
   reached through daemon/types.ts when they are declared in contracts/replay.ts,
   the doctor types through a daemon handler module, and so on. Each hop invented
   a cross-zone edge the architecture never asked for — including every apparent
   replay -> daemon and utils -> commands dependency. They now name the module
   that declares them. Within-zone hops are left alone; those are a local style
   choice, not a boundary claim.

2. `find`'s three positional/flag checks existed in both daemon entry points with
   hand-repeated messages, and the copy in dispatchFindReadOnlyViaRuntime was
   unreachable — its only caller validates first. Both now call checkFindArgs in
   selectors/find.ts, beside parseFindArgs and isReadOnlyFindAction, for the
   reason that module's own comment already gives: so the two paths cannot
   disagree. The refusal is returned rather than thrown, because the two
   mechanisms are not observationally identical in the session event log.

Type-only spine inversions: 61 -> 35. 4470 unit tests and every gate pass.

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

* feat(layering): ratchet type-only spine inversions (R6)

R5 ignores type-only edges by design — they cost nothing at runtime and do not
affect cold start — so nothing was watching the direction they point. Ranking
them the same way found 61 inversions, including contracts/ and utils/ declared
in terms of rank-4 zones. 26 are fixed by the preceding commits; R6 pins the
rest per zone pair so they can only shrink, and a new pair fails outright rather
than being added to the baseline.

The two remaining clusters each need their own change, and the baseline says so:
the per-command Options/Result vocabulary declared inside the public Node-client
surface, and the ADR 0003 daemon facet shape that core's descriptor registry
composes.

Both ratchet directions are covered: growth fails, and shrinking without
lowering the number fails too, so the baseline cannot quietly stop describing
the tree.

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

* docs: record the import-graph findings behind this refactor

A dated snapshot, not a normative document: when it disagrees with
scripts/layering/, the gate wins. The graph tool that produced it lives on the
claude/depgraph-viewer branch, deliberately out of this change.

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

* refactor(selectors): state the shared selector argument rules once

R2 (commands-floor) forbids the daemon from importing commands/, and that is the
right call: commands/ is the client-side surface — its only consumers are cli/,
cli-schema/, mcp/, client/ and the composition roots — while the daemon is the
executor on the other side of the wire. ADR 0008 protects exactly that seam.
Relaxing R2 would let the executor depend on a client projection and pull CLI
grammar and output formatting into the daemon's bundle.

But the rule does force duplication: the daemon must validate independently
because it accepts requests from any client, so 10 refusal messages existed in
both zones. The only place a shared rule can live is below both, and selectors/
already held the parsers (splitIsSelectorArgs, splitSelectorFromArgs,
isSupportedPredicate) and even the `is` predicate message — just not the checks
that use them.

Three drifts had already appeared in the `is` predicate rule alone:

- commands/interaction/selectors.ts re-implemented the predicate list as an
  inlined seven-way `!==` chain while importing the message and hint from
  selectors/predicates.ts, so adding a predicate to the shared list would not
  have reached the CLI grammar.
- That inlined chain compared the raw token, so the CLI rejected `is TEXT ...`
  while the daemon it hands the command to accepts it. The CLI now matches the
  executor; this is an intentional alignment, not an accident.
- isCommand raised the same refusal without IS_PREDICATE_USAGE_HINT, so whether
  an agent got recovery guidance depended on which layer noticed first — the
  failure mode ADR 0010's audit calls out.

checkIsPredicate, checkIsArgs, checkGetFormat, checkElementTargetArgs and
checkWaitText now hold those rules, each beside the parser it wraps, and report
a refusal rather than choosing how to raise it: the daemon returns a response,
the command surface throws. Those mechanisms are not interchangeable — they
write different session events — so the shared check stays out of that decision.

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

* feat(daemon): give ADR 0014's ref frame one transition, pin SessionState owners

`SessionStore.get()` returns the live record out of a private Map and `set()`
re-puts the same reference, so every `session.<field> = …` in the daemon is a
durable write to store-owned state: 57 of them across 17 files, against 26
`set()` calls that are therefore ceremonial. Nothing at the store boundary can
check what those writes are supposed to keep true.

Measuring which module writes which field showed the problem is narrower than
the raw count suggests — 16 of 27 fields already have exactly one writer. The
sharp case is ADR 0014's ref frame: `refFrameState`, `refFrameScope`,
`refFrameTree` and `refFrameGeneration` must move together or the frame is
incoherent (an `active` state with a stale tree resolves refs against a
namespace nobody authorized), yet complete issuance wrote them in ref-frame.ts
and partial issuance wrote the same four in session-snapshot.ts. ref-frame.ts's
own header claims to be "the single owner of the frame's transitions", and
session-snapshot.ts documented itself as the exception. Both forms now go
through `activateRefFrame`; they differ only in scope.

`recordSession` deliberately moves alone in two paths (recording without arming
a publication), so the save-script cluster gets no invented abstraction — it
gets ownership instead. R7 records every field's owner and stops the set from
growing quietly: a new SessionState field must declare one, a foreign write
fails naming the owner to call, and an owner that stops writing must be removed
so the table cannot drift into fiction. Field names are read out of the
`SessionState` declaration, so a daemon module with an unrelated local named
`session` — a provider or runner session — cannot trip it.

4475 unit tests and every gate pass.

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

* docs: record the reference semantics and refresh the findings

SessionStore.get/set now document that the record is handed out live, since that
is the fact behind R7. The findings snapshot picks up the resolved R2 question,
the ref-frame consolidation and the two new gate scopes.

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

* refactor(boundaries): rank every satellite zone, extract the provider port

Second-order effect of the earlier rounds. With `utils` on the spine and
`(root)` emptied of shared contracts, the eleven zones that were unranked
"because ranking them would invent an order the architecture had not committed
to" turned out to have a consistent rank already — the order was there,
unasserted. Solving the constraint system showed one blocker: `utils/remote-config.ts`
projected a remote-config profile into `CliFlags` while reaching up into
`remote/`, and its only three consumers were in `cli/`. It moves there as
`cli/remote-config-flags.ts`, and every satellite zone joins the spine.

Ranked coverage goes from 730/895 files to 882/895. Only `(root)` stays out, and
now for one stated reason: R2 forbids `daemon/` from importing `commands/`, so
the files that wire them compose the spine from above.

Ranking them exposed 22 type-only inversions R6 had never been able to see, and
they were concentrated rather than scattered:

- The device-provider port. `providers/` and `cloud-webdriver/` implement what
  the daemon calls, so both sides name `DeviceLease`, `LeaseLifecycleProvider`,
  `LeaseLifecycleContext` and `DeviceInventoryProvider` — now declared in
  contracts/device-provider.ts, below both. The adapters also imported the
  daemon's NARROWED `DaemonRequest` while only ever reading `req.flags`; they now
  name the public one from kernel/contracts.
- `MetroPrepareKind` and the remote-config profile field groups move to
  contracts/ for the same reason: the command surface validates them and
  contracts/cli-flags.ts is composed from them.

Two clusters remain, ratcheted with their reasons in TYPE_INVERSION_BASELINE:
the client-types vocabulary, and `SessionAction`, which needs `CommandFlags` and
`DaemonBatchStep` to move with it.

Also fixes two things CI caught: the eight type re-exports my earlier import
redirection orphaned (none published through any src/sdk/* entrypoint, so no
public surface changes) and `isSupportedPredicate`, now module-private since
`checkIsPredicate` is the admission API. `fallow-baselines/health.json` is keyed
by path, so the moved cli-config entry moves with the file rather than being
regenerated.

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

* fix(selectors): use the admitted predicate, not the raw option

Review finding. `isCommand` called `checkIsPredicate` and then kept reading
`options.predicate` for the capture policy, the `exists` branch,
`evaluateIsPredicate`, the failure message and the returned result. Admission
normalizes case, so an upper-case predicate was let past the gate and then
evaluated against lower-case branches: `EXISTS` skipped its own branch and fell
through to the generic path, and the result echoed the raw token. I widened
admission at that surface without threading the normalized value through it —
the CLI-grammar surface in the same change does use the admitted value.

Every decision after admission now reads it.

Two tests, both verified to fail without the fix:

- a production-route regression driving `device.selectors.is` with
  `EXISTS`/`TEXT`, plus one pinning that an unknown predicate is still refused
  WITH the ADR 0010 usage hint;
- a surface parity gate (selectors/__tests__/is-argument-surface-parity.test.ts)
  in the repo's existing parity style, asserting the daemon and CLI-grammar
  surfaces reach the same verdict and hand the same normalized predicate
  downstream across an input table. A helper-only test cannot catch a surface
  that admits correctly and then discards the result, which is what happened
  here.

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

* docs: name the pre-push gate, and the formatter's path allowlist

Both misses in this PR's review were process, not judgement, and the docs
pointed the wrong way for both.

AGENTS.md said "prefer the aggregate package.json scripts" without naming which
aggregate, and CONTRIBUTING listed `pnpm test` and the targeted checks but never
`pnpm check`. `check:tooling` looks like the gate and is a subset of it: it stops
before the Fallow audit, so the dead exports this PR introduced passed a clean
`check:tooling` and failed CI. Both files now name `pnpm check`, say what it
covers, and say what it cannot (the device matrix).

The same gap produced a second mistake twice: `oxfmt <path>` reformats whatever
you point it at, while the repo's `format` script is an allowlist that excludes
`scripts/` and every `.md`. One run reformatted 50 unrelated script files into a
commit; the next nearly did it to AGENTS.md. AGENTS.md now says to run
`pnpm format`, never `oxfmt <path>`.

It also records the rule that cost a CI cycle: Fallow's baselines are keyed by
path, so a renamed file needs its baseline entry moved, not the baselines
regenerated.

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

* revert: undo stray formatter output across docs and scripts

Three separate `oxfmt <path>` runs in this branch reformatted files the repo's
`format` script deliberately excludes: 55 files under scripts/maestro-conformance
plus scripts/perf, sync-mcp-metadata and the slow-test reporter, and 12 markdown
files including six ADRs and docs/agents/. All of it was whitespace, quote style
and markdown table padding — no content — but it inflated the diff a reviewer has
to read and would have rewritten prose ownership across files this change has no
business touching.

All 70 are back to their origin/main content, so the diff outside src/ is now
exactly this change's scope: three docs, scripts/layering, the Fallow baseline,
and five provider integration tests.

The rule this violated is now in AGENTS.md: run `pnpm format`, never
`oxfmt <path>`.

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

* style: reformat two provider tests with the repo's pinned oxfmt

`pnpm format:check` failed in CI on the two files whose imports I merged by hand.
The repo pins oxfmt 0.42.0 as a devDependency and both `format` scripts invoke
`./node_modules/oxfmt/bin/oxfmt`; I had reformatted with `npx oxfmt`, which
resolved 0.60.0, and the two versions disagree about wrapping a 100-column import.

This is the rule AGENTS.md already states — run `pnpm format`, never oxfmt
directly — so there is nothing to add to the docs, only to do.

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

* fix(ci): install deps for the layering guard, and gate the zero-dep contract

The Layering Guard job failed with ERR_MODULE_NOT_FOUND on `oxc-parser`. The job
ran with `install-deps: false` — no `pnpm install`, so no `node_modules` — and R7
had started parsing the daemon with oxc-parser instead of matching assignment
operators with a regex. `pnpm check:layering` passed on every local run, because
locally `node_modules` is always there.

The job now installs dependencies. The alternative was to put R7 back on a regex,
which cannot see `??=` or a computed `session[key] =` write, so it would trade a
correct rule for a fast job.

That leaves the interesting part: the zero-dep contract is real for the jobs that
keep it, and it is invisible to every local run, which is the worst combination a
constraint can have. R8 makes it checkable. It reads the zero-dep job list out of
`.github/workflows/` rather than restating it — declaring a job zero-dep is what
puts it under the rule — walks each job's entry scripts and their whole
relative-import closure, and requires every specifier to be a Node builtin or
another repo file. A zero-dep job whose entry scripts the scan cannot identify
fails too, so the rule cannot be escaped by changing how the job invokes them.

Specifiers come from oxc-parser's module record, not a line scan. The closures
include `--test` files, and a test about imports naturally embeds import syntax in
a fixture string; the line scanner reported two such phantom violations in
model.test.ts before the switch, which is how a gate stops being trusted.

Verified by re-running the real gate against three injected regressions: the
layering job back on `install-deps: false` (reproduces the exact CI failure,
pointing at session-state.ts:24), a package import added to the still-zero-dep
affected-selector closure, and a zero-dep job whose run step names no script.

Also corrects the CONTEXT.md spine paragraph, which still described the satellite
zones as deliberately unranked after they had all joined the ranked spine.

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

* fix(layering): make R7 exhaustive, and follow session records through aliases

Review finding: `SESSION_STATE_FIELD_OWNERS` covered 27 of `SessionState`'s 42
fields and nothing asserted parity, so a new field could be added and pass the
gate by being invisible to it. R7's advertised claim — "every SessionState write
is inside its declared owner" — was broader than what it checked.

Investigating that turned up a second, larger gap the finding did not name: the
scan only recognized a binding literally named `session`. The daemon names these
records by role, so `nextSession`, `provisionalSession`, `completedSession`,
`preRunSession` and `preEntrySession` were all invisible — and three of those
writes were genuine violations R7 existed to catch:

  src/daemon/snapshot-runtime.ts:256  nextSession.snapshotScopeSource
  src/daemon/snapshot-runtime.ts:265  nextSession.snapshotGeneration
  src/daemon/handlers/session-replay-runtime.ts:707
                                      preEntrySession.pendingRecordAndHeal

The first two are the #1076 versioned-ref invariant: the generation advances
exactly when the stored tree is replaced. That rule lived in `setSessionSnapshot`
and had acquired a second statement of itself in snapshot-runtime.ts, whose own
comment admitted the bypass. It now goes through `setSnapshotLineage` in the
owning module. The third clears a watermark stamped by session-replay-resume.ts;
`clearPendingRecordAndHealWatermark` puts the clear beside the stamp.

Gate changes:
- Binding detection accepts aliases, paired with the existing declared-field
  filter so an unrelated `…Session` local only registers if it also writes a
  field SessionState owns — where the remedy is the same anyway.
- `fieldClassificationDrift` asserts parity in all three directions:
  unclassified, in-both, and naming a field SessionState no longer declares.
- `STORE_OWNED_SESSION_STATE_FIELDS` classifies the 11 fields the store
  establishes at construction. It is a positive claim, so a direct write to one
  fails and names both remedies.
- Four fields the widened scan made visible (`lease`, `deviceClaim`, `appName`,
  `saveScriptComplete`) got real owners.

`nextSnapshotGeneration` is now module-private: replacing its only external call
site orphaned the export, which `pnpm check` caught via Fallow.

Verified against three injected regressions: a new SessionState field with no
direct write (the reviewer's exact scenario), a foreign write through an alias
binding, and a direct write to a store-established field. All three rejected.
`pnpm check` green, 4486 unit tests.

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

* docs(daemon): correct the snapshot-lineage claim, and pin the real contract

Device verification of the snapshot-lineage route found that a ref pinned before
a `diff` keeps resolving with no pinned-ref warning. That is the designed ADR
0014 behaviour, not a regression — the comment describing it was wrong, and I
propagated it.

`main`'s comment in snapshot-runtime.ts said a diff "leaves client refs pinned to
the previous generation, which is exactly what the pinned warning diagnoses". The
counter and the authorization epoch are different clocks:

  - `diff` passes `issuesRefsToClient: false`, so it never reactivates the frame;
  - `resolveRefStalenessWarning` compares a pin against the frame EPOCH, not the
    observation counter, and its own comment says why — a capture that bumped the
    counter must not make a valid pin from the issuing frame look stale.

So advancing the counter is not the same as invalidating client refs, and the
observable the comment promised does not exist. I carried the sentence into
`setSnapshotLineage`'s doc when the transition moved, and then into a hardware
verification request, which cost a reviewer a device run against a false claim.

`setSnapshotLineage` itself is unchanged and was a pure move: same expressions,
same inputs as the inline assignments it replaced, so this route behaves exactly
as it does on main.

A comment that contradicts the code should be an assertion instead, so the
contract is now pinned in session-snapshot.test.ts: the diff advances the counter,
preserves the epoch, leaves the pre-diff pin resolving without a warning, and
still warns for a pin from a different frame. Verified to fail when the epoch
comparison is swapped for the counter. A second test covers the keep-current
branch, which had no coverage.

`pnpm check` green, 4488 unit tests.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-27 08:08:34 +02:00
Michał Pierzchała 9a61ac71a1 refactor(cli): decompose runCli into explicit phase functions (#1373)
runCli inlined parse/help short-circuits, binding resolution, remote
auth + materialization, four special-cased command kinds, dispatch, and
a catch block coupled to ~10 closure-mutated let bindings across ~390
lines. Each phase is now a named function over an explicit CliRunContext:
parseCliInputOrExit, resolveRunContextOrExit, runReactDevtoolsCli,
resolveRemoteContext, buildClientConfig, maybeStartDaemonLogTail,
createReplayReporterForTest, dispatchCliCommand, handleRunCliFailure.

The context is mutated in place by resolveRemoteContext so the failure
handler observes exactly the state the throwing phase saw — matching the
previous closure semantics, including the close-with-no-daemon success
path and daemon-log-tail-on-error. Behavior is unchanged; runCli itself
is now ~75 lines of orchestration.
2026-07-23 09:57:02 +02:00
Michał Pierzchała e7c02a9f4c feat: add advisory device claims (#1329)
* feat: add advisory device claims

* fix: preserve advisory claim ownership

* fix: retain claims after incomplete cleanup

* fix: retain claims across pre-open effects
2026-07-20 11:29:59 +02:00
Michał Pierzchała d0227998d4 feat: add daemon stop lifecycle (#1323)
* feat: add daemon stop lifecycle

* fix: harden daemon stop cleanup

* fix: fail closed daemon stop cleanup

* fix: bound daemon shutdown lease releases

* fix: await active shutdown lease release

* fix: release provider leases independently on shutdown
2026-07-17 18:21:06 +02:00
Michał Pierzchała 117f78107e feat: add direct Limrun provider runtime (#1278)
* feat: add direct Limrun cloud runtime

* refactor: reuse Android provider runtime for Limrun

* refactor: pass runner context to provider runtimes

* fix: remove Android gesture swipe fallback

* fix: reconcile Limrun direct runtime with main

* refactor: compose Android provider interactors in core

* fix: satisfy packaged Limrun runtime checks

* perf: load Limrun provider runtime on demand

* docs: document Limrun device cloud flow

* refactor: reuse Android reverse provider for Limrun

* fix: isolate provider-owned iOS sessions

* fix: preserve provider runtime boundaries

* refactor: split close repair lifecycle

* fix: reject unavailable provider leases

* fix: reconcile provider runtime review feedback

* test: stabilize alert deadline smoke assertion

* fix: recover expired provider leases

* fix: limit Limrun to remote simulators

* fix: make Limrun provider cleanup durable

* test: cover Limrun connect through CLI

* fix: make provider expiry recovery durable

* refactor: remove Limrun compatibility cleanup

* fix: release live provider leases on expiry
2026-07-16 19:26:20 +02:00
devin-ai-integration[bot] 0a8ea3a57b refactor: consolidate architecture ownership and client results (#1210)
* refactor: consolidate architecture ownership and client results

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: keep selector parse chunk grouping current

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: update moved architecture breadcrumbs

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: enforce moved selector architecture

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: keep selector guarantee ownership current

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* docs: update selector ownership references

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-11 09:40:24 +02:00
Michał Pierzchała 9a2277c045 fix: alias launch/relaunch to open and suggest canonical commands for unknown names (#1166)
* fix: suggest canonical commands for unknown command names

Agents commonly guess command names that don't exist, e.g. relaunch/launch
instead of `open <app> --relaunch`, burning turns on Unknown command errors
that only say "run --help". Add a curated alias-to-canonical-shape map for
the most common guesses (launch/relaunch/start/restart, touch, input/
settext/entertext, screencap/capture, dismiss), backed by a nearest-name
edit-distance fallback derived from the live command registry so
suggestions can't drift. Also hint that `open` takes the app/bundle id as
a positional when an unknown flag looks like a bundle-id guess (e.g.
--bundle-id), and apply the same suggestion to `help <unknown>`.

Suggestions are display-only; nothing auto-executes and the error code
stays INVALID_ARGS.

* fix: address review — dead export, case-insensitive suggestions, tighter nearest-name matching

- Drop the export on getNearestCommandNames (module-private; only
  suggestCommandFor uses it) to satisfy the Fallow unused-export gate.
- Lowercase the input token before both the curated-map lookup and the
  nearest-name pass, so RELAUNCH/Relaunch/TAP/Touch get the same hint as
  their lowercase forms. Added a curated `tap` entry: lowercase `tap` is
  normalized to press before the unknown-command check, so the entry only
  catches case variants like TAP.
- Tighten the nearest-name fallback: exact prefix matches win outright
  (`clos` now suggests only `close`, not "one of: close, logs"),
  otherwise only ties at the minimum edit distance are kept, and 1-2
  character tokens never get a suggestion (`ls` no longer suggests `is`).
- Share the "open <app> --relaunch" example string between the curated
  map and the unknown-flag hint, and extend the registry-drift tests to
  parse each curated example end-to-end (validates open --relaunch as a
  registered flag) plus assert keyboard dismiss is a real keyboard action.

* feat: promote launch and relaunch to true open aliases

Follow the tap -> press precedent: `relaunch <app>` now runs
`open <app>` with --relaunch injected, and `launch <app>` runs a plain
`open <app>` (no forced restart — that would silently destroy app
state). Both are normalized in normalizeCommandAlias before parsing, so
command identity stays `open` for daemon requests and telemetry, all
other args/flags pass through to open's normal validation (URL targets
still get the daemon's existing --relaunch guidance), and an explicit
--relaunch stays idempotent. Alias matching is now case-insensitive
(TAP, RELAUNCH, Launch), so the curated tap suggestion entry is dead
and removed along with launch/relaunch; start/restart and the rest of
the map stay suggestion-only since start is genuinely ambiguous.
2026-07-09 19:00:41 +02:00
Michał Pierzchała e3566b638a perf: smaller minified output and a lighter eager CLI startup path (#1065)
* build: run SWC minifier compress with 3 passes

Measured -9.7 kB raw / -7.0 kB npm tarball on the emitted JS at no
runtime cost. splitChunks minSize tuning was also evaluated for the
tiny-chunk overhead but measured as a byte-identical no-op (the sub-kB
files are dynamic-import boundaries, not split products), so it is
intentionally not included.

* perf(cli): keep help text, replay reporting, and diff runtime off the eager command path

Every CLI invocation eagerly parsed 599 kB of JS. Three static imports
dragged in code that most commands never run:

- cli.js -> cli-help.js (75 kB of help text) via the usage()/
  usageForCommand() builders in parser/args.ts, which are only needed
  on help and usage-error paths. They now lazy-import cli-help.ts.
- cli.ts/generic.ts -> replay/test/reporting.ts (~17 kB), only needed
  by the test command. Now imported at the call sites.
- cli/commands/screenshot.ts -> createAgentDevice (68 kB client-side
  command runtime chunk incl. screenshot pixel diffing), only needed
  by the diff screenshot branch. Now imported inside diffCommand.

Eager closure of cli.js drops 599 kB -> 431 kB (-28%), ~2.5 ms median
module-load per command invocation. --help/--version fast paths in
bin.ts are unchanged. Package size is unchanged by design (the code
moves to lazy chunks; it does not disappear).
2026-07-04 09:04:32 +02:00
Michał Pierzchała db07c200f4 refactor: centralize known CLI command checks (#1057)
* refactor: centralize known cli command checks

* feat: emit drift diagnostic for registered-but-unhandled commands

Folds #1055's telemetry into this branch: known-command fall-through now
emits cli_known_command_unhandled at error level alongside the distinct
user-facing message, so catalog/dispatch drift is visible in diagnostics
as well as to the user who hits it.
2026-07-03 12:32:52 +02:00
Michał Pierzchała a707348b9b feat: add hosted WebDriver provider support (#948)
* feat: add cloud webdriver artifacts

* fix: clean up local session after provider release failure

* fix: tag cloud webdriver provider requests

* feat: connect hosted webdriver providers

* docs: document hosted provider credentials

* refactor: tighten cloud webdriver provider internals

* refactor: consolidate cloud webdriver provider definitions

* refactor: collapse hosted webdriver runtime wrapper

* refactor: reduce cloud webdriver smell surface

* docs: clarify hosted provider interfaces

* fix: avoid regex slash trimming in webdriver urls

* docs: rename hosted providers to device clouds

* fix: align provider profile imports with remote modules

* fix: skip local android recovery for provider devices

* test: classify cloud provider integration flags

* fix: close active cloud connection session

* test: cover provider disconnect cli flow

* fix: make cloud webdriver sessions launchable

* fix: align cloud webdriver input gestures

* fix: avoid keyboard input during cloud scroll

* fix: constrain cloud webdriver scroll gestures

* refactor: isolate cloud webdriver scroll frame

* refactor: deduplicate cloud webdriver helpers

* refactor: tighten cloud webdriver action types

* fix: harden cloud webdriver release

* fix: polish provider disconnect diagnostics

* refactor: group connection profile helpers

* fix: repair rebased internal paths

* fix: satisfy cloud webdriver CI guards
2026-07-01 13:01:47 +02:00
Michał Pierzchała f4882bc706 feat: support live replay test reporters (#959)
* feat: support live replay test reporters

* refactor: simplify replay progress readers

* fix: preserve verbose replay reporter progress

* feat: expose semantic replay reporter hooks

* refactor: trim replay reporter context

* refactor: trim reporter progress internals

* refactor: move replay test reporting under replay

* refactor: make live replay reporter hooks synchronous and simplify dispatch

Live reporter hooks (onSuiteStart/onTestStart/onTestStep/onTestResult)
were typed as `void | Promise<void>` but fired from the synchronous daemon
progress stream reader without being awaited, so a stateful async reporter
could receive onSuiteEnd before its live work settled. Type them as `void`
to make the contract honest; onSuiteEnd stays awaited for async flushing.

A returned promise from a misbehaving custom JS reporter is still caught so
it cannot crash the CLI with an unhandled rejection, but it is documented as
unsupported and not awaited.

Collapse the four near-identical per-event hook dispatch branches into a
single table-driven path, and document the synchronous-hook and
exit-code-escalation contracts. Add a regression test covering a throwing
live hook.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-30 21:10:22 +02:00
Michał Pierzchała bcf910a2bb refactor: split daemon client driver into daemon/client/ — Phase 5 (#962)
Move the daemon CLIENT driver (the in-process side that sends requests to a
running daemon) out of the src/ root into src/daemon/client/, per
plans/perfect-shape.md §5.5 ('daemon/client/ <- daemon-client*.ts'; the
daemon- prefix co-located client driver + server bootstrap at src root).

Files moved (7): daemon-client{,-lifecycle,-metadata,-progress,-rpc,-timeout,
-transport}.

- git renames; 19 importers repointed via the resolve-based codemod
  (intra-set stays ./, kernel -> ../../, daemon/remote deps recomputed)
- Layering Guard verified: none import src/commands/* (safe under src/daemon/)
- not a public export; no rslib impact
- update fallow-baselines/health.json keys

Behaviorless path codemod; typecheck/lint/format/build/tests green.
2026-06-30 15:51:53 +02:00
Michał Pierzchała 189b062519 refactor: extract client/ folder — Phase 5 (#960)
Move the SDK client + companion-tunnel cluster out of the src/ root into a
dedicated src/client/ folder, per plans/perfect-shape.md §5.5 (~8k LOC
client/remote unfoldered at src root; remote/ already extracted in #951).

Files moved (9): client, client-types, client-shared, client-normalizers,
client-companion-tunnel{,-contract,-worker}, client-react-devtools-companion,
companion-tunnel.

- git renames; 68 importers repointed via the resolve-based codemod
  (intra-client stays ./, staying deps recomputed)
- companion-tunnel.ts keeps rslib key 'internal/companion-tunnel' so dist
  output stays dist/src/internal/companion-tunnel.js (public subpath
  byte-identical; verified by build)
- update non-src importers (3 integration tests, vitest coverage include),
  .fallowrc.json entrypoint, fallow-baselines/health.json keys

backend.ts and daemon-client*.ts are intentionally left for follow-up
(daemon/client split). Behaviorless path codemod; all gates green.
2026-06-30 14:36:59 +02:00
Michał Pierzchała 65227c6719 refactor: absorb CLI parser into cli/parser/ — Phase 5 (#958)
Move the CLI argument/flag/help parser out of utils/ into a dedicated
src/cli/parser/ folder, per plans/perfect-shape.md §5.5 (utils/ hosts a 3k
CLI parser among its buried subsystems).

Files moved (3): args, cli-flags, cli-help (args->cli-help intra-set import
stays relative).

- git renames; importers repointed via the resolve-based codemod
  (64 importers; staying-utils/kernel deps recomputed to ../../)
- no public-export/rslib impact
- update scripts/integration-progress-model.ts import + fallow-baselines/
  health.json keys (args incl. :high impact variant)

Behaviorless path codemod. typecheck/lint/format/build/tests green;
integration-progress model still runs.
2026-06-30 13:56:04 +02:00
Michał Pierzchała 548bf0c47c refactor: extract remote/ folder — Phase 5 (#951)
Move the remote/proxy/upload subsystem out of the src/ root cluster into a
dedicated src/remote/ intent folder, per plans/perfect-shape.md §5.5:

  daemon-proxy · daemon-artifacts · upload-client(-artifact) · remote-config
  · remote-config-core · remote-config-schema · remote-connection-state

- 8 files moved (git renames); imports repointed via a resolve-based codemod
  (path.relative recomputation — correctly distinguishes the root remote-config
  from the unrelated src/utils/remote-config.ts)
- rslib entry keeps key 'remote-config' so dist output stays
  dist/src/remote-config.js; public 'agent-device/remote-config' byte-identical
- update .fallowrc.json entrypoint + fallow-baselines/health.json keys +
  vitest.config.ts coverage include + the integration test import paths

Behaviorless path codemod. typecheck/lint/build/fallow/tests all green.
Stacked on #950 (contracts→kernel).
2026-06-30 11:47:39 +02:00
Michał Pierzchała b6a9bb2186 refactor: move contracts.ts into src/kernel — Phase 5 (#950)
Relocate the central contracts barrel into the kernel/ dependency sink
alongside device/errors/redaction/snapshot (kernel now owns the pure
domain types per plans/perfect-shape.md §5.5).

- src/contracts.ts -> src/kernel/contracts.ts (git rename)
- repoint all 44 internal importers to ../kernel/contracts.ts
- rslib entry keeps key 'contracts' so dist output stays dist/src/contracts.js;
  the public 'agent-device/contracts' subpath is byte-identical (proven by the
  metro precedent in #947 and verified via build + package-exports test)
- update .fallowrc.json entrypoint + fallow-baselines/health.json key

Behaviorless path codemod (49 files, +57/-57). typecheck/lint/build/fallow
audit/public-contract tests all green.
2026-06-30 11:34:16 +02:00
Michał Pierzchała 43d0a639d0 feat: leveled response views + --level knob with snapshot digest — Phase 4 (#942)
* feat: leveled response views + --level knob, with a snapshot digest — Phase 4

Add the agent-cost leveled-response system: a responseLevel knob
(digest | default | full) plumbed end to end behind a global --level flag
(mirroring --cost), and a per-command ResponseView registry applied in the
router on the success path.

- contracts: RESPONSE_LEVELS/ResponseLevel + meta.responseLevel + boundary
  schema whitelist. Plumbing mirrors --cost: cli-flags FlagDefinition +
  GLOBAL_FLAG_KEYS, AgentDeviceClientConfig + overrides, buildClientConfig,
  buildMeta. ResponseLevel exported from the public root.
- src/daemon/response-views.ts: the ResponseView registry. Seeds the snapshot
  digest — the full node tree (the dominant token sink) collapses to
  { nodeCount, refs: first 12 hittable/non-occluded refs with labels } plus the
  cheap top-level signals (truncated/visibility/snapshotQuality). full returns
  today's shape (nothing richer is computed yet).
- router graft (applyResponseLevelView + applyAgentCostGrafts): composes with
  the existing cost block. With responseLevel default (or unset) AND no
  registered view AND no --cost, the original response is returned UNCHANGED —
  byte-identical to today (Maestro .ad recompare safe). cost.nodeCount reads the
  original node tree so it stays accurate even after a digest.

Tests: snapshot view unit test (digest filters hittable/occluded, drops the
tree, keeps cheap signals; default/full passthrough); router graft test via an
injected view (default identity byte-identical, digest applies, full passthrough,
digest+cost composition, unregistered-command passthrough, boundary parse).

Verified: tsc, oxfmt + oxlint --deny-warnings, fallow audit clean, rslib build,
Layering Guard empty, 1106 daemon/contracts/client tests pass (incl. the
existing cost/typed-error grafts after the restructure).

* fix: repoint MCP output-schemas import to kernel/device (rebase fixup)

The kernel move (#940) deleted src/utils/device.ts; #941's
command-output-schemas.ts (merged after #940's codemod ran) still imported the
old path. Same one-line fix as #943; de-dups once that lands.

* fix: re-classify responseLevel flag in integration-progress model

The --level/responseLevel flag is a diagnostics/output flag (not device-
observable), classified in the exclusion bucket alongside --cost. (Lost in an
earlier rebase; re-applying.)
2026-06-30 07:53:28 +02:00
Michał Pierzchała 7a1640e53f refactor: move errors/redaction/device into src/kernel — Phase 5 slice 3 (#940)
* refactor: move errors/redaction/device into src/kernel — Phase 5 slice 3

Relocates the foundational primitive trio from src/utils/ into the kernel/ layer
(joining snapshot.ts from slice 2), per the target folder DAG in
plans/perfect-shape.md §5.5. A pure path codemod, no behavior change.

They form a closed cluster — device -> errors -> redaction, with redaction a
leaf — so kernel/ takes no upward dependency, and every importer becomes a clean
downward import toward kernel. errors.ts is the most-imported module in the
tree; device.ts the §5.5-named headliner. Moving all three atomically avoids a
half-state where one would import another across the utils/kernel boundary.

Imports rewritten by a resolve-based codemod (compares each specifier's resolved
path to the moved files, so the unrelated commands/management/device.ts and
other same-named files are untouched): 483 sites across 402 files. The two
platform-descriptor doc comments and the fallow health baseline key for
device.ts are updated to the new path; the contracts-schema-public guard that
asserts the error helpers pull no diagnostics/node: deps now reads kernel/.

Verified: tsc --noEmit, oxfmt + oxlint --deny-warnings, rslib build, full vitest
suite (2877 pass), fallow audit clean (411 changed files), Layering Guard empty;
kernel/ files import only within kernel.

* docs: update guidance references to kernel/{device,errors} after the move

AGENTS.md (Apple-family sync rule + normalizeError), ADR-0009, and
plans/apple-platform-consolidation.md still named the old src/utils/ paths.
Point them at src/kernel/. plans/perfect-shape.md's utils/device.ts mention is
left as-is — it describes the pre-move diagnosis.
2026-06-30 07:25:02 +02:00
Michał Pierzchała 9dc07cc56e perf: reuse Apple runner cache across version bumps (#900)
* perf: reuse Apple runner cache across version bumps

* perf: remove unused Apple runner symbols

* perf: keep Swift runner unit tests out of runtime builds

* perf: skip Apple runner asset catalog in runtime builds

* perf: use concrete simulator for xcuitest script builds

* perf: show cold Apple runner startup progress

* perf: prewarm Apple runner cache during simulator boot

* refactor: dedupe Apple runner option plumbing
2026-06-29 15:43:15 +02:00
Michał Pierzchała 62f923dec4 feat: opt-in agent-cost wallClockMs behind --cost — Phase 4 slice 1 (#922)
* feat: opt-in agent-cost wallClockMs behind --cost

Add per-command wall-clock latency as a purely additive, opt-in response
field (cost.wallClockMs) gated behind a new global --cost flag.

The flag plumbs end to end mirroring --debug: cli-flags definition +
GLOBAL_FLAG_KEYS, AgentDeviceClientConfig/overrides, buildClientConfig,
buildMeta (meta.includeCost), the DaemonRequestMeta contract, and the
boundary parse in daemonCommandRequestSchema so it survives the HTTP edge.

The graft lives in request-router handleRequest (the seam that owns the
outer wall-clock incl. lock + execute + finalize). It mirrors the
conditional registerDownloadableArtifacts spread: when --cost is off OR the
response is an error, the response is returned untouched. Only on an opted-in
successful response is cost appended, so the default serialized DaemonResponse
is byte-identical to today (Maestro .ad recompare safe). Proven by the parity
test (flag-off identity, flag-on additive-only, error path, boundary survival).

Additive / semver-minor. MCP exposure and richer signals (roundTrips,
nodeCount) are deferred to follow-up slices.

* test: classify --cost flag as outside provider-backed integration

The integration progress guard (test:integration:progress:check) treats every
public CLI flag as either device-observable (requiring provider-backed coverage)
or intentionally excluded. --cost is a diagnostics/output flag (a purely additive
response field, not device-observable), so it joins json/help/version/verbose in
the 'config, output, diagnostics, and transport' exclusion bucket.
2026-06-29 13:50:12 +02:00
Michał Pierzchała a822325375 feat: add integrated device leasing (#890)
* feat: add integrated device leasing

* fix: keep metro bearer token out of generated proxy profile

The proxy connect profile is written to disk as a non-secret remote config,
but it unconditionally copied `metroBearerToken` into that file, leaking the
secret at rest. Mirror the cloud path, which keeps `daemonAuthToken` in-memory
only: the token still flows through this connect via the returned flags, and
later commands re-supply it via AGENT_DEVICE_METRO_BEARER_TOKEN. Extend the
non-secret-profile test to assert the bearer token is absent from disk.

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

* fix: always release device lease on session close

releaseSessionLease + sessionStore.delete ran only on the happy path, after
several awaits (app-log/perf/snapshot teardown, platform close dispatch,
runner stop) that can throw. A failed close therefore stranded the device
lease until the inactivity expiry. Wrap teardown in try/finally so ownership
is always freed; the original error still propagates after finally.

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

* fix: reconcile integrated device leasing

* docs: simplify remote lease guidance

* refactor: satisfy leasing fallow checks

* fix: harden integrated device leasing

* refactor: deepen device lease lifecycle

* refactor: centralize lease scope projection

* fix: harden proxy lease e2e flow

* fix: address lease review feedback

* refactor: tighten lease release cleanup

* fix: simplify proxy startup output

* fix: harden cloud lease identity

* fix: color proxy startup output

* fix: simplify proxy tunnel placeholder

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-26 19:37:08 +02:00
Michał Pierzchała 8c2b1adeb1 feat: support agent-cdp remote bridge sessions (#878) 2026-06-26 12:51:21 +02:00
Michał Pierzchała be1e1c9931 feat: add cdp command agent-cdp passthrough (#873)
* feat: add agent-cdp passthrough

* docs: narrow agent-cdp memory guidance

* chore: pin agent-cdp 1.6.0

* test: cover agent-cdp guidance

* fix: preserve agent-cdp passthrough flags

* fix: expose CDP wrapper as cdp

* docs: move CDP workflow to debugging guide

* docs: mention cdp in command reference
2026-06-25 21:44:47 +02:00
Michał Pierzchała d47cd30117 feat: add agent-device proxy command (#844) 2026-06-23 17:20:08 +02:00
Michał Pierzchała d05cfc727b feat: manage web backend setup (#833) 2026-06-20 11:44:00 +02:00
Michał Pierzchała afd413021a feat: improve maestro test reporter (#811) 2026-06-16 16:51:47 +01:00
Michał Pierzchała a35c444df6 feat: add perf memory diagnostics (#759)
* feat: add perf memory diagnostics

* fix: address perf memory CI findings

* fix: address perf memory review feedback

* docs: explain perf memory diagnostics benefits
2026-06-11 14:58:09 +02:00
Michał Pierzchała 5462d516d9 chore(daemon): takeover notice, dev state-dir pruning, session state-dir command surface (#754)
* chore(daemon): takeover notice, dev state-dir pruning, session state-dir command surface

Implements the three follow-ups from #737:

1. Print a one-line stderr notice when the client replaces a running
   daemon, stating identity and reason (version mismatch, code-signature
   mismatch, or unreachable). Best effort; never fails the command.

2. Add 'pnpm clean:daemon --prune-dev' to remove worktree-scoped state
   dirs under ~/.agent-device/dev/ that no live daemon owns (same
   pid/start-time liveness check as server-lifecycle) and that have been
   idle for 14+ days. Scoped dirs only; one line printed per removal.

3. Fold 'session state-dir' into the regular command surface: the
   session contract resolves it locally via the new
   client.sessions.stateDir(), the cli.ts pre-dispatch special case is
   removed, and the MCP session tool now exposes the state-dir action.

Closes #737

https://claude.ai/code/session_013WBrUjQ4WRxRkfVruALKX3

* docs: surface clean:daemon --prune-dev in AGENTS.md

Local agents discover daemon state-dir hygiene through AGENTS.md, not
the website docs, so document the prune flag next to the existing
worktree-scoped state-dir guidance.

https://claude.ai/code/session_013WBrUjQ4WRxRkfVruALKX3

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-11 07:41:34 +02:00
Michał Pierzchała 0f7187f543 fix: scope source daemon state by worktree (#719)
* fix: scope source daemon state by worktree

* docs: clarify worktree daemon state tradeoffs

* ci: harden Apple runner cache

* chore: keep daemon state helper internal

* ci: validate Apple runner cache restores

* ci: simplify Apple runner cache setup
2026-06-10 10:32:58 +02:00
Michał Pierzchała 491ad7e922 fix: improve session ownership and recovery guidance (#674)
* fix: improve session recovery guidance

* fix: quote session recovery commands

* test: simplify record session cleanup fixtures

* test: cover recovery hint edge cases

* test: trim duplicate recovery quoting coverage

* fix: scope implicit sessions by caller workspace

* fix: surface session state directory

* fix: remove unused session store check

* test: trim duplicate session recovery coverage
2026-06-02 14:06:00 -07:00
Michał Pierzchała 4f8e0af898 fix: improve maestro test output (#647) 2026-06-01 18:23:41 +02:00
Michał Pierzchała 8e1f8a9f8f perf: lazy load daemon handlers and report bundle size (#608)
* perf: lazy load daemon handlers

* perf: thin command metadata paths

* refactor: isolate platform inventory loading

* refactor: trim lazy loading cleanup

* test: guard daemon routing metadata drift

* ci: report startup timing with size
2026-05-30 16:49:21 +02:00
Michał Pierzchała 99e97c873e feat: stream replay test progress (#605)
* feat: stream replay test progress

* fix: format replay test progress duration

* fix: harden replay progress transports

* refactor: clean up replay progress fallow findings

* refactor: trim replay progress cleanup
2026-05-28 17:54:44 +02:00
Michał Pierzchała 136d313a19 build: enable noUncheckedIndexedAccess (#600)
* build: enable noUncheckedIndexedAccess

* refactor: remove redundant indexed access fallbacks

* fix: address rebased unchecked indexed access

* fix: address unchecked access review feedback

* refactor: simplify replay fill positional typing

* refactor: avoid wait positional assertion

* refactor: prefer value guards for indexed access

* chore: keep fallow baseline for strictness changes
2026-05-28 11:02:43 +02:00
Michał Pierzchała 819d7dc8aa feat: expose structured MCP command tools (#593)
* feat: expose semantic MCP tools

* docs: remove semantic mcp prd

* refactor: deepen semantic command surface

* refactor: add mcp execution seam

* refactor: deepen command grammar

* refactor: remove legacy command definitions

* refactor: collapse semantic cli wrappers

* refactor: remove local mcp placeholders

* refactor: derive semantic cli routing

* refactor: trim mcp status metadata

* refactor: derive semantic input contracts

* refactor: split semantic grammar modules

* refactor: derive batch input schema

* refactor: centralize cli command schema catalog

* refactor: share semantic cli output projections

* refactor: remove legacy cli output paths

* refactor: consolidate command interface surface

* docs: align command contract wording

* refactor: split command projection from cli grammar

* refactor: trim projection exports

* fix: satisfy fallow command contract audit

* refactor: structure public batch steps

* chore: clean batch architecture references

* fix: keep legacy cli batch steps working

* fix: serialize mcp batches

* chore: tighten command surface cleanup

* fix: serialize mcp stdin requests

* chore: keep mcp config out of command contracts

* fix: project structured batch targets

* chore: harden command input typing

* fix: project maestro backend for replay tests

* fix: preserve session mcp request options
2026-05-28 09:20:55 +02:00
Michał Pierzchała 068d4c5d0a chore: remove ensure-simulator lifecycle command (#552) 2026-05-15 19:23:45 +02:00
Michał Pierzchała 7e14decce3 fix: improve Android text entry stability (#540)
* fix: improve android text entry stability

* fix: stabilize android daemon diagnostics

* chore: refresh fallow baselines

* refactor: tighten android text fallback

* fix: tighten android input ownership diagnostics

* fix: require settled android fill verification

* test: cover android fill prefix verification
2026-05-15 12:16:06 +02:00
Michał Pierzchała 10fd292569 fix: stabilize remote DevTools and install workflow (#490)
* 0.14.6

* fix: harden remote install workflow

* fix: prevent companion startup state race

* fix: guide remote ios devtools reconnects
2026-05-01 07:27:36 -04:00
Michał Pierzchała 6a2bf9f626 feat: export batch orchestration helpers (#472) 2026-04-29 10:48:17 -04:00
Michał Pierzchała 40fe5e23cc fix: improve cloud remote auth UX (#452) 2026-04-26 18:42:45 -04:00
Michał Pierzchała f926d2c9e1 feat: add cloud remote auth flow (#443) 2026-04-25 20:25:55 -04:00
Michał Pierzchała 6c7e8323e1 fix: support remote Android React DevTools tunnel (#438)
* 0.13.0

* fix: support remote android react devtools tunnel
2026-04-25 19:30:12 -04:00
Michał Pierzchała b0813cad84 feat: add react devtools passthrough (#435)
* feat: add react devtools passthrough

* docs: clarify react devtools passthrough

* fix: preserve react devtools global flags

* docs: add react devtools validation guidance

* docs: simplify react devtools validation notes
2026-04-24 18:54:14 -04:00
Michał Pierzchała 33083b5a86 chore: codebase cleanup pass (dedup, types, dead code, cycles) (#420)
* refactor: remove dead code and unused exports

- Delete interaction-get.ts, interaction-is.ts, interaction-selector.ts (superseded by selector-runtime dispatchers)
- Remove handleWaitCommand and its private helpers from snapshot-wait.ts (replaced by dispatchWaitViaRuntime)
- Remove unused distanceFromSafeViewportBand/isRectWithinSafeViewportBand from rect-visibility
- Trim Linux platform barrel to only the re-export still in use (snapshotLinux)

* refactor: consolidate duplicated helpers

- Move sleep() to utils/timeouts.ts; remove 6 duplicate implementations
- Use existing isApplePlatform() helper for ios||macos checks (5 sites)
- Export trimRuntimeValue from runtime-hints; drop duplicate trimRuntimeString
- Merge normalizeTextSurfaceType/normalizeType into single text-surface helper
- Remove pointless isScrollableContainerType wrapper

* refactor: consolidate accidentally-duplicated type definitions

- SnapshotDiffLine/Summary: single definition in utils/snapshot-diff, re-exported from capture-snapshot
- FindLocator: single definition in utils/finders, re-exported from client-types
- JsonRpc envelope in http-server now uses JsonRpcRequestEnvelope/JsonRpcId from contracts
- Inline 'primary'|'secondary'|'middle' literals replaced with ClickButton (internal sites only)

* refactor: strengthen weak types and fix DEVICE_IN_USE downgrade bug

- Add toAppErrorCode() validator and DEVICE_IN_USE to AppErrorCode union
- Replace 'as any' casts on wire error codes with the validator (daemon-error, daemon-client, request-router, http-server)
- Type Metro worker payload as MetroTunnelResponseMessage
- Type xctestrun plist parsing with explicit partial schema
- Narrow 'details.stderr' via typeof checks on Android error paths
- Type runner-session parseRunnerResponse with RunnerResponsePayload

Bug fix: handler emitted 'DEVICE_IN_USE' but router cast silently downgraded to
'COMMAND_FAILED' because the code was missing from AppErrorCode. Clients can now
react to same-device contention.

* refactor: collapse redundant 'ignore' comments in empty catch blocks

- Replace 16 instances of 3-line catch { // ignore } with 1-line catch {}
- Remove one self-describing 'Re-export public API' comment

Specific 'ignore shutdown races' / 'ignore malformed pid files' style comments
that name concrete failure modes are kept.

* refactor: eliminate circular dependencies by extracting shared types to leaves

Resolves all 44 cycles reported by madge. Pattern throughout: extract shared
type into a leaf module; both producer and consumer import from the leaf;
original module re-exports for API stability.

New leaf type modules:
- src/runtime-contract.ts (AgentDeviceRuntime, CommandContext, ...)
- src/metro-types.ts (MetroRuntimeHints, MetroBridgeResult, ...)
- src/commands/runtime-types.ts (CommandResult, RuntimeCommand, ...)
- src/commands/diagnostics-types.ts
- src/cli/commands/router-types.ts (ClientCommandParams, ...)
- src/core/interactor-types.ts (Interactor, BackMode, ...)
- src/platforms/ios/runner-session-types.ts (RunnerSession)
- src/utils/screenshot-diff-region-types.ts (MutableDiffRegion)
- src/daemon/handlers/record-trace-types.ts

madge --circular now reports 0 cycles (was 44). No runtime behavior changes.

* fix: preserve wire error codes verbatim (addresses codex review)

The initial weak-types pass validated wire error codes against a closed
union, silently downgrading any unknown code to COMMAND_FAILED. This
dropped signals like AMBIGUOUS_MATCH that handlers emit and clients are
documented to handle (skills/agent-device/references/exploration.md).

- Widen AppErrorCode to 'KnownAppErrorCode | (string & {})' so autocomplete
  of known codes is preserved while any wire code flows through
- toAppErrorCode now preserves any non-empty code; fallback only when
  undefined or empty
- Add AMBIGUOUS_MATCH to KnownAppErrorCode (documented public code)
- Add test coverage for preservation and fallback behavior

* refactor: address review follow-ups

- Add DEVICE_IN_USE to the batch error taxonomy in exploration.md (now
  observable by clients after earlier fix, needs agent-facing guidance)
- Delete one-line src/platforms/linux/index.ts barrel; both consumers
  (core/dispatch, daemon/handlers/snapshot-capture) now import from
  platforms/linux/snapshot directly
- Replace inline { tenantId; runId; leaseId } shapes with MetroBridgeScope
  alias at client-types, metro, and cli/commands/connection-runtime
- Consolidate remaining inline setTimeout wrappers onto utils/timeouts.ts#sleep
  (12 files, ~18 sites). Left test files and the runtime-clock aware helper
  in commands/selector-read-utils alone. Also removes the local sleepMs
  helper from daemon-client.ts.

* refactor: address low-priority review follow-ups

- daemon-client RPC error path: stringify any non-null data.code instead
  of only forwarding strings. Preserves numeric codes from hypothetical
  future proxies/servers; for first-party daemon today this is a no-op
  since handlers already emit strings.
- errors.ts: expand AppErrorCode comment to call out the exhaustiveness
  tradeoff of the '(string & {})' widening for SDK consumers.
2026-04-17 13:22:46 +02:00
Michał Pierzchała 63ab9d625c fix: propagate remote config through remote flows (#422)
* fix: propagate remote config through commands

* fix: complete remote React Native flow

* test: cover remote install source lease reuse

* fix: honor remote config session scope

* refactor: drop remote react native shortcut

* docs: simplify remote tenancy skill flow

* docs: use trusted artifact placeholders

* docs: prune remote tenancy prompt note
2026-04-17 12:26:42 +02:00
Michał Pierzchała 1b71dfee4c fix: defer remote connect lease and metro setup (#417) 2026-04-16 17:53:04 +02:00
Michał Pierzchała 47a1529e98 feat: add remote connect lifecycle (#407) 2026-04-13 14:11:40 +02:00
Michał Pierzchała 8e42f2e654 fix: harden public API boundaries (#399) 2026-04-11 21:01:09 +02:00
Michał Pierzchała ffd472a5bf feat: expose typed client command methods (#380) 2026-04-11 16:24:16 +02:00
Michał Pierzchała 20021c3d1c refactor: reduce duplication and simplify codebase (#374)
* refactor: reduce duplication and simplify codebase

Eliminate copy-pasted functions, inline error boilerplate, and repeated
patterns across daemon handlers and platform modules.

Key changes:
- Deduplicate isEnvTruthy, displayNodeLabel, roundPercent into single sources
- Extract throwDaemonError helper for client/CLI daemon response errors
- Add sessionNotFoundResponse/unsupportedOperationResponse helpers and
  adopt errorResponse() across ~30 handler files (-565 lines)
- Unify BATCH_PARENT_FLAG_KEYS/REPLAY_PARENT_FLAG_KEYS into shared
  mergeParentFlags helper in handler-utils
- Extract createLinuxToolResolver for screenshot/clipboard tool detection
- Remove stale type-sync comments from contracts.ts and metro.ts

* refactor: deeper structural simplification pass

Bigger wins from restructuring, not just mechanical dedup:

- cli.ts: move logTailStopper to try/finally (eliminates 28 duplicate
  calls), extract writeCommandCliOutput/writeLogsCliOutput/writeNetworkCliOutput
  from 350-line if/else chain, fix remaining throwDaemonError site
- session-store.ts: replace 67-line sanitizeFlags destructure/reconstruct
  with 10-line pick-from-array loop
- record-trace: extract finalizeRecordingOverlay helper, replacing 4
  copies of the telemetry+overlay block across ios/android/recording files
- Deduplicate normalizeText (finders.ts + selectors-match.ts)
- Fix isEnvTruthy to preserve whitespace-tolerant parsing (.trim())

* fix: ensure logTailStopper runs before process.exit

process.exit() does not unwind the stack, so finally blocks are
skipped. Restore explicit logTailStopper() calls before each
process.exit() to prevent leaking the background daemon log tail
process. The finally block remains as a safety net for normal
return paths.

Also refactor writeCommandCliOutput to return an exit code instead
of calling process.exit() directly, keeping the exit decision in
the caller where cleanup is visible.

* refactor: simplify daemon failure responses

* refactor: remove redundant daemon response cast
2026-04-11 12:53:53 +02:00