Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
32f6b22
feat(flow): key recording sessions by flow path instead of module glo…
hubgan Jul 27, 2026
eb317b3
test(flow): cover concurrent recordings and device-scoped teardown
hubgan Jul 27, 2026
f7f1a26
fix(flow): serialize every mutation of a flow file, not just appends
hubgan Jul 27, 2026
903b45b
fix(flow): keep a failed finish recoverable, and scope what errors di…
hubgan Jul 27, 2026
451cc84
fix(flow): hold the invariant for the whole critical section, and pin it
hubgan Jul 27, 2026
6110382
fix(flow): make the recording cap evict the least recently USED session
hubgan Jul 27, 2026
af14e43
fix(flow): stop a bare IP from claiming every wireless device at that…
hubgan Jul 27, 2026
6f9adc2
test(flow): make the eviction test prove recency, and drop the inert …
Jul 28, 2026
746585e
fix(flow): stop the superseded-step error from advising a destructive…
Jul 28, 2026
dc7722b
test(flow): pin the lock's self-cleanup and the append's disk re-read
Jul 28, 2026
5bc82cf
fix(flow): make flow-file writes atomic, and share the device→service…
Jul 28, 2026
e8552f3
fix(flow): close the remaining teardown gaps, and pin what the tests …
Jul 28, 2026
cdcf43f
fix(flow): make the scope key strict, and correct what the docs promise
Jul 29, 2026
666b3cb
test(flow): pin the lock map's self-cleanup, the one half nothing obs…
Jul 29, 2026
35624da
fix(flow): count the discarded take from disk, and correct what the d…
Jul 29, 2026
0da0275
fix(flow): qualify what the disk count promises, and stop the mock fr…
Jul 29, 2026
881d305
docs(flow): cut the recording tool descriptions back to what the tool…
Jul 30, 2026
3f07d88
test(flow): give the echo interaction-message case the params its sch…
Jul 30, 2026
a9e7148
fix(flow): rebind the recorded device scope, and correct what the com…
Aug 3, 2026
0068f0c
fix(flow): keep the restart discard-report correct under a racing evi…
hubgan Aug 4, 2026
2d53f6c
fix(flow): name the flow file when an atomic write fails, and correct…
hubgan Aug 4, 2026
73f1e19
docs: correct the device-services ownership, matcher, and device-arg …
hubgan Aug 4, 2026
efd15d6
test(flow): pin the coverage gaps, and tighten the create-flow skill
hubgan Aug 4, 2026
3d9a6f9
fix(flow): count a device-list arg as acting on a device
hubgan Aug 6, 2026
197c99a
fix(screen-recording,profiler): say a teardown reaped the session, no…
hubgan Aug 6, 2026
987dbe0
fix(debugger): explain a console history the teardown deleted
hubgan Aug 6, 2026
c95ffe2
fix(flow): keep the scratch path out of a write failure, and blame th…
hubgan Aug 6, 2026
6dccd7c
test: pin the five behaviours the suite was not holding
hubgan Aug 6, 2026
fec70d0
fix(flow): reconcile the recorder with main's run: gate and symlinked…
hubgan Aug 6, 2026
a7ce547
fix(flow): key a recording by the file the filesystem resolves, not t…
hubgan Aug 6, 2026
c17405b
fix(flow): let a cleanup flow run again, and scope its teardown when …
hubgan Aug 6, 2026
95ead1d
fix(flow): keep a recorded teardown's device scope in the YAML
hubgan Aug 6, 2026
548edf0
fix(debugger): keep the log-registry's promise on Chromium too
hubgan Aug 6, 2026
a7de7d9
fix(flow): classify a failed flows-dir creation as a flow failure
hubgan Aug 6, 2026
02bd72d
fix(flow): point a symlinked flow's write failure at the directory th…
hubgan Aug 6, 2026
65a0d5c
fix(flow): validate project_root on the flow_path branch again
hubgan Aug 6, 2026
61f2732
docs: correct what the telemetry, swap and start-recording comments c…
hubgan Aug 6, 2026
9ee6674
test: pin the changed behaviour the suite was leaving to inference
hubgan Aug 6, 2026
a91aa82
fix(flow): reject rather than throw synchronously from resolveFlowKey
hubgan Aug 6, 2026
0c5ab0b
fix(stop): name the debugger sessions a device scope cannot reach
hubgan Aug 6, 2026
2c175cc
fix(react-profiler): stop the in-app profiler when a teardown dispose…
hubgan Aug 6, 2026
eb7d107
fix(flow): stop a replayed cleanup flow retargeting a device it never…
hubgan Aug 6, 2026
ed39aa8
fix(flow): stop the alias guard asserting a truncation that may not h…
hubgan Aug 6, 2026
73fdd9f
fix(flow): write through a dangling flow-file symlink instead of repl…
hubgan Aug 6, 2026
9070744
fix(native-profiler): fail a start whose session a teardown destroyed…
hubgan Aug 6, 2026
b57d543
fix(flow): stop the write hint calling an ordinary flow file a symlink
hubgan Aug 6, 2026
947c4b5
fix(debugger): clear the reaped-session breadcrumb on an explicit con…
hubgan Aug 6, 2026
c2474d6
fix(reaped-sessions): stop the breadcrumb pinning the teardown on one…
hubgan Aug 6, 2026
8d5d4e3
fix(native-profiler): leave a breadcrumb for a capped or crashed capt…
hubgan Aug 6, 2026
ce56604
fix(flow): stop the cleanup-flow device resolve swallowing genuine fa…
hubgan Aug 6, 2026
28920bc
fix(flow): clear a finished recording by the key the session holds
hubgan Aug 6, 2026
15d4280
test(flow): pin that a deleted vault target no longer orphans the rec…
hubgan Aug 6, 2026
c368be5
fix(stop): honour the request abort signal during the sweep
hubgan Aug 6, 2026
b0b2c09
fix(flow): keep a flow file's mode, and refuse to overwrite a read-on…
hubgan Aug 6, 2026
20358cb
test(flow): make the finish-vs-append race pin both outcomes, not one…
hubgan Aug 6, 2026
266a62f
fix(test): repair the test-only typecheck after the session-api change
hubgan Aug 6, 2026
e1712c6
test(stop): make the scoped-stop cases prove scoping, and the mock mi…
hubgan Aug 6, 2026
fbc61e7
test: cover the changed lines the suite was leaving to inference
hubgan Aug 6, 2026
1344af0
test(flow): guard the flow-add-step schema the CLI tests hand-copy
hubgan Aug 6, 2026
4c30ae2
docs: correct what the deviceless test and the e2e cleanup phase claim
hubgan Aug 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 49 additions & 2 deletions packages/argent-cli/test/flag-parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,25 @@ describe("flag-parser array + -json interleave never throws a raw error", () =>
});

