@swmansion/argent 0.21.0 → 0.21.1-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
Binary file
Binary file
Binary file
@@ -98753,6 +98753,19 @@ var ARGENT_BOOTSTRAP_DYLIB_BASENAMES = /* @__PURE__ */ new Set([
98753
98753
  "libArgentInjectionBootstrap.dylib",
98754
98754
  "libInjectionBootstrap.dylib"
98755
98755
  ]);
98756
+ function processCarriesInjection(env, endpoint) {
98757
+ const inserted = [...ARGENT_BOOTSTRAP_DYLIB_BASENAMES].some((name) => env.includes(name));
98758
+ if (!inserted) return false;
98759
+ const expected = endpoint.transport === "tcp" ? `NATIVE_DEVTOOLS_IOS_CDP_PORT=${endpoint.port}` : `NATIVE_DEVTOOLS_IOS_CDP_SOCKET=${endpoint.socketPath}`;
98760
+ return env.split(/\s+/).includes(expected);
98761
+ }
98762
+ var PS_PROBE_TIMEOUT_MS = 5e3;
98763
+ function parsePsElapsedSeconds(etime) {
98764
+ const match = etime.trim().match(/^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/);
98765
+ if (!match) return null;
98766
+ const [, days, hours, minutes, seconds] = match;
98767
+ return Number(days ?? 0) * 86400 + Number(hours ?? 0) * 3600 + Number(minutes) * 60 + Number(seconds);
98768
+ }
98756
98769
  function splitDyldInsertLibraries(value) {
98757
98770
  return value.split(":").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
98758
98771
  }
@@ -98861,17 +98874,20 @@ async function setupNativeDevtoolsEnvRemote(udid, endpoint) {
98861
98874
  }
98862
98875
  await setSimulatorEnv(udid, "NATIVE_DEVTOOLS_IOS_CDP_PORT", String(endpoint.port));
98863
98876
  }
