@swmansion/argent 0.23.1-next.0 → 0.23.1-next.2

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.
package/README.md CHANGED
@@ -23,7 +23,7 @@ Argent drives a growing set of targets through a single toolkit, each with the r
23
23
 
24
24
  | Platform | Targets | Interaction |
25
25
  | ----------------- | ----------------------------------------------------------------------- | ---------------- |
26
- | **iOS** | Simulators and physical iPhones (USB) | Touch / gesture |
26
+ | **iOS** | Simulators and physical iPhones | Touch / gesture |
27
27
  | **Android** | Emulators (AVDs) and physical devices over adb | Touch / gesture |
28
28
  | **TV** | Apple TV (tvOS), Android TV / Google TV, Amazon Fire TV (Vega) | D-pad / remote |
29
29
  | **Desktop & web** | Electron and Chromium apps (incl. React Native Web / Expo web) over CDP | Mouse / keyboard |
package/dist/cli-cmds.mjs CHANGED
@@ -1805,14 +1805,10 @@ async function sweepDeadStateFiles() {
1805
1805
  continue;
1806
1806
  }
1807
1807
  if (fs.existsSync(fresh.bundlePath)) continue;
1808
- const guarded = process.platform !== "win32";
1809
- if (guarded && !processCommandMatches(fresh.pid, fresh.bundlePath)) continue;
1808
+ if (!couldBeOurToolServer(fresh.pid, fresh.bundlePath)) continue;
1810
1809
  await unlink(file).catch(() => {
1811
1810
  });
1812
- void terminatePid(
1813
- fresh.pid,
1814
- guarded ? () => processCommandMatches(fresh.pid, fresh.bundlePath) : void 0
1815
- );
1811
+ void terminatePid(fresh.pid, () => couldBeOurToolServer(fresh.pid, fresh.bundlePath));
1816
1812
  }
1817
1813
  }
1818
1814
  var readState = readToolsServerState;
@@ -1851,20 +1847,37 @@ async function killToolServer(bundlePath) {
1851
1847
  await terminatePid(state2.pid);
1852
1848
  await clearToolsServerState(bundlePath ?? state2.bundlePath);
1853
1849
  }