// A tool (like flow-add-step) whose schema declares its own `args` field — a
// JSON string holding the recorded step's tool arguments.
// JSON string holding the recorded step's tool arguments. Mirrors the schema the
// registry advertises for the real tool (zodObjectToJsonSchema over
// packages/tool-server/src/tools/flows/flow-add-step.ts): recordings are keyed by
// `name` + `project_root`, so both are required alongside `command`.
//
// This fixture is hand-copied: `@argent/cli` does not depend on the tool-server,
// so it cannot derive the schema. The guard that catches drift lives where the
// schema does — `flow-tools.test.ts`'s "the flow-add-step schema the CLI tests
// hand-copy". If that fails, this fixture is what it is telling you to update.
const flowAddStepSchema: JsonSchema = {
type: "object",
properties: {
name: { type: "string" },
project_root: { type: "string" },
command: { type: "string" },
args: { type: "string" },
delayMs: { type: "integer" },
},
required: ["command"],
required: ["name", "project_root", "command"],
};

// A tool (like gesture-tap) with NO `args` field — here `--args` must stay the
Expand All @@ -138,6 +148,43 @@ const gestureTapSchema: JsonSchema = {
};

describe("parseFlags — schema-aware --args", () => {
it("routes the recording identity through the plain scalar path", () => {
const result = parseFlags(
[
"--name",
"checkout-e2e",
"--project_root",
"/Users/dev/My Projects/demo-app",
"--command",
"gesture-tap",
"--args",
'{"udid":"X"}',
],
flowAddStepSchema
);
expect(result.args.name).toBe("checkout-e2e");
// `project_root` is the only schema field carrying an underscore, so it pins
// that flag names reach the payload verbatim — a parser that normalised them
// to camel/kebab case would file the value under the wrong key and the server
// would reject the step for a missing `project_root`. The value also holds a
// space: argv arrives already split, so it must survive whole.
expect(result.args.project_root).toBe("/Users/dev/My Projects/demo-app");
expect(result.args.command).toBe("gesture-tap");
expect(result.args.args).toBe('{"udid":"X"}');
expect(result.rawArgs).toBeNull();
});

it("routes the recording identity through the inline --field=<value> form too", () => {
const result = parseFlags(
["--name=checkout-e2e", "--project_root=/Users/dev/demo-app", "--command=screenshot"],
flowAddStepSchema
);
expect(result.args.name).toBe("checkout-e2e");
expect(result.args.project_root).toBe("/Users/dev/demo-app");
expect(result.args.command).toBe("screenshot");
expect(result.rawArgs).toBeNull();
});

it("treats --args as the tool's own string field (space-separated form)", () => {
const result = parseFlags(
["--command", "gesture-tap", "--args", '{"udid":"X","x":0.5}'],
Expand Down
124 changes: 110 additions & 14 deletions packages/argent-cli/test/run-flow-add-step-payload.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import { run, type RunCommandOptions } from "../src/run.js";
// End-to-end regression guard for issue #452 at the `run()` layer.
//
// The documented per-flag form
// argent run flow-add-step --command gesture-tap --args '{"udid":...}'
// must reach the tool-server with BOTH `command` AND the tool's own `args`
// field in the payload. The bug shadowed the `args` field with the
// whole-payload escape hatch, so `args` was consumed as the entire payload and
// the field arrived `undefined` (with udid/x/y hoisted to the top level).
// argent run flow-add-step --name t --project_root /p --command gesture-tap \
// --args '{"udid":...}'
// must reach the tool-server with the recording identity (`name` +
// `project_root`), the `command`, AND the tool's own `args` field in the
// payload. The bug shadowed the `args` field with the whole-payload escape
// hatch, so `args` was consumed as the entire payload and the field arrived
// `undefined` (with udid/x/y hoisted to the top level).
//
// `parseFlags` is unit-tested directly, and `--help` suppression is covered in
// run-help.test.ts. Neither drives the whole `run()` path through to the wire.
Expand All @@ -32,15 +34,38 @@ function startServer(cap: Captured): Promise<{ url: string; close: () => Promise
tools: [
{
name: "flow-add-step",
description: "Add a step to the active flow recording",
// Leading sentence of the real tool description, verbatim.
description:
"Execute a tool call and record it as a step in the flow named by `name` + `project_root` (the recording must already be open — see flow-start-recording).",
// Mirrors what the registry advertises for the real tool —
// zodObjectToJsonSchema over the zod schema in
// packages/tool-server/src/tools/flows/flow-add-step.ts. `name`
// and `project_root` identify which open recording the step
// belongs to and are required alongside `command`.
//
// Only `properties` is load-bearing here: `parseFlags` reads it
// to decide whether `args` belongs to the tool, and reads
// `required` nowhere (its one consumer is `formatSchemaUsage`,
// the help renderer, which this file never invokes — that is
// covered by run-help.test.ts). The array is kept faithful so the
// fixture stays readable as the real schema, not because dropping
// an entry would fail here.
//
// Hand-copied because `@argent/cli` does not depend on the
// tool-server. The guard that catches drift lives where the schema
// does — flow-tools.test.ts's "the flow-add-step schema the CLI
// tests hand-copy"; if that fails, this is one of the fixtures it
// is telling you to update.
inputSchema: {
type: "object",
properties: {
name: { type: "string" },
project_root: { type: "string" },
command: { type: "string" },
args: { type: "string" },
delayMs: { type: "integer" },
delayMs: { type: "integer", minimum: 0, maximum: 9007199254740991 },
},
required: ["command"],
required: ["name", "project_root", "command"],
},
},
],
Expand Down Expand Up @@ -88,6 +113,11 @@ describe("CLI run — flow-add-step --args reaches the payload (issue #452)", ()

const opts: RunCommandOptions = { paths: {} as never }; // unused: ARGENT_TOOLS_URL is set

const FLOW = "checkout-e2e";
// A path with a space: the shell hands argv already split, so the value must
// arrive verbatim rather than being re-split or truncated by the parser.
const ROOT = "/Users/dev/My Projects/demo-app";

beforeEach(async () => {
cap = { path: null, body: null };
server = await startServer(cap);
Expand All @@ -109,26 +139,92 @@ describe("CLI run — flow-add-step --args reaches the payload (issue #452)", ()
await server.close();
});

it("per-flag form: --command X --args '<json>' sends BOTH fields verbatim to the server", async () => {
it("per-flag form: every required field plus --args '<json>' reaches the server verbatim", async () => {
const stepArgs = '{"udid":"SIM-1","x":0.5,"y":0.35}';

await run(["flow-add-step", "--command", "gesture-tap", "--args", stepArgs], opts);
await run(
[
"flow-add-step",
"--name",
FLOW,
"--project_root",
ROOT,
"--command",
"gesture-tap",
"--args",
stepArgs,
],
opts
);

expect(cap.path).toMatch(/^\/tools\/flow-add-step/);
expect(cap.body).not.toBeNull();
const payload = JSON.parse(cap.body!) as Record<string, unknown>;
// The exact regression from #452: `args` survives as the tool's own string
// field (the raw JSON passed through untouched), and its keys are NOT
// hoisted to the top level as they were when `--args` was swallowed whole.
expect(payload).toEqual({ command: "gesture-tap", args: stepArgs });
// The recording identity rides alongside it — without both `name` and
// `project_root` the server cannot find the open recording, so a payload
// missing either is a failed step, not a mislabelled one.
expect(payload).toEqual({
name: FLOW,
project_root: ROOT,
command: "gesture-tap",
args: stepArgs,
});
});

it("inline --args=<json> form also sends both fields", async () => {
it("inline --field=<value> form sends the same payload", async () => {
const stepArgs = '{"udid":"SIM-1","x":0.5,"y":0.35}';

await run(["flow-add-step", "--command", "gesture-tap", `--args=${stepArgs}`], opts);
await run(
[
"flow-add-step",
`--name=${FLOW}`,
`--project_root=${ROOT}`,
"--command",
"gesture-tap",
`--args=${stepArgs}`,
],
opts
);

const payload = JSON.parse(cap.body!) as Record<string, unknown>;
expect(payload).toEqual({
name: FLOW,
project_root: ROOT,
command: "gesture-tap",
args: stepArgs,
});
});

it("coerces --delayMs by its declared integer type and omits absent optionals", async () => {
// `delayMs` is the only non-string field in the schema, so it is the one
// place the payload can arrive with the wrong JSON type: a string "250"
// fails the server's zod validation. `args` is optional — omitting the flag
// must leave the key out rather than sending null/"".
await run(
[
"flow-add-step",
"--name",
FLOW,
"--project_root",
ROOT,
"--command",
"screenshot",
"--delayMs",
"250",
],
opts
);

const payload = JSON.parse(cap.body!) as Record<string, unknown>;
expect(payload).toEqual({ command: "gesture-tap", args: stepArgs });
expect(payload).toEqual({
name: FLOW,
project_root: ROOT,
command: "screenshot",
delayMs: 250,
});
expect(payload).not.toHaveProperty("args");
});
});
53 changes: 49 additions & 4 deletions packages/argent-cli/test/run-help.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,37 @@ vi.mock("@argent/tools-client", () => ({

vi.mock("@argent/telemetry", () => telemetryMock);

// A tool (like flow-add-step) that owns its `args` field.
// A tool (like flow-add-step) that owns its `args` field. Schema and
// description mirror what the registry advertises for the real tool -
// zodObjectToJsonSchema over the zod schema in
// packages/tool-server/src/tools/flows/flow-add-step.ts. Recordings are keyed
// by `name` + `project_root`, so both are required alongside `command` and only
// `args` / `delayMs` are optional. The assertions below pin `required` in both
// directions — each entry against its `(required)` marker, each non-entry
// against a negative lookahead — so dropping or adding one here fails loudly.
// The drift that does pass silently is the opposite one: if the real
// flow-add-step schema ever relaxes, nothing here notices this fixture went
// stale.
//
// This fixture is hand-copied: `@argent/cli` does not depend on the tool-server,
// so it cannot derive the schema. The guard that catches drift lives where the
// schema does — `flow-tools.test.ts`'s "the flow-add-step schema the CLI tests
// hand-copy". If that fails, this fixture is what it is telling you to update.
const flowAddStepMeta = {
name: "flow-add-step",
description: "Add a step to the active flow recording",
// Leading sentence of the real tool description, verbatim.
description:
"Execute a tool call and record it as a step in the flow named by `name` + `project_root` (the recording must already be open — see flow-start-recording).",
inputSchema: {
type: "object",
properties: {
name: { type: "string" },
project_root: { type: "string" },
command: { type: "string" },
args: { type: "string" },
delayMs: { type: "integer" },
delayMs: { type: "integer", minimum: 0, maximum: 9007199254740991 },
},
required: ["command"],
required: ["name", "project_root", "command"],
},
};

Expand Down Expand Up @@ -100,4 +119,30 @@ describe("argent run --help — whole-payload --args advertisement", () => {
expect(help).toContain("--args <value>");
expect(toolsClientMock.callTool).not.toHaveBeenCalled();
});

it("renders each required flag with the (required) marker and leaves the optionals unmarked", async () => {
toolsClientMock.fetchTool.mockResolvedValue(flowAddStepMeta);

await run(["flow-add-step", "--help"], { paths: {} as never });

const help = capturedHelp();
// The tool's own prose is printed ABOVE the flag block. Asserting mere
// containment says almost nothing here — `help` is rendered from this same
// fixture, so it reduces to `x.toContain(x)` and holds for any renderer
// that emits the description anywhere at all, including below the flags.
// Pin the placement, which is the part the renderer decides.
const descriptionAt = help.indexOf(flowAddStepMeta.description);
expect(descriptionAt).toBeGreaterThanOrEqual(0);
expect(descriptionAt).toBeLessThan(help.indexOf("--name <value>"));
// The recording identity is required alongside `command`: omitting either
// flag fails the server's zod validation, so the help has to say so up front
// instead of presenting them as optional extras.
expect(help).toMatch(/--name <value>\s+string \(required\)/);
expect(help).toMatch(/--project_root <value>\s+string \(required\)/);
expect(help).toMatch(/--command <value>\s+string \(required\)/);
// ...while the two genuinely optional fields must NOT carry the marker.
expect(help).toMatch(/--args <value>\s+string(?! \(required\))/);
expect(help).toMatch(/--delayMs <value>\s+integer(?! \(required\))/);
expect(toolsClientMock.callTool).not.toHaveBeenCalled();
});
});
3 changes: 2 additions & 1 deletion packages/argent-mcp/src/mcp-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,8 @@ export async function startMcpServer(options: StartMcpServerOptions): Promise<vo
instructions:
"Argent — iOS Simulator, Android Emulator, and Chromium app control for interacting, testing, profiling and debugging mobile and Chromium applications. " +
"Always use discovery tools (describe / debugger-component-tree / screenshot) before tapping — never guess coordinates. " +
"On session end: call stop-all-simulator-servers and perform any necessary cleanup. " +
"On session end: call stop-all-simulator-servers with devices: [...] naming the devices this session used, and perform any necessary cleanup. " +
"One tool-server is shared by every agent using this argent install, so an unscoped call tears down their devices too — reserve it for a deliberate machine-wide cleanup. " +
"Full guidance is in the argent rule loaded from .claude/rules/argent.md.",
}
);
Expand Down
3 changes: 2 additions & 1 deletion packages/registry/src/failure-codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ export const FAILURE_CODES = {
NATIVE_PROFILER_XCTRACE_READY_TIMEOUT: "NATIVE_PROFILER_XCTRACE_READY_TIMEOUT",
NATIVE_PROFILER_TRACE_TEMPLATE_MISSING: "NATIVE_PROFILER_TRACE_TEMPLATE_MISSING",
NATIVE_PROFILER_NO_ACTIVE_SESSION: "NATIVE_PROFILER_NO_ACTIVE_SESSION",
NATIVE_PROFILER_SESSION_TORN_DOWN: "NATIVE_PROFILER_SESSION_TORN_DOWN",
NATIVE_PROFILER_APP_PROCESS_NOT_FOUND: "NATIVE_PROFILER_APP_PROCESS_NOT_FOUND",
NATIVE_PROFILER_NO_EXPORTED_TRACE: "NATIVE_PROFILER_NO_EXPORTED_TRACE",
// Android perfetto start-failure modes — mirror the iOS xctrace set so a
Expand All @@ -218,11 +219,11 @@ export const FAILURE_CODES = {
SCREEN_RECORDING_STREAM_UNAVAILABLE: "SCREEN_RECORDING_STREAM_UNAVAILABLE",
SCREEN_RECORDING_FFMPEG_NOT_FOUND: "SCREEN_RECORDING_FFMPEG_NOT_FOUND",

FLOW_PROJECT_ROOT_REQUIRED: "FLOW_PROJECT_ROOT_REQUIRED",
FLOW_PROJECT_ROOT_INVALID: "FLOW_PROJECT_ROOT_INVALID",
FLOW_NAME_INVALID: "FLOW_NAME_INVALID",
FLOW_NO_ACTIVE_RECORDING: "FLOW_NO_ACTIVE_RECORDING",
FLOW_FILE_INVALID: "FLOW_FILE_INVALID",
FLOW_FILE_WRITE_FAILED: "FLOW_FILE_WRITE_FAILED",
FLOW_ENTRY_UNRECOGNIZED: "FLOW_ENTRY_UNRECOGNIZED",
FLOW_E2E_HAS_PREREQUISITE: "FLOW_E2E_HAS_PREREQUISITE",
FLOW_DEVICE_RESOLUTION: "FLOW_DEVICE_RESOLUTION",
Expand Down
12 changes: 7 additions & 5 deletions packages/registry/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,13 @@ export interface InvokeToolOptions {
*
* The outer request's AI client is inherited unchanged. The platform is
* re-derived from each sub-tool's own `childArgs` (its `udid` / `device_id` /
* `avdName`), falling back to the outer request's platform when the sub-tool
* carries no device arg — an orchestrator like flow-execute has no platform of
* its own and a single flow can target several devices, so the child's device
* arg is the only correct platform source. Opaque to the registry — it neither
* reads nor validates the recorded metadata.
* `devices` / `avdName`), falling back to the outer request's platform when the
* sub-tool carries no device arg — an orchestrator like flow-execute has no
* platform of its own and a single flow can target several devices, so the
* child's device arg is the only correct platform source. (A replayed
* `stop-all-simulator-servers` step carries `devices`, injected by
* `bindDeviceArgs`, so it resolves rather than falling back.) Opaque to the
* registry — it neither reads nor validates the recorded metadata.
*/
recordChildInvocation?: (toolInvocationId: string, childArgs?: unknown) => () => void;
/**
Expand Down
5 changes: 4 additions & 1 deletion packages/skills/rules/argent.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ Decision order:
Call `screenshot` separately only for a baseline before any action or after a delay.
- Always open apps with `launch-app` or `open-url` — never tap home screen icons.
- Always use `run-sequence` when performing multiple sequential device actions where you don't need to observe the screen between steps. More in `argent-device-interact` skill.
- When the session ends or the user says they are done: call `stop-all-simulator-servers`.
- When the session ends or the user says they are done: call `stop-all-simulator-servers` with `devices: [...]`
naming the devices this session actually used. One tool-server is shared by every other agent using this
argent install, so an unscoped call tears down their devices too; reserve that form for a deliberate
machine-wide cleanup.
If the user started Metro separately, ask whether to call `stop-metro` (specify the port if not 8081).
- 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.
- 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.
Expand Down
Loading