98864
- function parseUIKitApplicationBundleIds(stdout) {
98865
- const bundleIds = /* @__PURE__ */ new Set();
98877
+ function parseUIKitApplicationJobs(stdout) {
98878
+ const jobs = /* @__PURE__ */ new Map();
98866
98879
  for (const line of stdout.split("\n")) {
98867
98880
  const match = line.match(/UIKitApplication:([^[]+)/);
98868
- if (match) {
98869
- bundleIds.add(match[1].trim());
98870
- }
98881
+ if (!match) continue;
98882
+ const pid = line.match(/^(\d+)\s/);
98883
+ jobs.set(match[1].trim(), pid ? Number(pid[1]) : null);
98871
98884
  }
98872
- return bundleIds;
98885
+ return jobs;
98873
98886
  }
98874
- async function listRunningUIKitApplicationBundleIds(udid) {
98887
+ function parseUIKitApplicationBundleIds(stdout) {
98888
+ return new Set(parseUIKitApplicationJobs(stdout).keys());
98889
+ }
98890
+ async function listRunningApps(udid) {
98875
98891
  const { stdout } = await execFileAsync8(
98876
98892
  "xcrun",
98877
98893
  await simctlArgsForUdid(udid, ["spawn", udid, "launchctl", "list"]),
@@ -98881,7 +98897,42 @@ async function listRunningUIKitApplicationBundleIds(udid) {
98881
98897
  killSignal: SIMCTL_KILL_SIGNAL
98882
98898
  }
98883
98899
  );
98884
- return parseUIKitApplicationBundleIds(stdout);
98900
+ return stdout;
98901
+ }
98902
+ async function listRunningUIKitApplicationBundleIds(udid) {
98903
+ return parseUIKitApplicationBundleIds(await listRunningApps(udid));
98904
+ }
98905
+ async function readProcessLaunchState(pid) {
98906
+ let stdout;
98907
+ try {
98908
+ ({ stdout } = await execFileAsync8(PS_BIN, ["eww", "-p", String(pid), "-o", "etime=,command="], {
98909
+ encoding: "utf8",
98910
+ timeout: PS_PROBE_TIMEOUT_MS,
98911
+ // Matches the other `ps` probes (vega-process.ts). An environment can run
98912
+ // to `kern.argmax` (1 MiB), exactly Node's default cap, so the default
98913
+ // would ENOBUFS on a maximal one instead of reading it.
98914
+ maxBuffer: 16 * 1024 * 1024
98915
+ }));
98916
+ } catch (err) {
98917
+ process.stderr.write(`[ios-host] ps probe failed for pid ${pid}: ${String(err)}
98918
+ `);
98919
+ return null;
98920
+ }
98921
+ const trimmed = stdout.trim();
98922
+ const boundary = trimmed.search(/\s/);
98923
+ if (boundary === -1) return null;
98924
+ const ageSeconds = parsePsElapsedSeconds(trimmed.slice(0, boundary));
98925
+ if (ageSeconds === null) return null;
98926
+ return { pid, ageMs: ageSeconds * 1e3, env: trimmed.slice(boundary + 1) };
98927
+ }
98928
+ async function inspectRunningAppLocal(udid, bundleId) {
98929
+ const jobs = parseUIKitApplicationJobs(await listRunningApps(udid));
98930
+ if (!jobs.has(bundleId)) return { running: false, process: null };
98931
+ const pid = jobs.get(bundleId) ?? null;
98932
+ const process3 = pid === null ? null : await readProcessLaunchState(pid);
98933
+ if (process3 !== null) return { running: true, process: process3 };
98934
+ const stillListed = parseUIKitApplicationJobs(await listRunningApps(udid)).has(bundleId);
98935
+ return { running: stillListed, process: null };
98885
98936
  }
98886
98937
  function spawnAxDaemonLocal(udid, endpoint) {
98887
98938
  const binaryPath = endpoint.transport === "tcp" ? axServiceBinaryPathTcp() : axServiceBinaryPath();
@@ -98931,6 +98982,7 @@ var localIosHost = {
98931
98982
  requiresTcp: false,
98932
98983
  setupNativeDevtoolsEnv: setupNativeDevtoolsEnvLocal,
98933
98984
  listRunningBundleIds: listRunningUIKitApplicationBundleIds,
98985
+ inspectRunningApp: inspectRunningAppLocal,
98934
98986
  async bootstrapAx(udid) {
98935
98987
  await ensureAutomationEnabled(udid);
98936
98988
  return { entitlementBypassActive: await isEntitlementBypassActive(udid) };
@@ -98954,6 +99006,13 @@ var remoteIosHost = {
98954
99006
  const { stdout } = await simctlSpawn(udid, { args: ["launchctl", "list"] });
98955
99007
  return parseUIKitApplicationBundleIds(stdout);
98956
99008
  },
99009
+ // App processes live on the orchestrator, so the local process table says
99010
+ // nothing about how they were launched. Only running-ness is answerable; the
99011
+ // null process keeps callers on their no-evidence path.
99012
+ async inspectRunningApp(udid, bundleId) {
99013
+ const { stdout } = await simctlSpawn(udid, { args: ["launchctl", "list"] });
99014
+ return { running: parseUIKitApplicationJobs(stdout).has(bundleId), process: null };
99015
+ },
98957
99016
  // Apply the accessibility defaults the tool-server needs (the local host does
98958
99017
  // this via `defaults write`; here we run the same writes through the remote
98959
99018
  // generic spawn). The entitlement-bypass plist is assumed active on cloud
@@ -112178,6 +112237,22 @@ function isInjectableBundleId(bundleId) {
112178
112237
  var NON_INJECTABLE_NATIVE_WARNING = "Do not fall back to the native-devtools feature tools (native-describe-screen, native-find-views, native-full-hierarchy, native-network-logs, native-view-at-point, native-user-interactable-view-at-point) \u2014 they run the same injection precheck and fail with the same non-injectable error.";
112179
112238
  var NON_INJECTABLE_RECOVERY = "Use the standard `describe` tool (its accessibility path reads the screen without injection) or `screenshot` (then interact by coordinate). " + NON_INJECTABLE_NATIVE_WARNING;
112180
112239
  var MAX_NATIVE_DEVTOOLS_INIT_ATTEMPTS = 3;
112240
+ var NATIVE_DEVTOOLS_AGE_SLOP_MS = 3e3;
112241
+ var NATIVE_DEVTOOLS_CONNECT_BUDGET_MS = 15e3;
112242
+ function buildAppStateMessage(bundleId, state3) {
112243
+ switch (state3) {
112244
+ case "not_running":
112245
+ return `${bundleId} has no running process on this simulator, so there is no injected process to read. Call launch-app (or restart-app) then retry. If that launch fails rather than starting the app, the bundle id is not installed on this device \u2014 this state cannot tell the two apart; install it and no relaunch will be needed.`;
112246
+ case "stale_process":
112247
+ return `The running ${bundleId} process cannot reach this simulator's native-devtools endpoint \u2014 it was launched either before argent's instrumentation was in place or against an earlier tool-server's listener. A fresh process picks up the current one: call restart-app then retry.`;
112248
+ case "unregistered":
112249
+ return `${bundleId} is running with argent's native devtools injected and pointed at this simulator's devtools endpoint, but the service never registered its connection. Restarting the app cannot change that \u2014 it already launched under exactly the terms a restart would recreate. Restart the tool-server (\`argent server stop && argent server start --detach\`) and retry. If you have already restarted the tool-server for this app and it reads this way again, stop: the process is loading argent's dylib but never dialing, which no further restart on either side fixes. Treat native devtools as unavailable \u2014 read the screen with describe or screenshot and drive it by coordinate.`;
112250
+ case "connecting":
112251
+ return `${bundleId} is running with argent's native devtools injected and pointed at this simulator's devtools endpoint, and it launched moments ago \u2014 its connection has not finished being established. Wait a few seconds and retry the same call. Do NOT restart the app: launching it is what starts the connection, so a relaunch discards the one in progress and returns you to this same state.`;
112252
+ case "indeterminate":
112253
+ return `Native devtools are not connected to ${bundleId}, and its process could not be inspected to tell whether it is injected. Call restart-app then retry. If it is still not connected after that restart, the native-devtools service is stale rather than the app being uninjected \u2014 do not keep restarting the app; restart the tool-server (\`argent server stop && argent server start --detach\`) and retry.`;
112254
+ }
112255
+ }
112181
112256
  function buildInitFailedResult(udid, failure) {
112182
112257
  return {
112183
112258
  status: "init_failed",
@@ -112185,10 +112260,33 @@ function buildInitFailedResult(udid, failure) {
112185
112260
  attempts: failure.attempts
112186
112261
  };
112187
112262
  }
112263
+ var NATIVE_DEVTOOLS_PRECHECK_TOOLS = /* @__PURE__ */ new Set([
112264
+ "native-describe-screen",
112265
+ "native-find-views",
112266
+ "native-full-hierarchy",
112267
+ "native-network-logs",
112268
+ "native-view-at-point",
112269
+ "native-user-interactable-view-at-point",
112270
+ "native-devtools-status",
112271
+ "launch-app",
112272
+ "restart-app"
112273
+ ]);
112274
+ var NATIVE_DEVTOOLS_BLOCK_STATUSES = /* @__PURE__ */ new Set([
112275
+ "init_failed",
112276
+ "restart_required",
112277
+ "service_stale",
112278
+ "connect_pending"
112279
+ ]);
112280
+ function isNativeDevtoolsBlockResult(toolId, result) {
112281
+ if (!NATIVE_DEVTOOLS_PRECHECK_TOOLS.has(toolId)) return false;
112282
+ if (typeof result !== "object" || result === null) return false;
112283
+ const status = result.status;
112284
+ return typeof status === "string" && NATIVE_DEVTOOLS_BLOCK_STATUSES.has(status);
112285
+ }
112188
112286
  async function precheckNativeDevtools(api, udid, bundleId) {
112189
112287
  if (bundleId !== void 0 && !isInjectableBundleId(bundleId)) {
112190
112288
  throw new FailureError(
112191
- `${bundleId} is an Apple system app: it is a platform binary with library validation, so Argent native devtools can never be injected into it. ` + NON_INJECTABLE_RECOVERY,
112289
+ `${bundleId} is an Apple system app: it is a platform binary with library validation, so Argent native devtools cannot be relied on to inject into it \u2014 treat it as unavailable rather than retrying. ` + NON_INJECTABLE_RECOVERY,
112192
112290
  {
112193
112291
  error_code: FAILURE_CODES.NATIVE_DEVTOOLS_NOT_INJECTABLE,
112194
112292
  failure_stage: "native_devtools_precheck",
@@ -112210,13 +112308,21 @@ async function precheckNativeDevtools(api, udid, bundleId) {
112210
112308
  givenUp: false
112211
112309
  });
112212
112310
  }
112213
- if (bundleId !== void 0 && await api.requiresAppRestart(bundleId)) {
112214
- return {
112215
- status: "restart_required",
112216
- message: "Native devtools are not injected into the running app. Call restart-app then retry."
112217
- };
112218
- }
112219
- return null;
112311
+ if (bundleId === void 0) return null;
112312
+ const state3 = await api.appConnectionState(bundleId).catch(() => {
112313
+ const failure = api.getInitFailure();
112314
+ return failure ? buildInitFailedResult(udid, failure) : "indeterminate";
112315
+ });
112316
+ if (typeof state3 !== "string") return state3;
112317
+ if (state3 === "connected") return null;
112318
+ return {
112319
+ // Neither `unregistered` (a relaunch provably cannot fix it) nor
112320
+ // `connecting` (a relaunch aborts the handshake and resets the age the
112321
+ // verdict reads) may be reported as restart_required — obeying that would
112322
+ // return here forever.
112323
+ status: state3 === "unregistered" ? "service_stale" : state3 === "connecting" ? "connect_pending" : "restart_required",
112324
+ message: buildAppStateMessage(bundleId, state3)
112325
+ };
112220
112326
  }
112221
112327
  function nativeDevtoolsRef(device, { transport = "unix" } = {}) {
112222
112328
  const transportSuffix = transport === "tcp" ? ":tcp" : "";
@@ -112478,6 +112584,7 @@ var nativeDevtoolsBlueprint = {
112478
112584
  } else {
112479
112585
  await bindNativeDevtoolsUnixSocket(server, socketPath);
112480
112586
  }
112587
+ const listeningSince = Date.now();
112481
112588
  await ensureEnvReady().catch(() => {
112482
112589
  });
112483
112590
  const api = {
@@ -112489,10 +112596,28 @@ var nativeDevtoolsBlueprint = {
112489
112596
  isConnected: (bundleId) => connections2.has(bundleId),
112490
112597
  isAppRunning,
112491
112598
  listConnectedBundleIds: () => [...connections2.keys()],
112492
- async requiresAppRestart(bundleId) {
112493
- if (connections2.has(bundleId)) return false;
112599
+ async appConnectionState(bundleId) {
112600
+ if (connections2.has(bundleId)) return "connected";
112494
112601
  await reverifyEnv();
112495
- return true;
112602
+ const inspection = await host.inspectRunningApp(udid, bundleId).catch((err) => {
112603
+ process.stderr.write(
112604
+ `[native-devtools] app inspection failed for ${bundleId}: ${String(err)}
112605
+ `
112606
+ );
112607
+ return null;
112608
+ });
112609
+ if (connections2.has(bundleId)) return "connected";
112610
+ if (inspection === null) return "indeterminate";
112611
+ if (!inspection.running) return "not_running";
112612
+ if (inspection.process === null) return "indeterminate";
112613
+ if (!processCarriesInjection(inspection.process.env, endpoint)) return "stale_process";
112614
+ const listenerAgeMs = Date.now() - listeningSince;
112615
+ const processAgeMs = inspection.process.ageMs;
112616
+ if (processAgeMs + NATIVE_DEVTOOLS_AGE_SLOP_MS >= listenerAgeMs) {
112617
+ return "stale_process";
112618
+ }
112619
+ if (processAgeMs < NATIVE_DEVTOOLS_CONNECT_BUDGET_MS) return "connecting";
112620
+ return "unregistered";
112496
112621
  },
112497
112622
  activateNetworkInspection(bundleId) {
112498
112623
  activatedBundleIds.add(bundleId);
@@ -112846,7 +112971,7 @@ function tcpArtifactHint(err) {
112846
112971
  return /TCP-transport (?:binary|dylib) not found/.test(message) ? message : void 0;
112847
112972
  }
112848
112973
  var TVOS_HINT = "This is an Apple TV (tvOS) simulator, which the iOS accessibility service does not support. Use the `describe` tool to read the focused and focusable elements, `tv-remote` (up/down/left/right/select/back/menu/home) to move focus, and `keyboard` to type. See the argent-tv-interact skill.";
112849
- var NON_INJECTABLE_HINT = "This is an Apple system app (com.apple.*), which cannot load argent's native-devtools instrumentation \u2014 the native view hierarchy is unavailable and restarting the app will NOT help. Take a `screenshot` to see the screen and interact by coordinate. " + NON_INJECTABLE_NATIVE_WARNING;
112974
+ var NON_INJECTABLE_HINT = "This is an Apple system app (com.apple.*), which cannot be relied on to load argent's native-devtools instrumentation \u2014 the native view hierarchy is unavailable and restarting the app will NOT help. Take a `screenshot` to see the screen and interact by coordinate. " + NON_INJECTABLE_NATIVE_WARNING;
112850
112975
  function emptyTree() {
112851
112976
  return parseDescribeResult({
112852
112977
  role: "AXGroup",
@@ -112878,27 +113003,43 @@ async function describeIos(registry2, device, params, options = {}) {
112878
113003
  if (params.bundleId && !isInjectableBundleId(params.bundleId)) {
112879
113004
  return { tree, source: "ax-service", hint: hint ?? NON_INJECTABLE_HINT };
112880
113005
  }
113006
+ let nativeApi;
112881
113007
  try {
112882
113008
  const ndRef = nativeDevtoolsRef(device);
112883
- const nativeApi = await registry2.resolveService(ndRef.urn, ndRef.options);
113009
+ nativeApi = await registry2.resolveService(ndRef.urn, ndRef.options);
113010
+ } catch (err) {
113011
+ return { tree, source: "ax-service", hint: unexplainedHint(hint, errMsg(err), params) };
113012
+ }
113013
+ try {
112884
113014
  const target = await resolveNativeTargetApp(nativeApi, params.bundleId);
112885
- if (await nativeApi.requiresAppRestart(target.bundleId)) {
112886
- return { tree, source: "ax-service", should_restart: true, hint };
113015
+ const state3 = await nativeApi.appConnectionState(target.bundleId).catch(() => "indeterminate");
113016
+ if (state3 !== "connected") {
113017
+ const diagnosis = buildAppStateMessage(target.bundleId, state3);
113018
+ const merged = hint ? `${hint} ${diagnosis}` : diagnosis;
113019
+ return state3 === "unregistered" || state3 === "connecting" ? { tree, source: "ax-service", hint: merged } : { tree, source: "ax-service", should_restart: true, hint: merged };
112887
113020
  }
112888
113021
  const rawResult = await nativeApi.queryViewHierarchy(
112889
113022
  target.bundleId,
112890
113023
  "ViewHierarchy.describeScreen"
112891
113024
  );
112892
113025
  if (rawResult.error) {
112893
- return { tree, source: "ax-service", hint };
113026
+ return { tree, source: "ax-service", hint: unexplainedHint(hint, rawResult.error, params) };
112894
113027
  }
112895
113028
  const parsed = parseNativeDescribeScreenResult(rawResult);
112896
113029
  const nativeTree = adaptNativeDescribeToDescribeResult(parsed);
112897
113030
  return { tree: nativeTree, source: "native-devtools", hint };
112898
- } catch {
112899
- return { tree, source: "ax-service", hint };
113031
+ } catch (err) {
113032
+ return { tree, source: "ax-service", hint: unexplainedHint(hint, errMsg(err), params) };
112900
113033
  }
112901
113034
  }
113035
+ function errMsg(err) {
113036
+ return err instanceof Error ? err.message : String(err);
113037
+ }
113038
+ function unexplainedHint(hint, detail, params) {
113039
+ const next = params.bundleId ? "Take a `screenshot` to see what is there." : "Pass `bundleId` to have the connection state measured, or take a `screenshot` to see what is there.";
113040
+ const why = `The native view hierarchy could not be read (${detail}), so this empty accessibility tree is not evidence that nothing is on screen. ${next}`;
113041
+ return hint ? `${hint} ${why}` : why;
113042
+ }
112902
113043
 
112903
113044
  // ../tool-server/src/tools/describe/platforms/android/index.ts
112904
113045
  init_src();
@@ -117164,6 +117305,9 @@ var zodSchema2 = external_exports.object({
117164
117305
  udid: external_exports.string().describe("Simulator UDID"),
117165
117306
  bundleId: external_exports.string().describe("Bundle ID of the app to check (e.g. com.example.MyApp)")
117166
117307
  });
117308
+ function envSetupReading(api, connected) {
117309
+ return api.isEnvSetup() && (connected || api.getInitFailure() === null);
117310
+ }
117167
117311
  var nativeDevtoolsStatusTool = {
117168
117312
  id: "native-devtools-status",
117169
117313
  interaction: {
@@ -117179,18 +117323,23 @@ var nativeDevtoolsStatusTool = {
117179
117323
  description: `Check whether native devtools are connected to a specific app and whether the next launch is prepared for injection.
117180
117324
  Use when you need to verify native devtools readiness before calling native-full-hierarchy, native-describe-screen, or native-network-logs.
117181
117325
 
117182
- Returns { envSetup, appRunning, connected, requiresRestart, nextLaunchWillBeInjected, injectable }:
117326
+ Returns { envSetup, appRunning, connected, requiresRestart, state, message, nextLaunchWillBeInjected, injectable }:
117183
117327
  - envSetup: DYLD_INSERT_LIBRARIES is configured in the simulator's launchd environment
117184
117328
  - appRunning: the target bundle currently has a running UIKit process on the simulator
117185
117329
  - connected: the dylib is active in the current running process for this bundleId
117186
- - requiresRestart: the app is already running but its current process does not have native devtools injected (always false for a non-injectable app)
117330
+ - requiresRestart: the app is already running and a fresh process would reach this simulator's devtools endpoint where the current one does not \u2014 it carries no argent injection, was pointed at an earlier tool-server's listener, or could not be inspected to tell. Always false for a non-injectable app, and false when state is unregistered or connecting, where a relaunch cannot help.
117331
+ - state: why devtools are or aren't live, measured from the running process. "connected"; "not_running"; "stale_process" (the process cannot reach this simulator's devtools endpoint \u2014 launched either before argent's instrumentation was in place or against an earlier tool-server's listener \u2014 so restart-app fixes it); "unregistered" (the process IS injected and pointed at this simulator's devtools endpoint yet the service never registered it, so restarting the app cannot help); "connecting" (the process IS injected but launched moments ago and is still connecting, so waiting is what helps); "indeterminate" (the process could not be inspected). Omitted when injectable is false, which is terminal on its own.
117332
+ - message: the remedy for that state, in full. Omitted when connected or non-injectable. Prefer it over inferring one from the booleans \u2014 it is the only field that can tell you to stop restarting the app.
117187
117333
  - nextLaunchWillBeInjected: if you launch this bundle now, native devtools env setup is already in place (always false for a non-injectable app)
117188
- - injectable: whether native devtools can ever be injected into this app. Apple system apps (bundle ids under com.apple.) are platform binaries with library validation, so the dylib can never load into them.
117334
+ - injectable: whether native devtools can be relied on to inject into this app. Apple system apps (bundle ids under com.apple.) are platform binaries with library validation, so the dylib cannot be counted on to load into them \u2014 it has been observed both loading and not loading, depending on the simulator runtime.
117189
117335
 
117190
117336
  Call this before using app-scoped native hierarchy tools or native-network-logs.
117191
- If injectable is false: this is a TERMINAL state \u2014 the app can never be injected. Do NOT restart/retry. Use the standard \`describe\` tool (its accessibility path reads the screen without injection) or \`screenshot\` (then interact by coordinate). Do not fall back to the native-devtools feature tools (native-describe-screen, native-find-views, native-full-hierarchy, native-network-logs, native-view-at-point, native-user-interactable-view-at-point) \u2014 they run the same injection precheck and fail with the same non-injectable error.
117337
+ If injectable is false: treat this as TERMINAL \u2014 injection cannot be relied on for this app, and no relaunch changes which way it goes. Do NOT restart/retry. Use the standard \`describe\` tool (its accessibility path reads the screen without injection) or \`screenshot\` (then interact by coordinate). Do not fall back to the native-devtools feature tools (native-describe-screen, native-find-views, native-full-hierarchy, native-network-logs, native-view-at-point, native-user-interactable-view-at-point) \u2014 they run the same injection precheck and fail with the same non-injectable error.
117192
117338
  If appRunning is false and nextLaunchWillBeInjected is true: use launch-app normally.
117193
- If requiresRestart is true: call restart-app, then proceed with the native feature.
117339
+ If requiresRestart is true: call restart-app once, then proceed with the native feature. Read state before acting on a second such reading \u2014 indeterminate reaches this rule too, and its line below bounds it at that one restart.
117340
+ If state is unregistered: do NOT restart the app again \u2014 it already launched under the terms a restart would recreate. Restart the tool-server (\`argent server stop && argent server start --detach\`), then retry. If it reads unregistered again after that restart, stop: the process loads argent's dylib but never dials, and no further restart on either side changes it \u2014 treat native devtools as unavailable, then use \`describe\` or \`screenshot\` and drive by coordinate.
117341
+ If state is connecting: do NOT restart the app \u2014 launching it is what starts the connection, so a relaunch discards the one in progress and returns this same state. Wait a few seconds and repeat this call.
117342
+ If state is indeterminate: the process could not be inspected, so restart-app is worth one attempt. If this call still reports it after that restart, do NOT restart the app again \u2014 the service is stale rather than the app uninjected, so restart the tool-server (\`argent server stop && argent server start --detach\`) and retry. Remote simulators can never inspect the process, so this is the only unconnected state a running app reaches there.
117194
117343
  Returns { status: "init_failed", message, attempts } instead when the simulator's native-devtools environment failed to initialize.
117195
117344
  Fails if the simulator server is not running for the given UDID.`,
117196
117345
  zodSchema: zodSchema2,
@@ -117202,18 +117351,19 @@ Fails if the simulator server is not running for the given UDID.`,
117202
117351
  await ensureDeps(device.platform === "ios-remote" ? ["sim-remote"] : ["xcrun"]);
117203
117352
  const api = services.nativeDevtools;
117204
117353
  if (!isInjectableBundleId(params.bundleId)) {
117354
+ const connected2 = api.isConnected(params.bundleId);
117205
117355
  let appRunning2;
117206
117356
  try {
117207
117357
  appRunning2 = await api.isAppRunning(params.bundleId);
117208
117358
  } catch (err) {
117209
- const blocked2 = await precheckNativeDevtools(api, params.udid);
117210
- if (blocked2) return blocked2;
117359
+ const failure = api.getInitFailure();
117360
+ if (failure) return buildInitFailedResult(params.udid, failure);
117211
117361
  throw err;
117212
117362
  }
117213
117363
  return {
117214
- envSetup: api.isEnvSetup(),
117364
+ envSetup: envSetupReading(api, connected2),
117215
117365
  appRunning: appRunning2,
117216
- connected: api.isConnected(params.bundleId),
117366
+ connected: connected2,
117217
117367
  requiresRestart: false,
117218
117368
  nextLaunchWillBeInjected: false,
117219
117369
  injectable: false
@@ -117221,18 +117371,41 @@ Fails if the simulator server is not running for the given UDID.`,
117221
117371
  }
117222
117372
  const blocked = await precheckNativeDevtools(api, params.udid);
117223
117373
  if (blocked) return blocked;
117224
- const appRunning = await api.isAppRunning(params.bundleId);
117225
- const connected = api.isConnected(params.bundleId);
117226
- if (!connected) {
117227
- await api.reverifyEnv().catch(() => {
117228
- });
117374
+ const measured = await api.appConnectionState(params.bundleId).catch(() => "indeterminate");
117375
+ const connected = measured === "connected";
117376
+ let appRunning;
117377
+ let state3 = measured;
117378
+ if (state3 === "indeterminate") {
117379
+ try {
117380
+ appRunning = await api.isAppRunning(params.bundleId);
117381
+ } catch (err) {
117382
+ const failure = api.getInitFailure();
117383
+ if (failure) return buildInitFailedResult(params.udid, failure);
117384
+ throw err;
117385
+ }
117386
+ if (!appRunning) state3 = "not_running";
117387
+ } else {
117388
+ appRunning = state3 !== "not_running";
117229
117389
  }
117230
- const envSetup = api.isEnvSetup();
117390
+ const envSetup = envSetupReading(api, connected);
117231
117391
  return {
117232
117392
  envSetup,
117233
117393
  appRunning,
117234
117394
  connected,
117235
- requiresRestart: appRunning && !connected,
117395
+ // Derived from the one state, so it can never disagree with it: a relaunch
117396
+ // provably changes nothing for `unregistered`, destroys the handshake for
117397
+ // `connecting`, and `not_running` needs a launch. That leaves the two a
117398
+ // fresh process fixes — `indeterminate` among them, since an uninspectable
117399
+ // host (ios-remote) supports no finer reading. Both already carry a live
117400
+ // process (the settling above rewrote an empty `indeterminate` to
117401
+ // `not_running`), so an `appRunning` conjunct would only restate the state.
117402
+ requiresRestart: state3 === "stale_process" || state3 === "indeterminate",
117403
+ state: state3,
117404
+ // The booleans cannot express "one restart, then stop" — the shape
117405
+ // `indeterminate` needs, and the only one ios-remote can report for a
117406
+ // running app. Carrying the same prose as every other consumer keeps that
117407
+ // escape off the agent having read this tool's description.
117408
+ ...state3 === "connected" ? {} : { message: buildAppStateMessage(params.bundleId, state3) },
117236
117409
  nextLaunchWillBeInjected: envSetup,
117237
117410
  injectable: true
117238
117411
  };
@@ -117258,8 +117431,9 @@ var nativeNetworkLogsTool = {
117258
117431
  description: `Retrieve network requests captured at the native NSURLProtocol level.
117259
117432
  Unlike the JS-level network inspector (view-network-logs), this captures ALL network traffic from the app including native modules, Swift/Objective-C networking, and background transfers that bypass JS fetch.
117260
117433
  Use when you need to inspect native-level HTTP traffic that is invisible to JS fetch interception.
117261
- Returns { status, count, events } where each event contains URL, method, status code, headers, and timing. Returns { status: "restart_required" } if the dylib is not injected - if this happens, call "restart-app" then retry.
117262
- Fails if native devtools are not connected or the app is not running.`,
117434
+ Returns { status, count, events } where each event contains URL, method, status code, headers, and timing.
117435
+ If status is restart_required: follow the message (usually restart-app), then retry. If status is service_stale: the app is already injected, so restarting it cannot help \u2014 restart the tool-server (\`argent server stop && argent server start --detach\`) and retry. If the same status comes back after that restart, stop restarting: follow the message, which names the terminal fallback. If status is connect_pending: the app is injected and still connecting \u2014 do not restart it, wait a few seconds and retry. If status is init_failed: the simulator's native-devtools environment could not be initialised \u2014 follow the message (re-boot the simulator) rather than retrying this tool.
117436
+ A not-connected or not-running app comes back as one of those statuses rather than a failure. Failures are separate: an Apple system app is rejected outright (terminal \u2014 never retry it), while a missing host dependency or a udid that is not an Apple device is not.`,
117263
117437
  zodSchema: zodSchema3,
117264
117438
  services: (params) => ({
117265
117439
  nativeDevtools: nativeDevtoolsRef(resolveDevice(params.udid))
@@ -117309,7 +117483,8 @@ var nativeFindViewsTool = {
117309
117483
  Use when you need to locate a specific view by its properties without dumping the entire hierarchy.
117310
117484
  Returns { status: "ok", matches } with matching views including their frames, properties, optional ancestors, and optional children. Much more targeted than native-full-hierarchy.
117311
117485
  At least one of className, identifier, label, tag, or nativeID must be provided.
117312
- Fails if native devtools are not connected, the app is not running, or status is restart_required (call restart-app then retry).`,
117486
+ If status is restart_required: follow the message (usually restart-app), then retry. If status is service_stale: the app is already injected, so restarting it cannot help \u2014 restart the tool-server (\`argent server stop && argent server start --detach\`) and retry. If the same status comes back after that restart, stop restarting: follow the message, which names the terminal fallback. If status is connect_pending: the app is injected and still connecting \u2014 do not restart it, wait a few seconds and retry. If status is init_failed: the simulator's native-devtools environment could not be initialised \u2014 follow the message (re-boot the simulator) rather than retrying this tool.
117487
+ A not-connected or not-running app comes back as one of those statuses rather than a failure. Failures are separate: an Apple system app is rejected outright (terminal \u2014 never retry it), and the hierarchy query itself can error or time out.`,
117313
117488
  zodSchema: zodSchema4,
117314
117489
  services: (params) => ({
117315
117490
  nativeDevtools: nativeDevtoolsRef(resolveDevice(params.udid))
@@ -117376,8 +117551,9 @@ var nativeFullHierarchyTool = {
117376
117551
  WARNING: Output can be extremely large (100KB\u2013500KB+) for complex apps, especially those built with SwiftUI. Prefer native-find-views for targeted queries.
117377
117552
  Use skipClasses / skipClassPrefixes to prune SwiftUI internal subtrees and reduce output size. Use the fields param to request only the properties you need.
117378
117553
  Use when you need deep layout debugging, finding views with no accessibility labels, or verifying view structure not exposed through the accessibility tree.
117379
- Returns { status: "ok", windows } with the full view hierarchy, or { status: "restart_required" } if the dylib is not injected.
117380
- Fails if native devtools are not connected or the app is not running.`,
117554
+ Returns { status: "ok", windows } with the full view hierarchy.
117555
+ If status is restart_required: follow the message (usually restart-app), then retry. If status is service_stale: the app is already injected, so restarting it cannot help \u2014 restart the tool-server (\`argent server stop && argent server start --detach\`) and retry. If the same status comes back after that restart, stop restarting: follow the message, which names the terminal fallback. If status is connect_pending: the app is injected and still connecting \u2014 do not restart it, wait a few seconds and retry. If status is init_failed: the simulator's native-devtools environment could not be initialised \u2014 follow the message (re-boot the simulator) rather than retrying this tool.
117556
+ A not-connected or not-running app comes back as one of those statuses rather than a failure. Failures are separate: an Apple system app is rejected outright (terminal \u2014 never retry it), and the hierarchy query itself can error or time out.`,
117381
117557
  zodSchema: zodSchema5,
117382
117558
  services: (params) => ({
117383
117559
  nativeDevtools: nativeDevtoolsRef(resolveDevice(params.udid))
@@ -117435,9 +117611,12 @@ Returns a flat list of accessibility leaf elements with:
117435
117611
  This is a low-level native inspection tool. The normalized fields are intended to help
117436
117612
  with backend migration work, but the public describe contract is still separate.
117437
117613
 
117438
- Useful for evaluating or debugging the lower-level native data that powers the public describe tool.
117614
+ Use when you are evaluating or debugging the lower-level native data behind the public
117615
+ describe tool, or when you need its raw point-space geometry rather than describe's
117616
+ normalized contract.
117439
117617
 
117440
- If status is restart_required: call restart-app then retry.`,
117618
+ If status is restart_required: follow the message (usually restart-app), then retry. If status is service_stale: the app is already injected, so restarting it cannot help \u2014 restart the tool-server (\`argent server stop && argent server start --detach\`) and retry. If the same status comes back after that restart, stop restarting: follow the message, which names the terminal fallback. If status is connect_pending: the app is injected and still connecting \u2014 do not restart it, wait a few seconds and retry. If status is init_failed: the simulator's native-devtools environment could not be initialised \u2014 follow the message (re-boot the simulator) rather than retrying this tool.
117619
+ A not-connected or not-running app comes back as one of those statuses rather than a failure. Failures are separate: an Apple system app is rejected outright (terminal \u2014 never retry it), and the screen query itself can error or time out.`,
117441
117620
  zodSchema: zodSchema6,
117442
117621
  services: (params) => ({
117443
117622
  nativeDevtools: nativeDevtoolsRef(resolveDevice(params.udid))
@@ -117504,10 +117683,19 @@ var nativeViewAtPointTool = {
117504
117683
  Unlike native-user-interactable-view-at-point, this ignores userInteractionEnabled,
117505
117684
  so it answers "what is visually here?" rather than "what would receive the touch?".
117506
117685
 
117686
+ Use when a screenshot shows something the accessibility tree does not name \u2014 an
117687
+ unlabeled icon, a decorative overlay, a custom-drawn cell \u2014 and you need the class,
117688
+ identifier or nativeID of whatever draws it.
117689
+
117690
+ Returns { status: "ok", view }: the matched view with its class name, frames,
117691
+ identifier, label and layer name, its ancestor chain by default, and its subviews on
117692
+ request. view is null when nothing is drawn at that point.
117693
+
117507
117694
  IMPORTANT: x and y are raw iOS window coordinates in points, NOT normalized [0,1]
117508
117695
  simulator tap coordinates.
117509
117696
 
117510
- If status is restart_required: call restart-app then retry.`,
117697
+ If status is restart_required: follow the message (usually restart-app), then retry. If status is service_stale: the app is already injected, so restarting it cannot help \u2014 restart the tool-server (\`argent server stop && argent server start --detach\`) and retry. If the same status comes back after that restart, stop restarting: follow the message, which names the terminal fallback. If status is connect_pending: the app is injected and still connecting \u2014 do not restart it, wait a few seconds and retry. If status is init_failed: the simulator's native-devtools environment could not be initialised \u2014 follow the message (re-boot the simulator) rather than retrying this tool.
117698
+ A not-connected or not-running app comes back as one of those statuses rather than a failure. Failures are separate: an Apple system app is rejected outright (terminal \u2014 never retry it), and the point query itself can error or time out.`,
117511
117699
  zodSchema: zodSchema7,
117512
117700
  services: (params) => ({
117513
117701
  nativeDevtools: nativeDevtoolsRef(resolveDevice(params.udid))
@@ -117580,10 +117768,19 @@ var nativeUserInteractableViewAtPointTool = {
117580
117768
  Unlike native-view-at-point, this respects userInteractionEnabled and is closer to
117581
117769
  UIKit hit-testing semantics.
117582
117770
 
117771
+ Use when a tap lands somewhere unexpected or does nothing, to see which control
117772
+ UIKit would hand the touch to \u2014 a transparent overlay swallowing it, a parent
117773
+ recognizer, a disabled button.
117774
+
117775
+ Returns { status: "ok", view }: the hit-test winner with its class name, frames,
117776
+ identifier, label and layer name, its ancestor chain by default, and its subviews on
117777
+ request. view is null when no touchable view sits under that point.
117778
+
117583
117779
  IMPORTANT: x and y are raw iOS window coordinates in points, NOT normalized [0,1]
117584
117780
  simulator tap coordinates.
117585
117781
 
117586
- If status is restart_required: call restart-app then retry.`,
117782
+ If status is restart_required: follow the message (usually restart-app), then retry. If status is service_stale: the app is already injected, so restarting it cannot help \u2014 restart the tool-server (\`argent server stop && argent server start --detach\`) and retry. If the same status comes back after that restart, stop restarting: follow the message, which names the terminal fallback. If status is connect_pending: the app is injected and still connecting \u2014 do not restart it, wait a few seconds and retry. If status is init_failed: the simulator's native-devtools environment could not be initialised \u2014 follow the message (re-boot the simulator) rather than retrying this tool.
117783
+ A not-connected or not-running app comes back as one of those statuses rather than a failure. Failures are separate: an Apple system app is rejected outright (terminal \u2014 never retry it), and the point query itself can error or time out.`,
117587
117784
  zodSchema: zodSchema8,
117588
117785
  services: (params) => ({
117589
117786
  nativeDevtools: nativeDevtoolsRef(resolveDevice(params.udid))
@@ -129257,17 +129454,17 @@ function tagExpWithClosingIndex(xmlData, i, closingChar = ">") {
129257
129454
  }
129258
129455
  }
129259
129456
  }
129260
- function findClosingIndex(xmlData, str, i, errMsg3) {
129457
+ function findClosingIndex(xmlData, str, i, errMsg4) {
129261
129458
  const closingIndex = xmlData.indexOf(str, i);
129262
129459
  if (closingIndex === -1) {
129263
- throw new Error(errMsg3);
129460
+ throw new Error(errMsg4);
129264
129461
  } else {
129265
129462
  return closingIndex + str.length - 1;
129266
129463
  }
129267
129464
  }
129268
- function findClosingChar(xmlData, char, i, errMsg3) {
129465
+ function findClosingChar(xmlData, char, i, errMsg4) {
129269
129466
  const closingIndex = xmlData.indexOf(char, i);
129270
- if (closingIndex === -1) throw new Error(errMsg3);
129467
+ if (closingIndex === -1) throw new Error(errMsg4);
129271
129468
  return closingIndex;
129272
129469
  }
129273
129470
  function readTagExp(xmlData, i, removeNSPrefix, closingChar = ">") {
@@ -145259,6 +145456,16 @@ var FULL_HIERARCHY_FIELDS = [
145259
145456
  // request, which just leaves the wait's poll unconfirmed.
145260
145457
  "firstResponder"
145261
145458
  ];
145459
+ async function unreadableHierarchyReason(nativeApi, bundleId) {
145460
+ if (!isInjectableBundleId(bundleId)) {
145461
+ return `${bundleId} is an Apple system app: it is a platform binary with library validation, so argent's native devtools cannot be relied on to inject into it, and without them a flow has no view hierarchy to resolve selectors against. Replace the selector steps with coordinate ones \u2014 \`tap: { x: 0.5, y: 0.35 }\` takes a point directly and reads no tree \u2014 or target an app argent installs.`;
145462
+ }
145463
+ const state3 = await nativeApi.appConnectionState(bundleId).catch(() => "indeterminate");
145464
+ if (state3 === "connected") {
145465
+ return `native devtools reported no connected app while this tree was being read, but ${bundleId} is connected now \u2014 the connection arrived mid-read. Retry: flows resolve selectors against the full view hierarchy native devtools serve.`;
145466
+ }
145467
+ return `${buildAppStateMessage(bundleId, state3)} Flows resolve selectors against the full view hierarchy native devtools serve.`;
145468
+ }
145262
145469
  async function queryFullHierarchyTree(registry2, device, launchedNativeApp) {
145263
145470
  let nativeApi;
145264
145471
  try {
@@ -145266,10 +145473,13 @@ async function queryFullHierarchyTree(registry2, device, launchedNativeApp) {
145266
145473
  nativeApi = await registry2.resolveService(ndRef.urn, ndRef.options);
145267
145474
  } catch (err) {
145268
145475
  throw new Error(
145269
- `native devtools is unavailable (${errMsg(err)}) \u2014 flows resolve selectors against the full view hierarchy it serves`,
145476
+ `native devtools is unavailable (${errMsg2(err)}) \u2014 flows resolve selectors against the full view hierarchy it serves`,
145270
145477
  { cause: err }
145271
145478
  );
145272
145479
  }
145480
+ if (launchedNativeApp !== void 0 && nativeApi.listConnectedBundleIds().length === 0) {
145481
+ throw new Error(await unreadableHierarchyReason(nativeApi, launchedNativeApp));
145482
+ }
145273
145483
  let target;
145274
145484
  try {
145275
145485
  target = await resolveNativeTargetApp(nativeApi, void 0);
@@ -145281,11 +145491,6 @@ async function queryFullHierarchyTree(registry2, device, launchedNativeApp) {
145281
145491
  throw err;
145282
145492
  }
145283
145493
  }
145284
- if (await nativeApi.requiresAppRestart(target.bundleId)) {
145285
- throw new Error(
145286
- `${target.bundleId} was launched before argent's instrumentation loaded \u2014 relaunch it (launch-app, or a flow \`launch\` step) so the full view hierarchy is readable`
145287
- );
145288
- }
145289
145494
  const rawResult = await nativeApi.queryViewHierarchy(
145290
145495
  target.bundleId,
145291
145496
  "ViewHierarchy.getFullHierarchy",
@@ -145305,7 +145510,7 @@ async function queryFullHierarchyTree(registry2, device, launchedNativeApp) {
145305
145510
  const { tree, screen } = adaptFullHierarchy(rawResult);
145306
145511
  return { tree, source: "native-devtools", ...screen ? { screen } : {} };
145307
145512
  }
145308
- function errMsg(err) {
145513
+ function errMsg2(err) {
145309
145514
  return err instanceof Error ? err.message : String(err);
145310
145515
  }
145311
145516
 
@@ -145538,15 +145743,23 @@ var REPLAY_TREE_SOURCES = {
145538
145743
  ios: "native-devtools",
145539
145744
  android: "android-devtools"
145540
145745
  };
145746
+ function recordedLaunchedApp(session, platform) {
145747
+ for (let i = session.flow.steps.length - 1; i >= 0; i--) {
145748
+ const step = session.flow.steps[i];
145749
+ if (step.kind === "launch") return appIdForPlatform(step.app, platform) ?? void 0;
145750
+ }
145751
+ return void 0;
145752
+ }
145541
145753
  function fallbackSourceWarning(source, platform) {
145542
145754
  const expected = REPLAY_TREE_SOURCES[platform];
145543
145755
  if (!expected || source === expected) return void 0;
145544
145756
  return `selector captured from the fallback ${source} tree (${expected} unavailable) \u2014 replay resolves against the full hierarchy, which may not match it`;
145545
145757
  }
145546
- async function captureTapSelector(registry2, udid, point) {
145758
+ async function captureTapSelector(registry2, session, udid, point) {
145547
145759
  try {
145548
145760
  const device = resolveDevice(udid);
145549
- const { tree, source } = await fetchFlowTree(registry2, device);
145761
+ const launched = recordedLaunchedApp(session, device.platform);
145762
+ const { tree, source } = await fetchFlowTree(registry2, device, launched);
145550
145763
  const node = nodeAtPoint(tree, point);
145551
145764
  if (!node) return { warning: "no element found under the tap; kept coordinates (brittle)" };
145552
145765
  const selector = deriveSelector(node);
@@ -145702,7 +145915,7 @@ If a step was recorded by mistake, edit the .yaml to remove it \u2014 against a
145702
145915
  const isTap = params.command === "gesture-tap" && params.delayMs === void 0 && typeof args.udid === "string" && typeof args.x === "number" && typeof args.y === "number";
145703
145916
  let captured;
145704
145917
  if (isTap) {
145705
- captured = await captureTapSelector(registry2, args.udid, {
145918
+ captured = await captureTapSelector(registry2, session, args.udid, {
145706
145919
  x: args.x,
145707
145920
  y: args.y
145708
145921
  });
@@ -149129,8 +149342,9 @@ var fileInputs3 = [
149129
149342
  ];
149130
149343
  var MAX_RUN_DEPTH = 20;
149131
149344
  var POST_LAUNCH_SETTLE_MS = 1500;
149132
- var NATIVE_READY_TIMEOUT_MS = 15e3;
149345
+ var NATIVE_READY_TIMEOUT_MS = NATIVE_DEVTOOLS_CONNECT_BUDGET_MS;
149133
149346
  var NATIVE_READY_POLL_MS = 250;
149347
+ var LAUNCH_TO_VERDICT_MS = POST_LAUNCH_SETTLE_MS + NATIVE_READY_TIMEOUT_MS;
149134
149348
  var FOREGROUND_CHANGING_TOOLS = /* @__PURE__ */ new Set([
149135
149349
  "launch-app",
149136
149350
  "restart-app",
@@ -149143,15 +149357,35 @@ async function waitForNativeDevtools(registry2, device, bundleId, signal) {
149143
149357
  try {
149144
149358
  const ref = nativeDevtoolsRef(device);
149145
149359
  api = await registry2.resolveService(ref.urn, ref.options);
149146
- } catch {
149147
- return false;
149360
+ } catch (err) {
149361
+ if (!isInjectableBundleId(bundleId)) return null;
149362
+ return `the native-devtools service is unavailable for ${bundleId} (${errMsg3(err)})`;
149148
149363
  }
149149
149364
  const deadline = Date.now() + NATIVE_READY_TIMEOUT_MS;
149150
149365
  for (; ; ) {
149151
- if (signal?.aborted) return false;
149152
- if (api.isConnected(bundleId)) return true;
149153
- if (Date.now() >= deadline) return false;
149154
- if (!await sleepOrAbort(NATIVE_READY_POLL_MS, signal)) return false;
149366
+ if (signal?.aborted) return null;
149367
+ if (api.isConnected(bundleId)) return null;
149368
+ if (Date.now() >= deadline) break;
149369
+ if (!await sleepOrAbort(NATIVE_READY_POLL_MS, signal)) return null;
149370
+ }
149371
+ if (!isInjectableBundleId(bundleId)) return null;
149372
+ const state3 = await api.appConnectionState(bundleId).catch(() => "indeterminate");
149373
+ if (state3 === "connected") return null;
149374
+ return flowLaunchGateReason(bundleId, state3);
149375
+ }
149376
+ function flowLaunchGateReason(bundleId, state3) {
149377
+ const measured = buildAppStateMessage(bundleId, state3);
149378
+ switch (state3) {
149379
+ case "not_running":
149380
+ return `${bundleId} was relaunched by this step and is no longer running ${LAUNCH_TO_VERDICT_MS} ms later, so it exited after launch rather than failing to connect. Re-running the flow repeats the same launch: start it by hand (launch-app, then describe or screenshot) to see the crash or early exit first.`;
149381
+ case "stale_process":
149382
+ return `${measured} This step already relaunched it, so the process it measured predates whatever the relaunch would have given it \u2014 re-run the flow to launch again. If it lands here twice, the simulator's launchd environment is not holding argent's instrumentation: re-boot the device (boot-device with force) before re-running.`;
149383
+ case "unregistered":
149384
+ return `${measured} A cold start slower than the ${LAUNCH_TO_VERDICT_MS} ms this step waited reads the same way \u2014 if that is likely, re-run the flow to relaunch and wait again before restarting anything.`;
149385
+ case "connecting":
149386
+ return `${measured} This step launched it ${LAUNCH_TO_VERDICT_MS} ms before that reading, so the process being measured started after the step's own launch \u2014 something relaunched it in between. Re-run the flow once the app is settled.`;
149387
+ case "indeterminate":
149388
+ return `${measured} This step already performed that one restart, so re-run the flow at most once more before restarting the tool-server rather than the app.`;
149155
149389
  }
149156
149390
  }
149157
149391
  async function waitForVegaAutomation(device, signal) {
@@ -149178,9 +149412,9 @@ async function androidDevtoolsReady(registry2, device) {
149178
149412
  }
149179
149413
  async function treeSourceGate(registry2, device, bundleId, signal) {
149180
149414
  if (device.platform === "ios" && !signal?.aborted) {
149181
- const connected = await waitForNativeDevtools(registry2, device, bundleId, signal);
149182
- if (!connected && !signal?.aborted) {
149183
- return `could not connect to native devtools for ${bundleId}. Re-run to relaunch the app and retry. If it keeps failing, a stale or duplicate argent server may be holding the devtools connection \u2014 restart the argent server and try again.`;
149415
+ const reason = await waitForNativeDevtools(registry2, device, bundleId, signal);
149416
+ if (reason !== null && !signal?.aborted) {
149417
+ return `could not connect to native devtools. ${reason}`;
149184
149418
  }
149185
149419
  }
149186
149420
  if (device.platform === "android" && !signal?.aborted) {
@@ -149209,11 +149443,15 @@ async function runLaunch(state3, app) {
149209
149443
  reason: `no app id declared for platform "${device.platform}" \u2014 add a launch entry for it`
149210
149444
  };
149211
149445
  }
149446
+ let restart;
149212
149447
  try {
149213
- await invokeOnDevice(env, "restart-app", { bundleId });
149448
+ restart = await invokeOnDevice(env, "restart-app", { bundleId });
149214
149449
  } catch (err) {
149215
149450
  if (signal?.aborted) return ABORTED_OUTCOME;
149216
- return { ok: false, reason: `restart-app failed: ${errMsg2(err)}` };
149451
+ return { ok: false, reason: `restart-app failed: ${errMsg3(err)}` };
149452
+ }
149453
+ if (isNativeDevtoolsBlockResult("restart-app", restart)) {
149454
+ return { ok: false, reason: `restart-app did not start ${bundleId}: ${restart.message}` };
149217
149455
  }
149218
149456
  if (!await sleepOrAbort(POST_LAUNCH_SETTLE_MS, signal)) return ABORTED_OUTCOME;
149219
149457
  const gate = await treeSourceGate(registry2, device, bundleId, signal);
@@ -149247,7 +149485,7 @@ async function runChromiumLaunch(state3, app) {
149247
149485
  } catch (err) {
149248
149486
  return {
149249
149487
  ok: false,
149250
- reason: `could not attach to chromium instance "${device.id}": ${errMsg2(err)}`
149488
+ reason: `could not attach to chromium instance "${device.id}": ${errMsg3(err)}`
149251
149489
  };
149252
149490
  }
149253
149491
  state3.attachedAppPath = await resolveAppPath(spec.path, state3.flowsDir);
@@ -149314,7 +149552,7 @@ function singleInstanceLockHint(suspects) {
149314
149552
  return `A clean exit before CDP comes up is the signature of a single-instance lock \u2014 an already-running copy of the app quits the new one at startup. ${clauses.join(" ")}`;
149315
149553
  }
149316
149554
  async function chromiumBootFailureReason(state3, err) {
149317
- const base = `could not boot the chromium app: ${errMsg2(err)}`;
149555
+ const base = `could not boot the chromium app: ${errMsg3(err)}`;
149318
149556
  if (!singleInstanceLockSignal(err)) return base;
149319
149557
  return `${base} ${singleInstanceLockHint(await liveLockSuspects(state3))}`;
149320
149558
  }
@@ -149597,7 +149835,7 @@ function resolveOpts(params) {
149597
149835
  function hoistedBootFailure(err) {
149598
149836
  const signal = singleInstanceLockSignal(err);
149599
149837
  if (!signal) return err;
149600
- return wrapFailure(err, signal, `${errMsg2(err)} ${singleInstanceLockHint(NO_LOCK_SUSPECTS)}`);
149838
+ return wrapFailure(err, signal, `${errMsg3(err)} ${singleInstanceLockHint(NO_LOCK_SUSPECTS)}`);
149601
149839
  }
149602
149840
  function pinnedToChromium(device) {
149603
149841
  return device !== void 0 && resolveDevice(device).platform === "chromium";
@@ -150017,7 +150255,7 @@ async function execRunStep(state3, step, scope) {
150017
150255
  try {
150018
150256
  fragment = parseFlow(await fs49.readFile(canonical, "utf8"));
150019
150257
  } catch (err) {
150020
- return fail(`could not load fragment "${target}": ${errMsg2(err)}`);
150258
+ return fail(`could not load fragment "${target}": ${errMsg3(err)}`);
150021
150259
  }
150022
150260
  pushReport(state3, {
150023
150261
  index,
@@ -150072,7 +150310,7 @@ async function execLeafStep(state3, step, index, scope) {
150072
150310
  ...r.warning !== void 0 ? { warning: r.warning } : {}
150073
150311
  };
150074
150312
  } catch (err) {
150075
- return { ...base, status: "error", reason: errMsg2(err) };
150313
+ return { ...base, status: "error", reason: errMsg3(err) };
150076
150314
  }
150077
150315
  }
150078
150316
  case "wait": {
@@ -150101,7 +150339,7 @@ async function execLeafStep(state3, step, index, scope) {
150101
150339
  artifacts: r.artifacts
150102
150340
  };
150103
150341
  } catch (err) {
150104
- return { ...base, status: "error", reason: errMsg2(err) };
150342
+ return { ...base, status: "error", reason: errMsg3(err) };
150105
150343
  }
150106
150344
  }
150107
150345
  case "tool": {
@@ -150157,16 +150395,31 @@ async function execLeafStep(state3, step, index, scope) {
150157
150395
  args
150158
150396
  };
150159
150397
  }
150398
+ if (isNativeDevtoolsBlockResult(step.name, result)) {
150399
+ return {
150400
+ ...base,
150401
+ status: "fail",
150402
+ tool: step.name,
150403
+ reason: `${step.name} did not run (${result.status}): ${result.message}`,
150404
+ result,
150405
+ outputHint,
150406
+ args
150407
+ };
150408
+ }
150409
+ if (step.name === "launch-app" || step.name === "restart-app") {
150410
+ const launched = args.bundleId;
150411
+ if (typeof launched === "string") state3.launchedNativeApp = launched;
150412
+ }
150160
150413
  return { ...base, status: "pass", tool: step.name, result, outputHint, args };
150161
150414
  } catch (err) {
150162
- return { ...base, status: "error", tool: step.name, reason: errMsg2(err) };
150415
+ return { ...base, status: "error", tool: step.name, reason: errMsg3(err) };
150163
150416
  }
150164
150417
  }
150165
150418
  default:
150166
150419
  return { ...base, status: "error", reason: `unsupported step kind` };
150167
150420
  }
150168
150421
  }
150169
- function errMsg2(err) {
150422
+ function errMsg3(err) {
150170
150423
  return err instanceof Error ? err.message : String(err);
150171
150424
  }
150172
150425
  async function resolveFlowSource(params, fileInput, flowPathInput) {
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swmansion/argent",
3
- "version": "0.21.0",
3
+ "version": "0.21.1-next.0",
4
4
  "mcpName": "io.github.software-mansion/argent",
5
5
  "description": "MCP server for iOS Simulator and Android Emulator control",
6
6
  "license": "Apache-2.0",
@@ -43,7 +43,7 @@ The full iOS flow tree exists only for an app launched by Argent with instrument
43
43
 
44
44
  1. If Metro, Expo, Xcode, an icon, or a prior process launched the app, call `restart-app`. Restore the source screen and retry capture. `launch-app` can only foreground the existing process.
45
45
  2. Tap capture does **not** wait for that connection. It makes one tree read and turns any failure straight into the kept-coordinates warning. A recording-time `restart-app` returns before the devtools connection opens, so the first tap after a restart can warn transiently. Re-record that tap once before escalating; only a warning that survives the retry is evidence of a real fault.
46
- 3. If the warning survives, call `native-devtools-status` with the same UDID and bundle id. If `requiresRestart` is true, restart once and check again.
46
+ 3. If the warning survives, call `native-devtools-status` with the same UDID and bundle id and follow its `message`, which names the one action that helps and says when to stop. `requiresRestart` covers only the states a fresh process fixes: an `unregistered` or `connecting` app reports it false, because it already launched under the terms a restart would recreate.
47
47
  4. If an injectable app remains disconnected, call `stop-all-simulator-servers` once, **scoped to `devices: [<this simulator's UDID>]`**. One tool-server serves every agent on this Argent install, so an unscoped call tears down their devices too. This does not change app or account data. Then restart and check status again.
48
48
  5. If it still fails, report an instrumentation blocker. Do not replace selectors with coordinates in a QA flow.
49
49
 
@@ -53,20 +53,20 @@ Use the same explicit UDID throughout. Multiple booted simulators are not an inj
53
53
 
54
54
  This fallback applies only to `com.apple.*` system apps. A connection failure in another app never authorizes it.
55
55
 
56
- Apple system apps cannot load the instrumentation, and nothing in the launch path exempts them. A `launch:` step on iOS always waits for the devtools connection, so for one of these apps it spends that budget, fails, and every later step is skipped.
56
+ Apple system apps are platform binaries with library validation, so the instrumentation cannot be relied on to load into them it has been seen both loading and not loading, depending on the simulator runtime. Either way it is no basis for a selector.
57
57
 
58
- **Never give such a flow a `launch:` step.** Start it with a raw `tool: restart-app`, which terminates and relaunches without the readiness gate. Accept that the result is a **fragment**: its first non-echo step is not `launch:`, so the runner never classifies it as e2e. The rest of the injection-free form:
58
+ Give the flow a `launch:` step as usual. On iOS the launch waits the full devtools budget out, then passes for one of these bundle ids: starting the app is all that step is for, and a coordinate-driven flow needs nothing more. The flow stays e2e; it just pays roughly sixteen seconds at the launch. Where the impossibility bites is selector resolution, and the first selector step reports it there terminally, naming the coordinate remedy rather than as a launch failure. The rest of the injection-free form:
59
59
 
60
60
  - Raw `tool: await-ui-element` accessibility checks.
61
61
  - Point taps or long-presses derived from `describe`, each named by an echo.
62
62
  - A point focus tap plus a raw text-only `keyboard` with `delayMs: 500`, and a second raw `keyboard` with `key: "enter"` to submit.
63
63
  - Raw swipes with `settle: true` because `scroll-to` needs the missing flow tree. Momentum-free scrolling keeps later coordinate taps valid.
64
64
 
65
- Every point tap or long-press in such a flow passes **carrying a warning**. The app loads no instrumentation, so every tree read fails and each [selector-less gesture](flow-yaml.md#directives) dispatches unsettled. Nothing here repairs it. Accept the warnings, read each green as "the gesture was sent, not that it landed", and put an explicit `wait:` or a raw `tool: await-ui-element` before a gesture that follows a transition. Raw `tool:` steps never take that settle, so they never warn.
65
+ Every point tap or long-press in such a flow passes **carrying a warning** for as long as the app serves no tree: each [selector-less gesture](flow-yaml.md#directives) dispatches unsettled. Nothing here repairs it. Accept the warnings, read each green as "the gesture was sent, not that it landed", and put an explicit `wait:` or a raw `tool: await-ui-element` before a gesture that follows a transition. Raw `tool:` steps never take that settle, so they never warn.
66
66
 
67
67
  Report that the flow is injection-free and its coordinates are not portable. It cannot satisfy the QA contract. Report the artifact and platform blocker instead.
68
68
 
69
- The same fragment fallback covers a normally injectable app that is broken in the environment: raw `restart-app` in place of `launch:` still makes a self-resetting flow. Either way it is not e2e and cannot complete `argent-qa-flows`, which requires a leading `launch:`. Report the blocker rather than labeling that fallback a completed QA test.
69
+ A normally injectable app that is broken in the environment gets the same coordinate-only treatment, but not the same launch: there the `launch:` step fails, since the gate withholds its verdict only for a bundle id injection may never reach. Start such a flow with a raw `tool: restart-app`, which terminates and relaunches without the readiness gate, and accept that the result is a **fragment** its first non-echo step is not `launch:`, so the runner never classifies it as e2e, and it cannot complete `argent-qa-flows`, which requires a leading `launch:`. Report the blocker rather than labeling that fallback a completed QA test.
70
70
 
71
71
  ## Tree source recovery on Android, Chromium, and Vega
72
72
 
@@ -106,7 +106,7 @@ Read the exact error and choose the action that matches it:
106
106
  - `describe` succeeds but is not detailed enough for a React Native app:
107
107
  use `debugger-component-tree` next.
108
108
  - You need app-scoped inspection with full UIKit properties (`accessibilityIdentifier`, `viewClassName`):
109
- use `native-describe-screen` with an explicit `bundleId`. This requires native devtools (dylib) injection — call `restart-app` first if needed.
109
+ use `native-describe-screen` with an explicit `bundleId`. This requires native devtools (dylib) injection.
110
110
  - You already have a candidate point and want to confirm what would actually receive touch:
111
111
  use `native-user-interactable-view-at-point`. Use `native-view-at-point` when you want the visually deepest view instead of the hit-test target.
112
112