1854
- function processCommandMatches(pid, marker) {
1850
+ var PS_BIN = ["/bin/ps", "/usr/bin/ps"].find((p) => fs.existsSync(p)) ?? "ps";
1851
+ var PS_WIDTH_FLAGS = ["-ww"];
1852
+ function readProcessCommandLine(pid, flags2 = PS_WIDTH_FLAGS) {
1853
+ return execFileSync(PS_BIN, [...flags2, "-p", String(pid), "-o", "command="], {
1854
+ encoding: "utf8",
1855
+ timeout: 2e3,
1856
+ // A recycled pid can sit on a process with an argv past Node's 1 MiB exec
1857
+ // default, where the overrun surfaces as ENOBUFS instead of a command line.
1858
+ // Same ceiling as tool-server's vega-process ps probes.
1859
+ maxBuffer: 16 * 1024 * 1024,
1860
+ // Piping ps's stderr is what puts a rejected flag ("ps: invalid option --
1861
+ // 'w'") into the thrown error's message.
1862
+ stdio: ["ignore", "pipe", "pipe"]
1863
+ }).trim();
1864
+ }
1865
+ function couldBeOurToolServer(pid, marker) {
1855
1866
  if (!marker) return false;
1867
+ if (process.platform === "win32") return true;
1868
+ let cmd;
1856
1869
  try {
1857
- const cmd = execFileSync("ps", ["-p", String(pid), "-o", "command="], {
1858
- encoding: "utf8",
1859
- timeout: 2e3,
1860
- stdio: ["ignore", "pipe", "ignore"]
1861
- }).trim();
1862
- if (!cmd) return false;
1863
- const escaped = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1864
- return new RegExp(`(?:^|\\s)${escaped} start(?:\\s|$)`).test(cmd);
1865
- } catch {
1870
+ cmd = readProcessCommandLine(pid);
1871
+ } catch (err) {
1872
+ process.stderr.write(
1873
+ `[launcher] ps could not read pid ${pid}'s command line; leaving it alone: ${String(err)}
1874
+ `
1875
+ );
1866
1876
  return false;
1867
1877
  }
1878
+ if (!cmd) return false;
1879
+ const escaped = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1880
+ return new RegExp(`(?:^|\\s)${escaped} start(?:\\s|$)`).test(cmd);
1868
1881
  }
1869
1882
  var LOCK_WAIT_TIMEOUT_MS = 3e4;
1870
1883
  var LOCK_STALE_MS = 45e3;
@@ -1953,8 +1966,8 @@ async function ensureToolsServer(paths) {
1953
1966
  const state2 = await readState(paths.bundlePath);
1954
1967
  const reuse = await reusableHandle(state2, paths.bundlePath, paths.version);
1955
1968
  if (reuse) return reuse;
1956
- if (state2 && state2.managed === "autospawn" && state2.bundlePath === paths.bundlePath && isProcessAlive(state2.pid) && processCommandMatches(state2.pid, state2.bundlePath)) {
1957
- await terminatePid(state2.pid, () => processCommandMatches(state2.pid, state2.bundlePath));
1969
+ if (state2 && state2.managed === "autospawn" && state2.bundlePath === paths.bundlePath && isProcessAlive(state2.pid) && couldBeOurToolServer(state2.pid, state2.bundlePath)) {
1970
+ await terminatePid(state2.pid, () => couldBeOurToolServer(state2.pid, state2.bundlePath));
1958
1971
  }
1959
1972
  await clearToolsServerState(paths.bundlePath);
1960
1973
  await sweepDeadStateFiles();
@@ -7240,7 +7253,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
7240
7253
  var SESSION_ID2 = randomUUID5();
7241
7254
  function readCliVersion() {
7242
7255
  if (true) {
7243
- return "0.23.1-next.0";
7256
+ return "0.23.1-next.2";
7244
7257
  }
7245
7258
  return "0.0.0";
7246
7259
  }
@@ -16625,7 +16625,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
16625
16625
  var SESSION_ID = randomUUID4();
16626
16626
  function readCliVersion() {
16627
16627
  if (true) {
16628
- return "0.23.1-next.0";
16628
+ return "0.23.1-next.2";
16629
16629
  }
16630
16630
  return "0.0.0";
16631
16631
  }
@@ -23046,15 +23046,11 @@ async function killToolServerForInstallDir(packageDir) {
23046
23046
  const fresh = await readStateFile(file);
23047
23047
  if (!fresh || fresh.pid !== state2.pid || fresh.bundlePath !== state2.bundlePath) continue;
23048
23048
  const alive = isProcessAlive(fresh.pid);
23049
- const guarded = process.platform !== "win32";
23050
- if (alive && guarded && !processCommandMatches(fresh.pid, fresh.bundlePath)) {
23049
+ if (alive && !couldBeOurToolServer(fresh.pid, fresh.bundlePath)) {
23051
23050
  continue;
23052
23051
  }
23053
23052
  if (alive) {
23054
- await terminatePid(
23055
- fresh.pid,
23056
- guarded ? () => processCommandMatches(fresh.pid, fresh.bundlePath) : void 0
23057
- );
23053
+ await terminatePid(fresh.pid, () => couldBeOurToolServer(fresh.pid, fresh.bundlePath));
23058
23054
  }
23059
23055
  await unlink(file).catch(() => {
23060
23056
  });
@@ -23062,20 +23058,37 @@ async function killToolServerForInstallDir(packageDir) {
23062
23058
  }
23063
23059
  return killed;
23064
23060
  }
23065
- function processCommandMatches(pid, marker) {
23061
+ var PS_BIN = ["/bin/ps", "/usr/bin/ps"].find((p) => fs16.existsSync(p)) ?? "ps";
23062
+ var PS_WIDTH_FLAGS = ["-ww"];
23063
+ function readProcessCommandLine(pid, flags = PS_WIDTH_FLAGS) {
23064
+ return execFileSync5(PS_BIN, [...flags, "-p", String(pid), "-o", "command="], {
23065
+ encoding: "utf8",
23066
+ timeout: 2e3,
23067
+ // A recycled pid can sit on a process with an argv past Node's 1 MiB exec
23068
+ // default, where the overrun surfaces as ENOBUFS instead of a command line.
23069
+ // Same ceiling as tool-server's vega-process ps probes.
23070
+ maxBuffer: 16 * 1024 * 1024,
23071
+ // Piping ps's stderr is what puts a rejected flag ("ps: invalid option --
23072
+ // 'w'") into the thrown error's message.
23073
+ stdio: ["ignore", "pipe", "pipe"]
23074
+ }).trim();
23075
+ }
23076
+ function couldBeOurToolServer(pid, marker) {
23066
23077
  if (!marker) return false;
23078
+ if (process.platform === "win32") return true;
23079
+ let cmd;
23067
23080
  try {
23068
- const cmd = execFileSync5("ps", ["-p", String(pid), "-o", "command="], {
23069
- encoding: "utf8",
23070
- timeout: 2e3,
23071
- stdio: ["ignore", "pipe", "ignore"]
23072
- }).trim();
23073
- if (!cmd) return false;
23074
- const escaped = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
23075
- return new RegExp(`(?:^|\\s)${escaped} start(?:\\s|$)`).test(cmd);
23076
- } catch {
23081
+ cmd = readProcessCommandLine(pid);
23082
+ } catch (err) {
23083
+ process.stderr.write(
23084
+ `[launcher] ps could not read pid ${pid}'s command line; leaving it alone: ${String(err)}
23085
+ `
23086
+ );
23077
23087
  return false;
23078
23088
  }
23089
+ if (!cmd) return false;
23090
+ const escaped = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
23091
+ return new RegExp(`(?:^|\\s)${escaped} start(?:\\s|$)`).test(cmd);
23079
23092
  }
23080
23093
 
23081
23094
  // ../argent-tools-client/src/link-config.ts
@@ -16057,14 +16057,10 @@ async function sweepDeadStateFiles() {
16057
16057
  continue;
16058
16058
  }
16059
16059
  if (fs.existsSync(fresh.bundlePath)) continue;
16060
- const guarded = process.platform !== "win32";
16061
- if (guarded && !processCommandMatches(fresh.pid, fresh.bundlePath)) continue;
16060
+ if (!couldBeOurToolServer(fresh.pid, fresh.bundlePath)) continue;
16062
16061
  await unlink(file).catch(() => {
16063
16062
  });
16064
- void terminatePid(
16065
- fresh.pid,
16066
- guarded ? () => processCommandMatches(fresh.pid, fresh.bundlePath) : void 0
16067
- );
16063
+ void terminatePid(fresh.pid, () => couldBeOurToolServer(fresh.pid, fresh.bundlePath));
16068
16064
  }
16069
16065
  }
16070
16066
  var readState = readToolsServerState;
@@ -16097,20 +16093,37 @@ async function terminatePid(pid, stillOurs) {
16097
16093
  }
16098
16094
  await waitForExit(pid, SIGKILL_GRACE_MS);
16099
16095
  }
16100
- function processCommandMatches(pid, marker) {
16096
+ var PS_BIN = ["/bin/ps", "/usr/bin/ps"].find((p) => fs.existsSync(p)) ?? "ps";
16097
+ var PS_WIDTH_FLAGS = ["-ww"];
16098
+ function readProcessCommandLine(pid, flags = PS_WIDTH_FLAGS) {
16099
+ return execFileSync(PS_BIN, [...flags, "-p", String(pid), "-o", "command="], {
16100
+ encoding: "utf8",
16101
+ timeout: 2e3,
16102
+ // A recycled pid can sit on a process with an argv past Node's 1 MiB exec
16103
+ // default, where the overrun surfaces as ENOBUFS instead of a command line.
16104
+ // Same ceiling as tool-server's vega-process ps probes.
16105
+ maxBuffer: 16 * 1024 * 1024,
16106
+ // Piping ps's stderr is what puts a rejected flag ("ps: invalid option --
16107
+ // 'w'") into the thrown error's message.
16108
+ stdio: ["ignore", "pipe", "pipe"]
16109
+ }).trim();
16110
+ }
16111
+ function couldBeOurToolServer(pid, marker) {
16101
16112
  if (!marker) return false;
16113
+ if (process.platform === "win32") return true;
16114
+ let cmd;
16102
16115
  try {
16103
- const cmd = execFileSync("ps", ["-p", String(pid), "-o", "command="], {
16104
- encoding: "utf8",
16105
- timeout: 2e3,
16106
- stdio: ["ignore", "pipe", "ignore"]
16107
- }).trim();
16108
- if (!cmd) return false;
16109
- const escaped = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16110
- return new RegExp(`(?:^|\\s)${escaped} start(?:\\s|$)`).test(cmd);
16111
- } catch {
16116
+ cmd = readProcessCommandLine(pid);
16117
+ } catch (err) {
16118
+ process.stderr.write(
16119
+ `[launcher] ps could not read pid ${pid}'s command line; leaving it alone: ${String(err)}
16120
+ `
16121
+ );
16112
16122
  return false;
16113
16123
  }
16124
+ if (!cmd) return false;
16125
+ const escaped = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16126
+ return new RegExp(`(?:^|\\s)${escaped} start(?:\\s|$)`).test(cmd);
16114
16127
  }
16115
16128
  var LOCK_WAIT_TIMEOUT_MS = 3e4;
16116
16129
  var LOCK_STALE_MS = 45e3;
@@ -16199,8 +16212,8 @@ async function ensureToolsServer(paths) {
16199
16212
  const state = await readState(paths.bundlePath);
16200
16213
  const reuse = await reusableHandle(state, paths.bundlePath, paths.version);
16201
16214
  if (reuse) return reuse;
16202
- if (state && state.managed === "autospawn" && state.bundlePath === paths.bundlePath && isProcessAlive(state.pid) && processCommandMatches(state.pid, state.bundlePath)) {
16203
- await terminatePid(state.pid, () => processCommandMatches(state.pid, state.bundlePath));
16215
+ if (state && state.managed === "autospawn" && state.bundlePath === paths.bundlePath && isProcessAlive(state.pid) && couldBeOurToolServer(state.pid, state.bundlePath)) {
16216
+ await terminatePid(state.pid, () => couldBeOurToolServer(state.pid, state.bundlePath));
16204
16217
  }
16205
16218
  await clearToolsServerState(paths.bundlePath);
16206
16219
  await sweepDeadStateFiles();
@@ -93916,7 +93916,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
93916
93916
  var SESSION_ID = (0, import_node_crypto3.randomUUID)();
93917
93917
  function readCliVersion() {
93918
93918
  if (true) {
93919
- return "0.23.1-next.0";
93919
+ return "0.23.1-next.2";
93920
93920
  }
93921
93921
  return "0.0.0";
93922
93922
  }
@@ -95451,6 +95451,11 @@ function subjectValue(subject, key2) {
95451
95451
  }
95452
95452
  return null;
95453
95453
  }
95454
+ function signingLabel(commonName) {
95455
+ const match = /^([^:]+):.*\(([A-Za-z0-9]+)\)\s*$/.exec(commonName);
95456
+ if (match) return `${match[1].trim()} (${match[2]})`;
95457
+ return commonName.split(":")[0].trim();
95458
+ }
95454
95459
  function parseSigningTeams(pem) {
95455
95460
  const newestByTeam = /* @__PURE__ */ new Map();
95456
95461
  for (const block of pem.match(PEM_BLOCK_RE) ?? []) {
@@ -95461,11 +95466,12 @@ function parseSigningTeams(pem) {
95461
95466
  continue;
95462
95467
  }
95463
95468
  const teamId = subjectValue(cert.subject, "OU");
95464
- const label = subjectValue(cert.subject, "CN");
95469
+ const commonName = subjectValue(cert.subject, "CN");
95465
95470
  const issuedAtMs = Date.parse(cert.validFrom);
95466
- if (!teamId || !label || Number.isNaN(issuedAtMs)) {
95471
+ if (!teamId || !commonName || Number.isNaN(issuedAtMs)) {
95467
95472
  continue;
95468
95473
  }
95474
+ const label = signingLabel(commonName);
95469
95475
  const known = newestByTeam.get(teamId);
95470
95476
  if (!known || issuedAtMs > known.issuedAtMs) {
95471
95477
  newestByTeam.set(teamId, { teamId, label, issuedAtMs });
@@ -113260,7 +113266,9 @@ function extractDeviceArg(data) {
113260
113266
  return null;
113261
113267
  }
113262
113268
  function targetsIosPhysicalDevice(data) {
113263
- const deviceArg = extractDeviceArg(data);
113269
+ const record2 = data && typeof data === "object" ? data : null;
113270
+ const flowDevice = typeof record2?.device === "string" ? record2.device : null;
113271
+ const deviceArg = extractDeviceArg(data) ?? flowDevice;
113264
113272
  return deviceArg !== null && isIosPhysicalDevice(resolveDevice(deviceArg));
113265
113273
  }
113266
113274
  function inferPlatform(deviceId) {
@@ -122048,7 +122056,7 @@ var gestureSwipeTool = {
122048
122056
  Generates interpolated Move events for a natural feel (~60fps).
122049
122057
  Swipe up (fromY > toY) to scroll content down.
122050
122058
  Use when you need to scroll a list, dismiss a modal, drag an element, or navigate between pages. Not supported on Chromium \u2014 use gesture-scroll there instead.
122051
- Physical iOS: an edge gesture (back-swipe) needs fromX 0 exactly.
122059
+ Physical iOS: an edge gesture (back-swipe) needs fromX 0 exactly; durationMs sets drag speed, not time; momentum:false only rests 300ms at the end and does not damp.
122052
122060
  Pass momentum:false for a momentum-free swipe that lands where the finger lifts (little to no fling at the 300 default), when you need a deterministic scroll distance; it needs durationMs >= 150 and is rejected below that, a shorter ease-out leaving the OS too little wall clock to read the deceleration as a stop. At 150 it lands short of the lift point instead, and 2 of 47 runs still flung backwards. A plain swipe takes any duration up to 10000ms and is delivered as close to the speed it was authored as a 16ms frame allows: below ~32ms the whole travel lands in one or two frames, which the OS flings as hard as it flings anything. Returns { swiped: true, timestampMs }. On physical iOS, reactivated: true = app was re-fronted; re-describe. Fails if the simulator-server / emulator backend is not reachable for the given device.`,
122053
122061
  alwaysLoad: true,
122054
122062
  searchHint: "swipe scroll drag pan gesture device simulator emulator touch move",
@@ -133313,7 +133321,7 @@ The tree carries no z-order or occlusion information: an element listed at a poi
133313
133321
  by an overlay (e.g. a toolbar over list rows), so check a screenshot when a tap lands unexpectedly.
133314
133322
 
133315
133323
  For app-scoped inspection with full UIKit properties (accessibilityIdentifier, viewClassName),
133316
- use native-describe-screen with an explicit bundleId instead (iOS only).
133324
+ use native-describe-screen with an explicit bundleId instead (iOS simulator only).
133317
133325
  For React Native apps, debugger-component-tree returns React component names with tap coordinates.
133318
133326
 
133319
133327
  On a TV target (Apple TV / Android TV \u2014 a \`list-devices\` entry with runtimeKind 'tv') this returns
@@ -145637,7 +145645,7 @@ function connectedIosDeviceHint(devices, platform) {
145637
145645
  if (platform && platform !== "ios") return "";
145638
145646
  const ids = devices.filter((d) => d.platform === "ios" && d.kind === "device" && d.state === "connected").map((d) => d.udid).filter((id) => Boolean(id));
145639
145647
  if (ids.length === 0) return "";
145640
- return ` A physical iPhone is connected (${ids.join(", ")}); hardware is never picked automatically, pass --device <udid> to run on it.`;
145648
+ return ` A physical iPhone is connected (${ids.join(", ")}); hardware is never picked automatically, pass device <udid> (--device on the CLI) to run on it.`;
145641
145649
  }
145642
145650
  function describeDevice(d) {
145643
145651
  return `${deviceEntryId(d) ?? "?"} (${d.platform}${d.state ? `, ${d.state}` : ""})`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swmansion/argent",
3
- "version": "0.23.1-next.0",
3
+ "version": "0.23.1-next.2",
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",
package/rules/argent.md CHANGED
@@ -83,7 +83,7 @@ Decision order:
83
83
  argent install, so an unscoped call tears down their devices too; reserve that form for a deliberate
84
84
  machine-wide cleanup.
85
85
  If the user started Metro separately, ask whether to call `stop-metro` (specify the port if not 8081).
86
- - If tools provided by mcp-server are not sufficient and action can be done using `xcrun`, `adb`, or other commands, use the command. Examples: changing device options, performing a device action such as lock, shake, etc.
86
+ - If tools provided by mcp-server are not sufficient and action can be done using `xcrun`, `adb`, or other commands, use the command. Examples: changing device options, performing a device action such as lock, shake, etc. Not on a physical iPhone.
87
87
  - When waiting for an action, do not call `screenshot` repeatedly without a proper wait mechanism. Use the `await-ui-element` tool to block until the UI settles (e.g. wait for an element to become `visible`/`hidden`, or to contain expected `text`) instead of polling.
88
88
  </general_rules>
89
89
 
@@ -114,7 +114,7 @@ When: Beginning a task that involves the Android emulator, no emulator running y
114
114
 
115
115
  PHYSICAL iPHONE (USB)
116
116
  Skills: `argent-ios-device-setup` (cable, trust, signing), then `argent-ios-device-interact` (the app-scoped interaction contract)
117
- When: The user names a physical iPhone, a real device, or hardware, or the target `list-devices` iOS entry has kind `"device"`. Never for a simulator, and never because a cabled phone is listed first. On hardware every interaction starts with `launch-app`; `paste`, `settings-permissions`, two-finger gestures, `rotate` and `shake` do not exist there.
117
+ When: The user names a physical iPhone, a real device, or hardware, or the target `list-devices` iOS entry has kind `"device"`. Never for a simulator, and never because a cabled phone is listed first. On hardware every interaction starts with `launch-app`; `paste`, `settings-permissions`, two-finger gestures, `rotate`, `shake`, screen recording, `debugger-*`, `react-profiler-*`, `native-profiler-*` and `native-*` do not exist there.
118
118
  Prompt keywords: physical iPhone, real device, on my phone, USB, hardware
119
119
 
120
120
  TAPPING, SWIPING, TYPING, GESTURES, SCREENSHOTS, SCROLLING
@@ -13,7 +13,7 @@ For a saved QA test case, ticket, or acceptance criterion, load `argent-qa-flows
13
13
 
14
14
  - Before creating or changing a flow, read [Live authoring](references/live-authoring.md) completely.
15
15
  - When polishing, composing, or manually reviewing YAML, read [Flow YAML](references/flow-yaml.md). For Vega, read its platform limits before recording remote or keyboard tools.
16
- - Flows run on physical iPhones (an iOS `list-devices` entry with kind `"device"`), but replay never auto-binds one, even when no simulator is booted: pass the phone's udid as `device` (CLI `--device`), and only a `connected` phone can run. `pinch`/`rotate` steps fail there like the live tools. See `argent-ios-device-interact` for the hardware contract.
16
+ - Flows run on physical iPhones (an iOS `list-devices` entry with kind `"device"`), but replay never auto-binds one, even when no simulator is booted: pass the phone's udid as `device` (CLI `--device`), and only a `connected` phone can run. `pinch`/`rotate` steps fail there like the live tools. On hardware the flow tree is the `describe` tree: same ids and roles, no UIView hierarchy. See `argent-ios-device-interact` for the hardware contract.
17
17
  - On capture warnings, raw coordinates, unavailable trees, mistimed transitions, overlays, or replay failures, read [Reliability and recovery](references/reliability-and-recovery.md).
18
18
 
19
19
  ## Non-negotiable rules
@@ -9,10 +9,12 @@ Read this only for a physical iPhone. `argent-device-interact` still applies for
9
9
 
10
10
  ## Contract
11
11
 
12
- - Observation never changes the screen; mutation may. `describe` and `await-ui-element` fail on a backgrounded target instead of re-fronting it. Gestures and `keyboard` re-front it and return `reactivated: true`; re-describe before the next step.
13
- - Each tool's description states its own hardware limits (named keys, `gesture-custom` shapes, buttons, edge swipes, tap counts), and every failure names its fix.
12
+ - Observation never changes the screen; mutation may. `describe` and `await-ui-element` fail on a backgrounded target instead of re-fronting it; `await-screen-idle` returns `settled: false` with no reason. The first `describe` within a second of `button home` can still return the old tree; describe again. Gestures and `keyboard` re-front it and return `reactivated: true`; re-describe before the next step.
13
+ - `launch-app`, `open-url` and `restart-app` set the app under automation; `reinstall-app` clears it; a tool-server restart forgets it: launch again.
14
+ - Each tool's description states its own hardware limits (named keys, `gesture-custom` shapes, buttons, edge swipes, tap counts). A gated tool fails with a bare `not supported on ios device`: the fix is in the list below, not in the error.
14
15
 
15
16
  ## Tools on hardware
16
17
 
17
- - Only these exist: `list-devices`, `launch-app`, `restart-app`, `reinstall-app`, `open-url`, `describe`, `screenshot`, `screenshot-diff`, `gesture-tap`, `gesture-swipe`, `gesture-custom`, `button`, `keyboard`, `await-ui-element`, `await-screen-idle`, `run-sequence`, the flow tools, and `stop-simulator-server`. Everything else fails with `not supported on ios device`.
18
- - No two-finger gestures, `rotate`, `shake`, `paste`, or `settings-permissions`: drive the app's own zoom and rotate UI with taps and drags, move the phone by hand, type with `keyboard`, and change permissions in the phone's Settings.
18
+ - Only these exist: `list-devices`, `launch-app`, `restart-app`, `reinstall-app`, `open-url`, `describe`, `screenshot`, `screenshot-diff`, `gesture-tap`, `gesture-swipe`, `gesture-custom`, `button` (`home`, `volumeUp`, `volumeDown`, `actionButton` on models that have one), `keyboard`, `await-ui-element`, `await-screen-idle`, `run-sequence`, the flow tools, `stop-simulator-server` and `stop-all-simulator-servers` (session end). Every other device tool fails with `not supported on ios device`.
19
+ - No two-finger gestures, `rotate`, `shake`, `paste`, `settings-permissions`, screen recording, `debugger-*`, `react-profiler-*`, `native-profiler-*`, `native-*`, `boot-device`: drive the app's own zoom and rotate UI with taps and drags, move the phone by hand, type with `keyboard`, change permissions in the phone's Settings, and debug, profile or record on a simulator.
20
+ - `gesture-swipe`: `durationMs` sets drag speed, not time; `momentum:false` only rests 300 ms at the end, no damping. `open-url` https lands in Safari, never in the app that owns the link: pass `bundleId`.
@@ -10,11 +10,11 @@ Read this only for a physical iPhone. Simulators use `argent-ios-simulator-setup
10
10
  ## First run
11
11
 
12
12
  1. Cable the phone, unlock it, keep the screen awake, and turn on Developer Mode (Settings > Privacy & Security > Developer Mode). `list-devices` must show it `connected`.
13
- 2. Run the first interaction tool. It builds and signs the on-device runner: seconds from cache, minutes on a cold build.
13
+ 2. `launch-app` only registers the app. The first `describe`, gesture or `screenshot` builds, signs and starts the on-device runner: minutes cold, tens of seconds from cache or after a tool-server restart. Build cap 15 min, runner ready 120 s.
14
14
  3. On the first install the phone asks to trust the developer (Settings > General > VPN & Device Management), and an **ArgentRunner** app appears on the home screen. Tell the user it is argent's automation runner and must stay installed.
15
15
 
16
16
  ## Signing and failures
17
17
 
18
- Signing needs no configuration. The result note or the error text names the team picked, the `ARGENT_IOS_TEAM_ID` override, and the fix for every signing, cable, lock, or trust failure: apply it and retry the same call.
18
+ Signing needs no configuration. The first phone call of a tool-server process carries a note naming the team picked and the `ARGENT_IOS_TEAM_ID` override (tool-server environment; a change forces a cold rebuild). Cable, lock, trust, expired-profile and missing-certificate errors name their fix: apply it, retry the same call. Also: `errSecInternalComponent`: run `security set-key-partition-list -S apple-tool:,apple:,codesign: -s ~/Library/Keychains/login.keychain-db` (asks the user's login password), retry. `team has no devices`: keep the phone cabled, retry. Bundle id registration failed: free-team app-id cap, wait days or sign under a paid team. Any other xcodebuild failure prints raw `error:` lines: read `~/.argent/ios-device-runner/logs/runner-<udid8>.log`. `RUNNER_WEDGED`: `stop-simulator-server` for the udid, retry.
19
19
 
20
20
  Then read `argent-ios-device-interact`.
@@ -8,7 +8,7 @@ description: Set up and connect to an iOS simulator using argent MCP tools. Use
8
8
  If you delegate simulator tasks to sub-agents, make sure they have MCP permissions.
9
9
 
10
10
  1. **Find a booted simulator**
11
- Use `list-devices`. Filter for entries with `platform: "ios"` booted iPhones are listed first.
11
+ Use `list-devices`. Filter for entries with `platform: "ios"` and skip any with `kind: "device"` (a physical iPhone, never a simulator target): booted simulators are listed first.
12
12
  If none are booted, call `boot-device` with `udid: <chosen UDID>`.
13
13
 
14
14
  2. **Verify connection**
@@ -5,6 +5,8 @@ description: Debug a JS runtime via CDP using argent debugger tools. Primary pat
5
5
 
6
6
  ## 1. Prerequisites
7
7
 
8
+ Physical iPhone: not supported; every `debugger-*` tool rejects `kind: "device"`. Use a simulator.
9
+
8
10
  For **React Native (iOS / Android)**: requires **Metro dev server running** (default `localhost:8081`) and **a React Native app connected to Metro** (at least one CDP target). Verify via `debugger-status` — it returns `status: "connected"` or `status: "not_connected"` with a `reason` and `guidance` (it does not fail when the debugger is unreachable).
9
11
 
10
12
  For **Vega (Fire TV)**: requires a **Debug `.vpkg`** (a Release build never attaches) and **Metro reachable from the device** (`vega device start-port-forwarding --port 8081 --forward false`). Verify via `debugger-status`. `debugger-component-tree`, `debugger-inspect-element`, `debugger-reload-metro` and the `react-profiler-*` / `profiler-*` tools are unavailable there — see the `argent-tv-interact` skill.
@@ -10,6 +10,7 @@ description: Native profiling for CPU hotspots, UI hangs, memory issues. iOS via
10
10
  - `native-profiler-analyze` — parse exported trace data and return a structured bottleneck payload.
11
11
  - `profiler-stack-query` — drill into parsed data: hang stacks, function callers, thread breakdown, leak details.
12
12
  - `profiler-load` — list and reload previous trace sessions from disk for re-investigation.
13
+ - Physical iPhone: not supported; use a simulator.
13
14
 
14
15
  ---
15
16
 
@@ -11,7 +11,7 @@ Load `argent-create-flow` as the authoring engine. Follow its required reference
11
11
 
12
12
  **Apple TV and Android TV are out of scope.** The runner does not reject touch directives there, so they fail at the gesture layer instead of with authoring guidance. Use `argent-tv-interact` and report the limitation.
13
13
 
14
- **Physical iPhones** run QA flows, with two hardware limits: replay never auto-binds a phone, so pass its udid as `device` (CLI `--device`) and keep it `connected`, and `pinch`/`rotate` steps fail there like the live tools, so drive the app's own zoom UI instead. Read `argent-ios-device-interact` for the app-scoped contract before recording.
14
+ **Physical iPhones** run QA flows, with three hardware limits: replay never auto-binds a phone, so pass its udid as `device` (CLI `--device`) and keep it `connected`; `pinch`/`rotate` steps fail there like the live tools, so drive the app's own zoom UI instead; the flow tree is the `describe` tree (same ids and roles), so a selector authored on a simulator can miss there. Read `argent-ios-device-interact` for the app-scoped contract before recording.
15
15
 
16
16
  ## Definition of done
17
17
 
@@ -3,6 +3,8 @@ name: argent-react-native-app-workflow
3
3
  description: Step-by-step workflows for developing or debugging React Native apps on iOS simulator or Android emulator. Use when starting the app, debugging Metro, fixing builds, diagnosing runtime errors, or running tests.
4
4
  ---
5
5
 
6
+ Physical iPhone (`kind: "device"`): Metro debugging and profiling tools reject it. Use a simulator.
7
+
6
8
  ## 1. Starting the React Native App
7
9
 
8
10
  ### 1.1 Explore Configuration (MANDATORY — Do This First)
@@ -5,6 +5,8 @@ description: Profile a React Native Hermes app to measure re-render and CPU perf
5
5
 
6
6
  This skill is complementary to `argent-react-native-optimization`, not a replacement for it.
7
7
 
8
+ Physical iPhone: not supported; `react-profiler-*` reject `kind: "device"`. Profile on a simulator.
9
+
8
10
  ## 2. Tool Overview
9
11
 
10
12
  ### React Profiler (Hermes / React commits)
@@ -5,6 +5,8 @@ description: Autonomously test an app UI (iOS or Android) by running interact-sc
5
5
 
6
6
  ## Platform-agnostic
7
7
 
8
+ Physical iPhone (`kind: "device"`): read `argent-ios-device-interact` first. `launch-app` before anything; `describe` fails while the app is backgrounded.
9
+
8
10
  The interaction tool names are identical on iOS and Android — `gesture-tap`, `gesture-swipe`, `describe`, `screenshot`, `launch-app`, etc. — and the tool-server auto-dispatches based on the `udid` you pass (UUID-shape → iOS, adb serial → Android).
9
11
 
10
12
  **Before testing, resolve which device to test on.** Call `list-devices` and follow `<device_selection_rule>`: prefer a running device on any platform;