diff --git a/packages/command-registry/src/flag-definitions-action.ts b/packages/command-registry/src/flag-definitions-action.ts index 9df695c6ec..90a8ec9da1 100644 --- a/packages/command-registry/src/flag-definitions-action.ts +++ b/packages/command-registry/src/flag-definitions-action.ts @@ -9,7 +9,7 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'string', usageLabel: '--keyframes ', usageDescription: - 'Fold: timed {atMs, angle} keyframes; final atMs is motion duration (up to 60000ms)', + 'fold: timed {atMs, angle} keyframes. The last atMs is the motion duration (maximum 60000ms)', projectConfig: false, recorded: true, }, @@ -20,7 +20,7 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ min: 1, max: 200, usageLabel: '--count ', - usageDescription: 'Repeat count for press/swipe series', + usageDescription: 'Number of iterations in a press or swipe series', projectConfig: true, recorded: true, }, @@ -31,7 +31,7 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ min: 1, max: 2, usageLabel: '--pointer-count <1|2>', - usageDescription: 'Gesture pan: number of touch pointers (default 1)', + usageDescription: 'gesture pan: number of touch pointers (default: 1)', projectConfig: true, recorded: true, }, @@ -43,7 +43,7 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ max: 120, usageLabel: '--fps ', usageDescription: - 'Record: target frames per second (iOS physical device runner); also lowers the touch-overlay rate, which never exceeds 30', + 'record: target frames per second (iOS physical device runner). It also lowers the touch-overlay rate, which is never more than 30', projectConfig: true, recorded: true, }, @@ -53,7 +53,7 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'string', usageLabel: '--quality ', usageDescription: - 'Record: output quality preset; Android maps this to screenrecord bitrate, Limrun sessions map medium to recorder quality 5 and high to 8; Apple export always preserves the captured resolution, so it has no effect there. Legacy numeric values 5-7 map to medium; 8-10 map to high', + 'record: output quality preset. Android maps it to the screenrecord bitrate. Limrun sessions map medium to recorder quality 5 and high to 8. Apple export always keeps the captured resolution, so the preset has no effect on Apple export. Legacy numeric values 5-7 map to medium, and 8-10 map to high', projectConfig: true, recorded: true, }, @@ -62,7 +62,8 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--hide-touches'], type: 'boolean', usageLabel: '--hide-touches', - usageDescription: 'Record: skip touch-overlay post-processing for faster raw benchmark videos', + usageDescription: + 'record: skip the touch-overlay post-processing to export raw benchmark videos faster', projectConfig: true, recorded: true, }, @@ -73,7 +74,7 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ enumValues: RECORDING_SCOPE_VALUES, usageLabel: '--scope ', usageDescription: - 'Record: app requires an active app session; device/system records the whole screen', + 'record: the app scope requires an active app session. The device and system scopes record the whole screen', projectConfig: true, recorded: false, }, @@ -104,7 +105,8 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--record-as'], type: 'string', usageLabel: '--record-as ', - usageDescription: 'Fill: send the live text but publish ${VAR} in an armed .ad recording', + usageDescription: + 'fill: send the text to the live app, but publish ${VAR} in an armed .ad recording', projectConfig: true, recorded: false, }, @@ -114,7 +116,7 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'boolean', usageLabel: '--text-stdin', usageDescription: - 'Fill: read the text from stdin (max 64 KiB, one trailing newline removed) instead of argv', + 'fill: read the text from stdin instead of argv (maximum 64 KiB; the command removes one trailing newline)', projectConfig: false, recorded: false, }, @@ -125,7 +127,7 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ min: 0, max: 10_000, usageLabel: '--duration-ms ', - usageDescription: 'Scroll: pace the gesture over this duration when supported', + usageDescription: 'scroll: pace the gesture over this duration when the backend supports it', projectConfig: true, recorded: false, }, @@ -136,7 +138,7 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ min: 0, max: 10_000, usageLabel: '--hold-ms ', - usageDescription: 'Press hold duration for each iteration', + usageDescription: 'press: hold duration for each iteration', projectConfig: true, recorded: true, }, @@ -147,7 +149,7 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ min: 0, max: 100, usageLabel: '--jitter-px ', - usageDescription: 'Deterministic coordinate jitter radius for press', + usageDescription: 'press: radius of the deterministic coordinate jitter', projectConfig: true, recorded: true, }, @@ -158,7 +160,7 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ min: 1, max: 100_000, usageLabel: '--pixels ', - usageDescription: 'Scroll: explicit gesture distance in pixels', + usageDescription: 'scroll: explicit gesture distance in pixels', projectConfig: true, recorded: false, }, @@ -167,7 +169,7 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--until'], type: 'string', usageLabel: '--until ', - usageDescription: 'Scroll: repeat passes until the selector is visible on screen', + usageDescription: 'scroll: repeat passes until the selector is visible on the screen', projectConfig: true, // #3197: the stop condition IS the step. A recorded scroll that hunted for // an off-screen target has to replay as the same hunt; recorded as a bare @@ -180,7 +182,7 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--double-tap'], type: 'boolean', usageLabel: '--double-tap', - usageDescription: 'Use double-tap gesture per press iteration', + usageDescription: 'press: use a double-tap gesture for each iteration', projectConfig: true, recorded: true, }, @@ -190,7 +192,7 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'boolean', usageLabel: '--verify', usageDescription: - 'Capture cheap post-action evidence (AX digest, node counts, changedFromBefore) instead of a follow-up snapshot', + 'Capture low-cost post-action evidence (AX digest, node counts, changedFromBefore) instead of a follow-up snapshot', projectConfig: true, recorded: false, }, @@ -200,7 +202,7 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'boolean', usageLabel: '--settle', usageDescription: - 'After the action, wait for the UI to go quiet and return the settled diff vs the pre-action tree in the same response (best-effort; never fails the action)', + 'After the action, wait until the UI is quiet. Then return the settled diff against the pre-action tree in the same response. This is best-effort and never fails the action', projectConfig: true, recorded: false, }, @@ -211,7 +213,7 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ min: 0, usageLabel: '--settle-quiet ', usageDescription: - 'Settle: quiet window the UI must hold to count as settled (default 500ms; 300ms when the settled tree comes from the iOS simulator snapshot bridge). On iOS, a broad screen replacement such as a modal dismissal is confirmed for at least 1500ms whatever window is asked for, within the --timeout settle deadline; a replacement from a screen the pre-action capture read as presenting nothing, such as a tab switch, gets no 1500ms confirmation and keeps the asked window', + '--settle: the quiet window that the UI must hold to be settled. The default is 500ms. When the settled tree comes from the iOS simulator snapshot bridge, the default is 300ms. On iOS, a broad screen replacement, such as a modal dismissal, gets a confirmation of at least 1500ms for any requested window. This confirmation stays within the --timeout settle deadline. On iOS, a replacement can start from a screen that the pre-action capture read as presenting nothing, such as a tab switch. Such a replacement gets no 1500ms confirmation and keeps the requested window', projectConfig: true, recorded: false, }, @@ -221,7 +223,8 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'enum', enumValues: ['primary', 'secondary', 'middle'], usageLabel: '--button primary|secondary|middle', - usageDescription: 'Click: choose mouse button (middle reserved for future macOS support)', + usageDescription: + 'click: the mouse button to use (middle is reserved for future macOS support)', projectConfig: true, recorded: true, }, @@ -234,7 +237,7 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ enumValues: ['in-app', 'system'], setValue: 'in-app', usageLabel: '--in-app', - usageDescription: 'Back: use app-provided back UI when available', + usageDescription: 'back: use the back UI of the app when it is available', projectConfig: true, recorded: false, }, @@ -245,7 +248,7 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ enumValues: ['in-app', 'system'], setValue: 'system', usageLabel: '--system', - usageDescription: 'Back: use system back input or gesture when available', + usageDescription: 'back: use the system back input or gesture when it is available', projectConfig: true, recorded: false, }, @@ -266,7 +269,7 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'enum', enumValues: ['one-way', 'ping-pong'], usageLabel: '--pattern one-way|ping-pong', - usageDescription: 'Swipe repeat pattern', + usageDescription: 'swipe: repeat pattern', projectConfig: true, recorded: true, }, @@ -276,7 +279,7 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'boolean', usageLabel: '--debug, --verbose, -v', usageDescription: - 'Enable debug diagnostics; test --verbose prints per-test step timings without debug logs', + 'Enable debug diagnostics. test --verbose prints per-test step timings without debug logs', projectConfig: true, recorded: true, }, @@ -285,7 +288,8 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--cost'], type: 'boolean', usageLabel: '--cost', - usageDescription: 'Include per-command wall-clock latency (cost.wallClockMs) in the response', + usageDescription: + 'Include the wall-clock latency of the command (cost.wallClockMs) in the response', projectConfig: true, recorded: false, }, @@ -296,7 +300,7 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ enumValues: RESPONSE_LEVELS, usageLabel: '--level digest|default|full', usageDescription: - 'Response detail level: digest (token-cheap), default (today), or full. Default keeps the wire shape unchanged.', + 'Response detail level: digest (low token cost), default (current behavior), or full. The default level keeps the wire shape unchanged', projectConfig: true, recorded: false, }, @@ -305,7 +309,7 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--json'], type: 'boolean', usageLabel: '--json', - usageDescription: 'JSON output', + usageDescription: 'Print the output as JSON', projectConfig: true, recorded: false, }, @@ -332,7 +336,7 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--diff'], type: 'boolean', usageLabel: '--diff', - usageDescription: 'Snapshot: show structural diff against the previous session baseline', + usageDescription: 'snapshot: return the structural diff against the previous session baseline', projectConfig: true, recorded: false, }, @@ -342,7 +346,7 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'booleanOrString', usageLabel: '--save-script [path]', usageDescription: - 'Arm evidence capture on open, publish the armed recording on close; close --save-script alone (without an armed open) is rejected — start with open --save-script, or use session save-script mid-session. Optional custom output path.', + 'open: arm evidence capture. close: publish the armed recording. close --save-script without an armed open is refused. To arm evidence capture, start with open --save-script. To publish the armed recording during the session, use session save-script. The optional value is a custom output path', projectConfig: false, recorded: true, }, @@ -352,7 +356,7 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'enum', enumValues: ['summary', 'headers', 'body', 'all'], usageLabel: '--include summary|headers|body|all', - usageDescription: 'Network: include headers, bodies, or both in output', + usageDescription: 'network: include headers, bodies, or both in the output', projectConfig: true, recorded: false, }, @@ -361,7 +365,8 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--shutdown'], type: 'boolean', usageLabel: '--shutdown', - usageDescription: 'close: shutdown associated simulator/emulator after ending session', + usageDescription: + 'close: shut down the associated simulator or emulator after the session ends', projectConfig: true, recorded: false, }, @@ -370,7 +375,7 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--relaunch'], type: 'boolean', usageLabel: '--relaunch', - usageDescription: 'open: terminate app process before launching it', + usageDescription: 'open: stop the app process before the launch', projectConfig: true, recorded: true, }, @@ -380,9 +385,9 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'boolean', usageLabel: '--foreground', usageDescription: - 'open: keep normal app/device selection and return an initial snapshot; without an app, resolve the sole running app on the sole booted iOS simulator', + 'open: keep the normal app and device selection, and return an initial snapshot. Without an app, resolve the only running app on the only booted iOS simulator', inputDescription: - 'Include an initial interactive snapshot in a fresh open response. With no app argument, discover the sole running app on the sole booted iOS simulator; ambiguous environments fail closed.', + 'Include an initial interactive snapshot in a fresh open response. With no app argument, find the only running app on the only booted iOS simulator. If the environment is ambiguous, the open fails closed.', projectConfig: false, recorded: false, }, @@ -391,7 +396,8 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--restart'], type: 'boolean', usageLabel: '--restart', - usageDescription: 'logs clear: stop active stream, clear logs, then start streaming again', + usageDescription: + 'logs clear: stop the active stream, clear the logs, and then start the stream again', projectConfig: true, recorded: false, }, @@ -400,7 +406,7 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--retain-paths'], type: 'boolean', usageLabel: '--retain-paths', - usageDescription: 'install-from-source: keep materialized artifact paths after install', + usageDescription: 'install-from-source: keep materialized artifact paths after the install', projectConfig: true, recorded: false, }, @@ -432,7 +438,7 @@ export const ACTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ // the synopsis stays the invocation they are choosing between (#1271 stage 2). usageHidden: true, usageDescription: - 'Force-record this action even though its command is observation-only and would otherwise be excluded from a repair-armed heal by default (mutually exclusive with --no-record)', + 'Always record this action, although its command is observation-only. By default, a repair-armed heal excludes observation-only actions (mutually exclusive with --no-record)', projectConfig: true, recorded: true, }, diff --git a/packages/command-registry/src/flag-definitions-connection.ts b/packages/command-registry/src/flag-definitions-connection.ts index 4d3aada9c4..f83a8f8498 100644 --- a/packages/command-registry/src/flag-definitions-connection.ts +++ b/packages/command-registry/src/flag-definitions-connection.ts @@ -12,7 +12,7 @@ export const CONNECTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'string', usageLabel: '--config ', usageDescription: - 'Load CLI defaults from a specific config file (required for connection/provider defaults outside user config)', + 'Load CLI defaults from a specific config file (required for connection or provider defaults outside the user config)', projectConfig: false, recorded: false, }, @@ -21,7 +21,7 @@ export const CONNECTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--remote-config'], type: 'string', usageLabel: '--remote-config ', - usageDescription: 'Load remote host + Metro workflow settings from a specific profile file', + usageDescription: 'Load remote host and Metro workflow settings from a specific profile file', projectConfig: false, recorded: false, }, @@ -31,7 +31,7 @@ export const CONNECTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'string', usageLabel: '--state-dir ', usageDescription: - 'Daemon state directory (defaults to ~/.agent-device for packages, or a worktree-scoped dev dir from source)', + 'Daemon state directory (default: ~/.agent-device for packages, or a worktree-scoped dev directory when run from source)', projectConfig: false, recorded: false, }, @@ -40,7 +40,8 @@ export const CONNECTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--daemon-base-url'], type: 'string', usageLabel: '--daemon-base-url ', - usageDescription: 'Explicit remote HTTP daemon base URL (skip local daemon discovery/startup)', + usageDescription: + 'Explicit remote HTTP daemon base URL (skips local daemon discovery and startup)', projectConfig: false, recorded: false, }, @@ -50,7 +51,7 @@ export const CONNECTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'string', usageLabel: '--daemon-auth-token ', usageDescription: - 'Remote HTTP daemon or proxy auth token (sent as request token and bearer header)', + 'Remote HTTP daemon or proxy auth token (sent as the request token and the bearer header)', projectConfig: false, recorded: false, }, @@ -70,7 +71,7 @@ export const CONNECTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'enum', enumValues: ['socket', 'http', 'dual'], usageLabel: '--daemon-server-mode socket|http|dual', - usageDescription: 'Daemon server mode used when spawning daemon', + usageDescription: 'Daemon server mode to use when the CLI spawns a daemon', projectConfig: false, recorded: false, }, @@ -79,7 +80,7 @@ export const CONNECTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--host'], type: 'string', usageLabel: '--host ', - usageDescription: 'Proxy: host interface to bind (default: 127.0.0.1)', + usageDescription: 'proxy: host interface to bind (default: 127.0.0.1)', projectConfig: false, recorded: false, }, @@ -90,7 +91,7 @@ export const CONNECTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ min: 1, max: 65535, usageLabel: '--port ', - usageDescription: 'Proxy: TCP port to bind (default: 0, choose a free port)', + usageDescription: 'proxy: TCP port to bind (default: 0, which selects a free port)', projectConfig: false, recorded: false, }, @@ -109,7 +110,7 @@ export const CONNECTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'enum', enumValues: ['none', 'tenant'], usageLabel: '--session-isolation none|tenant', - usageDescription: 'Session isolation strategy (tenant prefixes session namespace)', + usageDescription: 'Session isolation strategy (tenant adds a prefix to the session namespace)', projectConfig: false, recorded: false, }, @@ -118,7 +119,7 @@ export const CONNECTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--run-id'], type: 'string', usageLabel: '--run-id ', - usageDescription: 'Run identifier used for tenant lease admission checks', + usageDescription: 'Run identifier for tenant lease admission checks', projectConfig: false, recorded: false, }, @@ -165,7 +166,7 @@ export const CONNECTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'string', usageLabel: '--provider-app ', usageDescription: - 'Cloud provider app reference or local app path used when creating hosted WebDriver sessions', + 'Cloud provider app reference or local app path for the creation of hosted WebDriver sessions', projectConfig: false, recorded: false, }, @@ -185,7 +186,7 @@ export const CONNECTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ enumValues: PROVIDER_DEVICE_TYPES, usageLabel: '--provider-device-type real|virtual', usageDescription: - 'TestMu AI device pool: real devices or virtual devices (emulators and simulators). Defaults to virtual', + 'TestMu AI device pool: real devices, or virtual devices, which are emulators and simulators (default: virtual)', projectConfig: false, recorded: false, }, @@ -223,7 +224,7 @@ export const CONNECTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ enumValues: PROVIDER_DEVICE_ORIENTATIONS, usageLabel: '--provider-device-orientation portrait|landscape', usageDescription: - 'Screen orientation a hosted cloud provider session starts in. Set this rather than rotating after launch: a session that boots landscape renders login webviews landscape', + 'Screen orientation at the start of a hosted cloud provider session. Set this instead of a rotation after launch. A session that boots in landscape renders login webviews in landscape', projectConfig: false, recorded: false, }, @@ -251,7 +252,7 @@ export const CONNECTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'string', usageLabel: '--provider-appium-version ', usageDescription: - 'Hosted cloud provider Appium server version, for example 3.2.0. Without it each provider starts its own default (Appium 1.x on BrowserStack)', + 'Hosted cloud provider Appium server version, for example 3.2.0. Without it, each provider starts its own default version (Appium 1.x on BrowserStack)', projectConfig: false, recorded: false, }, @@ -299,7 +300,7 @@ export const CONNECTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'boolean', usageLabel: '--provider-no-resign-app', usageDescription: - 'iOS only: keep an Enterprise-signed app as uploaded instead of letting the provider re-sign it, which strips entitlements such as push notifications', + 'iOS only: keep an Enterprise-signed app as uploaded. Without it, the provider re-signs the app, which removes entitlements such as push notifications', projectConfig: false, recorded: false, }, @@ -355,7 +356,7 @@ export const CONNECTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'boolean', usageLabel: '--force, --overwrite', usageDescription: - 'Force replacement of existing state (connect: replace an active connection; --save-script: overwrite an existing target instead of refusing)', + 'Force the replacement of existing state (connect: replace an active connection; --save-script: overwrite an existing target instead of a refusal)', projectConfig: true, recorded: true, }, @@ -364,7 +365,8 @@ export const CONNECTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--clean'], type: 'boolean', usageLabel: '--clean', - usageDescription: 'Daemon stop: remove retained runner processes and leases after stopping', + usageDescription: + 'daemon stop: remove retained runner processes and leases after the daemon stops', projectConfig: true, recorded: false, }, @@ -373,7 +375,7 @@ export const CONNECTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--no-login'], type: 'boolean', usageLabel: '--no-login', - usageDescription: 'Connect: fail instead of starting implicit cloud login', + usageDescription: 'connect: fail instead of starting an implicit cloud login', projectConfig: true, recorded: false, }, @@ -384,7 +386,7 @@ export const CONNECTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ enumValues: ['reject', 'strip'], usageLabel: '--session-lock reject|strip', usageDescription: - 'Lock bound-session device routing for this CLI invocation and nested batch steps; strip drops conflicting platform/scope selectors only, and a selector naming a different device always fails', + 'Lock bound-session device routing for this CLI invocation and its nested batch steps. strip removes only conflicting platform or scope selectors. A selector that names a different device always fails', projectConfig: true, recorded: false, }, diff --git a/packages/command-registry/src/flag-definitions-target.ts b/packages/command-registry/src/flag-definitions-target.ts index 7ca52fcaa2..68c4eb6423 100644 --- a/packages/command-registry/src/flag-definitions-target.ts +++ b/packages/command-registry/src/flag-definitions-target.ts @@ -10,7 +10,7 @@ export const TARGET_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'enum', enumValues: PLATFORM_SELECTORS, usageLabel: `--platform ${PLATFORM_SELECTORS.join('|')}`, - usageDescription: 'Platform to target (`apple` aliases the Apple automation backend)', + usageDescription: 'Platform to target (`apple` is an alias for the Apple automation backend)', projectConfig: true, recorded: true, }, @@ -29,7 +29,7 @@ export const TARGET_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--device'], type: 'string', usageLabel: '--device ', - usageDescription: 'Device name to target (a UDID belongs in --udid, a serial in --serial)', + usageDescription: 'Device name to target (put a UDID in --udid and a serial in --serial)', projectConfig: true, recorded: true, }, @@ -39,7 +39,7 @@ export const TARGET_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'string', usageLabel: '--udid ', usageDescription: - 'Apple device or simulator UDID; the only selector that pins one device when several share a --device name', + 'Apple device or simulator UDID. It is the only selector that pins one device when several devices share a --device name', projectConfig: true, recorded: true, }, @@ -57,7 +57,7 @@ export const TARGET_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--stale'], type: 'boolean', usageLabel: '--stale', - usageDescription: 'Device status: show only claims with a provably stale owner', + usageDescription: 'device status: list only the claims that have a provably stale owner', projectConfig: true, recorded: false, }, @@ -67,7 +67,7 @@ export const TARGET_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'enum', enumValues: SESSION_SURFACES, usageLabel: '--surface app|frontmost-app|desktop|menubar', - usageDescription: 'macOS session surface for open (defaults to app)', + usageDescription: 'open: macOS session surface (default: app)', projectConfig: true, recorded: true, }, @@ -76,7 +76,7 @@ export const TARGET_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--headless'], type: 'boolean', usageLabel: '--headless', - usageDescription: 'Boot: launch Android emulator without a GUI window', + usageDescription: 'boot: launch the Android emulator without a GUI window', projectConfig: true, recorded: false, }, @@ -86,7 +86,7 @@ export const TARGET_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'string', usageLabel: '--app ', usageDescription: - 'Target an app without opening it: doctor verifies an installed app by id or name; settings applies an app-scoped change (permission, iOS location) to a bundle id or package', + 'Target an app without opening it. doctor confirms an installed app by id or name. settings applies an app-scoped change (permission, iOS location) to a bundle id or package', projectConfig: false, recorded: false, }, @@ -95,7 +95,7 @@ export const TARGET_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--metro-host'], type: 'string', usageLabel: '--metro-host ', - usageDescription: 'Session-scoped Metro/debug host hint', + usageDescription: 'Session-scoped Metro or debug host hint', projectConfig: false, recorded: true, }, @@ -107,7 +107,7 @@ export const TARGET_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ max: 65535, usageLabel: '--metro-port ', usageDescription: - 'Session-scoped Metro/debug port hint; on an emulator/simulator the host defaults to the loopback alias, physical devices still need --metro-host', + 'Session-scoped Metro or debug port hint. On an emulator or simulator, the host defaults to the loopback alias. Physical devices still need --metro-host', projectConfig: false, recorded: true, }, @@ -136,7 +136,7 @@ export const TARGET_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--template'], type: 'string', usageLabel: '--template ', - usageDescription: 'Perf xctrace template name, for example Time Profiler', + usageDescription: 'perf: xctrace template name, for example Time Profiler', projectConfig: true, recorded: false, }, @@ -155,7 +155,7 @@ export const TARGET_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--public-base-url'], type: 'string', usageLabel: '--public-base-url ', - usageDescription: 'metro prepare: public base URL used for direct dev-server bundle hints', + usageDescription: 'metro prepare: public base URL for direct dev-server bundle hints', projectConfig: false, recorded: false, }, @@ -174,7 +174,7 @@ export const TARGET_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'string', usageLabel: '--bearer-token ', usageDescription: - 'metro prepare: host bridge bearer token (or AGENT_DEVICE_METRO_BEARER_TOKEN; falls back to AGENT_DEVICE_DAEMON_AUTH_TOKEN)', + 'metro prepare: host bridge bearer token (or AGENT_DEVICE_METRO_BEARER_TOKEN, with AGENT_DEVICE_DAEMON_AUTH_TOKEN as the fallback)', projectConfig: false, recorded: false, }, @@ -194,7 +194,7 @@ export const TARGET_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--listen-host'], type: 'string', usageLabel: '--listen-host ', - usageDescription: 'metro prepare: host dev server listens on (default: 0.0.0.0)', + usageDescription: 'metro prepare: host that the dev server listens on (default: 0.0.0.0)', projectConfig: false, recorded: false, }, @@ -204,7 +204,7 @@ export const TARGET_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'string', usageLabel: '--status-host ', usageDescription: - 'metro prepare: host used for local dev-server /status polling (default: 127.0.0.1)', + 'metro prepare: host for the local dev-server /status polls (default: 127.0.0.1)', projectConfig: false, recorded: false, }, @@ -214,7 +214,7 @@ export const TARGET_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'int', min: 1, usageLabel: '--startup-timeout-ms ', - usageDescription: 'metro prepare: timeout while waiting for the dev server to become ready', + usageDescription: 'metro prepare: timeout for the dev server to become ready', projectConfig: false, recorded: false, }, @@ -233,7 +233,7 @@ export const TARGET_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--runtime-file'], type: 'string', usageLabel: '--runtime-file ', - usageDescription: 'metro prepare: optional file path to persist the JSON result', + usageDescription: 'metro prepare: optional file path to save the JSON result', projectConfig: false, recorded: false, }, @@ -251,7 +251,8 @@ export const TARGET_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--no-install-deps'], type: 'boolean', usageLabel: '--no-install-deps', - usageDescription: 'metro prepare: skip package-manager install when node_modules is missing', + usageDescription: + 'metro prepare: skip the package-manager install when node_modules is missing', projectConfig: false, recorded: false, }, @@ -269,7 +270,7 @@ export const TARGET_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--launch-url'], type: 'string', usageLabel: '--launch-url ', - usageDescription: 'Session-scoped deep link / launch URL hint', + usageDescription: 'Session-scoped deep link or launch URL hint', projectConfig: true, recorded: true, }, @@ -278,7 +279,7 @@ export const TARGET_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--ios-simulator-device-set'], type: 'string', usageLabel: '--ios-simulator-device-set ', - usageDescription: 'Scope iOS simulator discovery/commands to this simulator device set', + usageDescription: 'Limit iOS simulator discovery and commands to this simulator device set', projectConfig: false, recorded: false, }, @@ -296,7 +297,7 @@ export const TARGET_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--ios-xctest-derived-data-path'], type: 'string', usageLabel: '--ios-xctest-derived-data-path ', - usageDescription: 'Derived data path for external iOS XCTest runner execution', + usageDescription: 'Derived data path for the execution of an external iOS XCTest runner', projectConfig: false, recorded: false, }, @@ -305,7 +306,7 @@ export const TARGET_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--ios-xctest-env-dir'], type: 'string', usageLabel: '--ios-xctest-env-dir ', - usageDescription: 'Writable directory for per-session iOS XCTest runner env overlays', + usageDescription: 'Writable directory for the iOS XCTest runner env overlays of each session', projectConfig: false, recorded: false, }, @@ -315,7 +316,7 @@ export const TARGET_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'boolean', usageLabel: '--test-ime', usageDescription: - 'open/test/replay: activate the headless Android test IME for deterministic Unicode text entry (default on for emulators; opt-in on real devices; on test/replay it applies to the sessions the flow opens)', + 'open, test, replay: activate the headless Android test IME for deterministic Unicode text entry (default: on for emulators, opt-in on real devices). On test and replay, it applies to the sessions that the flow opens', projectConfig: true, recorded: false, }, @@ -326,7 +327,7 @@ export const TARGET_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ setValue: false, usageLabel: '--no-test-ime', usageDescription: - 'open/test/replay: keep the real Android keyboard even on emulators (opt out of the headless test IME; on test/replay it applies to the sessions the flow opens)', + 'open, test, replay: keep the real Android keyboard, also on emulators (opt out of the headless test IME). On test and replay, it applies to the sessions that the flow opens', projectConfig: true, recorded: false, }, @@ -335,7 +336,8 @@ export const TARGET_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--android-device-allowlist'], type: 'string', usageLabel: '--android-device-allowlist ', - usageDescription: 'Comma/space separated Android serial allowlist for discovery/selection', + usageDescription: + 'Android serial allowlist for discovery and selection, separated by commas or spaces', projectConfig: false, recorded: false, }, @@ -344,7 +346,8 @@ export const TARGET_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--remote'], type: 'boolean', usageLabel: '--remote', - usageDescription: 'Doctor: check remote connection setup instead of local device inventory', + usageDescription: + 'doctor: examine the remote connection setup instead of the local device inventory', projectConfig: true, recorded: false, }, @@ -354,7 +357,7 @@ export const TARGET_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'string', usageLabel: '--activity ', usageDescription: - 'Android launch activity (package/Class, .Class, or Class) or HarmonyOS ability name; not for URL opens', + 'Android launch activity (package/Class, .Class, or Class) or HarmonyOS ability name. Not for URL opens', projectConfig: true, recorded: false, }, @@ -374,7 +377,7 @@ export const TARGET_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ multiple: true, usageLabel: '--launch-args ', usageDescription: - 'open: repeatable launch argument forwarded verbatim to the platform launch command (iOS app process args; Android adb shell am start args). Linux and macOS reject the flag.', + 'open: repeatable launch argument that the command forwards verbatim to the platform launch command (iOS app process args; Android adb shell am start args). Linux and macOS refuse the flag', projectConfig: true, recorded: false, }, @@ -393,7 +396,7 @@ export const TARGET_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--github-actions-artifact'], type: 'string', usageLabel: '--github-actions-artifact ', - usageDescription: 'install-from-source: GitHub Actions artifact resolved by a remote daemon', + usageDescription: 'install-from-source: GitHub Actions artifact that a remote daemon resolves', projectConfig: false, recorded: false, }, @@ -410,7 +413,8 @@ export const TARGET_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--session'], type: 'string', usageLabel: '--session ', - usageDescription: 'Named session (implicit default is scoped to this workspace + platform)', + usageDescription: + 'Named session (the implicit default is scoped to this workspace and platform)', projectConfig: true, recorded: false, }, diff --git a/packages/command-registry/src/flag-definitions-workflow.ts b/packages/command-registry/src/flag-definitions-workflow.ts index d1e82d5b67..592d40ef6d 100644 --- a/packages/command-registry/src/flag-definitions-workflow.ts +++ b/packages/command-registry/src/flag-definitions-workflow.ts @@ -8,8 +8,8 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'boolean', usageLabel: '--update, -u', usageDescription: - 'Replay: retired as a rewrite (ADR 0012) — never edits the .ad file; every divergence already ' + - 'carries ranked selector suggestions, --update or not', + 'replay: retired as a rewrite (ADR 0012). It never edits the .ad file. Every divergence already ' + + 'carries ranked selector suggestions, with or without --update', projectConfig: true, recorded: false, }, @@ -20,8 +20,8 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ min: 1, usageLabel: '--from ', usageDescription: - 'Replay: resume at 1-based plan step n, skipping 1..n-1 without executing them (requires ' + - "--plan-digest; see a divergence report's resume field). replay only, not test", + 'replay: resume at 1-based plan step n, and skip steps 1..n-1 without running them (requires ' + + '--plan-digest; see the resume field of a divergence report). For replay only, not test', projectConfig: true, recorded: false, }, @@ -31,8 +31,8 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'string', usageLabel: '--plan-digest ', usageDescription: - 'Replay: the plan digest a --from resume must match (from a prior divergence report); mismatch, ' + - 'edits, or include/platform-expansion changes fail INVALID_ARGS before any action', + 'replay: the plan digest that a --from resume must match (from a prior divergence report). A mismatch, ' + + 'edits, or include or platform-expansion changes fail with INVALID_ARGS before any action', projectConfig: true, recorded: false, }, @@ -42,7 +42,7 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'boolean', usageLabel: '--keep-session', usageDescription: - 'Replay: leave the session active by suppressing exactly an authored terminal close in a native .ad script; replay only, not test or Maestro YAML', + 'replay: keep the session active. It suppresses exactly an authored terminal close in a native .ad script. For replay only, not test or Maestro YAML', projectConfig: true, recorded: false, }, @@ -52,7 +52,7 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'boolean', usageLabel: '--maestro', usageDescription: - 'Replay: treat input as a supported Maestro YAML subset; unsupported syntax fails loudly. ' + + 'replay: read the input as a supported Maestro YAML subset. Unsupported syntax fails with an explicit error. ' + 'See agent-device help maestro for commands and boundaries', projectConfig: true, recorded: false, @@ -63,8 +63,7 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'string', multiple: true, usageLabel: '-e KEY=VALUE, --env KEY=VALUE', - usageDescription: - 'Replay/Test: inject or override a ${KEY} variable for the script (repeatable)', + usageDescription: 'replay, test: set or override a ${KEY} variable for the script (repeatable)', projectConfig: false, recorded: false, }, @@ -74,7 +73,7 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'boolean', usageLabel: '--fail-fast', usageDescription: - 'Test: stop the suite after the first failing script; with sharding, each shard stops independently', + 'test: stop the suite after the first failed script. With sharding, each shard stops independently', projectConfig: true, recorded: false, }, @@ -85,7 +84,7 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ min: 1, usageLabel: '--timeout ', usageDescription: - 'Boot/Open/Prepare: startup budget covering the device boot (and runner preparation for prepare). Replay/Snapshot/Test: maximum wall-clock time for the command or attempt. With --settle: the settle-wait deadline (default 10s)', + 'boot, open, prepare: startup budget for the device boot (and for the runner preparation on prepare). replay, snapshot, test: maximum wall-clock time for the command or attempt. With --settle: the deadline of the settle wait (default: 10s)', projectConfig: true, recorded: false, }, @@ -97,9 +96,9 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ max: 120000, usageLabel: '--wait ', usageDescription: - 'Open: wait up to n ms for a device another session is holding before failing with DEVICE_IN_USE', + 'open: if another session holds the device, wait up to n ms before the open fails with DEVICE_IN_USE', inputDescription: - "Block this open for up to n milliseconds (100-120000) while another session holds the device, then fail with DEVICE_IN_USE naming that session. Only session contention is waited for: a device claim held by another workspace is refused at once with its recovery command. A device that never frees, or is taken again while this open waits, costs the full budget, which extends this command's timeout envelope rather than eating into it.", + 'While another session holds the device, block this open for up to n milliseconds (100-120000). Then fail with DEVICE_IN_USE, which names that session. The open waits only for session contention. If another workspace holds a device claim, the open is refused immediately with the recovery command of that claim. If the device never becomes free, or another session takes it again during the wait, the wait uses the full budget. The budget is added to the timeout envelope of this command, not taken from it.', projectConfig: false, recorded: false, }, @@ -110,7 +109,7 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ min: 0, max: 3, usageLabel: '--retries ', - usageDescription: 'Test: retry each failed script up to n additional times', + usageDescription: 'test: retry each failed script up to n additional times', projectConfig: true, recorded: false, }, @@ -119,7 +118,7 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--record-video'], type: 'boolean', usageLabel: '--record-video', - usageDescription: 'Test: record each replay attempt to recording.mp4 in its attempt artifacts', + usageDescription: 'test: record each replay attempt to recording.mp4 in its attempt artifacts', projectConfig: true, recorded: false, }, @@ -128,7 +127,7 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--artifacts-dir'], type: 'string', usageLabel: '--artifacts-dir ', - usageDescription: 'Test: root directory for suite artifacts', + usageDescription: 'test: root directory for suite artifacts', projectConfig: false, recorded: false, }, @@ -139,7 +138,7 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ multiple: true, usageLabel: '--reporter ', usageDescription: - 'Test: add a replay suite reporter; use default, junit:, or a custom reporter path (repeatable)', + 'test: add a replay suite reporter: default, junit:, or a custom reporter path (repeatable)', projectConfig: false, recorded: false, }, @@ -148,7 +147,7 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--report-junit'], type: 'string', usageLabel: '--report-junit ', - usageDescription: 'Test: compatibility alias for --reporter junit:', + usageDescription: 'test: compatibility alias for --reporter junit:', projectConfig: false, recorded: false, }, @@ -159,7 +158,7 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ min: 1, usageLabel: '--shard-all ', usageDescription: - 'Test: run the full suite on each of n devices; combine with --device id1,id2 for explicit connected devices; AD_SHARD_INDEX is zero-based', + 'test: run the full suite on each of n devices. To use explicit connected devices, add --device id1,id2. AD_SHARD_INDEX is zero-based', projectConfig: true, recorded: false, }, @@ -170,7 +169,7 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ min: 1, usageLabel: '--shard-split ', usageDescription: - 'Test: split runnable suite entries across n devices; AD_SHARD_INDEX is zero-based', + 'test: divide the runnable suite entries across n devices. AD_SHARD_INDEX is zero-based', projectConfig: true, recorded: false, }, @@ -179,7 +178,7 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--steps'], type: 'string', usageLabel: '--steps ', - usageDescription: 'Batch: JSON array of {"command","input"} steps', + usageDescription: 'batch: JSON array of {"command","input"} steps', projectConfig: false, recorded: false, }, @@ -188,7 +187,7 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--steps-file'], type: 'string', usageLabel: '--steps-file ', - usageDescription: 'Batch: read steps JSON from file', + usageDescription: 'batch: read the steps JSON from a file', projectConfig: false, recorded: false, }, @@ -198,7 +197,7 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'enum', enumValues: ['stop'], usageLabel: '--on-error stop', - usageDescription: 'Batch: stop when a step fails', + usageDescription: 'batch: stop when a step fails', projectConfig: true, recorded: false, }, @@ -209,7 +208,7 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ min: 1, max: 1000, usageLabel: '--max-steps ', - usageDescription: 'Batch: maximum number of allowed steps', + usageDescription: 'batch: maximum number of allowed steps', projectConfig: true, recorded: false, }, @@ -220,7 +219,7 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ enumValues: ['user-installed', 'all'], setValue: 'all', usageLabel: '--all', - usageDescription: 'Apps: include system/OEM apps', + usageDescription: 'apps: include system and OEM apps', projectConfig: true, recorded: false, }, @@ -229,7 +228,7 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['-i'], type: 'boolean', usageLabel: '-i', - usageDescription: 'Snapshot: interactive elements only', + usageDescription: 'snapshot: include only interactive elements', projectConfig: true, recorded: true, }, @@ -239,7 +238,7 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'int', min: 0, usageLabel: '--depth, -d ', - usageDescription: 'Snapshot: limit snapshot depth', + usageDescription: 'snapshot: limit the snapshot depth', projectConfig: true, recorded: true, }, @@ -248,7 +247,7 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--scope', '-s'], type: 'string', usageLabel: '--scope, -s ', - usageDescription: 'Snapshot: scope snapshot to label/identifier', + usageDescription: 'snapshot: limit the snapshot to a label or identifier', projectConfig: true, recorded: true, }, @@ -257,7 +256,7 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--raw'], type: 'boolean', usageLabel: '--raw', - usageDescription: 'Snapshot: raw node output', + usageDescription: 'snapshot: raw node output', projectConfig: true, recorded: true, }, @@ -267,9 +266,9 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'boolean', usageLabel: '--actions', usageDescription: - 'Snapshot: name the affordances merged inside an element (iOS sim); not directly invokable — reach them via the detail screen, labeled children, or coordinates', + 'snapshot: name the affordances merged inside an element (iOS simulator). You cannot invoke them directly. Reach them through the detail screen, labeled children, or coordinates', inputDescription: - 'Name the affordances an element merged away (iOS UIAccessibilityCustomAction, React Native accessibilityActions) — a card whose reply/options controls are not separate elements still lists them here. The names are for PLANNING, not invocation: there is no API to trigger them, so reach the affordance through the element detail screen, through the same control exposed as a labeled element elsewhere, or by coordinates from its rect. iOS simulator only; costs one accessibility round trip per merged element.', + 'Name the affordances that an element merged away (iOS UIAccessibilityCustomAction, React Native accessibilityActions). For example, a card whose reply and options controls are not separate elements still lists them here. The names are for PLANNING, not invocation. No API triggers them. Reach the affordance through the element detail screen, through the same control exposed as a labeled element in a different location, or by coordinates from its rect. iOS simulator only. It costs one accessibility round trip for each merged element.', projectConfig: true, recorded: true, }, @@ -279,9 +278,9 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'boolean', usageLabel: '--observe-only', usageDescription: - 'Snapshot: observe the iOS session app without activation or foreground repair', + 'snapshot: observe the iOS session app without activation or foreground repair', inputDescription: - 'iOS only. Refuse instead of activating or repairing the session app. Reports XCUIApplication state, which does not prove screen ownership (#2696).', + 'iOS only. Refuse instead of activating or repairing the session app. It reports the XCUIApplication state, which does not prove screen ownership (#2696).', projectConfig: false, recorded: false, }, @@ -290,7 +289,7 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--force-full'], type: 'boolean', usageLabel: '--force-full', - usageDescription: 'Snapshot: re-emit the full tree even when unchanged', + usageDescription: 'snapshot: return the full tree again, also when it is unchanged', projectConfig: true, recorded: false, }, @@ -299,7 +298,7 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--first'], type: 'boolean', usageLabel: '--first', - usageDescription: 'Find: pick the first match when ambiguous', + usageDescription: 'find: select the first match when the match is ambiguous', projectConfig: true, recorded: false, }, @@ -308,7 +307,7 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--last'], type: 'boolean', usageLabel: '--last', - usageDescription: 'Find: pick the last match when ambiguous', + usageDescription: 'find: select the last match when the match is ambiguous', projectConfig: true, recorded: false, }, @@ -326,7 +325,7 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--artifact'], type: 'string', usageLabel: '--artifact ', - usageDescription: 'Debug symbols: Apple crash artifact path (.ips, .crash, or .log)', + usageDescription: 'debug symbols: Apple crash artifact path (.ips, .crash, or .log)', projectConfig: false, recorded: false, }, @@ -335,7 +334,7 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--dsym'], type: 'string', usageLabel: '--dsym ', - usageDescription: 'Debug symbols: matching .dSYM bundle path', + usageDescription: 'debug symbols: matching .dSYM bundle path', projectConfig: false, recorded: false, }, @@ -344,7 +343,7 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--search-path'], type: 'string', usageLabel: '--search-path ', - usageDescription: 'Debug symbols: directory to scan for matching .dSYM bundles', + usageDescription: 'debug symbols: directory to scan for matching .dSYM bundles', projectConfig: false, recorded: false, }, @@ -354,7 +353,7 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'boolean', usageLabel: '--overlay-refs', usageDescription: - 'Screenshot: draw current snapshot refs and target rectangles onto the saved PNG; diff screenshot: also write a separate current-screen overlay guide', + 'screenshot: draw the current snapshot refs and target rectangles on the saved PNG. diff screenshot: also write a separate overlay guide for the current screen', projectConfig: true, recorded: false, }, @@ -364,7 +363,7 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--baseline', '-b'], type: 'string', usageLabel: '--baseline, -b ', - usageDescription: 'Diff screenshot: path to baseline image file', + usageDescription: 'diff screenshot: path to the baseline image file', projectConfig: true, recorded: false, }, @@ -373,7 +372,7 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--threshold'], type: 'string', usageLabel: '--threshold <0-1>', - usageDescription: 'Diff screenshot: color distance threshold (default 0.1)', + usageDescription: 'diff screenshot: color distance threshold (default: 0.1)', projectConfig: true, recorded: false, }, diff --git a/packages/contracts/src/client-app.ts b/packages/contracts/src/client-app.ts index 765785d57e..23e7ebebe0 100644 --- a/packages/contracts/src/client-app.ts +++ b/packages/contracts/src/client-app.ts @@ -70,11 +70,12 @@ export type AppOpenOptions = AgentDeviceRequestOverrides & /** Startup budget in milliseconds: bounds the Simulator boot wait on a cold device. */ timeoutMs?: number; /** - * Block this open for up to n milliseconds (100-120000) while another session holds the device, - * then fail with DEVICE_IN_USE naming that session. Only session contention is waited for: a - * device claim held by another workspace is refused at once with its recovery command. A device - * that never frees, or is taken again while this open waits, costs the full budget, which - * extends this command's timeout envelope rather than eating into it. + * While another session holds the device, block this open for up to n milliseconds + * (100-120000). Then fail with DEVICE_IN_USE, which names that session. The open waits only + * for session contention. If another workspace holds a device claim, the open is refused + * immediately with the recovery command of that claim. If the device never becomes free, or + * another session takes it again during the wait, the wait uses the full budget. The budget is + * added to the timeout envelope of this command, not taken from it. */ waitMs?: number; // Editor documentation for a public type: a `.d.ts` is read where no @@ -84,8 +85,8 @@ export type AppOpenOptions = AgentDeviceRequestOverrides & // `commands/command-input-option-field.test.ts`. /** * Include an initial interactive snapshot in a fresh open response. With no - * app argument, discover the sole running app on the sole booted iOS - * simulator; ambiguous environments fail closed. + * app argument, find the only running app on the only booted iOS simulator. + * If the environment is ambiguous, the open fails closed. */ foreground?: boolean; saveScript?: boolean | string; diff --git a/packages/contracts/src/client-capture.ts b/packages/contracts/src/client-capture.ts index 6024cbb0b0..fe5a44f61f 100644 --- a/packages/contracts/src/client-capture.ts +++ b/packages/contracts/src/client-capture.ts @@ -31,13 +31,13 @@ export type CaptureSnapshotOptions = AgentDeviceRequestOverrides & AgentDeviceSelectionOptions & Omit & { /** - * Name the affordances an element merged away (iOS UIAccessibilityCustomAction, - * React Native accessibilityActions) — a card whose reply/options controls are not - * separate elements still lists them here. The names are for PLANNING, not - * invocation: there is no API to trigger them, so reach the affordance through the - * element detail screen, through the same control exposed as a labeled element - * elsewhere, or by coordinates from its rect. iOS simulator only; costs one - * accessibility round trip per merged element. + * Name the affordances that an element merged away (iOS UIAccessibilityCustomAction, + * React Native accessibilityActions). For example, a card whose reply and options + * controls are not separate elements still lists them here. The names are for + * PLANNING, not invocation. No API triggers them. Reach the affordance through the + * element detail screen, through the same control exposed as a labeled element in a + * different location, or by coordinates from its rect. iOS simulator only. It costs one + * accessibility round trip for each merged element. */ customActions?: SnapshotCommandOptionFields['customActions']; timeoutMs?: number; diff --git a/packages/contracts/src/settings-catalog.ts b/packages/contracts/src/settings-catalog.ts index 58c6be1570..ff4aca1761 100644 --- a/packages/contracts/src/settings-catalog.ts +++ b/packages/contracts/src/settings-catalog.ts @@ -99,17 +99,17 @@ export type SettingDefinition = Readonly<{ export const SETTINGS = { wifi: { forms: [{ kind: 'on-off' }], - help: 'Android switches Wi-Fi with svc wifi; an Apple simulator overrides only its status bar.', + help: 'Android switches Wi-Fi with svc wifi. An Apple simulator overrides only its status bar.', }, airplane: { forms: [{ kind: 'on-off' }], - help: 'Android applies it through the connectivity service (Android 11+) and reports the airplaneMode that service holds; older builds fail without changing anything. An Apple simulator overrides only its status bar.', + help: 'Android applies it through the connectivity service (Android 11+) and reports the airplaneMode that the service holds. Older builds fail and change nothing. An Apple simulator overrides only its status bar.', }, 'mobile-data': { forms: [{ kind: 'on-off' }], app: 'input', appScope: { mobile: 'device-level' }, - help: 'Android only. Runs svc data and reports the stored mobileData state, or fails with mobile-data-not-applied or mobile-data-unreadable. Multi-SIM read-back is not covered yet, and --app is refused.', + help: 'Android only. It runs svc data and reports the stored mobileData state, or fails with mobile-data-not-applied or mobile-data-unreadable. Multi-SIM read-back is not covered at this time, and --app is refused.', }, location: { forms: [ @@ -117,7 +117,7 @@ export const SETTINGS = { { kind: 'coordinates', appScope: { apple: 'device-level', mobile: 'device-level' } }, ], app: 'input', - help: "on|off writes Android's device-wide location_mode, and on an Apple simulator grants or revokes the app's location permission. set moves the device's own coordinates on Apple simulators and Android emulators.", + help: 'On Android, on|off writes the device-wide location_mode. On an Apple simulator, on|off grants or revokes the location permission of the app. set moves the coordinates of the device itself on Apple simulators and Android emulators.', }, animations: { forms: [{ kind: 'on-off' }], @@ -130,7 +130,7 @@ export const SETTINGS = { 'text-size': { forms: [{ kind: 'choice', states: TEXT_SIZE_CATEGORIES }], readable: true, - help: 'Reads the preferred text size without a category and applies one with it, on iPhone and iPad simulators (simctl content size) and Android (system font_scale). Android has no ladder of its own: a read names the nearest rung and reports the multiplier as platformValue. A running app adopts a new size at its next configuration change, so relaunch it.', + help: 'Without a category, it reads the preferred text size. With a category, it applies that size. This works on iPhone and iPad simulators (simctl content size) and Android (system font_scale). Android has no ladder of its own, so a read names the nearest rung and reports the multiplier as platformValue. A running app uses a new size at its next configuration change, so relaunch the app.', }, faceid: { forms: [{ kind: 'choice', states: ['match', 'nonmatch', 'enroll', 'unenroll'] }], @@ -149,11 +149,11 @@ export const SETTINGS = { app: 'positional', appRequired: true, appScope: { apple: 'app-scoped', mobile: 'app-scoped' }, - help: 'Clears the data of the positional app, the --app app, or the session app, and refuses two different apps. It leaves the keychain alone. An app with no data container, such as a simulator system app, refuses with app-no-data-container.', + help: 'Clears the data of the positional app, the --app app, or the session app, and refuses two different apps. It does not change the keychain. An app with no data container, such as a simulator system app, refuses with app-no-data-container.', }, 'reset-keychain': { forms: [{ kind: 'choice', states: ['clear'] }], - help: 'Resets the whole iOS simulator keychain, for every app on it: simctl has no per-app reset. A fresh-install reset needs this and clear-app-state; relaunch the app afterwards to see it signed out.', + help: 'Resets the whole iOS simulator keychain for all apps on it, because simctl has no per-app reset. A fresh-install reset needs this setting and clear-app-state. To see the app signed out, relaunch it after the reset.', }, permission: { forms: [ @@ -167,7 +167,7 @@ export const SETTINGS = { ], app: 'input', appScope: { apple: 'app-scoped', mobile: 'app-scoped', 'macos-host': 'device-level' }, - help: 'Changes a permission of the --app app or the session app. An iOS simulator resolves --app to an installed bundle id or display name, Android to an installed package, and a Limrun iOS session only through the settings alias. On Android, deny|reset of a held permission kills a running app; the response reports priorGrantState and warns, and open --relaunch restores it. Android needs a resolvable foreground user and changes nothing without one. On the macOS host, grant|reset change the host TCC grants for the three desktop targets and take no --app.', + help: 'Changes a permission of the --app app or the session app. An iOS simulator resolves --app to an installed bundle id or display name. Android resolves it to an installed package. A Limrun iOS session resolves it only through the settings alias. On Android, deny|reset of a held permission kills a running app. The response reports priorGrantState and a warning, and open --relaunch restores the app. Android needs a resolvable foreground user, and without one it changes nothing. On the macOS host, grant|reset change the host TCC grants for the three desktop targets and take no --app.', }, } as const satisfies Record; diff --git a/packages/maestro/src/replay-backend/replay-maestro-declaration.ts b/packages/maestro/src/replay-backend/replay-maestro-declaration.ts index e169ef4a08..5469244eec 100644 --- a/packages/maestro/src/replay-backend/replay-maestro-declaration.ts +++ b/packages/maestro/src/replay-backend/replay-maestro-declaration.ts @@ -15,7 +15,7 @@ Run Maestro compatibility flows with replay --maestro or test ${formatMaestroCompatibilityReference()} -Unsupported syntax fails loudly rather than being skipped. Architecture, performance tradeoffs, and declared conformance divergences: ${MAESTRO_COMPATIBILITY_ADR_URL} +Unsupported syntax fails with an explicit error. The command does not skip it. Architecture, performance tradeoffs, and declared conformance divergences: ${MAESTRO_COMPATIBILITY_ADR_URL} Focused compatibility request: ${MAESTRO_COMPATIBILITY_ISSUE_URL}`, }, }; diff --git a/scripts/help-conformance-bench.mjs b/scripts/help-conformance-bench.mjs index 06bcf178f8..723c76c3eb 100644 --- a/scripts/help-conformance-bench.mjs +++ b/scripts/help-conformance-bench.mjs @@ -22,7 +22,7 @@ const RUN_TIMEOUT_MS = Number(process.env.HELP_BENCH_TIMEOUT_MS ?? 90_000); // Runner x case pairs run concurrently, capped low: these are paid LLM calls // and the CLI help subprocess calls behind loadDocs share this same machine. const CONCURRENCY = Number(process.env.HELP_BENCH_CONCURRENCY ?? 4); -const DEFAULT_RUNNERS = ['codex:gpt-5.4-mini', 'claude:claude-haiku-4-5']; +const DEFAULT_RUNNERS = ['codex:gpt-5.6-luna', 'claude:claude-haiku-4-5']; const USAGE = `Usage: node scripts/help-conformance-bench.mjs [options] Feeds a help slice + task into one non-agentic LLM call per runner x case and diff --git a/src/__tests__/cli-help.test.ts b/src/__tests__/cli-help.test.ts index 22526b2b61..7c8f378968 100644 --- a/src/__tests__/cli-help.test.ts +++ b/src/__tests__/cli-help.test.ts @@ -6,7 +6,7 @@ test('help appstate prints command help and skips daemon dispatch', async () => const result = await runCliCapture(['help', 'appstate']); assert.equal(result.code, 0); assert.equal(result.calls.length, 0); - assert.match(result.stdout, /Show foreground app\/activity/); + assert.match(result.stdout, /Report the foreground app and activity/); assert.doesNotMatch(result.stdout, /Command flags:/); assert.doesNotMatch(result.stdout, /Global flags:/); assert.doesNotMatch(result.stdout, /Global Flags:/); @@ -56,7 +56,7 @@ test('connect help documents cloud auth environment origins', async () => { assert.equal(result.code, 0); assert.equal(result.calls.length, 0); assert.match(result.stdout, /AGENT_DEVICE_CLOUD_BASE_URL/); - assert.match(result.stdout, /bridge\/control-plane API origin/); + assert.match(result.stdout, /bridge or control-plane API origin/); assert.match(result.stdout, /AGENT_DEVICE_DAEMON_AUTH_TOKEN/); }); @@ -213,7 +213,7 @@ test('help unknown command prints error plus root decision card and skips daemon assert.equal(result.calls.length, 0); assert.match(result.stderr, /Error \(INVALID_ARGS\): Unknown command: not-a-command/); assert.match(result.stdout, /All \d+ commands: agent-device help commands/); - assert.match(result.stdout, /When starting a task with a known app/); + assert.match(result.stdout, /When you start a task with a known app/); assert.doesNotMatch(result.stdout, /Global Flags:/); }); diff --git a/src/commands/__tests__/command-surface-metadata.test.ts b/src/commands/__tests__/command-surface-metadata.test.ts index dc9871b768..4697d6c2f2 100644 --- a/src/commands/__tests__/command-surface-metadata.test.ts +++ b/src/commands/__tests__/command-surface-metadata.test.ts @@ -246,8 +246,8 @@ test('command family facets keep daemon writers as an explicit projection subset // fails instead of shipping a cross-surface mismatch. test('every emitted input schema carries the aligned device-selector descriptions', () => { const expected: Record = { - device: 'Device name selector (a UDID belongs in udid, a serial in serial).', - udid: 'Apple device or simulator UDID; the selector that pins one device when several share a name.', + device: 'Device name selector (put a UDID in udid and a serial in serial).', + udid: 'Apple device or simulator UDID. It is the selector that pins one device when several devices share a name.', serial: 'Android, HarmonyOS, or Vega VVD serial selector.', }; let pinned = 0; diff --git a/src/commands/batch/index.ts b/src/commands/batch/index.ts index 04f090ad9d..677c7061e4 100644 --- a/src/commands/batch/index.ts +++ b/src/commands/batch/index.ts @@ -51,10 +51,10 @@ const BATCH_STEP_EXAMPLES: readonly BatchCommandStep[] = [ */ function buildBatchCliDetail(): string { const prose = [ - 'Each step is {"command":"","input":{...}}, and input is that command\'s structured input object rather than its terminal spelling: a CLI positional becomes a named field (target, text, direction) and a CLI flag becomes a camelCase key (--settle -> "settle":true, -i -> "interactiveOnly":true). There is no positional step form; args, argv, positionals, and flags are not step fields. agent-device help lists arguments in CLI spelling only. The examples below carry the structured spelling; over MCP, the command tool schema states it in full.', - 'Steps run serially in one daemon request against the same session, in order. Mutating UI verbs are included (press, click, fill, longpress, scroll, back), which is where the round-trip saving is; --on-error stop halts at the first failing step.', + 'Each step is {"command":"","input":{...}}. input is the structured input object of that command, not its terminal spelling. A CLI positional becomes a named field (target, text, direction), and a CLI flag becomes a camelCase key (--settle -> "settle":true, -i -> "interactiveOnly":true). There is no positional step form. args, argv, positionals, and flags are not step fields. agent-device help lists arguments in CLI spelling only. The examples below use the structured spelling. Over MCP, the command tool schema gives it in full.', + 'Steps run serially and in order, in one daemon request against the same session. Mutating UI verbs are included (press, click, fill, longpress, scroll, back), and they give the round-trip saving. --on-error stop stops at the first failed step.', `Available through batch: ${[...STRUCTURED_BATCH_COMMAND_NAMES].sort().join(', ')}.`, - 'Every other command is excluded: batch and replay never nest, and session, daemon, connection, and host tooling commands own lifecycle a batch request cannot carry. Run those on their own.', + 'All other commands are excluded. batch and replay never nest. session, daemon, connection, and host tooling commands own a lifecycle that a batch request cannot carry. Run those commands separately.', ].join(' '); const examples = [ ` agent-device batch --steps '${JSON.stringify(BATCH_STEP_EXAMPLES)}'`, diff --git a/src/commands/batch/metadata.ts b/src/commands/batch/metadata.ts index 63ba96aba9..97be39da10 100644 --- a/src/commands/batch/metadata.ts +++ b/src/commands/batch/metadata.ts @@ -46,7 +46,7 @@ export function createBatchCommandMetadata( const fields = batchFields(nestedCommands); return defineFieldCommandMetadata( 'batch', - 'Execute multiple commands in one daemon request.', + 'Run multiple commands in one daemon request.', fields, { readInput: (input) => readBatchInput(input, fields) }, ); @@ -66,7 +66,7 @@ function batchFields(nestedCommands: readonly string[]) { ), ), onError: enumField(['stop'] as const, 'Batch failure policy.'), - maxSteps: integerField('Maximum number of steps accepted for this batch.', { + maxSteps: integerField('Maximum number of steps that this batch accepts.', { min: 1, max: 1000, }), @@ -98,7 +98,7 @@ function batchStepSchema(nestedCommands: readonly string[]): JsonSchema { runtime: { type: 'object', additionalProperties: true, - description: 'Optional per-step runtime payload.', + description: 'Optional runtime payload for the step.', }, }, required: ['command', 'input'], diff --git a/src/commands/capture/alert.ts b/src/commands/capture/alert.ts index 2ffffb133f..28cf1d70b9 100644 --- a/src/commands/capture/alert.ts +++ b/src/commands/capture/alert.ts @@ -18,7 +18,7 @@ import { AppError } from '@agent-device/kernel/errors'; const ALERT_COMMAND_NAME = 'alert'; const alertCommandDescription = - 'Inspect, wait for, accept, or dismiss a platform alert. Use get before acting when the alert content matters; accept and dismiss change the active alert state. Each iOS XCTest execution activates once, then only observes. On the Apple XCUITest runner, dismiss taps only a recognized decline label (Cancel, Don\'t Allow, Ask App Not to Track, and similar) or the close element of a popover whose accessibility marker reads exactly "dismiss popup"; when no button fits the action, accept and dismiss answer ALERT_BUTTON_NOT_FOUND having tapped nothing — read labels with alert get, then press the button by label. Inspect again after an unconfirmed action; never assume a timeout means nothing happened.'; + 'Examine, wait for, accept, or dismiss a platform alert. If the alert content is important, use get before you act. accept and dismiss change the active alert state. Each iOS XCTest execution activates once, and then only observes. On the Apple XCUITest runner, dismiss taps only a recognized decline label (Cancel, Don\'t Allow, Ask App Not to Track, and similar) or the close element of a popover whose accessibility marker reads exactly "dismiss popup"; when no button fits the action, accept and dismiss answer ALERT_BUTTON_NOT_FOUND having tapped nothing — read labels with alert get, then press the button by label. After an unconfirmed action, examine the alert again. Never assume that a timeout means that nothing occurred.'; const alertCommandMetadata = defineFieldCommandMetadata( ALERT_COMMAND_NAME, @@ -46,7 +46,7 @@ export const alertDaemonWriter: DaemonWriter = direct(PUBLIC_COMMANDS.alert, (in export const alertCommandFacet = defineCommandFacet({ name: ALERT_COMMAND_NAME, text: { - summary: 'Inspect, accept, or dismiss a platform alert', + summary: 'Examine, accept, or dismiss a platform alert', }, metadata: alertCommandMetadata, run: (client, input) => client.command.alert(input), diff --git a/src/commands/capture/diff.ts b/src/commands/capture/diff.ts index 3eee927c46..57224f6855 100644 --- a/src/commands/capture/diff.ts +++ b/src/commands/capture/diff.ts @@ -63,7 +63,7 @@ export const DIFF_COMMAND_OUTPUT_SCHEMAS = { } satisfies Pick, 'diff'>; const diffCommandDescription = - 'Compare accessibility snapshots or screenshots to identify UI changes. Use snapshot comparisons for semantic tree changes and screenshot comparisons for pixel differences.'; + 'Compare accessibility snapshots or screenshots to find UI changes. For semantic tree changes, compare snapshots. For pixel differences, compare screenshots.'; const diffCommandMetadata = defineFieldCommandMetadata(DIFF_COMMAND_NAME, diffCommandDescription, { kind: requiredField(jsonSchemaField<'snapshot'>({ type: 'string', const: 'snapshot' })), @@ -104,9 +104,9 @@ const diffDaemonWriter: DaemonWriter = direct(PUBLIC_COMMANDS.diff, (input) => [ export const diffCommandFacet = defineCommandFacet({ name: DIFF_COMMAND_NAME, text: { - summary: 'Diff snapshot or screenshot', + summary: 'Diff a snapshot or a screenshot', cliDetail: - 'Screenshot --threshold is a per-pixel RGB tolerance: 0 requires exact colors and 1 ignores color differences; image dimensions must still match. Both screenshot inputs are decoded from their bytes, so a baseline or current image may be PNG or JPEG, and the diff image is always PNG. JPEG is lossy, so keep --threshold above 0 whenever either input is JPEG. Live iOS simulator screenshot diffs normalize status-bar chrome by default; use screenshot --normalize-status-bar when capturing reusable baselines.', + 'For screenshots, --threshold is a per-pixel RGB tolerance: 0 requires exact colors, and 1 ignores color differences. The image dimensions must still match. The command decodes both screenshot inputs from their bytes, so a baseline or current image may be PNG or JPEG. The diff image is always PNG. JPEG is lossy, so if either input is JPEG, keep --threshold above 0. Live iOS simulator screenshot diffs normalize the status-bar chrome by default. To capture reusable baselines, use screenshot --normalize-status-bar.', }, metadata: diffCommandMetadata, run: (client, input) => client.capture.diff(input), diff --git a/src/commands/capture/screenshot.ts b/src/commands/capture/screenshot.ts index 13ab745d18..230dfbeccb 100644 --- a/src/commands/capture/screenshot.ts +++ b/src/commands/capture/screenshot.ts @@ -26,7 +26,7 @@ import { defineFieldCommandMetadata } from '../field-command-contract.ts'; const SCREENSHOT_COMMAND_NAME = 'screenshot'; const screenshotCommandDescription = - 'Capture a screenshot of the active app or web session. Choose the capture scope, density, size, or annotations through the corresponding input fields when needed.'; + 'Capture a screenshot of the active app or web session. If necessary, choose the capture scope, density, size, or annotations through the related input fields.'; const screenshotCommandMetadata = defineFieldCommandMetadata( SCREENSHOT_COMMAND_NAME, @@ -34,7 +34,7 @@ const screenshotCommandMetadata = defineFieldCommandMetadata( { path: stringField('Output path.'), cropOn: stringField( - 'Selector expression; the capture is cropped to the frame the selector resolves on the same screen.', + 'Selector expression. The command crops the capture to the frame that the selector resolves on the same screen.', ), overlayRefs: booleanField(), pixelDensity: integerField('Output screenshot pixel density in pixels per logical point.', { @@ -74,7 +74,7 @@ export const screenshotCommandFacet = defineCommandFacet({ text: { summary: 'Capture a screenshot', cliDetail: - 'Web defaults to the viewport; use --fullscreen, --full, or -f for the entire page. iOS simulators default to 1x logical-point output; use --pixel-density to request a different screenshot density. macOS app sessions default to the app window; use --fullscreen for full desktop, --scale to downscale, --crop-on to crop the capture to the frame the selector resolves on the same screen (currently iOS simulators and Android emulators), --overlay-refs to annotate current refs, --normalize-status-bar for deterministic iOS simulator chrome, or --no-stabilize for low-latency Android capture loops. On macOS, any --surface other than app (desktop, menubar, frontmost-app) always captures the main display and refuses an explicit --fullscreen. On single-display iOS simulators and Android devices, the result reports displayRotation, the rotation the display rendered the capture in, when the device returns a reading in time; otherwise the field is absent.', + 'Web defaults to the viewport. For the entire page, use --fullscreen, --full, or -f. iOS simulators default to 1x logical-point output. On iOS simulators, use --pixel-density to request a different screenshot density. macOS app sessions default to the app window. On macOS, use --fullscreen for the full desktop. Use --scale to downscale, --crop-on to crop the capture to the frame that the selector resolves on the same screen (at this time, iOS simulators and Android emulators), --overlay-refs to annotate the current refs, --normalize-status-bar for deterministic iOS simulator chrome, or --no-stabilize for low-latency Android capture loops. On macOS, any --surface other than app (desktop, menubar, frontmost-app) always captures the main display and refuses an explicit --fullscreen. On single-display iOS simulators and Android devices, the result reports displayRotation when the device returns a reading in time. displayRotation is the rotation in which the display rendered the capture. Without a reading in time, the field is absent.', }, metadata: screenshotCommandMetadata, run: (client, input) => client.capture.screenshot(input), diff --git a/src/commands/capture/settings.ts b/src/commands/capture/settings.ts index 195fb1a7c4..9de9aae0ce 100644 --- a/src/commands/capture/settings.ts +++ b/src/commands/capture/settings.ts @@ -26,7 +26,7 @@ import { messageOutput } from '../output-common.ts'; const SETTINGS_COMMAND_NAME = 'settings'; const settingsCommandDescription = - 'Read or change supported operating-system settings, animation scales, appearance, or app permissions on the selected target. Platform support varies by setting and action.'; + 'Read or change supported operating-system settings, animation scales, appearance, or app permissions on the selected target. Platform support depends on the setting and the action.'; const settingsCommandMetadata = defineFieldCommandMetadata( SETTINGS_COMMAND_NAME, @@ -36,7 +36,7 @@ const settingsCommandMetadata = defineFieldCommandMetadata( // A readable setting answers with no state; the parse decides per setting. state: stringField(), app: stringField( - 'App the change targets, by bundle id, package name, or app name: the app `clear-app-state` clears, or the app a `permission` change (or an iOS-simulator `location on|off` privacy change) lands on. A name resolves to the installed app as `open` resolves it: an iOS simulator matches display names and accepts only an installed bundle id, Android accepts only an installed package and refuses a name that resolves to an intent with `setting_app_not_package`, and a Limrun iOS session resolves only the `settings` alias. One that resolves to nothing is refused with `app_not_installed` before anything changes. The app need not be running. Without it the session app is used; a device-wide change (`location set` on any target, the Android `location on|off` toggle, `mobile-data`, macOS permissions) refuses one with `setting_app_not_consumed`. The CLI reads it only from this invocation, never from `AGENT_DEVICE_TARGET_APP` or config `targetApp`.', + 'The app that the change targets, by bundle id, package name, or app name. It is the app that `clear-app-state` clears, or the app that a `permission` change (or an iOS-simulator `location on|off` privacy change) applies to. A name resolves to the installed app in the same way as `open` resolves it. An iOS simulator matches display names and accepts only an installed bundle id. Android accepts only an installed package, and refuses a name that resolves to an intent with `setting_app_not_package`. A Limrun iOS session resolves only the `settings` alias. If the app resolves to nothing, the command refuses it with `app_not_installed` before anything changes. The app does not have to be running. Without it, the command uses the session app. A device-wide change (`location set` on any target, the Android `location on|off` toggle, `mobile-data`, macOS permissions) refuses an app with `setting_app_not_consumed`. The CLI reads it only from this invocation, never from `AGENT_DEVICE_TARGET_APP` or config `targetApp`.', ), latitude: numberField(), longitude: numberField(), @@ -102,7 +102,7 @@ export const settingsCommandFacet = defineCommandFacet({ text: { summary: 'Change OS settings and app permissions', cliDetail: [ - 'A bare readable setting reads the value the target holds. A target that does not serve a setting refuses it with UNSUPPORTED_OPERATION before anything changes.', + 'A readable setting without a state reads the value that the target holds. If a target does not serve a setting, it refuses the setting with UNSUPPORTED_OPERATION before anything changes.', '', 'Settings:', ...settingsHelpLines().map((line) => ` ${line}`), diff --git a/src/commands/capture/snapshot.ts b/src/commands/capture/snapshot.ts index 3927b6d2c8..5ff07acf52 100644 --- a/src/commands/capture/snapshot.ts +++ b/src/commands/capture/snapshot.ts @@ -19,7 +19,7 @@ import { captureCliOutputFormatters } from './output.ts'; const SNAPSHOT_COMMAND_NAME = 'snapshot'; const snapshotCommandDescription = - 'Capture the accessibility tree or compare it with the previous session baseline. Use the returned refs for subsequent semantic interactions and the diff option to verify UI changes.'; + 'Capture the accessibility tree or compare it with the previous session baseline. Use the returned refs for later semantic interactions. To confirm UI changes, use the diff option.'; const snapshotBackendCapabilityHelp = Object.entries(SNAPSHOT_BACKEND_CAPABILITIES) .map(([backend, capability]) => { @@ -46,7 +46,7 @@ const snapshotCommandMetadata = defineFieldCommandMetadata( // both flags, mirroring `--no-record`/`--record` on the CLI. noRecord: booleanField('Do not record this action.'), record: booleanField( - 'Force-record this out-of-band observation into a repair-armed heal (mutually exclusive with noRecord). Authored replay steps are recorded automatically and never need this.', + 'Always record this out-of-band observation into a repair-armed heal (mutually exclusive with noRecord). Authored replay steps are recorded automatically and never need this field.', ), }, ); @@ -76,7 +76,7 @@ export const snapshotCommandFacet = defineCommandFacet({ name: SNAPSHOT_COMMAND_NAME, text: { summary: 'Capture or diff the accessibility tree', - cliDetail: `--observe-only is local iOS snapshot-only: an already-ready compatible runner reads the session app without activation or foreground repair, refuses unavailable observation, and reports XCUIApplication state rather than verified foreground ownership (#2696). Regular targetActivation disclosure stays unchanged (#2682, #2694). Repeated equivalent unfiltered Android snapshots return a compact unchanged acknowledgement. Use --force-full to re-emit the tree; --json and --raw retain full output. Android Appium JSON snapshots publish a display viewport only when measurements before and after the tree agree, independent of tree extent or keyboard content resizing. On Android, JSON value carries field content rather than static labels; saved selectors and replay scripts using value= for static labels must use label= instead; a reported showing hint means value is empty, and text equal to the placeholder is preserved when hint state is unknown. For iOS raw-coordinate fallback after a no-op ref press, inspect rects with snapshot -i --json, press the rect center, then verify with diff snapshot -i or snapshot --diff. iOS backend capability contract: ${snapshotBackendCapabilityHelp}.`, + cliDetail: `--observe-only is for local iOS snapshots only. An already-ready compatible runner reads the session app without activation or foreground repair. It refuses unavailable observation, and it reports the XCUIApplication state, not verified foreground ownership (#2696). Regular targetActivation disclosure does not change (#2682, #2694). Repeated equivalent unfiltered Android snapshots return a compact unchanged acknowledgement. To return the full tree again, use --force-full. --json and --raw keep the full output. Android Appium JSON snapshots publish a display viewport only when the measurements before and after the tree agree. This is independent of the tree extent and of keyboard content resizing. On Android, the JSON value carries field content, not static labels. On Android, saved selectors and replay scripts that use value= for static labels must use label= instead. On Android, a reported showing hint means that value is empty. On Android, if the hint state is unknown, text equal to the placeholder is kept. For an iOS raw-coordinate fallback after a no-op ref press, examine the rects with snapshot -i --json and press the rect center. Then confirm with diff snapshot -i or snapshot --diff. iOS backend capability contract: ${snapshotBackendCapabilityHelp}.`, }, metadata: snapshotCommandMetadata, run: (client, input) => client.capture.snapshot(input), diff --git a/src/commands/capture/wait.ts b/src/commands/capture/wait.ts index 89ec919c3b..a6f759f1f0 100644 --- a/src/commands/capture/wait.ts +++ b/src/commands/capture/wait.ts @@ -60,7 +60,7 @@ export const WAIT_COMMAND_OUTPUT_SCHEMAS = { } satisfies Pick, 'wait'>; const waitCommandDescription = - 'Wait for a duration, text, snapshot ref, selector to appear, selector to be strictly absent, or stable UI. Use the strict absence target when zero selector matches are required; stable waits until the UI stays quiet for the requested window.'; + 'Wait for a duration, text, a snapshot ref, a selector to appear, a selector to be strictly absent, or a stable UI. If zero selector matches are required, use the strict absence target. stable waits until the UI stays quiet for the requested window.'; const waitCommandMetadata = defineFieldCommandMetadata(WAIT_COMMAND_NAME, waitCommandDescription, { kind: enumField(WAIT_KIND_VALUES), diff --git a/src/commands/command-input.ts b/src/commands/command-input.ts index fbce60a059..8a1f091f9c 100644 --- a/src/commands/command-input.ts +++ b/src/commands/command-input.ts @@ -377,17 +377,17 @@ export function pointField(description: string): CommandField { export function selectorSnapshotFields() { return { depth: integerField('Snapshot traversal depth.', { min: 0 }), - scope: stringField('Snapshot scope selector used before resolution.'), + scope: stringField('Snapshot scope selector to apply before resolution.'), raw: booleanField('Use raw snapshot data during selector resolution.'), }; } export function repeatedFields() { return { - count: integerField('Number of press/click repetitions.', { min: 1 }), - intervalMs: integerField('Delay between repeated press/click actions.', { min: 0 }), + count: integerField('Number of press or click repetitions.', { min: 1 }), + intervalMs: integerField('Delay between repeated press or click actions.', { min: 0 }), holdMs: integerField('Hold duration for each action.', { min: 0 }), - jitterPx: integerField('Randomization radius in pixels.', { min: 0 }), + jitterPx: integerField('Deterministic coordinate jitter radius in pixels.', { min: 0 }), doubleTap: booleanField('Request a double-tap action.'), }; } @@ -568,7 +568,7 @@ function elementTargetSchemaVariants(): JsonSchema[] { type: 'object', properties: { kind: { type: 'string', const: 'ref' }, - ref: { type: 'string', description: 'Snapshot element ref such as @e12.' }, + ref: { type: 'string', description: 'Snapshot element ref, for example @e12.' }, label: { type: 'string', description: 'Optional human label for the ref.' }, }, required: ['kind', 'ref'], diff --git a/src/commands/common-input-fields.ts b/src/commands/common-input-fields.ts index 55169762e5..84bb2e89e1 100644 --- a/src/commands/common-input-fields.ts +++ b/src/commands/common-input-fields.ts @@ -105,7 +105,7 @@ const COMMON_INPUT_FIELDS = { schema: { type: 'string', enum: PLATFORM_SELECTORS, - description: 'Platform selector used to resolve a device.', + description: 'Platform selector for device resolution.', }, read: (record) => optionalEnum(record, 'platform', PLATFORM_SELECTORS), flagKey: 'platform', @@ -134,7 +134,7 @@ const COMMON_INPUT_FIELDS = { device: { schema: { type: 'string', - description: 'Device name selector (a UDID belongs in udid, a serial in serial).', + description: 'Device name selector (put a UDID in udid and a serial in serial).', }, read: (record) => optionalString(record, 'device'), flagKey: 'device', @@ -144,7 +144,7 @@ const COMMON_INPUT_FIELDS = { schema: { type: 'string', description: - 'Apple device or simulator UDID; the selector that pins one device when several share a name.', + 'Apple device or simulator UDID. It is the selector that pins one device when several devices share a name.', }, read: (record) => optionalString(record, 'udid'), flagKey: 'udid', @@ -162,7 +162,7 @@ const COMMON_INPUT_FIELDS = { iosSimulatorDeviceSet: { schema: { type: 'string', - description: 'iOS simulator device-set path used for device resolution.', + description: 'iOS simulator device-set path for device resolution.', }, read: (record) => optionalString(record, 'iosSimulatorDeviceSet'), audience: operatorAudience({ envFlagKeys: [], operatorConfig: true }), @@ -183,7 +183,7 @@ const COMMON_INPUT_FIELDS = { iosXctestDerivedDataPath: { schema: { type: 'string', - description: 'Derived data path for external iOS XCTest runner execution.', + description: 'Derived data path for the execution of an external iOS XCTest runner.', }, read: (record) => optionalString(record, 'iosXctestDerivedDataPath'), audience: ENV_OR_OPERATOR_CONFIG, @@ -203,7 +203,7 @@ const COMMON_INPUT_FIELDS = { androidDeviceAllowlist: { schema: { type: 'string', - description: 'Android serial allowlist used for device resolution.', + description: 'Android serial allowlist for device resolution.', }, read: (record) => optionalString(record, 'androidDeviceAllowlist'), flagKey: 'androidDeviceAllowlist', diff --git a/src/commands/debugging/index.ts b/src/commands/debugging/index.ts index ed0b586917..d2ff8adc13 100644 --- a/src/commands/debugging/index.ts +++ b/src/commands/debugging/index.ts @@ -11,7 +11,7 @@ const DEBUG_COMMAND_NAME = 'debug'; const DEBUG_ACTION_VALUES = ['symbols'] as const; const debugCommandDescription = - 'Symbolicate Apple crash artifacts with matching dSYM UUIDs. This debug namespace is intentionally narrow: use logs for app logs, network for HTTP evidence, perf for performance samples, record/trace for media and traces, and react-devtools for React Native components and render profiles.'; + 'Symbolicate Apple crash artifacts with matching dSYM UUIDs. This debug namespace is intentionally narrow. For app logs, use logs. For HTTP evidence, use network. For performance samples, use perf. For media and traces, use record or trace. For React Native components and render profiles, use react-devtools.'; export const debugCommandMetadata = defineFieldCommandMetadata( DEBUG_COMMAND_NAME, diff --git a/src/commands/interaction/index.ts b/src/commands/interaction/index.ts index 0ebaf14cc6..96ecc53bb8 100644 --- a/src/commands/interaction/index.ts +++ b/src/commands/interaction/index.ts @@ -229,7 +229,7 @@ const tapInteractionResponseDataSchema = interactionResponseDataSchema({ count: numberSchema('Number of press/click repetitions.'), intervalMs: numberSchema('Delay between repeated press/click actions.'), holdMs: numberSchema('Hold duration for each action.'), - jitterPx: numberSchema('Randomization radius in pixels.'), + jitterPx: numberSchema('Deterministic coordinate jitter radius in pixels.'), doubleTap: booleanSchema('Whether the command requested a double-tap action.'), }, }); @@ -466,7 +466,8 @@ const pressCommandFacet = defineCommandFacet({ name: 'press', text: { summary: 'Short-press a UI target', - cliDetail: 'The hold duration is positional on longpress, not press --hold-ms.', + cliDetail: + 'On longpress, the hold duration is a positional argument. It is not press --hold-ms.', }, metadata: metadata('press'), run: (client, input) => client.interactions.press(toPressOptions(input)), @@ -481,7 +482,7 @@ const fillCommandFacet = defineCommandFacet({ text: { summary: 'Replace text in a UI input', cliDetail: - 'Every positional after an @ref is the replacement text, so fill @e57 good morning enters "good morning"; quote the text when the shell must preserve exact whitespace. Clear a field with an empty text argument: fill @e57 "" (the argument must be present — fill @e57 alone is a missing argument, not a clear). When visible label text also matches a non-input element, constrain the target with editable=true, for example fill \'label="Email" editable=true\' "qa@example.com". To keep a secret out of argv, omit the text and pipe it in: printf %s "$PASSWORD" | fill @e57 --text-stdin (max 64 KiB, one trailing newline removed).', + 'Every positional after an @ref is the replacement text, so fill @e57 good morning enters "good morning". If the shell must keep exact whitespace, quote the text. To clear a field, give an empty text argument: fill @e57 "". The argument must be present: fill @e57 alone is a missing argument, not a clear. If visible label text also matches a non-input element, add editable=true to the target, for example fill \'label="Email" editable=true\' "qa@example.com". To keep a secret out of argv, omit the text and pipe it in: printf %s "$PASSWORD" | fill @e57 --text-stdin (maximum 64 KiB; the command removes one trailing newline).', }, metadata: metadata('fill'), run: (client, input) => client.interactions.fill(toFillOptions(input)), @@ -495,7 +496,8 @@ const longPressCommandFacet = defineCommandFacet({ name: 'longpress', text: { summary: 'Hold a UI target to open a context menu', - cliDetail: 'Duration is positional, for example longpress @e12 800 or longpress 300 500 800.', + cliDetail: + 'The duration is a positional argument, for example longpress @e12 800 or longpress 300 500 800.', }, metadata: metadata('longpress'), run: (client, input) => client.interactions.longPress(toLongPressOptions(input)), @@ -510,7 +512,7 @@ const hoverCommandFacet = defineCommandFacet({ text: { summary: 'Hover the pointer over a UI target (web only)', cliDetail: - 'The pointer stays where hover left it: read the revealed UI (--settle or snapshot -i) and act on it before another click or hover moves the pointer away.', + 'The pointer stays at the position where hover left it. Read the UI that the hover showed (--settle or snapshot -i), and act on it before another click or hover moves the pointer away.', }, metadata: metadata('hover'), run: (client, input) => client.interactions.hover(toHoverOptions(input)), @@ -535,7 +537,7 @@ const swipeCommandFacet = defineCommandFacet({ const focusCommandFacet = defineCommandFacet({ name: 'focus', text: { - summary: 'Focus input at screen coordinates', + summary: 'Focus the input at screen coordinates', }, metadata: metadata('focus'), run: (client, input) => client.interactions.focus(input as FocusOptions), @@ -585,7 +587,7 @@ const getCommandFacet = defineCommandFacet({ const isCommandFacet = defineCommandFacet({ name: 'is', text: { - summary: 'Check a UI predicate on a selector', + summary: 'Examine a UI predicate on a selector', }, metadata: metadata('is'), run: (client, input) => client.interactions.is(input as IsOptions), @@ -598,7 +600,7 @@ const isCommandFacet = defineCommandFacet({ const findCommandFacet = defineCommandFacet({ name: 'find', text: { - summary: 'Find an element and act', + summary: 'Find an element and run an action', }, metadata: metadata('find'), run: (client, input) => client.interactions.find(input as FindOptions), @@ -613,7 +615,7 @@ const gestureCommandFacet = defineCommandFacet({ text: { summary: 'Run pan, fling, swipe, pinch, rotate, transform, or drag gestures', cliDetail: - 'Argument shapes: pan [durationMs], fling [distance], swipe , pinch [x] [y], rotate [x] [y], transform [durationMs], or drag [sourceHoldMs] [moveMs] [destinationHoldMs]. For command plans, output only command lines. Android transform verification should use all app-observable effects, for example wait text "pan changed yes", wait text "pinch changed yes", and wait text "rotate changed yes", not exact transform values.', + 'Argument shapes: pan [durationMs], fling [distance], swipe , pinch [x] [y], rotate [x] [y], transform [durationMs], or drag [sourceHoldMs] [moveMs] [destinationHoldMs]. For command plans, write only command lines. To verify an Android transform, you should use all app-observable effects, not exact transform values. For example, use wait text "pan changed yes", wait text "pinch changed yes", and wait text "rotate changed yes".', }, metadata: metadata('gesture'), run: async (client, input) => { diff --git a/src/commands/interaction/metadata.ts b/src/commands/interaction/metadata.ts index 0a2dcfedcd..a26565e955 100644 --- a/src/commands/interaction/metadata.ts +++ b/src/commands/interaction/metadata.ts @@ -56,25 +56,25 @@ const FIND_ACTION_VALUES = [ const interactionCommandDescriptions = { click: - "Activate a UI target by snapshot ref, selector, or coordinates. Prefer a ref or selector after a snapshot; use coordinates only when semantic targeting is unavailable. This can change app state. Success means the click was dispatched, not that it landed: use settle or verify to confirm the result without a follow-up snapshot, or wait for the expected screen. A ref or selector target is re-checked for motion before dispatch when the session's previous action just animated the screen, and is dispatched at its resting position; one that has not stopped refuses with target_still_moving rather than acting where it was.", + 'Activate a UI target by snapshot ref, selector, or coordinates. After a snapshot, prefer a ref or selector. Use coordinates only when semantic targeting is unavailable. This command can change the app state. Success means that the command dispatched the click, not that the click reached the intended target. To confirm the result without a follow-up snapshot, use settle or verify, or wait for the expected screen. If the previous action of the session animated the screen, the command examines a ref or selector target for motion before dispatch. It then dispatches at the resting position of the target. If the target has not stopped, the command refuses with target_still_moving and does not act at the earlier position.', press: - "Short-press a UI target by snapshot ref, selector, or coordinates. Use longpress instead when the target requires a context-menu or hold gesture. Success means the press was dispatched, not that it landed: use settle or verify to confirm the result, or wait for the expected screen. A ref or selector target is re-checked for motion before dispatch when the session's previous action just animated the screen, and is dispatched at its resting position; one that has not stopped refuses with target_still_moving rather than acting where it was.", - fill: 'Replace text in a UI input selected by snapshot ref, selector, or coordinates. Pass an empty text to clear the field. Inspect unconfirmed evidence and assert the expected value or resulting screen before continuing. Prefer refs or selectors after snapshot; use recordAs to keep sensitive text out of a recorded replay while sending it to the live app.', + 'Short-press a UI target by snapshot ref, selector, or coordinates. If the target requires a context-menu or hold gesture, use longpress instead. Success means that the command dispatched the press, not that the press reached the intended target. To confirm the result, use settle or verify, or wait for the expected screen. If the previous action of the session animated the screen, the command examines a ref or selector target for motion before dispatch. It then dispatches at the resting position of the target. If the target has not stopped, the command refuses with target_still_moving and does not act at the earlier position.', + fill: 'Replace the text in a UI input that a snapshot ref, selector, or coordinates select. To clear the field, pass an empty text. Before you continue, examine unconfirmed evidence, and assert the expected value or the resulting screen. After a snapshot, prefer refs or selectors. To send sensitive text to the live app but keep it out of a recorded replay, use recordAs.', longpress: - 'Hold a UI target by snapshot ref, selector, or coordinates to open a context menu or perform another hold gesture. Set durationMs when the default hold duration is unsuitable.', + 'Hold a UI target by snapshot ref, selector, or coordinates to open a context menu or to do a different hold gesture. If the default hold duration is unsuitable, set durationMs.', hover: - 'Move the pointer over a UI target by snapshot ref, selector, or coordinates without pressing, to reveal hover-gated UI such as row toolbars or menus. Web only; touch platforms have no hover state. Use settle to observe what the hover revealed without a follow-up snapshot.', - swipe: 'Quick coordinate fling with optional repeat pattern.', + 'Move the pointer over a UI target by snapshot ref, selector, or coordinates, without a press. Use it to show hover-gated UI, such as row toolbars or menus. Web only. Touch platforms have no hover state. To observe what the hover showed without a follow-up snapshot, use settle.', + swipe: 'Do a quick fling between coordinates, with an optional repeat pattern.', focus: - 'Move input focus to explicit screen coordinates without entering text. Prefer semantic interactions when a snapshot ref or selector is available; use type or fill after focus.', - type: 'Append text to the currently focused input. Use fill when the existing field value should be replaced, and focus first when no input is active.', + 'Move the input focus to explicit screen coordinates without text entry. If a snapshot ref or selector is available, prefer semantic interactions. After focus, use type or fill.', + type: 'Append text to the input that has focus. To replace the existing field value, use fill. If no input is active, use focus first.', scroll: - 'Scroll in a direction, or toward the top/bottom edge of scrollable content. Set until to a selector to reach an off-screen target in one command rather than a scroll-and-check loop. The optional amount is the finger-path fraction of the viewport axis, honored up to 0.8 of it; directional scrolls reduce release momentum, while app scroll physics determine the final content offset. A visible keyboard shortens the swiped band instead of being dismissed; when too little is left, the command refuses with scroll_keyboard_occludes_surface. A directional scroll also reports the movement it observed as movement: moved, at-edge, unchanged, or unobserved when the two reads could not back a claim either way; an unchanged surface inside a container that still hides content in that direction refuses with scroll_no_progress rather than repeating the requested distance. The movement field is absent where a tier verifies per pass (top/bottom, until), where the runtime cannot read a screen, or where a settle observation or a replay already owns that observation.', - get: 'Read text or accessibility attributes from a snapshot ref or selector without changing the app. Use format text for visible content or attrs for the element attribute map.', - is: 'Check whether a selector satisfies a UI predicate such as visible, hidden, exists, absent, editable, selected, focused, or text. `absent` passes only when one readable, complete, settled, unscoped, full-depth accessibility capture has zero matches. Use wait when the condition may appear asynchronously.', - find: 'Find by text/label/value/role/id and run action. The role locator matches snapshot kind vocabulary (button, text, text-field, switch, ...); pre-reconciliation leaf spellings (statictext, edittext, textview) stay accepted on the nodes that carried them during a deprecation window.', + 'Scroll in a direction, or toward the top or bottom edge of scrollable content. To reach an off-screen target in one command instead of a scroll-and-check loop, set until to a selector. The optional amount is the finger-path fraction of the viewport axis. The command honors an amount up to 0.8 of the axis. Directional scrolls reduce release momentum, but app scroll physics set the final content offset. A visible keyboard makes the swiped band shorter, and the command does not dismiss the keyboard. If too little of the band is left, the command refuses with scroll_keyboard_occludes_surface. A directional scroll also reports the movement that it observed as movement: moved, at-edge, unchanged, or unobserved. unobserved means that the two reads could not support a claim either way. If the surface is unchanged inside a container that still hides content in that direction, the command refuses with scroll_no_progress. It does not repeat the requested distance. The movement field is absent when a tier confirms each pass (top/bottom, until), when the runtime cannot read a screen, or when a settle observation or a replay already owns that observation.', + get: 'Read the text or the accessibility attributes of a snapshot ref or selector. The command does not change the app. For visible content, use format text. For the element attribute map, use format attrs.', + is: 'Examine whether a selector satisfies a UI predicate, such as visible, hidden, exists, absent, editable, selected, focused, or text. `absent` passes only when one readable, complete, settled, unscoped, full-depth accessibility capture has zero matches. If the condition may appear asynchronously, use wait.', + find: 'Find an element by text, label, value, role, or id, and run an action. The role locator matches the snapshot kind vocabulary (button, text, text-field, switch, ...). During a deprecation window, the command still accepts pre-reconciliation leaf spellings (statictext, edittext, textview) on the nodes that carried them.', gesture: - 'Perform a structured pan, fling, swipe, pinch, rotate, transform, or drag gesture. Select the gesture kind, then provide only the inputs that apply to that kind.', + 'Do a structured pan, fling, swipe, pinch, rotate, transform, or drag gesture. Select the gesture kind. Then give only the inputs that apply to that kind.', } as const; type InteractionCommandName = keyof typeof interactionCommandDescriptions; @@ -99,15 +99,17 @@ const fillFields = { // "" is accepted and means "replace with nothing", i.e. clear the field (#2063). The key // must still be present: omitting it is a missing argument, not a clear. text: requiredField( - stringField('Text to enter into the target. Pass "" to clear it.', { allowEmpty: true }), + stringField('Text to enter into the target. To clear the target, pass "".', { + allowEmpty: true, + }), ), delayMs: integerField('Delay between typed characters.', { min: 0 }), recordAs: stringField( - 'When script recording is armed, send text to the live app but publish it as ${VAR}. Use an uppercase replay variable name such as PASSWORD.', + 'When script recording is armed, send the text to the live app, but publish it as ${VAR}. Use an uppercase replay variable name, for example PASSWORD.', ), textStdin: operatorField( booleanField( - "Operator-only: text was read from the CLI's stdin, so it is kept out of diagnostics and recorded actions.", + 'Operator-only: the CLI read the text from its stdin, so the text is kept out of diagnostics and recorded actions.', ), { operatorPath: @@ -154,7 +156,7 @@ const scrollFields = { amount: numberField('Platform scroll amount.'), pixels: integerField('Pixel scroll amount.', { min: 0 }), until: stringField( - 'Repeat scroll passes until this selector is visible on screen, then stop. Not valid with the top/bottom edge directions, which carry their own stop condition.', + 'Repeat scroll passes until this selector is visible on the screen, then stop. Not valid with the top and bottom edge directions, because they have their own stop condition.', ), durationMs: integerField('Scroll duration in milliseconds when the backend supports pacing.', { min: 0, @@ -173,7 +175,7 @@ const scrollFields = { const recordControlFields = () => ({ noRecord: booleanField('Do not record this action.'), record: booleanField( - 'Force-record this out-of-band observation into a repair-armed heal (mutually exclusive with noRecord). Authored replay steps are recorded automatically and never need this. On find, valid only for a read-only action.', + 'Always record this out-of-band observation into a repair-armed heal (mutually exclusive with noRecord). Authored replay steps are recorded automatically and never need this field. On find, it is valid only for a read-only action.', ), }); @@ -216,13 +218,13 @@ const gestureFields = { distance: integerField('Fling distance.', { min: 0 }), scale: numberField('Pinch or transform scale.'), degrees: numberField('Rotation in degrees.'), - durationMs: integerField('Pan/transform duration.', { min: 16, max: 10_000 }), - pointerCount: integerField('Pan touch pointer count (1 or 2).', { min: 1, max: 2 }), + durationMs: integerField('Pan or transform duration.', { min: 16, max: 10_000 }), + pointerCount: integerField('Number of touch pointers for pan (1 or 2).', { min: 1, max: 2 }), source: stringField('Drag source @ref or selector.'), destination: stringField('Drag destination @ref or selector.'), - sourceHoldMs: integerField('Drag activation hold duration.', { min: 1, max: 10_000 }), + sourceHoldMs: integerField('Hold duration for drag activation.', { min: 1, max: 10_000 }), moveMs: integerField('Drag movement duration.', { min: 16, max: 10_000 }), - destinationHoldMs: integerField('Hold before releasing at the destination.', { + destinationHoldMs: integerField('Hold duration before the release at the destination.', { min: 0, max: 10_000, }), diff --git a/src/commands/management/app.ts b/src/commands/management/app.ts index 17f4e9fbce..31c6a2eb8f 100644 --- a/src/commands/management/app.ts +++ b/src/commands/management/app.ts @@ -24,33 +24,35 @@ import { managementCliOutputFormatters } from './output.ts'; const appsCommandMetadata = defineFieldCommandMetadata( 'apps', - 'List the apps installed on the selected device. Include system or OEM apps only when they are needed as automation targets.', + 'List the apps installed on the selected device. Include system or OEM apps only when you need them as automation targets.', { appsFilter: enumField( ['user-installed', 'all'], - 'Restrict the listing to user-installed apps, or include system and OEM apps.', + 'Limit the list to user-installed apps, or include system and OEM apps.', ), }, ); const openCommandMetadata = defineFieldCommandMetadata( 'open', - 'Boot the selected device when needed, then open an app, deep link, or URL in a session. Use the app or URL inputs to choose what becomes the foreground automation target.', + 'If necessary, boot the selected device. Then open an app, deep link, or URL in a session. To choose what becomes the foreground automation target, use the app or URL inputs.', { app: stringField('App name, bundle id, package, or URL.'), - url: stringField('Optional URL passed with an app shell.'), + url: stringField('Optional URL to give to an app shell.'), surface: enumField( SESSION_SURFACES, 'macOS presentation surface to open: the app itself, the frontmost app, the desktop, or the menu bar.', ), activity: stringField('Android activity name.'), - launchConsole: stringField('Launch console mode.'), + launchConsole: stringField( + 'File path on the daemon host for the stdout and stderr of the initial app launch. iOS simulator direct app launches only; not for URL opens.', + ), launchArgs: stringArrayField( - 'Launch arguments forwarded verbatim to the platform launch command.', + 'Launch arguments that the command forwards verbatim to the platform launch command.', ), - relaunch: booleanField('Force relaunch.'), + relaunch: booleanField('Force a relaunch.'), timeoutMs: integerField( - 'Startup budget in milliseconds. Bounds the Simulator boot wait, so a never-booted Simulator can finish its first-boot migration; omit for the default startup behavior.', + 'Startup budget in milliseconds. It limits the Simulator boot wait, so that a never-booted Simulator can finish its first-boot migration. For the default startup behavior, omit it.', { min: 1 }, ), waitMs: optionField('waitMs'), @@ -59,38 +61,38 @@ const openCommandMetadata = defineFieldCommandMetadata( oneOf: [booleanSchema(), stringSchema()], }), force: booleanField( - 'Overwrite an existing --save-script target instead of refusing (alias: --overwrite).', + 'Overwrite an existing --save-script target instead of a refusal (alias: --overwrite).', ), testIme: booleanField( - 'Activate the headless Android test IME for deterministic Unicode text entry (default on for emulators; opt-in on real devices).', + 'Activate the headless Android test IME for deterministic Unicode text entry (default: on for emulators, opt-in on real devices).', ), noRecord: booleanField('Do not record this action.'), - metroHost: stringField('Session-scoped Metro/debug host hint applied to the opened app.'), + metroHost: stringField('Session-scoped Metro or debug host hint for the opened app.'), metroPort: integerField( - 'Session-scoped Metro/debug port hint applied to the opened app. On an emulator/simulator the host defaults to the loopback alias (Android 10.0.2.2, iOS 127.0.0.1) when --metro-host is omitted; physical devices still require an explicit --metro-host.', + 'Session-scoped Metro or debug port hint for the opened app. On an emulator or simulator without --metro-host, the host defaults to the loopback alias (Android 10.0.2.2, iOS 127.0.0.1). Physical devices still require an explicit --metro-host.', { min: 1, max: 65535, }, ), - bundleUrl: stringField('Session-scoped bundle URL hint applied to the opened app.'), + bundleUrl: stringField('Session-scoped bundle URL hint for the opened app.'), launchUrl: stringField( - 'Launch URL for the opened app; does not select the server used by metro reload.', + 'Launch URL for the opened app. It does not select the server that metro reload uses.', ), }, ); const closeCommandMetadata = defineFieldCommandMetadata( 'close', - 'Close the named app, or close the active session app when app is omitted. Use shutdown only when the selected simulator or emulator should also stop. Android ADB close fails with android-app-close-timeout if the app stays foreground, or its process stays running, past its 2s check window.', + 'Close the named app. If app is omitted, close the active session app. Use shutdown only when the selected simulator or emulator should also stop. Android ADB close fails with android-app-close-timeout if the app stays in the foreground, or its process stays running, after its 2s check window.', { app: stringField('Optional app to close.'), - shutdown: booleanField('Shutdown the session/device where supported.'), + shutdown: booleanField('Shut down the session or device where this is supported.'), saveScript: jsonSchemaField({ oneOf: [booleanSchema(), stringSchema()], }), force: booleanField( - 'Overwrite an existing --save-script target instead of refusing (alias: --overwrite).', + 'Overwrite an existing --save-script target instead of a refusal (alias: --overwrite).', ), }, ); @@ -182,7 +184,7 @@ export const appsCommandFacet = defineCommandFacet({ text: { summary: 'List installed apps or deferred provider app assets', cliDetail: - 'Before provider allocation, lists uploaded app assets when the selected provider exposes a catalog. On a live device, defaults to user-installed apps; use --all to include system/OEM apps.', + 'Before provider allocation, the command lists uploaded app assets when the selected provider exposes a catalog. On a live device, it defaults to user-installed apps. To include system and OEM apps, use --all.', }, metadata: appsCommandMetadata, run: (client, input) => client.apps.list(input), @@ -195,11 +197,11 @@ export const appsCommandFacet = defineCommandFacet({ export const openCommandFacet = defineCommandFacet({ name: 'open', text: { - summary: 'Open an app, deep link or URL, save replays', + summary: 'Open an app, deep link, or URL, and save replays', cliDetail: - 'Use --platform to bind URL/deep-link opens to the target platform. For iOS simulator initial stdout/stderr, put --launch-console on this open command, for example agent-device open "Agent Device Tester" --platform ios --launch-console artifacts/launch-console.log. Expo Go/dev-client shells accept host + URL, for example agent-device open "Expo Go" exp://127.0.0.1:8081 --platform ios. macOS also supports --surface app|frontmost-app|desktop|menubar. --metro-host/--metro-port/--bundle-url choose the app\'s Metro server and save that address for later metro reload calls in the same session. A fresh open without Metro hints clears an older same-name binding; close also clears it. --launch-url supplies the app\'s launch URL; it does not select the reload server. On an iOS simulator without --launch-url, open connects a detected Expo development client through its expo-development-client URL. The app must declare a single exp+ scheme that no other installed app owns. If detection is inconclusive, the app opens normally. Bare React Native apps receive their server settings before launch. On an iOS simulator, when --launch-url raises an "Open in ?" prompt that keeps open from seeing the app, open accepts it if the URL scheme belongs to the session app (data.launchConfirmation: "accepted"); if the scheme belongs to another installed app, the prompt stays on screen and open fails with details.reason launch_confirmation_foreign_app; if no single installed app owns the scheme, the prompt stays on screen and open returns as usual. The prompt is read whenever open cannot see the app afterwards, including on a slow host where the app\'s target discovery fails on its own deadline. When an accept leaves the app provably not running, open hands the same URL over once more and reads again; still not running, it fails with details.reason launch_confirmation_unanswered. A recognized prompt still visible after a failed accept also fails with this reason, even if the app is running behind it; answer it with alert accept or alert dismiss. A simulator openurl that CoreSimulator never answers fails within 20s with details.reason ios-simulator-openurl-timeout. Only the English prompt title with Cancel and Open buttons is recognized, so on a Simulator set to another language the prompt stays on screen. A prompt that appears after open has already seen the app is not answered; use alert accept. A device another session is holding refuses at once; add --wait to block up to that budget for it and only then fail with DEVICE_IN_USE naming the owning session.', + 'To bind URL or deep-link opens to the target platform, use --platform. For the initial stdout and stderr of an iOS simulator, put --launch-console on this open command, for example agent-device open "Agent Device Tester" --platform ios --launch-console artifacts/launch-console.log. Expo Go and dev-client shells accept a host and a URL, for example agent-device open "Expo Go" exp://127.0.0.1:8081 --platform ios. macOS also supports --surface app|frontmost-app|desktop|menubar. --metro-host/--metro-port/--bundle-url choose the Metro server of the app and save that address for later metro reload calls in the same session. A fresh open without Metro hints clears an older same-name binding. close also clears it. --launch-url gives the launch URL of the app. It does not select the reload server. On an iOS simulator without --launch-url, open connects a detected Expo development client through its expo-development-client URL. The app must declare a single exp+ scheme that no other installed app owns. If detection is inconclusive, the app opens normally. Bare React Native apps receive their server settings before launch. On an iOS simulator, --launch-url can show an "Open in ?" prompt that prevents open from seeing the app. If the URL scheme belongs to the session app, open accepts the prompt (data.launchConfirmation: "accepted"). If the scheme belongs to a different installed app, the prompt stays on the screen, and open fails with details.reason launch_confirmation_foreign_app. If no single installed app owns the scheme, the prompt stays on the screen, and open returns as usual. open reads the prompt whenever it cannot see the app afterwards. This includes a slow host where the target discovery of the app fails on its own deadline. If an accept leaves the app provably not running, open gives the same URL once more and reads again. If the app is still not running, open fails with details.reason launch_confirmation_unanswered. A recognized prompt that is still visible after a failed accept also fails with this reason, even if the app is running behind it. Answer the prompt with alert accept or alert dismiss. A simulator openurl that CoreSimulator never answers fails within 20s with details.reason ios-simulator-openurl-timeout. open recognizes only the English prompt title with Cancel and Open buttons. On a Simulator set to a different language, the prompt stays on the screen. open does not answer a prompt that appears after open has already seen the app. For that prompt, use alert accept. If another session holds the device, open refuses immediately. To block for the device up to a budget, add --wait . With --wait, open fails with DEVICE_IN_USE, which names the owning session, only after that budget.', mcpDetail: - "metroHost, metroPort, and bundleUrl choose the app's Metro server and save its address for later reloads. launchUrl selects the app's launch URL and does not select the reload server. A fresh open without Metro hints clears an older same-name binding; close also clears it.", + 'metroHost, metroPort, and bundleUrl choose the Metro server of the app and save its address for later reloads. launchUrl selects the launch URL of the app. It does not select the reload server. A fresh open without Metro hints clears an older same-name binding. close also clears it.', }, metadata: openCommandMetadata, run: (client, input) => client.apps.open(toAppOpenOptions(input)), diff --git a/src/commands/management/device.ts b/src/commands/management/device.ts index c5b48e33cc..c1e5be0df1 100644 --- a/src/commands/management/device.ts +++ b/src/commands/management/device.ts @@ -63,23 +63,23 @@ export const DEVICE_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS = { const devicesCommandMetadata = defineFieldCommandMetadata( 'devices', - 'List available devices and simulators that can be selected for automation. Use platform, device, udid, or serial inputs on later commands to target one result.', + 'List the available devices and simulators that you can select for automation. To target one result, use the platform, device, udid, or serial inputs on later commands.', {}, ); const capabilitiesCommandMetadata = defineFieldCommandMetadata( 'capabilities', - 'List the commands supported by the selected device or active session. Use device-selection inputs when checking support before a session is open.', + 'List the commands that the selected device or the active session supports. To examine support before a session is open, use the device-selection inputs.', {}, ); const bootCommandMetadata = defineFieldCommandMetadata( 'boot', - 'Boot or prepare the selected device or simulator so later commands can target it. The device is chosen through the device-selection inputs, not by naming it here.', + 'Boot or prepare the selected device or simulator, so that later commands can target it. Choose the device through the device-selection inputs. Do not give its name here.', { - headless: booleanField('Boot without showing simulator UI when supported.'), + headless: booleanField('Boot without the simulator UI when this is supported.'), timeoutMs: integerField( - 'Startup budget in milliseconds. Bounds the boot wait, so a never-booted Simulator can finish its first-boot migration or a cold emulator its boot; omit for the default startup behavior.', + 'Startup budget in milliseconds. It limits the boot wait, so that a never-booted Simulator can finish its first-boot migration, or a cold emulator can finish its boot. For the default startup behavior, omit it.', { min: 1 }, ), }, @@ -87,7 +87,7 @@ const bootCommandMetadata = defineFieldCommandMetadata( const shutdownCommandMetadata = defineFieldCommandMetadata( 'shutdown', - 'Shutdown a selected simulator or emulator.', + 'Shut down a selected simulator or emulator.', {}, ); @@ -144,7 +144,7 @@ const capabilitiesCommandFacet = defineCommandFacet({ const bootCommandFacet = defineCommandFacet({ name: 'boot', text: { - summary: 'Boot target device/simulator', + summary: 'Boot the target device or simulator', }, metadata: bootCommandMetadata, run: (client, input) => client.devices.boot(input), @@ -157,7 +157,7 @@ const bootCommandFacet = defineCommandFacet({ const shutdownCommandFacet = defineCommandFacet({ name: 'shutdown', text: { - summary: 'Shutdown target simulator/emulator', + summary: 'Shut down the target simulator or emulator', }, metadata: shutdownCommandMetadata, run: (client, input) => client.devices.shutdown(input), diff --git a/src/commands/management/doctor.ts b/src/commands/management/doctor.ts index 20a4f6c93a..97b108fee6 100644 --- a/src/commands/management/doctor.ts +++ b/src/commands/management/doctor.ts @@ -54,12 +54,14 @@ export const DOCTOR_COMMAND_OUTPUT_SCHEMAS = { const doctorCommandMetadata = defineFieldCommandMetadata( 'doctor', - 'Diagnose device, app, development-server, and React Native or Expo readiness issues. Returns compact evidence for local inventory, sessions, optional app discovery, toolchains, and server reachability.', + 'Diagnose device, app, development-server, and React Native or Expo readiness issues. It returns compact evidence for local inventory, sessions, optional app discovery, toolchains, and server reachability.', { targetApp: stringField( - 'Installed app package/bundle id or app name to verify without opening a session.', + 'Installed app package, bundle id, or app name to confirm without opening a session.', + ), + remote: booleanField( + 'Examine the remote connection setup instead of the local device inventory.', ), - remote: booleanField('Check remote connection setup instead of local device inventory.'), }, ); @@ -81,9 +83,9 @@ export const doctorCommandFacet = defineCommandFacet({ text: { summary: 'Diagnose device, app, dev-server, and RN/Expo readiness', cliDetail: - 'Metro reachability is inferred from cwd/runtime. On iOS simulators it also warms the XCTest runner build cache in the background when missing. Pass --app to verify a target app on the one matching booted device without opening a session. Use --remote to check remote connection setup without probing local devices. Default output is compact; use --json for full checks and evidence.', + 'The command infers Metro reachability from cwd and runtime. On iOS simulators, if the XCTest runner build cache is missing, the command also builds it in the background. To confirm a target app on the one matching booted device without opening a session, pass --app. To examine the remote connection setup without a probe of local devices, use --remote. The default output is compact. For full checks and evidence, use --json.', mcpDetail: - 'On iOS simulators it also warms the XCTest runner build cache in the background when missing, so run it before the first Apple snapshot or interaction of a session.', + 'On iOS simulators, if the XCTest runner build cache is missing, this command also builds it in the background. Thus, run it before the first Apple snapshot or interaction of a session.', }, metadata: doctorCommandMetadata, run: (client, input) => client.command.doctor(input), diff --git a/src/commands/management/install.ts b/src/commands/management/install.ts index 9c5b0e21a1..b163ef042c 100644 --- a/src/commands/management/install.ts +++ b/src/commands/management/install.ts @@ -26,19 +26,19 @@ import { managementCliOutputFormatters } from './output.ts'; const installCommandMetadata = defineFieldCommandMetadata( 'install', - 'Install an app binary from a local path. Provide an app identifier with the path when the target needs explicit app selection; use reinstall to replace an already installed app.', + 'Install an app binary from a local path. If the target needs explicit app selection, give an app identifier with the path. To replace an app that is already installed, use reinstall.', { app: stringField('Optional app identifier hint.'), - appPath: requiredField(stringField('Path to app binary.')), + appPath: requiredField(stringField('Path to the app binary.')), }, ); const reinstallCommandMetadata = defineFieldCommandMetadata( 'reinstall', - 'Replace an installed app with a binary from a local path. Use this when preserving the same app identity while installing a new build on the selected device.', + 'Replace an installed app with a binary from a local path. Use this command to install a new build on the selected device and keep the same app identity.', { app: requiredField(stringField()), - appPath: requiredField(stringField('Path to app binary.')), + appPath: requiredField(stringField('Path to the app binary.')), }, ); @@ -52,7 +52,7 @@ const uninstallCommandMetadata = defineFieldCommandMetadata( const installFromSourceCommandMetadata = defineFieldCommandMetadata( 'install-from-source', - 'Install app builds from URLs, remote source specs, or CI artifacts resolved by a remote daemon.', + 'Install app builds from URLs, remote source specs, or CI artifacts that a remote daemon resolves.', { source: requiredField( jsonSchemaField(looseObjectSchema('Install source object.')), diff --git a/src/commands/management/prepare.ts b/src/commands/management/prepare.ts index 478a8c0799..38897fb925 100644 --- a/src/commands/management/prepare.ts +++ b/src/commands/management/prepare.ts @@ -94,7 +94,7 @@ export const PREPARE_COMMAND_OUTPUT_SCHEMAS = { const prepareCommandMetadata = defineFieldCommandMetadata( 'prepare', - 'Prepare platform helper infrastructure. ios-runner builds/reuses, starts, and health-checks the XCTest runner so later Apple snapshots and interactions do not pay first-use startup cost. In JSON output, top-level buildMs/connectMs/healthCheckMs are diagnostic fields and may overlap; use timing.additiveParts for additive wall-clock phase totals. In CI, run it after boot/install and before replay/test; if replay/test starts a separate daemon, stop the prepare daemon before replay/test so it does not keep the prepared runner lease. It is not a recovery step for "runner already owned by another agent-device daemon"; stop the owning daemon on the Mac with simulator access instead. Runner build/start output is written to the session runner.log; daemon.log is for daemon lifecycle/startup issues.', + 'Prepare platform helper infrastructure. ios-runner builds/reuses, starts, and health-checks the XCTest runner, so later Apple snapshots and interactions do not pay the first-use startup cost. In JSON output, top-level buildMs/connectMs/healthCheckMs are diagnostic fields and may overlap. For additive wall-clock phase totals, use timing.additiveParts. In CI, run it after boot/install and before replay/test. If replay/test starts a separate daemon, stop the prepare daemon before replay/test, so that it does not keep the prepared runner lease. It is not a recovery step for "runner already owned by another agent-device daemon". For that error, stop the owning daemon on the Mac that has simulator access. Runner build/start output is written to the session runner.log. daemon.log is for daemon lifecycle and startup issues.', { action: requiredField(enumField(PREPARE_ACTION_VALUES)), timeoutMs: integerField('Maximum wall-clock time for the prepare command.'), @@ -121,7 +121,7 @@ const prepareDaemonWriter: DaemonWriter = direct(PUBLIC_COMMANDS.prepare, (input export const prepareCommandFacet = defineCommandFacet({ name: 'prepare', text: { - summary: 'Pre-warm platform helpers before automation', + summary: 'Prepare platform helpers before automation', }, metadata: prepareCommandMetadata, run: (client, input) => client.command.prepare(input), diff --git a/src/commands/management/push.ts b/src/commands/management/push.ts index 4bcda94fd8..66d0c472c5 100644 --- a/src/commands/management/push.ts +++ b/src/commands/management/push.ts @@ -85,13 +85,13 @@ const pushCommandMetadata = defineFieldCommandMetadata( const triggerAppEventCommandMetadata = defineFieldCommandMetadata( 'trigger-app-event', - 'Ask the app to handle an app-defined automation or test event, with an optional structured payload. Call this only for event names and payload shapes the app documents.', + 'Ask the app to handle an app-defined automation or test event, with an optional structured payload. Use this command only for event names and payload shapes that the app documents.', { event: requiredField( - stringField('Name of an app-defined automation or test event the app documents.'), + stringField('Name of an app-defined automation or test event that the app documents.'), ), payload: jsonObjectField( - 'Structured payload passed to the event, in the shape the app documents for it.', + 'Structured payload for the event, in the shape that the app documents for it.', ), }, ); diff --git a/src/commands/management/session.ts b/src/commands/management/session.ts index 40011ffdc7..1dfef4bb29 100644 --- a/src/commands/management/session.ts +++ b/src/commands/management/session.ts @@ -9,11 +9,11 @@ import { managementCliOutputFormatters } from './output.ts'; const sessionCommandMetadata = defineFieldCommandMetadata( 'session', - 'List active sessions, print the effective daemon state directory, or publish an armed open-to-destination script without closing its session', + 'List active sessions, print the effective daemon state directory, or publish an armed open-to-destination script without closing its session.', { action: enumField( ['list', 'state-dir', 'save-script'], - 'list shows active sessions; state-dir prints the daemon state directory; save-script publishes an armed recording without teardown.', + 'list returns the active sessions. state-dir prints the daemon state directory. save-script publishes an armed recording without teardown.', ), path: stringField('Optional .ad output path for save-script.'), force: booleanField('Atomically replace an existing save-script target.'), @@ -38,7 +38,7 @@ const sessionCliReader: CliReader = (positionals, flags) => ({ export const sessionCommandFacet = defineCommandFacet({ name: 'session', text: { - summary: 'List sessions, show the state dir, or publish a script', + summary: 'List sessions, print the state dir, or publish a script', }, metadata: sessionCommandMetadata, run: async (client, { action, path, force, ...input }) => { diff --git a/src/commands/management/viewport.ts b/src/commands/management/viewport.ts index 614a76d5ab..87e4d18f82 100644 --- a/src/commands/management/viewport.ts +++ b/src/commands/management/viewport.ts @@ -34,7 +34,7 @@ export const VIEWPORT_COMMAND_OUTPUT_SCHEMAS = { const viewportCommandMetadata = defineFieldCommandMetadata( 'viewport', - 'Resize the active web viewport before taking snapshots or screenshots. Useful for fixed-layout or 100vh apps where changing the viewport reveals different content.', + 'Resize the active web viewport before you capture snapshots or screenshots. This is useful for fixed-layout or 100vh apps, where a different viewport shows different content.', { width: requiredField(integerField('Viewport width in CSS pixels.', { min: 1 })), height: requiredField(integerField('Viewport height in CSS pixels.', { min: 1 })), diff --git a/src/commands/metro/index.ts b/src/commands/metro/index.ts index 84ae09cf0d..f99d9754cf 100644 --- a/src/commands/metro/index.ts +++ b/src/commands/metro/index.ts @@ -30,7 +30,7 @@ const METRO_COMMAND_NAME = 'metro'; const METRO_ACTION_VALUES = ['prepare', 'reload'] as const; const metroCommandDescription = - 'Prepare a React Native development server or ask connected apps to reload, using the development server this session is bound to. Provide explicit runtime inputs only to override that binding for one call.'; + 'Prepare a React Native development server, or ask connected apps to reload. The command uses the development server that this session is bound to. Give explicit runtime inputs only to override that binding for one call.'; export const metroCommandMetadata = defineFieldCommandMetadata( METRO_COMMAND_NAME, @@ -132,9 +132,9 @@ export const metroCommandFacet = defineCommandFacet({ text: { summary: 'Prepare the dev server or reload apps', cliDetail: - "reload with no --metro-host/--metro-port/--bundle-url resolves against the dev server this session last bound via metro prepare or open's metro hint flags (falling back to localhost:8081 only when the session never bound one), so it never silently reloads a different project's server on the default port; pass an explicit flag to override the session hint for one call. After prepare, reload uses the local server address independently of the public or proxy address given to the device; a fresh open preserves it only when its client-config runtime points to the prepared device server, while explicit reload flags override it. For open or an explicit bundle URL, reload removes a .bundle/.jsbundle/.js entry filename and keeps the remaining path as the server mount, and when the server has no HTTP /reload route (Expo) the reload is broadcast over its /message websocket instead of trusting the app-page fallback. The binding is cleared when the session closes, and a fresh open without hint flags also clears any leftover binding from a previous same-name session. --kind expo (detected or forced) requests the virtual-entry bundle URL (.expo/.virtual-metro-entry.bundle) instead of index.bundle, since index.bundle 404s/500s against Expo dev servers in monorepos. Dependency install auto-detects the package manager from the nearest yarn.lock/pnpm-lock.yaml/bun.lock/bun.lockb/package-lock.json walking up from --project-root (bounded at the repo root), so Yarn/pnpm workspace monorepos with the lockfile at the repo root do not wrongly fall back to npm install (which fails on workspace: dependency specifiers); if install still fails, pass --no-install-deps when dependencies are already installed (for example via a monorepo root install).", + 'Without --metro-host/--metro-port/--bundle-url, reload resolves against the dev server that this session last bound through metro prepare or the metro hint flags of open. Thus, reload never silently reloads the server of a different project on the default port. Without explicit flags or a session binding, reload falls back to localhost:8081. To override the session hint for one call, pass an explicit flag. After prepare, reload uses the local server address, independently of the public or proxy address that the device received. A fresh open keeps that address only when its client-config runtime points to the prepared device server. Explicit reload flags override it. For open or an explicit bundle URL, reload removes a .bundle/.jsbundle/.js entry filename and keeps the remaining path as the server mount. When the server has no HTTP /reload route (Expo), reload broadcasts the reload over its /message websocket, and does not trust the app-page fallback. The binding is cleared when the session closes. A fresh open without hint flags also clears any leftover binding from a previous same-name session. --kind expo (detected or forced) requests the virtual-entry bundle URL (.expo/.virtual-metro-entry.bundle) instead of index.bundle, because index.bundle returns 404 or 500 from Expo dev servers in monorepos. The dependency install detects the package manager from the nearest yarn.lock/pnpm-lock.yaml/bun.lock/bun.lockb/package-lock.json. It searches up from --project-root and stops at the repo root. Thus, Yarn or pnpm workspace monorepos with the lockfile at the repo root do not incorrectly use npm install, which fails on workspace: dependency specifiers. If the install still fails and the dependencies are already installed (for example, through a monorepo root install), pass --no-install-deps.', mcpDetail: - 'The binding is cleared when the session closes, and a fresh open without runtime hints also clears any leftover binding from a previous same-name session.', + 'The binding is cleared when the session closes. A fresh open without runtime hints also clears any leftover binding from a previous same-name session.', }, metadata: metroCommandMetadata, run: async (client, input): Promise => diff --git a/src/commands/observability/index.ts b/src/commands/observability/index.ts index cd34adf6b0..66a12d3c95 100644 --- a/src/commands/observability/index.ts +++ b/src/commands/observability/index.ts @@ -32,12 +32,13 @@ const AUDIO_ACTION_VALUES = ['probe'] as const; const AUDIO_PROBE_ACTION_VALUES = ['start', 'status', 'stop'] as const; const logsCommandDescription = - 'Session app log info, start/stop streaming, diagnostics, and markers'; + 'Get session app log information, start or stop the log stream, run log diagnostics, and add markers.'; const eventsCommandDescription = - 'Read the daemon-owned session event timeline as paged JSON-friendly entries'; -const networkCommandDescription = 'Dump recent HTTP(s) traffic parsed from the session app log'; + 'Read the daemon-owned session event timeline as paged JSON-friendly entries.'; +const networkCommandDescription = + 'Dump recent HTTP(S) traffic that the command parses from the session app log.'; const audioCommandDescription = - 'Measure browser or host-rendered simulator/emulator audio as compact dBFS buckets. Start a probe before requesting its status or stopping it.'; + 'Measure browser audio, or host-rendered simulator or emulator audio, as compact dBFS buckets. Start a probe before you request its status or stop it.'; export const logsCommandMetadata = defineFieldCommandMetadata( LOGS_COMMAND_NAME, @@ -183,7 +184,7 @@ export const eventsCommandFacet = defineCommandFacet({ export const networkCommandFacet = defineCommandFacet({ name: NETWORK_COMMAND_NAME, text: { - summary: 'Inspect HTTP(S) traffic from session logs', + summary: 'Examine HTTP(S) traffic from session logs', }, metadata: networkCommandMetadata, run: (client, input) => client.observability.network(input), diff --git a/src/commands/perf/index.ts b/src/commands/perf/index.ts index fe46fdceb8..049701048a 100644 --- a/src/commands/perf/index.ts +++ b/src/commands/perf/index.ts @@ -43,7 +43,9 @@ export const perfCommandMetadata = defineFieldCommandMetadata( kind: enumField(PERF_KIND_VALUES), template: stringField('xctrace template name, for example Time Profiler.'), out: stringField('Output artifact path.'), - tracePath: stringField('Existing .trace path to report, defaults to the latest session trace.'), + tracePath: stringField( + 'Existing .trace path to report. The default is the latest session trace.', + ), }, ); @@ -72,11 +74,11 @@ export const perfDaemonWriter: DaemonWriter = direct(PERF_COMMAND_NAME, (input) export const perfCommandFacet = defineCommandFacet({ name: PERF_COMMAND_NAME, text: { - summary: 'Check frames, memory, or native profiles', + summary: 'Examine frames, memory, or native profiles', cliDetail: - 'Use perf frames for bounded frame-health evidence and perf memory sample for a compact process-memory reading. On iOS simulators and macOS, process sampling and captures target the resolved app executable and exclude other copies with the same name. Apple xctrace and Android Simpleperf/Perfetto captures keep raw artifacts on disk; report produces bounded agent-readable evidence. For React render performance, run agent-device react-devtools profile start before the interaction and react-devtools profile stop after it.', + 'For bounded frame-health evidence, use perf frames. For a compact process-memory reading, use perf memory sample. On iOS simulators and macOS, process samples and captures target the resolved app executable, and exclude other copies with the same name. Apple xctrace and Android Simpleperf/Perfetto captures keep raw artifacts on disk. report produces bounded agent-readable evidence. For React render performance, run agent-device react-devtools profile start before the interaction and react-devtools profile stop after it.', mcpDetail: - 'For CPU profiles, start and stop write the raw artifact while report writes a compact summary; request the report when the task needs readable native CPU evidence. Profiling output is evidence only: compact state, artifact path, and size.', + 'For CPU profiles, start and stop write the raw artifact, and report writes a compact summary. When the task needs readable native CPU evidence, request the report. Profile output is evidence only: compact state, artifact path, and size.', }, metadata: perfCommandMetadata, run: (client, input) => client.observability.perf(input), diff --git a/src/commands/post-action-observation-grammar.ts b/src/commands/post-action-observation-grammar.ts index 892cabed07..4b4d78e25f 100644 --- a/src/commands/post-action-observation-grammar.ts +++ b/src/commands/post-action-observation-grammar.ts @@ -26,18 +26,18 @@ import { booleanField, integerField } from './command-input.ts'; const verifyField = () => booleanField( - 'Capture cheap post-action evidence (AX digest, node counts, changedFromBefore) instead of a follow-up snapshot.', + 'Capture low-cost post-action evidence (AX digest, node counts, changedFromBefore) instead of a follow-up snapshot.', ); const settleFields = () => ({ settle: booleanField( - 'After the action, wait for the UI to go quiet and return the settled diff vs the pre-action tree in the same response. Best-effort; never fails the action.', + 'After the action, wait until the UI is quiet. Then return the settled diff against the pre-action tree in the same response. This is best-effort and never fails the action.', ), settleQuietMs: integerField( - 'Settle: quiet window in milliseconds (default 500; 300 when the settled tree comes from the iOS simulator snapshot bridge). On iOS, a broad screen replacement such as a modal dismissal is confirmed for at least 1500ms whatever window is asked for, within the timeoutMs settle deadline; a replacement from a screen the pre-action capture read as presenting nothing, such as a tab switch, gets no 1500ms confirmation and keeps the asked window.', + 'settle: quiet window in milliseconds. The default is 500. When the settled tree comes from the iOS simulator snapshot bridge, the default is 300. On iOS, a broad screen replacement, such as a modal dismissal, gets a confirmation of at least 1500ms for any requested window. This confirmation stays within the timeoutMs settle deadline. On iOS, a replacement can start from a screen that the pre-action capture read as presenting nothing, such as a tab switch. Such a replacement gets no 1500ms confirmation and keeps the requested window.', { min: 0 }, ), - timeoutMs: integerField('Settle: wait deadline in milliseconds (default 10000).', { min: 1 }), + timeoutMs: integerField('settle: wait deadline in milliseconds (default: 10000).', { min: 1 }), }); type VerifyFieldMap = { verify: ReturnType }; diff --git a/src/commands/react-native/index.ts b/src/commands/react-native/index.ts index 6f28c2351e..cd5b3f907e 100644 --- a/src/commands/react-native/index.ts +++ b/src/commands/react-native/index.ts @@ -10,7 +10,7 @@ const REACT_NATIVE_COMMAND_NAME = 'react-native'; const REACT_NATIVE_ACTION_VALUES = ['dismiss-overlay'] as const; const reactNativeCommandDescription = - 'Run supported React Native automation helpers. Use dismiss-overlay to close a visible development error overlay before continuing normal UI automation.'; + 'Run supported React Native automation helpers. Before you continue normal UI automation, use dismiss-overlay to close a visible development error overlay.'; export const reactNativeCommandMetadata = defineFieldCommandMetadata( REACT_NATIVE_COMMAND_NAME, diff --git a/src/commands/recording/index.ts b/src/commands/recording/index.ts index ca7a51535e..8640796893 100644 --- a/src/commands/recording/index.ts +++ b/src/commands/recording/index.ts @@ -29,9 +29,9 @@ const TRACE_COMMAND_NAME = 'trace'; const RECORDING_ACTION_VALUES = ['start', 'stop'] as const; const recordCommandDescription = - 'Start or stop a screen recording for the active app session or, where supported, the selected device, or build a contact-sheet PNG from a recording already exported. Long Android recordings can return multiple video artifacts; HarmonyOS supports whole-screen recording on physical devices.'; + 'Start or stop a screen recording for the active app session or, where supported, for the selected device. Alternatively, build a contact-sheet PNG from a recording that was already exported. Long Android recordings can return multiple video artifacts. HarmonyOS supports whole-screen recording on physical devices.'; const traceCommandDescription = - 'Start or stop trace-log capture and return the resulting artifact when capture ends. Without a path, start writes a timestamped trace log named after the session in the sessions directory of the daemon state; a path on stop moves the finished trace there.'; + 'Start or stop trace-log capture, and return the resulting artifact when the capture ends. Without a path, start writes a timestamped trace log, named after the session, in the sessions directory of the daemon state. A path on stop moves the finished trace to that path.'; // `record stop` prints the artifact path, which callers capture into a variable. const recordCommandOptions = { parseableOutput: true } as const; @@ -128,7 +128,7 @@ export const recordCommandFacet = defineCommandFacet({ text: { summary: 'Start or stop screen recording', cliDetail: - 'The default --scope app requires an active app session from open ; use --scope device/system to explicitly request whole-screen recording where the selected backend supports it. Android record start publishes a durable device manifest, recordings longer than the 180s adb screenrecord limit are returned as multiple MP4 chunks while the daemon stays alive, and daemon-restart recovery uses only manifest-owned chunks. Android screenrecord encodes a frame only when the screen changes, so a clip can end at the last frame the recorder encoded instead of at record stop; durationMs is host wall clock from record start until the export finished, and capturedDurationMs reports the video timeline when it can be measured, with a warning naming how much of the window that video covers. An Android manifest left by an unreachable recording is retired on the next start once its recorders are proven gone; one still owned refuses with non-retriable DEVICE_IN_USE and reason native_recovery_evidence_open naming the session to run record stop for, and a recorder that is still writing an artifact refuses with DEVICE_IN_USE and reason native_recording_artifact_claimed, which clears itself once that recorder ends at the 180s limit. HarmonyOS supports whole-screen recording on physical devices only: use --scope device/system; --fps, --quality, and --hide-touches are unsupported. Use --quality to choose medium or high export quality on supported backends. The touch overlay renders at most 30 frames a second, or at --fps when lower; a lower --fps makes record stop faster on a long recording, and an overlay export that runs past its budget returns the raw video with an overlayWarning. An iOS simulator host recording lock returns non-retriable DEVICE_IN_USE with reason apple_simulator_recording_busy. Stop the recording in its owning session; if a dead recorder left the host locked, ask the host operator to restart the CoreSimulator stream service. A record stop that cannot produce its export keeps what a retry reads back — on Android the device-side artifact and the native manifest — leaves the recording manifest of that session open, and returns its own error, so retry record stop in that session; only closing the session disposes them. Every stopped recording also reports what became of its recorder and of the file that recorder writes to: recorder is confirmed, or lost when the session holding it died, and nativePathDisposition is retirable while that file still owes a removal or retired once its removal was verified. Both are optional disclosures, not failures; ADR 0024 declares further states (unconfirmed, pending) for the steps that gain those probes, and no stop reports them yet. record contact-sheet [--out ] is a local report over a file you already exported: it decodes frames out of that MP4 and writes one PNG holding the cells where the screen visibly changed, each labeled with its elapsed time on the clip timeline. It needs no session and no device, rebuilds any past recording, and works for every backend that exports MP4; a WebM recording is refused with reason contact_sheet_container_unsupported. The sample grid is bounded and spread across the whole clip, so it reports coverage rather than a review: a flash that opens and closes entirely between two sample times is not in the sheet, which is why the result also reports how many times it sampled and how many frames came back. Frame decoding is Apple AVFoundation tooling, so the command runs only on macOS hosts and refuses elsewhere with reason contact_sheet_unsupported_host.', + 'The default --scope app requires an active app session from open . To request whole-screen recording explicitly where the selected backend supports it, use --scope device/system. Android record start publishes a durable device manifest. While the daemon stays alive, recordings longer than the 180s adb screenrecord limit are returned as multiple MP4 chunks. Daemon-restart recovery uses only manifest-owned chunks. Android screenrecord encodes a frame only when the screen changes, so a clip can end at the last frame that the recorder encoded, not at record stop. durationMs is the host wall clock from record start until the export finished. When the video timeline can be measured, capturedDurationMs reports it, with a warning that tells how much of the window that video covers. An Android manifest from an unreachable recording is retired on the next start, after its recorders are proven gone. If the manifest is still owned, the start refuses with non-retriable DEVICE_IN_USE and reason native_recovery_evidence_open, which names the session in which to run record stop. If a recorder is still writing an artifact, the start refuses with DEVICE_IN_USE and reason native_recording_artifact_claimed. This refusal clears itself when that recorder ends at the 180s limit. HarmonyOS supports whole-screen recording on physical devices only: use --scope device/system. On HarmonyOS, --fps, --quality, and --hide-touches are unsupported. To choose medium or high export quality on supported backends, use --quality. The touch overlay renders at most 30 frames a second, or at --fps when that is lower. A lower --fps makes record stop faster on a long recording. An overlay export that runs past its budget returns the raw video with an overlayWarning. An iOS simulator host recording lock returns non-retriable DEVICE_IN_USE with reason apple_simulator_recording_busy. Stop the recording in its owning session. If a dead recorder left the host locked, ask the host operator to restart the CoreSimulator stream service. A record stop that cannot produce its export keeps what a retry reads back: on Android, the device-side artifact and the native manifest. It leaves the recording manifest of that session open and returns its own error. Thus, retry record stop in that session. Only a close of the session disposes of what the stop kept. Every stopped recording also reports what became of its recorder and of the file that the recorder writes to. recorder is confirmed, or lost when the session that held it died. nativePathDisposition is retirable while that file still needs a removal, or retired after its removal was verified. Both are optional disclosures, not failures. ADR 0024 declares more states (unconfirmed, pending) for the steps that gain those probes, and no stop reports them yet. record contact-sheet [--out ] is a local report about a file that you already exported. It decodes frames from that MP4 and writes one PNG with the cells where the screen visibly changed. Each cell has a label with its elapsed time on the clip timeline. It needs no session and no device, rebuilds any past recording, and works for every backend that exports MP4. A WebM recording is refused with reason contact_sheet_container_unsupported. The sample grid is bounded and spread across the whole clip, so it reports coverage, not a review. A flash that opens and closes fully between two sample times is not in the sheet. For this reason, the result also reports how many times it sampled and how many frames came back. Frame decoding uses Apple AVFoundation tooling, so the command runs only on macOS hosts. On other hosts, it refuses with reason contact_sheet_unsupported_host.', }, metadata: recordCommandMetadata, run: (client, input) => client.recording.record(input as RecordOptions), diff --git a/src/commands/replay/index.ts b/src/commands/replay/index.ts index 28e13047f8..fef590f86f 100644 --- a/src/commands/replay/index.ts +++ b/src/commands/replay/index.ts @@ -41,8 +41,8 @@ const TEST_COMMAND_NAME = 'test'; const REPLAY_SHELL_ENV_PREFIX = 'AD_VAR_'; const replayCommandDescription = - 'Run a recorded automation script, including compatible Maestro YAML flows. A script without a terminal close leaves its session active for subsequent automation.'; -const testCommandDescription = 'Run one or more replay scripts as a serial test suite'; + 'Run a recorded automation script, including compatible Maestro YAML flows. A script without a terminal close leaves its session active for later automation.'; +const testCommandDescription = 'Run one or more replay scripts as a serial test suite.'; export const replayCommandMetadata = defineFieldCommandMetadata( REPLAY_COMMAND_NAME, @@ -53,9 +53,9 @@ export const replayCommandMetadata = defineFieldCommandMetadata( backend: stringField(), maestro: booleanField(), env: stringArrayField(), - metroHost: stringField('Metro/debug host hint inherited by replay-opened sessions.'), - metroPort: integerField('Metro/debug port hint inherited by replay-opened sessions.'), - bundleUrl: stringField('Bundle URL hint inherited by replay-opened sessions.'), + metroHost: stringField('Metro or debug host hint that replay-opened sessions inherit.'), + metroPort: integerField('Metro or debug port hint that replay-opened sessions inherit.'), + bundleUrl: stringField('Bundle URL hint that replay-opened sessions inherit.'), // ADR 0012 decision 4 / migration step 5: replay-only resume. Named // `resumeFrom`/`resumePlanDigest` (not `from`/`planDigest`) because // `from` already means a gesture's `PointInput` on `CommandInput` @@ -64,7 +64,7 @@ export const replayCommandMetadata = defineFieldCommandMetadata( resumeFrom: integerField(), resumePlanDigest: stringField(), keepSession: booleanField( - 'Leave the session active by suppressing exactly an authored terminal close in native .ad.', + 'Keep the session active. This suppresses exactly an authored terminal close in native .ad.', ), timeoutMs: integerField('Maximum wall-clock duration for the replay request.'), // ADR 0012 decision 6, R1/R6: arms agent-supervised re-record repair @@ -77,7 +77,7 @@ export const replayCommandMetadata = defineFieldCommandMetadata( // publish) instead of refusing. Alias: --overwrite. force: booleanField(), testIme: booleanField( - 'Activate the headless Android test IME for the sessions this replay opens (default on for emulators; opt-in on real devices).', + 'Activate the headless Android test IME for the sessions that this replay opens (default: on for emulators, opt-in on real devices).', ), }, ); @@ -91,9 +91,9 @@ export const testCommandMetadata = defineFieldCommandMetadata( backend: stringField(), maestro: booleanField(), env: stringArrayField(), - metroHost: stringField('Metro/debug host hint inherited by each test session.'), - metroPort: integerField('Metro/debug port hint inherited by each test session.'), - bundleUrl: stringField('Bundle URL hint inherited by each test session.'), + metroHost: stringField('Metro or debug host hint that each test session inherits.'), + metroPort: integerField('Metro or debug port hint that each test session inherits.'), + bundleUrl: stringField('Bundle URL hint that each test session inherits.'), failFast: booleanField(), timeoutMs: integerField(), retries: integerField(), @@ -102,7 +102,7 @@ export const testCommandMetadata = defineFieldCommandMetadata( shardAll: integerField(), shardSplit: integerField(), testIme: booleanField( - 'Activate the headless Android test IME for the sessions each suite attempt opens (default on for emulators; opt-in on real devices).', + 'Activate the headless Android test IME for the sessions that each suite attempt opens (default: on for emulators, opt-in on real devices).', ), }, ); @@ -178,7 +178,7 @@ const replayCliSchema = { // commits on that transaction's own teardown, not on a plain close. flagDescriptionOverrides: { saveScript: - 'Arm a repair transaction from this replay (ADR 0012): recording starts at step 1, and the healed script commits when the repair-armed session tears down (close, close --save-script, or idle-reap). Independent of the open/close authoring arm-on-open. Optional custom output path.', + 'Arm a repair transaction from this replay (ADR 0012). The recording starts at step 1. The healed script commits when the repair-armed session tears down (close, close --save-script, or idle-reap). This is independent of the arm-on-open of open/close authoring. The optional value is a custom output path.', }, } as const satisfies CommandSchemaOverride; @@ -297,7 +297,7 @@ export const replayCommandFacet = defineCommandFacet({ text: { summary: 'Replay a recorded session or Maestro flow', cliDetail: - 'For Maestro YAML compatibility flows, use replay --maestro and keep the target binding such as --platform ios on the replay command. A script with no terminal close leaves its session (and daemon) running until you close it or it idle-reaps — no different from a session opened interactively. For native .ad scripts, --keep-session suppresses exactly an authored terminal close so you can continue interactively. replay export converts compatible actions to Maestro YAML locally, including app switches with explicit launchApp.appId targets, deep links (including tel: and mailto:) as openLink, and home as pressKey: Home.', + 'For Maestro YAML compatibility flows, use replay --maestro, and keep the target binding, such as --platform ios, on the replay command. A script with no terminal close leaves its session (and daemon) running until you close it or it idle-reaps. This is the same as for a session that you opened interactively. For native .ad scripts, --keep-session suppresses exactly an authored terminal close, so that you can continue interactively. replay export converts compatible actions to Maestro YAML locally. This includes app switches with explicit launchApp.appId targets, deep links (including tel: and mailto:) as openLink, and home as pressKey: Home.', }, metadata: replayCommandMetadata, run: (client, input) => client.replay.run(withCommandRuntimeHints(input)), @@ -315,7 +315,7 @@ export const testCommandFacet = defineCommandFacet({ text: { summary: 'Run replay test suites', cliDetail: - "Relative globs are expanded on the caller from its working directory, whose name is treated literally. Quote glob inputs to defer expansion to test. Copied diagnostic artifacts receive numbered filenames when needed to preserve other artifacts, replay sources, timing traces, and attempt manifests. JUnit reports (--reporter junit:) replace characters forbidden by XML 1.0 with U+FFFD and preserve legal Unicode and whitespace. JSON and other reporters retain the original suite values. Custom reporter getExitCode hooks must return an integer from 0 to 255 or undefined; the highest valid code wins and cannot lower a failing suite's exit code.", + 'The caller expands relative globs from its working directory, and treats the directory name literally. To let test expand a glob, quote the glob input. When necessary, copied diagnostic artifacts receive numbered filenames, so that other artifacts, replay sources, timing traces, and attempt manifests are kept. JUnit reports (--reporter junit:) replace characters that XML 1.0 forbids with U+FFFD, and keep legal Unicode and whitespace. JSON and other reporters keep the original suite values. Custom reporter getExitCode hooks must return an integer from 0 to 255, or undefined. The highest valid code wins, and it cannot lower the exit code of a failed suite.', }, metadata: testCommandMetadata, run: (client, input) => client.replay.test(withCommandRuntimeHints(input)), diff --git a/src/commands/schema/cli-help-command-usage.test.ts b/src/commands/schema/cli-help-command-usage.test.ts index 7590c8dd78..a1496d9e31 100644 --- a/src/commands/schema/cli-help-command-usage.test.ts +++ b/src/commands/schema/cli-help-command-usage.test.ts @@ -7,8 +7,8 @@ test('usageForCommand documents open --launch-args', async () => { const help = await usageForCommand('open'); if (help === null) throw new Error('Expected open help text'); assert.match(help, /--launch-args /); - assert.match(help, /forwarded verbatim/); - assert.match(help, /Linux and macOS reject the flag/); + assert.match(help, /forwards verbatim/); + assert.match(help, /Linux and macOS refuse the flag/); assert.match(help, /--launch-console artifacts\/launch-console\.log/); }); @@ -35,7 +35,10 @@ test('usageForCommand documents screenshot web aliases and stabilization flags', test('usageForCommand documents screenshot diff normalization', async () => { const help = await usageForCommand('diff'); if (help === null) throw new Error('Expected diff help text'); - assert.match(help, /Live iOS simulator screenshot diffs normalize status-bar chrome by default/); + assert.match( + help, + /Live iOS simulator screenshot diffs normalize the status-bar chrome by default/, + ); assert.match(help, /screenshot --normalize-status-bar/); }); @@ -43,8 +46,8 @@ test('usageForCommand documents the screenshot diff input containers', async () const help = await usageForCommand('diff'); if (help === null) throw new Error('Expected diff help text'); assert.match(help, /a baseline or current image may be PNG or JPEG/); - assert.match(help, /the diff image is always PNG/); - assert.match(help, /keep --threshold above 0 whenever either input is JPEG/); + assert.match(help, /The diff image is always PNG/); + assert.match(help, /if either input is JPEG, keep --threshold above 0/); }); test('usageForCommand resolves longpress help', async () => { @@ -79,7 +82,10 @@ test('usageForCommand documents keyboard dismissal flow', async () => { const help = await usageForCommand('keyboard'); assert.equal(help === null, false); assert.match(help ?? '', /To hide the keyboard, use keyboard dismiss/); - assert.match(help ?? '', /taps the keyboard dismiss\/hide key when one is exposed/); + assert.match( + help ?? '', + /When the keyboard exposes a dismiss or hide key, keyboard dismiss taps it/, + ); assert.match(help ?? '', /UNSUPPORTED_OPERATION/); }); @@ -100,7 +106,7 @@ test('usageForCommand includes Maestro replay flag', async () => { assert.match(help, /--out /); assert.match(help, /--maestro/); assert.match(help, /supported Maestro YAML subset/); - assert.match(help, /unsupported syntax fails loudly/); + assert.match(help, /Unsupported syntax fails with an explicit error/); assert.match(help, /agent-device help maestro/); assert.doesNotMatch(help, /issues\/558/); }); @@ -112,22 +118,25 @@ test('usageForCommand includes Maestro test suite flag', async () => { assert.match(help, /--maestro/); assert.match(help, /--record-video/); assert.match(help, /--shard-all /); - assert.match(help, /combine with --device id1,id2/); + assert.match(help, /To use explicit connected devices, add --device id1,id2/); assert.match(help, /--shard-split /); assert.match(help, /AD_SHARD_INDEX is zero-based/); - assert.match(help, /Replay\/Test: inject or override/); + assert.match(help, /replay, test: set or override/); }); test('command help keeps scroll and gesture planning guidance', async () => { const scrollHelp = await usageForCommand('scroll'); if (scrollHelp === null) throw new Error('Expected scroll help text'); assert.match(scrollHelp, /Scroll in a direction/); - assert.match(scrollHelp, /top\/bottom edge/); + assert.match(scrollHelp, /top or bottom edge/); assert.match(scrollHelp, /finger-path fraction of the viewport axis/); const gestureHelp = await usageForCommand('gesture'); if (gestureHelp === null) throw new Error('Expected gesture help text'); - assert.match(gestureHelp, /Android transform verification should use all app-observable effects/); + assert.match( + gestureHelp, + /To verify an Android transform, you should use all app-observable effects/, + ); assert.match(gestureHelp, /wait text "pan changed yes"/); }); @@ -171,7 +180,10 @@ test('debug command help stays scoped to symbolication', async () => { if (help === null) throw new Error('Expected debug help text'); assert.match(help, /debug symbols --artifact/); assert.match(help, /intentionally narrow/); - assert.match(help, /use logs for app logs, network for HTTP evidence, perf for performance/); + assert.match( + help, + /For app logs, use logs\. For HTTP evidence, use network\. For performance samples, use perf/, + ); assert.doesNotMatch(help, /agent-device debug perf/); assert.doesNotMatch(help, /agent-device debug logs/); }); @@ -181,8 +193,8 @@ test('proxy command help describes tunnel usage', async () => { if (help === null) throw new Error('Expected command help text'); assert.match(help, /Usage:\s+agent-device proxy/); assert.match(help, /cloudflared tunnel --url http:\/\/127\.0\.0\.1:4310/); - assert.match(help, /--host \s+Proxy: host interface to bind/); - assert.match(help, /--port \s+Proxy: TCP port to bind/); + assert.match(help, /--host \s+proxy: host interface to bind/); + assert.match(help, /--port \s+proxy: TCP port to bind/); assert.match(help, /--daemon-auth-token \s+Remote HTTP daemon or proxy auth token/); assert.match(help, /--state-dir \s+Daemon state directory/); assert.match(help, /\/agent-device\/\*/); @@ -199,7 +211,7 @@ test('takeover command help documents lease-bound foreground and host API flows' assert.match(help, /until Ctrl\+C/); assert.match(help, /\/admin\/human-control\/holds/); assert.match(help, /active remote lease device/); - assert.match(help, /do not survive daemon restart/); + assert.match(help, /Holds are lost when the daemon restarts/); }); test('connect command help lists lease id in usage and flags', async () => { @@ -281,15 +293,15 @@ test('snapshot command usage documents diff alias', async () => { help, /Capture the accessibility tree or compare it with the previous session baseline/, ); - assert.match(help, /inspect rects with snapshot -i --json/); - assert.match(help, /verify with diff snapshot -i or snapshot --diff/); + assert.match(help, /examine the rects with snapshot -i --json/); + assert.match(help, /Then confirm with diff snapshot -i or snapshot --diff/); }); test('snapshot documents the synopsis-hidden record flag', async () => { const help = await usageForCommand('snapshot'); if (help === null) throw new Error('Expected command help text'); assert.doesNotMatch(help, /agent-device snapshot \[[^\n]*--record/); - assert.match(help, /--record\s+Force-record this action/); + assert.match(help, /--record\s+Always record this action/); }); test('network command usage documents include flag', async () => { @@ -301,7 +313,7 @@ test('network command usage documents include flag', async () => { test('command usage shows command flags without global flags', async () => { const help = await usageForCommand('swipe'); if (help === null) throw new Error('Expected command help text'); - assert.match(help, /Quick coordinate fling with optional repeat pattern/); + assert.match(help, /Do a quick fling between coordinates, with an optional repeat pattern/); assert.doesNotMatch(help, /duration positional is accepted as a deprecated alias to pan/); assert.match(help, /Command flags:/); assert.match(help, /--pattern one-way\|ping-pong/); @@ -325,7 +337,7 @@ test('open command usage documents surface and console log flags', async () => { assert.match(help, /macOS also supports --surface/); assert.match(help, /--launch-console /); assert.match(help, /iOS simulator launch console/); - assert.match(help, /Use --platform to bind URL\/deep-link opens/); + assert.match(help, /To bind URL or deep-link opens to the target platform, use --platform/); assert.match(help, /agent-device open "Expo Go" exp:\/\/127\.0\.0\.1:8081 --platform ios/); }); @@ -348,14 +360,14 @@ test('command usage shows record touch-overlay opt-out flag', async () => { assert.doesNotMatch(help, /--max-size/); assert.match(help, /--quality /); assert.match(help, /--hide-touches/); - assert.match(help, /skip touch-overlay post-processing/); + assert.match(help, /skip the touch-overlay post-processing/); assert.match(help, /multiple MP4 chunks/); }); test('command usage keeps detailed descriptions', async () => { const help = await usageForCommand('metro'); if (help === null) throw new Error('Expected command help text'); - assert.match(help, /Prepare a React Native development server or ask connected apps to reload/); + assert.match(help, /Prepare a React Native development server, or ask connected apps to reload/); assert.match(help, /metro reload/); assert.match(help, /--metro-host /); assert.match(help, /AGENT_DEVICE_METRO_BEARER_TOKEN/); @@ -365,7 +377,7 @@ test('metro command usage documents session-hint reload priority, expo bundle ki const help = await usageForCommand('metro'); if (help === null) throw new Error('Expected metro help text'); assert.match(help, /resolves against the dev server/); - assert.match(help, /never silently reload[s]? a different project/); + assert.match(help, /never silently reloads the server of a different project/); assert.match(help, /cleared when the session closes/); assert.match(help, /\.expo\/\.virtual-metro-entry\.bundle/); assert.match(help, /yarn\.lock\/pnpm-lock\.yaml\/bun\.lock\/bun\.lockb\/package-lock\.json/); @@ -384,7 +396,7 @@ test('open command usage documents metro session-hint setter flags', async () => test('command usage shows no command flags when unsupported', async () => { const help = await usageForCommand('appstate'); if (help === null) throw new Error('Expected command help text'); - assert.match(help, /Show foreground app\/activity/); + assert.match(help, /Report the foreground app and activity/); assert.doesNotMatch(help, /Command flags:/); assert.doesNotMatch(help, /Global flags:/); assert.doesNotMatch(help, /Global Flags:/); @@ -403,7 +415,7 @@ test('keyboard command usage is documented', async () => { assert.match(help, /keyboard \[status\|get\|dismiss\|enter\|return\]/); assert.match( help, - /Inspect Android keyboard visibility\/type or press\/dismiss the device keyboard/, + /Examine the Android keyboard visibility and type, or press or dismiss the device keyboard/, ); }); @@ -414,7 +426,7 @@ test('orientation command usage is documented', async () => { help, /orientation /, ); - assert.match(help, /Set device orientation on iOS and Android/); + assert.match(help, /Set the device orientation on iOS and Android/); }); test('removed rotate alias fails with a migration error', async () => { @@ -456,7 +468,7 @@ test('replay --save-script help describes arming a repair transaction, not open/ if (help === null) throw new Error('Expected replay help text'); assert.match(help, /Arm a repair transaction from this replay \(ADR 0012\)/); assert.match(help, /commits when the repair-armed session tears down/); - assert.doesNotMatch(help, /Arm evidence capture on open, publish the armed recording on close/); + assert.doesNotMatch(help, /open: arm evidence capture\. close: publish the armed recording/); }); test( @@ -466,7 +478,7 @@ test( if (help === null) throw new Error('Expected open help text'); assert.match( help, - /Arm evidence capture on open, publish the armed recording on close; close --save-script alone \(without an armed open\) is rejected/, + /open: arm evidence capture\. close: publish the armed recording\. close --save-script without an armed open is refused/, ); assert.doesNotMatch(help, /Arm a repair transaction from this replay/); }, @@ -479,7 +491,7 @@ test( if (help === null) throw new Error('Expected close help text'); assert.match( help, - /Arm evidence capture on open, publish the armed recording on close; close --save-script alone \(without an armed open\) is rejected/, + /open: arm evidence capture\. close: publish the armed recording\. close --save-script without an armed open is refused/, ); assert.doesNotMatch(help, /Arm a repair transaction from this replay/); }, diff --git a/src/commands/schema/cli-help-first-screen.test.ts b/src/commands/schema/cli-help-first-screen.test.ts new file mode 100644 index 0000000000..605e5fdc7e --- /dev/null +++ b/src/commands/schema/cli-help-first-screen.test.ts @@ -0,0 +1,171 @@ +import assert from 'node:assert/strict'; +import { test } from 'vitest'; +import { usage, usageForCommand } from '../../cli/parser/args.ts'; +import { helpTopicIds } from './cli-help.ts'; + +// Agents often read only the first screen of a help text, and the help benchmark caps the root +// help at 30 lines (`--help:first30`). Each entry pins the rules that keep an agent safe on that +// screen: prerequisites, refusals, verification steps, and recovery. A new topic must declare its +// own entry. +const FIRST_SCREEN_LINES = 30; +const ROOT_HELP = '--help'; + +const FIRST_SCREEN_RULES: Record = { + [ROOT_HELP]: [ + 'agent-device open --foreground', + 'Do not probe first with devices, apps, appstate, snapshot, or screenshot', + 'Never invent ids', + 'A bare screenshot is not verification', + 'End with: agent-device close', + ], + commands: ['Use agent-device help for exact flags and behavior', 'Commands:'], + 'manual-qa': [ + 'Do not explore unrelated screens', + 'stop and report the ambiguity', + 'Use --relaunch only when the script needs fresh state', + 'If --settle prints not settled, follow its hint', + 'A bare screenshot/snapshot is not verification', + ], + maestro: [ + 'replay --maestro', + 'there is no silent all: allow default', + 'do not place secrets in diagnostic identifiers', + 'not a security sandbox', + 'Unsupported syntax fails with an explicit error', + ], + workflow: [ + 'never a placeholder (@ref, @eN, @Label_Name)', + 'every platform rejects a stale pinned ref', + 'Never open artifact paths or invent package ids', + 'help tv (Android TV, tvOS, Vega VVD remote)', + 'Matches in distinct subtrees fail with AMBIGUOUS_MATCH', + ], + scripting: [ + 'scroll down --until ', + 'add an explicit landmark wait', + '--record-as PASSWORD', + 'contain only ${PASSWORD}', + 'refuses a terminal, empty input, invalid UTF-8', + ], + gestures: [ + 'Use coordinates only as a fallback', + 'Do not assume that requested values map exactly to recognizer output', + 'then run diff snapshot -i', + 'Coordinates are fallback-only. Document why you used them.', + ], + tv: [ + 'Move focus with remote/D-pad buttons, then activate the focused control', + 'not ADB', + 'Physical Fire TV devices remain unsupported', + 'The VVD never boots implicitly', + 'remain unsupported until their Vega backends are implemented', + ], + debugging: [ + 'Do not cat a full stale log into the agent context', + 'Do not split it into logs stop, logs clear, logs start', + '--launch-console is only for direct iOS simulator app launches, not URL opens', + 'Do not write network log headers', + ], + 'react-devtools': [ + 'React Native 0.74 or newer', + 'After either event, run find again', + 'fail with tree-unavailable', + 'Run profile start immediately before the interaction', + ], + cdp: [ + 'Do not use it as the default React Native', + 'adb reverse tcp:8081 tcp:8081', + 'agent-device cdp target select ', + 'cdp memory snapshot leak-triplet', + ], + 'react-native': [ + 'Do not run doctor as routine QA/dogfood prep', + 'prefer open --relaunch before the first snapshot', + 'Do not use agent-device reload', + 'the sandbox probe is not authoritative', + 'Do not invent app ids or URLs; ask instead', + ], + 'physical-device': [ + 'have Developer Mode enabled', + 'must be signed before commands can run on a physical device', + 'devtools_security_developer_mode_disabled is the DevToolsSecurity setting of the Mac, not the Developer Mode of the device', + 'device_developer_mode_disabled is read from the device before the runner builds', + 'The two device reasons are never inferred from tool output or from each other', + 'Enable USB debugging', + ], + 'ios-system-ui': [ + 'agent-device open com.apple.springboard --platform ios', + 'Do not rely on the literal strings', + 'This is the one deliberate coordinate step', + 'Do not hard-code Edit/Done/Add Widget', + 'Reopen the app bundle under test', + ], + foldable: [ + 'Do not add a screen flag to the normal loop', + 'Never carry coordinates or refs across a pose change', + 'After every fold, take a new snapshot', + 'fold-pose-unverified', + 'Report which poses the run covered', + ], + remote: [ + 'connect -> install/open -> commands -> close -> disconnect', + 'not the app artifact name, ARN, or lt:// id', + 'run its printed reconnect command before open', + 'Do not run devices as a pre-open catalog probe for direct providers', + 'It does not expose connect/disconnect', + ], + macos: [ + 'Do not write inspect as a command', + '--button secondary', + 'Do not let iOS simulator-set scoping hide macOS desktop targets', + 'Do not guess coordinates', + 'does not raise the app', + ], + web: [ + 'Use --platform web only for the minimal browser command loop', + 'agent-device web doctor', + 'Web automation requires Node 24+', + 'write only agent-device command lines', + ], + dogfood: [ + 'Do not read app source or invent findings from code', + 'Ask only if one of them is missing', + 'Keep stateful commands serial within the same session', + 'use react-native dismiss-overlay', + ], + validate: [ + 'Do not validate against stale dist output', + 'start with pnpm build', + 'run pnpm build:android before pnpm clean:daemon', + 'Do not build the Apple runner for TypeScript-only changes', + 'Close sessions and release leases before you finish', + ], +}; + +async function renderHelp(id: string): Promise { + if (id === ROOT_HELP) return await usage(); + const text = await usageForCommand(id); + if (text === null) throw new Error(`Expected help text for topic ${id}`); + return text; +} + +test('every help topic and the root help declare first-screen rules', () => { + assert.deepEqual(Object.keys(FIRST_SCREEN_RULES).sort(), [ROOT_HELP, ...helpTopicIds()].sort()); +}); + +test.each(Object.entries(FIRST_SCREEN_RULES))( + 'help %s keeps its key rules in the first 30 lines', + async (id, rules) => { + const lines = (await renderHelp(id)).split('\n'); + const firstScreen = lines.slice(0, FIRST_SCREEN_LINES).join('\n'); + for (const rule of rules) { + const line = lines.findIndex((text) => text.includes(rule)) + 1; + assert.ok( + firstScreen.includes(rule), + line > 0 + ? `"${rule}" is on line ${line}, below the first ${FIRST_SCREEN_LINES} lines of help ${id}` + : `"${rule}" is missing from help ${id}`, + ); + } + }, +); diff --git a/src/commands/schema/cli-help-overview.test.ts b/src/commands/schema/cli-help-overview.test.ts index 00c75a0310..5d49bd034a 100644 --- a/src/commands/schema/cli-help-overview.test.ts +++ b/src/commands/schema/cli-help-overview.test.ts @@ -11,12 +11,12 @@ test('root help is a bounded first-success decision card', async () => { `root help must stay within the 3000-byte decision-card budget, was ${Buffer.byteLength(help, 'utf8')} bytes`, ); assert.match(help, /All \d+ commands: agent-device help commands/); - assert.match(help, /starting a task with a known app/); + assert.match(help, /start a task with a known app/); assert.match(help, /agent-device open --foreground/); assert.match(help, /scroll \[amount\] --settle; back --settle/); assert.match(help, /Do not probe first with devices, apps, appstate, snapshot, or screenshot/); assert.match(help, /Copy refs exactly: @e12, @e12~s4/); - assert.match(help, /Coordinates are last resort/); + assert.match(help, /Coordinates are the last resort/); assert.doesNotMatch(help, /^Commands:/m); assert.doesNotMatch(help, /^Global Flags:/m); }); diff --git a/src/commands/schema/cli-help-overview.ts b/src/commands/schema/cli-help-overview.ts index f9bd11775a..76f4f2569a 100644 --- a/src/commands/schema/cli-help-overview.ts +++ b/src/commands/schema/cli-help-overview.ts @@ -12,37 +12,37 @@ Automates iOS, Android, macOS, TV, and web apps for AI agents. All ${listCliCommandNames().length} commands: agent-device help commands Start: - When starting a task with a known app, first run: + When you start a task with a known app, first run: agent-device open --foreground - It starts the session and returns the initial interactive snapshot with @refs. + This command starts the session and returns the initial interactive snapshot with @refs. Do not probe first with devices, apps, appstate, snapshot, or screenshot. - Unknown app id: devices, then apps, then open . Never invent ids. - Resuming an existing session: continue from its current state; do not reopen it. + If the app id is unknown, run devices, then apps, then open . Never invent ids. + If you resume an existing session, continue from its current state. Do not reopen it. Loop: press|click|fill|longpress ... --settle - hover --settle (web only; reveals hover-gated UI) + hover --settle (web only; shows hover-gated UI) scroll [amount] --settle; back --settle - acts, waits for quiet, and prints the UI diff. Continue from that diff. - Run snapshot -i only when the diff lacks the next target or did not settle. + Each command acts, waits until the UI is quiet, and prints the UI diff. Continue from it. + Run snapshot -i only if the diff does not show the next target, or the UI did not settle. Verify a named expectation with the diff, wait text "...", wait , wait absent , is, get, or find. A bare screenshot is not verification. End with: agent-device close Targets: - Copy refs exactly: @e12, @e12~s4. Keep @ and any ~sN pin; refs go stale - after mutations. A literal @handle is label="@handle", not a bare ref. + Copy refs exactly: @e12, @e12~s4. Keep the @ and any ~sN pin. + A mutation makes refs stale. A literal @handle is label="@handle", not a bare ref. Prefer refs, then id/label/role selectors. Selector keys: ${SELECTOR_KEY_NAMES.join(' ')}. - Coordinates are last resort: after snapshot -i shows no semantic target, or a - sparse/AX-unavailable warning says its refs and selectors are invalid. - Then screenshot, press , and re-snapshot on the changed screen. + Coordinates are the last resort. Use them only if snapshot -i shows no semantic target, + or a sparse/AX-unavailable warning says that its refs and selectors are invalid. + Then run screenshot and press , and take a new snapshot of the changed screen. Rules: - --settle is only for press/click/fill/longpress/hover/scroll/back; never open, - snapshot, or close. type never accepts --settle. - fill --settle replaces; type appends after focus. - Late network/debounce result: wait text "Expected", not snapshot polling. - Output full agent-device commands; no pipes, grep, jq, or pseudo-commands. + Use --settle only with press/click/fill/longpress/hover/scroll/back. + Never use it with open, snapshot, or close. type never accepts --settle. + fill --settle replaces the text. type appends to the focused field. + If a network or debounce result comes late, use wait text "Expected". Do not poll with snapshot. + Write full agent-device commands. Do not use pipes, grep, jq, or pseudo-commands. Stop when the requested end state is visible. Mutations run serially. More commands (exact shapes: agent-device help ): @@ -56,6 +56,6 @@ Guides (agent-device help ): manual-qa / dogfood / validate / debugging / scripting / gestures react-native / react-devtools / cdp / tv / web / macos / remote physical-device / ios-system-ui / maestro - foldable iPhone Duo panels, the fold command, and why refs die on a pose change + foldable iPhone Duo panels, the fold command, and why a pose change makes refs stale `; } diff --git a/src/commands/schema/cli-help-topics.test.ts b/src/commands/schema/cli-help-topics.test.ts index 783a564486..b4bb2a9da8 100644 --- a/src/commands/schema/cli-help-topics.test.ts +++ b/src/commands/schema/cli-help-topics.test.ts @@ -6,10 +6,10 @@ test('commands topic includes concise command catalog entries', async () => { const usageText = await usageForCommand('commands'); if (usageText === null) throw new Error('Expected commands help text'); assert.match(usageText, /install-from-source\s{2,}Install app builds from URLs or CI artifacts/); - assert.match(usageText, /prepare\s{2,}Pre-warm platform helpers before automation/); + assert.match(usageText, /prepare\s{2,}Prepare platform helpers before automation/); assert.match(usageText, /metro\s{2,}Prepare the dev server or reload apps/); assert.match(usageText, /batch --steps \| --steps-file /); - assert.match(usageText, /network\s{2,}Inspect HTTP\(S\) traffic from session logs/); + assert.match(usageText, /network\s{2,}Examine HTTP\(S\) traffic from session logs/); assert.match(usageText, /clipboard read \| clipboard write /); assert.match(usageText, /keyboard \[action\]/); assert.match(usageText, /trigger-app-event\s{2,}Invoke an app-defined automation event/); @@ -186,9 +186,9 @@ test('usageForCommand resolves workflow help topic', async () => { assert.match(help, /open -> snapshot -i -> settle -> verify -> close loop/); assert.match(help, /type never takes --settle/); assert.match(help, /snapshot\/get\/is\/find answer read-only questions/); - assert.match(help, /--settle confirms local UI quieted/); + assert.match(help, /--settle confirms that the local UI quieted/); assert.match(help, /wait text "Expected result" or wait instead of polling/); - assert.match(help, /strict disappearance uses wait absent /); + assert.match(help, /For strict disappearance, use wait absent /); assert.match( help, /Chain confident consecutive steps with &&: press 'label="Search"' --settle && fill 'label="Search"' "query" --settle/, @@ -218,7 +218,7 @@ test('usageForCommand resolves workflow help topic', async () => { help, /A known selector\/label after a mutation is often enough, since interaction commands refresh state internally/, ); - assert.match(help, /TV\/D-pad focus: help tv/); + assert.match(help, /TV\/D-pad focus: help tv \(Android TV, tvOS, Vega VVD remote\)/); assert.match(help, /not bare role keys \(button="Search"\)/); assert.match(help, /Matches in distinct subtrees fail with AMBIGUOUS_MATCH/); assert.match(help, /geometry never chooses a winner/); @@ -265,7 +265,6 @@ test('usageForCommand resolves workflow help topic', async () => { assert.match(help, /already owned by another agent-device daemon/); assert.match(help, /Env vars: help physical-device/); assert.match(help, /Escalate:/); - assert.match(help, /help scripting recording, save-script, batch, replay repair/); assert.match(help, /help gestures multi-touch gesture shapes\/quirks/); assert.match(help, /help react-devtools/); assert.match(help, /help react-native/); @@ -301,7 +300,7 @@ test('usageForCommand resolves scripting help topic', async () => { ); assert.match(help, /on iOS, XCTest writes its own diagnostic logs and its \.xcresult bundle/); assert.match(help, /typed text can appear in both, on a simulator or a physical device/); - assert.match(help, /use test-only credentials when they may be kept or shared/); + assert.match(help, /Use test-only credentials when the artifacts may be kept or shared/); assert.match(help, /test --json marks a failed test with infrastructure: true/); assert.match(help, /It remains a failed test/); assert.match(help, /REPLAY_DIVERGENCE with a bounded report/); @@ -348,9 +347,9 @@ test('usageForCommand resolves tv help topic', async () => { assert.match(help, /agent-device screenshot \.\/tv-focus\.png --overlay-refs/); assert.match(help, /tv-remote longpress select/); assert.match(help, /tv-remote press select --duration-ms 500/); - assert.match(help, /longpress is CLI sugar for --duration-ms 500/); + assert.match(help, /longpress is CLI shorthand for --duration-ms 500/); assert.match(help, /ok, center, and enter are input aliases for select/); - assert.match(help, /do not switch to raw adb keyevent/); + assert.match(help, /Do not switch to raw adb keyevent/); assert.match(help, /Use --platform ios --target tv/); assert.match(help, /agent-device devices --platform vega --target tv/); assert.match(help, /Vega OS uses the exact hold duration through inputd-cli/); @@ -545,12 +544,12 @@ test('usageForCommand resolves react-devtools help topic', async () => { assert.match(help, /through agent-device proxy, and on remote bridge sessions/); assert.match(help, /Add --app when several apps share the dev server/); assert.match(help, /--metro-device when one app runs on several devices/); - assert.match(help, /ranked findings, each naming a component, its evidence, and a fix/); + assert.match(help, /ranked findings\. Each finding names a component, its evidence, and a fix/); assert.match( help, /memo-defeated: a memoized component often re-renders because props change identity/, ); - assert.match(help, /compare runs instead of reading absolute numbers/); + assert.match(help, /Compare runs instead of reading absolute numbers/); assert.match(help, /logs clear --restart before the first logs mark/); assert.match(help, /Do not repeatedly raise table limits/); assert.match(help, /agent-device logs mark "before catalog search"/); @@ -570,7 +569,7 @@ test('usageForCommand resolves cdp help topic', async () => { assert.match(help, /memory snapshot leak-triplet --baseline ms_1 --action ms_2 --cleanup ms_3/); assert.match(help, /memory snapshot retainers --snapshot ms_3 --id /); assert.match(help, /Until cdp has a compact leak report command/); - assert.match(help, /Avoid cdp profile cpu, trace, network, and console by default/); + assert.match(help, /By default, avoid cdp profile cpu, trace, network, and console/); assert.match(help, /React Native\/Hermes implements a subset of browser CDP/); }); @@ -633,18 +632,21 @@ test('commands topic includes swipe and press series options', async () => { test('commands topic renders concise commands inline with descriptions', async () => { const help = await usageForCommand('commands'); if (help === null) throw new Error('Expected commands help text'); - assert.match(help, /Commands:[\s\S]*\n {2}boot\s{2,}Boot target device\/simulator/); - assert.match(help, /Commands:[\s\S]*\n {2}shutdown\s{2,}Shutdown target simulator\/emulator/); - assert.match(help, / {2}prepare\s{2,}Pre-warm platform helpers/); + assert.match(help, /Commands:[\s\S]*\n {2}boot\s{2,}Boot the target device or simulator/); + assert.match( + help, + /Commands:[\s\S]*\n {2}shutdown\s{2,}Shut down the target simulator or emulator/, + ); + assert.match(help, / {2}prepare\s{2,}Prepare platform helpers/); assert.match(help, / {2}metro\s{2,}Prepare the dev server or reload apps/); - assert.match(help, / {2}perf\s{2,}Check frames, memory, or native profiles/); - assert.match(help, / {2}cdp\s{2,}Inspect CDP targets, JS heap, and leaks/); + assert.match(help, / {2}perf\s{2,}Examine frames, memory, or native profiles/); + assert.match(help, / {2}cdp\s{2,}Examine CDP targets, JS heap, and leaks/); assert.match(help, / {2}react-devtools\s{2,}React Native components, props\/state\/hooks/); assert.match(help, / {2}proxy\s{2,}Expose a local daemon through an HTTP tunnel/); assert.match(help, / {2}batch --steps \| --steps-file \s{2,}Run multiple commands/); assert.match(help, / {2}test \.\.\.\s{2,}Run replay test suites/); assert.match(help, / {2}screenshot \[path\]\s{2,}Capture a screenshot/); - assert.match(help, / {2}session\s{2,}List sessions, show the state dir, or publish a script/); + assert.match(help, / {2}session\s{2,}List sessions, print the state dir, or publish a script/); assert.doesNotMatch(help, / {2}metro prepare[^\n]*--project-root/); assert.doesNotMatch(help, /\n {2}batch\s{2,}Run multiple commands/); assert.doesNotMatch(help, /agent-device-proxy/); diff --git a/src/commands/schema/cli-help-workflows.test.ts b/src/commands/schema/cli-help-workflows.test.ts index f3318c48f2..a0d2305590 100644 --- a/src/commands/schema/cli-help-workflows.test.ts +++ b/src/commands/schema/cli-help-workflows.test.ts @@ -45,7 +45,7 @@ test('usageForCommand resolves debugging help topic', async () => { assert.match(help, /prefill with clipboard write "some text"/); assert.match(help, /Android Gboard handwriting\/stylus UI can capture text/); assert.match(help, /targetInput\/actualInput details/); - assert.match(help, /Do not keep retrying fill\/type against the same field/); + assert.match(help, /On Android, do not keep retrying fill\/type against the same field/); }); test('usageForCommand resolves manual QA help topic', async () => { @@ -57,7 +57,7 @@ test('usageForCommand resolves manual QA help topic', async () => { assert.match(help, /press\/fill\/click\/longpress --settle/); assert.match(help, /A bare screenshot\/snapshot is not verification/); assert.match(help, /use fill --settle to replace/); - assert.match(help, /use type only to append to an already-focused field/); + assert.match(help, /Use type only to append to an already-focused field/); assert.match(help, /label="Email" editable=true/); assert.match(help, /press 'label="Follow"' --settle/); assert.match(help, /Do not use placeholders such as @ref/); @@ -72,7 +72,7 @@ test('usageForCommand resolves validate help topic', async () => { const help = await usageForCommand('validate'); if (help === null) throw new Error('Expected validate help text'); assert.match(help, /^agent-device \S+ — validate/); - assert.match(help, /validating a code change/); + assert.match(help, /validate a code change/); assert.match(help, /Required freshness gate before device verification/); assert.match(help, /For a TypeScript runtime or CLI output change, start with pnpm build/); assert.match(help, /For non-Android device verification, run pnpm clean:daemon next/); diff --git a/src/commands/schema/cli-help-workflows.ts b/src/commands/schema/cli-help-workflows.ts index 012b363ef9..a1af12cd13 100644 --- a/src/commands/schema/cli-help-workflows.ts +++ b/src/commands/schema/cli-help-workflows.ts @@ -16,20 +16,20 @@ export const manualQaHelpTopics = { summary: 'Follow manual test scripts with exact interactions and verification', body: `agent-device help manual-qa -Use this when asked to follow a manual QA script, test case, checklist, acceptance flow, or user-provided instructions. +Use this topic when the task asks you to follow a manual QA script, test case, checklist, acceptance flow, or user-provided instructions. Contract: Execute the script. Do not explore unrelated screens, read app source, invent missing requirements, or broaden scope. - Stop and report ambiguity when a required target or expected result is not visible after the documented recovery steps. + If a required target or expected result is not visible after the documented recovery steps, stop and report the ambiguity. Loop: - 1. Open the requested app; use --relaunch only when the script needs fresh state. + 1. Open the requested app. Use --relaunch only when the script needs fresh state. 2. Run snapshot -i to get current refs for the next step. - 3. Run press/fill/click/longpress --settle for each mutating step. - 4. Treat a settled:true diff as the next observation. Do not add wait stable or another snapshot when the diff already shows the next target or expected result. + 3. For each mutating step, run press/fill/click/longpress --settle. + 4. Treat a settled:true diff as the next observation. If the diff already shows the next target or expected result, do not add wait stable or another snapshot. 5. If --settle prints not settled, follow its hint before the next ref-based action. 6. Verify named expectations with wait text/selector/absent, get, is, find, or the settled diff. A bare screenshot/snapshot is not verification for a named expectation. - 7. Close the session when the script ends. + 7. When the script ends, close the session. Command shapes: agent-device open com.example.app --relaunch @@ -41,21 +41,21 @@ Command shapes: agent-device wait text "Order placed" 3000 agent-device wait absent 'label="Loading..."' 3000 agent-device close - --relaunch forces fresh app state; a deep link/URL open does not need it. Labels with an apostrophe or quote (label="Don't leave") are shell-quoting hazards: prefer the @ref from the latest snapshot/settle output over quoting the literal label. + --relaunch forces fresh app state. A deep link or URL open does not need it. A label with an apostrophe or a quote (label="Don't leave") is a shell-quoting hazard. For such a label, prefer the @ref from the latest snapshot or settle output to a quoted literal label. Targets: - Prefer refs from the latest snapshot -i or settled diff. Use durable selectors when the label/id is known: label="Search", id="submit", role=button label="Follow". If label text matches both a field and its caption, target the field with label="Email" editable=true. - For text fields, use fill --settle to replace the field value; use type only to append to an already-focused field. + Prefer refs from the latest snapshot -i or settled diff. If you know the label or id, use durable selectors: label="Search", id="submit", role=button label="Follow". If the label text matches both a field and its caption, target the field with label="Email" editable=true. + For a text field, use fill --settle to replace the field value. Use type only to append to an already-focused field. Do not use placeholders such as @ref, @eN,