diff --git a/.codex/config.toml b/.codex/config.toml index 6d7c39efc2..bfe5447325 100644 --- a/.codex/config.toml +++ b/.codex/config.toml @@ -1,2 +1,2 @@ [features] -collaboration_modes = true \ No newline at end of file +collaboration_modes = true diff --git a/.fallowrc.json b/.fallowrc.json index 8c9d51fdd1..5d8c8d322e 100644 --- a/.fallowrc.json +++ b/.fallowrc.json @@ -33,16 +33,12 @@ "scripts/write-xcuitest-cache-metadata.mjs", "scripts/help-conformance-sample-outputs.d.mts" ], - "ignoreDependencies": [ - "@theme" - ], + "ignoreDependencies": ["@theme"], "ignoreExports": [ { "comment": "Authoritative supported-command list; sole consumer is the conformance oracle (scripts/maestro-conformance, an ignored tooling tree).", "file": "src/compat/maestro/program-ir-command-parser.ts", - "exports": [ - "SUPPORTED_MAESTRO_COMMAND_NAMES" - ] + "exports": ["SUPPORTED_MAESTRO_COMMAND_NAMES"] }, { "comment": "Daemon route handlers are reached only through the dynamic `import()` table in request-handler-chain.ts, which --production analysis cannot follow to a consumer.", @@ -60,16 +56,12 @@ { "comment": "Published package surface (see package.json#exports); consumed by SDK users, not internally.", "file": "src/sdk/*.ts", - "exports": [ - "*" - ] + "exports": ["*"] }, { "comment": "Tool config default exports, loaded by the tool rather than imported.", "file": "{tsdown.config.ts,vitest.mutation.config.ts,website/rspress.config.ts,test/skillgym/skillgym.config.ts,test/skillgym/suites/*.ts}", - "exports": [ - "default" - ] + "exports": ["default"] }, { "comment": "Type-level `AssertTrue<...>` registry-totality guards. Exported only so `noUnusedLocals` keeps them alive; a consumer would defeat the point.", @@ -84,34 +76,20 @@ { "comment": "Mutation-lane seam: readTestScope is consumed by vitest.mutation.config.ts, a tool config outside --production analysis.", "file": "scripts/mutation/test-scope.ts", - "exports": [ - "readTestScope" - ] + "exports": ["readTestScope"] }, { "comment": "Mutation-lane completeness gate: ownedTestFiles enumerates the whole derived ownership map, which only the assertion in ownership.test.ts consumes — a production consumer would defeat the point.", "file": "scripts/mutation/ownership.ts", - "exports": [ - "ownedTestFiles" - ] + "exports": ["ownedTestFiles"] }, { "comment": "Help-benchmark conformance seams: helpTopicIds feeds the topic-coverage gate and PRIVATE_AX_RECOVERY_SAMPLE feeds the skillgym suite; both consumers are test-tree files outside --production analysis.", "file": "{src/cli/parser/cli-help.ts,scripts/help-conformance-sample-outputs.mjs}", - "exports": [ - "helpTopicIds", - "PRIVATE_AX_RECOVERY_SAMPLE" - ] + "exports": ["helpTopicIds", "PRIVATE_AX_RECOVERY_SAMPLE"] } ], - "usedClassMembers": [ - "name", - "listActiveLeases", - "delete", - "values", - "elapsedMs", - "isExpired" - ], + "usedClassMembers": ["name", "listActiveLeases", "delete", "values", "elapsedMs", "isExpired"], "rules": { "unused-types": "error", "duplicate-exports": "off" @@ -119,9 +97,7 @@ "production": { "dupes": true }, - "publicPackages": [ - "agent-device" - ], + "publicPackages": ["agent-device"], "audit": { "deadCodeBaseline": "fallow-baselines/dead-code.json", "healthBaseline": "fallow-baselines/health.json" diff --git a/.github/actions/boot-ios-test-simulator/action.yml b/.github/actions/boot-ios-test-simulator/action.yml index f2f5cb38ec..684ed38f72 100644 --- a/.github/actions/boot-ios-test-simulator/action.yml +++ b/.github/actions/boot-ios-test-simulator/action.yml @@ -1,26 +1,26 @@ -name: "Boot iOS Test Simulator" -description: "Resolve and boot the preferred iOS simulator for replay tests" +name: 'Boot iOS Test Simulator' +description: 'Resolve and boot the preferred iOS simulator for replay tests' inputs: runtime-version: - description: "Preferred iOS runtime version" + description: 'Preferred iOS runtime version' required: true preferred-device-name: - description: "Preferred simulator device name" + description: 'Preferred simulator device name' required: false - default: "iPhone 17 Pro" + default: 'iPhone 17 Pro' boot-timeout-seconds: - description: "Maximum time to wait for simulator bootstatus" + description: 'Maximum time to wait for simulator bootstatus' required: false - default: "300" + default: '300' outputs: simulator-udid: - description: "UDID of the booted iOS simulator" + description: 'UDID of the booted iOS simulator' value: ${{ steps.boot.outputs.simulator-udid }} runs: - using: "composite" + using: 'composite' steps: - name: Resolve and boot iOS test simulator id: boot diff --git a/.github/actions/setup-android-replay-host/action.yml b/.github/actions/setup-android-replay-host/action.yml index 252dbd073b..a50f321aa4 100644 --- a/.github/actions/setup-android-replay-host/action.yml +++ b/.github/actions/setup-android-replay-host/action.yml @@ -1,13 +1,13 @@ -name: "Setup Android Replay Host" -description: "Prepare the Android CI host for replay tests and expose the agent-device home path" +name: 'Setup Android Replay Host' +description: 'Prepare the Android CI host for replay tests and expose the agent-device home path' outputs: agent-home-dir: - description: "Resolved agent-device home directory" + description: 'Resolved agent-device home directory' value: ${{ steps.agent-home.outputs.dir }} runs: - using: "composite" + using: 'composite' steps: - name: Resolve agent-device home id: agent-home diff --git a/.github/actions/setup-apple-replay/action.yml b/.github/actions/setup-apple-replay/action.yml index f2375671ea..edea8a4fbb 100644 --- a/.github/actions/setup-apple-replay/action.yml +++ b/.github/actions/setup-apple-replay/action.yml @@ -1,43 +1,43 @@ -name: "Setup Apple Replay" -description: "Resolve shared Apple replay cache/build state and expose the agent-device home path" +name: 'Setup Apple Replay' +description: 'Resolve shared Apple replay cache/build state and expose the agent-device home path' inputs: derived-path: - description: "Derived data path used by the replay runner" + description: 'Derived data path used by the replay runner' required: true cache-key-prefix: - description: "Prefix for the cache key" + description: 'Prefix for the cache key' required: true cache-key-suffix: - description: "Optional suffix for the cache key" + description: 'Optional suffix for the cache key' required: false - default: "" + default: '' build-command: - description: "Command used to refresh replay artifacts before use" + description: 'Command used to refresh replay artifacts before use' required: true xcuitest-platform: - description: "Optional AGENT_DEVICE_XCUITEST_PLATFORM value" + description: 'Optional AGENT_DEVICE_XCUITEST_PLATFORM value' required: false - default: "" + default: '' xcuitest-destination: - description: "Optional AGENT_DEVICE_XCUITEST_DESTINATION value" + description: 'Optional AGENT_DEVICE_XCUITEST_DESTINATION value' required: false - default: "" + default: '' build-before-use: - description: "Whether this setup action should refresh replay artifacts after restoring the cache" + description: 'Whether this setup action should refresh replay artifacts after restoring the cache' required: false - default: "true" + default: 'true' outputs: agent-home-dir: - description: "Resolved agent-device home directory" + description: 'Resolved agent-device home directory' value: ${{ steps.agent-home.outputs.dir }} cache-hit: - description: "Whether a replay cache was restored before any build refresh" + description: 'Whether a replay cache was restored before any build refresh' value: ${{ steps.restore-prebuilt.outputs.cache-hit }} runs: - using: "composite" + using: 'composite' steps: - name: Resolve Xcode cache key id: xcode diff --git a/.github/actions/setup-fixture-app/action.yml b/.github/actions/setup-fixture-app/action.yml index 6043fea19a..890e9fe631 100644 --- a/.github/actions/setup-fixture-app/action.yml +++ b/.github/actions/setup-fixture-app/action.yml @@ -1,5 +1,5 @@ -name: "Setup Fixture App" -description: "Install a ready Release build of examples/test-app on the booted iOS simulator, from the shared build cache" +name: 'Setup Fixture App' +description: 'Install a ready Release build of examples/test-app on the booted iOS simulator, from the shared build cache' # Any job that needs a controlled app to drive can use this instead of an Apple # system app. It fetches the Release binary that test-app-build-cache.yml @@ -18,17 +18,17 @@ description: "Install a ready Release build of examples/test-app on the booted i inputs: device-name: - description: "Simulator device name used for the inline-build fallback" + description: 'Simulator device name used for the inline-build fallback' required: false - default: "iPhone 17 Pro" + default: 'iPhone 17 Pro' install: - description: "Install the app onto the booted simulator" + description: 'Install the app onto the booted simulator' required: false - default: "true" + default: 'true' outputs: app-path: - description: "Path to the ready .app bundle" + description: 'Path to the ready .app bundle' value: ${{ steps.locate.outputs.app-path }} app-id: description: "Bundle identifier read from the app's Info.plist" @@ -38,7 +38,7 @@ outputs: value: ${{ steps.fetch.outputs.source }} runs: - using: "composite" + using: 'composite' steps: # Installed even on a hit, because the fingerprint is derived from the # dependency graph and cannot be computed without it. diff --git a/.github/actions/setup-node-pnpm/action.yml b/.github/actions/setup-node-pnpm/action.yml index 81a71ef354..ac66c7d07c 100644 --- a/.github/actions/setup-node-pnpm/action.yml +++ b/.github/actions/setup-node-pnpm/action.yml @@ -6,10 +6,6 @@ inputs: description: 'Node.js version' required: false default: '24.13' - pnpm-version: - description: 'pnpm version' - required: false - default: '11.1.2' install-deps: description: 'Whether to install dependencies with pnpm install --frozen-lockfile' required: false @@ -18,10 +14,28 @@ inputs: runs: using: 'composite' steps: + # package.json `packageManager` is the single source of truth for the pnpm + # version. This action carries no default of its own: a second default is + # how local (corepack) and CI pnpm versions drift apart silently. + - name: Resolve pnpm version from packageManager + id: package-manager + run: | + set -euo pipefail + spec="$(node -p "require('$GITHUB_WORKSPACE/package.json').packageManager ?? ''")" + case "$spec" in + pnpm@[0-9]*) ;; + *) + echo "package.json packageManager must be pnpm@, got '${spec}'" >&2 + exit 1 + ;; + esac + echo "pnpm-version=${spec#pnpm@}" >> "$GITHUB_OUTPUT" + shell: bash + - name: Setup pnpm uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: - version: ${{ inputs.pnpm-version }} + version: ${{ steps.package-manager.outputs.pnpm-version }} - name: Setup Node.js uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 @@ -32,6 +46,21 @@ runs: # miss (any lockfile-changing PR) the post-job cache save fails the job. cache: ${{ inputs.install-deps == 'true' && 'pnpm' || '' }} + # Corepack resolves the same `packageManager` field locally, so a mismatch + # here means CI is running a pnpm nobody develops against. + - name: Assert pnpm matches packageManager + env: + EXPECTED_PNPM_VERSION: ${{ steps.package-manager.outputs.pnpm-version }} + run: | + set -euo pipefail + actual="$(pnpm --version)" + if [ "$actual" != "$EXPECTED_PNPM_VERSION" ]; then + echo "pnpm ${actual} is installed but package.json pins pnpm@${EXPECTED_PNPM_VERSION}" >&2 + exit 1 + fi + echo "pnpm ${actual} matches package.json packageManager" + shell: bash + - name: Install dependencies if: ${{ inputs.install-deps == 'true' }} run: pnpm install --frozen-lockfile diff --git a/.github/actions/upload-agent-device-artifacts/action.yml b/.github/actions/upload-agent-device-artifacts/action.yml index e46e5d26a7..e527683d6e 100644 --- a/.github/actions/upload-agent-device-artifacts/action.yml +++ b/.github/actions/upload-agent-device-artifacts/action.yml @@ -1,20 +1,20 @@ -name: "Upload Agent Device Artifacts" -description: "Upload shared agent-device diagnostics, session logs, and replay artifacts" +name: 'Upload Agent Device Artifacts' +description: 'Upload shared agent-device diagnostics, session logs, and replay artifacts' inputs: artifact-name: - description: "Artifact name" + description: 'Artifact name' required: true agent-home-dir: - description: "Resolved agent-device daemon state directory" + description: 'Resolved agent-device daemon state directory' required: true runner-derived-path: - description: "Optional Apple runner derived data path to upload cache metadata and Xcode logs" + description: 'Optional Apple runner derived data path to upload cache metadata and Xcode logs' required: false - default: ".agent-device-no-runner-derived" + default: '.agent-device-no-runner-derived' runs: - using: "composite" + using: 'composite' steps: - name: Upload artifacts uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000000..87494cb709 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,64 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended", + ":semanticCommitTypeAll(chore)", + "helpers:pinGitHubActionDigests" + ], + "timezone": "Etc/UTC", + "schedule": ["before 6am on monday"], + "labels": ["dependencies"], + "dependencyDashboard": true, + "automerge": false, + "prConcurrentLimit": 5, + "prHourlyLimit": 2, + "rebaseWhen": "conflicted", + "minimumReleaseAge": "7 days", + "lockFileMaintenance": { + "enabled": true, + "schedule": ["before 6am on monday"], + "automerge": false, + "commitMessageAction": "refresh" + }, + "vulnerabilityAlerts": { + "enabled": true, + "labels": ["dependencies", "security"], + "schedule": ["at any time"], + "minimumReleaseAge": null, + "automerge": false + }, + "osvVulnerabilityAlerts": true, + "packageRules": [ + { + "matchDepTypes": ["devDependencies"], + "groupName": "devDependencies", + "groupSlug": "dev-dependencies", + "automerge": false + }, + { + "matchDepTypes": ["dependencies", "optionalDependencies", "peerDependencies"], + "groupName": null, + "automerge": false + }, + { + "matchManagers": ["github-actions"], + "groupName": "github actions", + "groupSlug": "github-actions", + "pinDigests": true, + "automerge": false + }, + { + "matchFileNames": ["examples/test-app/package.json"], + "matchDepTypes": ["devDependencies"], + "groupName": "test-app fixture devDependencies", + "groupSlug": "test-app-dev", + "automerge": false + }, + { + "matchDepTypes": ["packageManager"], + "groupName": "pnpm (packageManager)", + "groupSlug": "package-manager", + "automerge": false + } + ] +} diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 5de411c089..588e9b1a61 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -3,13 +3,13 @@ name: Android on: pull_request: paths-ignore: - - "docs/**" - - "website/**" - - "README.md" - - ".github/actions/build-docs/action.yml" - - ".github/workflows/deploy.yml" - - ".github/workflows/pr-preview.yml" - - ".github/workflows/pr-preview-cleanup.yml" + - 'docs/**' + - 'website/**' + - 'README.md' + - '.github/actions/build-docs/action.yml' + - '.github/workflows/deploy.yml' + - '.github/workflows/pr-preview.yml' + - '.github/workflows/pr-preview-cleanup.yml' push: branches: - main diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6bd59879be..460fa8d489 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,13 +3,13 @@ name: CI on: pull_request: paths-ignore: - - "docs/**" - - "website/**" - - "README.md" - - ".github/actions/build-docs/action.yml" - - ".github/workflows/deploy.yml" - - ".github/workflows/pr-preview.yml" - - ".github/workflows/pr-preview-cleanup.yml" + - 'docs/**' + - 'website/**' + - 'README.md' + - '.github/actions/build-docs/action.yml' + - '.github/workflows/deploy.yml' + - '.github/workflows/pr-preview.yml' + - '.github/workflows/pr-preview-cleanup.yml' push: branches: - main @@ -182,7 +182,7 @@ jobs: - name: Setup Node.js 22.12 uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: - node-version: "22.12" + node-version: '22.12' - name: Install and run global CLI on Node.js 22.12 run: | @@ -328,7 +328,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 env: - AGENT_DEVICE_WEB_E2E: "1" + AGENT_DEVICE_WEB_E2E: '1' steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -336,7 +336,7 @@ jobs: - name: Setup toolchain uses: ./.github/actions/setup-node-pnpm with: - node-version: "24.13" + node-version: '24.13' - name: Run live web smoke run: | diff --git a/.github/workflows/conformance-differential.yml b/.github/workflows/conformance-differential.yml index 93065ee5c3..fcd103d7b3 100644 --- a/.github/workflows/conformance-differential.yml +++ b/.github/workflows/conformance-differential.yml @@ -15,11 +15,11 @@ name: Conformance Differential on: schedule: - - cron: "0 5 * * *" + - cron: '0 5 * * *' workflow_dispatch: inputs: only: - description: "Single scenario id to run (default: all)" + description: 'Single scenario id to run (default: all)' required: false permissions: @@ -32,11 +32,11 @@ concurrency: cancel-in-progress: true env: - AGENT_DEVICE_CLI: "--experimental-strip-types src/bin.ts" + AGENT_DEVICE_CLI: '--experimental-strip-types src/bin.ts' DIFFERENTIAL_ONLY: ${{ github.event.inputs.only || '' }} # CI should not phone home, and it keeps `maestro --version` to just the # version instead of prefixing an analytics notice. - MAESTRO_CLI_NO_ANALYTICS: "1" + MAESTRO_CLI_NO_ANALYTICS: '1' jobs: differential-ios: @@ -44,7 +44,7 @@ jobs: runs-on: macos-26 timeout-minutes: 90 env: - IOS_RUNTIME_VERSION: "26.2" + IOS_RUNTIME_VERSION: '26.2' AGENT_DEVICE_STATE_DIR: ${{ github.workspace }}/.tmp/agent-device-state AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH: ${{ github.workspace }}/.tmp/ios-runner-derived steps: diff --git a/.github/workflows/conformance-regenerate.yml b/.github/workflows/conformance-regenerate.yml index 9b0f397c31..abeaaebc9e 100644 --- a/.github/workflows/conformance-regenerate.yml +++ b/.github/workflows/conformance-regenerate.yml @@ -15,7 +15,7 @@ name: Conformance Regenerate Verify on: schedule: - - cron: "0 6 * * *" + - cron: '0 6 * * *' workflow_dispatch: permissions: @@ -44,7 +44,7 @@ jobs: uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4 with: distribution: temurin - java-version: "17" + java-version: '17' # Verifies the pinned jar SHA-256s against pinned-upstream.json, runs the # harness over the corpus, and rewrites fixtures/ + corpus/manifest.json. diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index cbbe29cc1c..de5f4cd301 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -3,13 +3,13 @@ name: Linux on: pull_request: paths-ignore: - - "docs/**" - - "website/**" - - "README.md" - - ".github/actions/build-docs/action.yml" - - ".github/workflows/deploy.yml" - - ".github/workflows/pr-preview.yml" - - ".github/workflows/pr-preview-cleanup.yml" + - 'docs/**' + - 'website/**' + - 'README.md' + - '.github/actions/build-docs/action.yml' + - '.github/workflows/deploy.yml' + - '.github/workflows/pr-preview.yml' + - '.github/workflows/pr-preview-cleanup.yml' push: branches: - main @@ -29,12 +29,12 @@ jobs: env: # Force X11 mode (Xvfb) — no Wayland on CI. XDG_SESSION_TYPE: x11 - DISPLAY: ":99" + DISPLAY: ':99' # Headless GTK: avoid dconf issues, enable accessibility bridge. GSETTINGS_BACKEND: memory - NO_AT_BRIDGE: "0" + NO_AT_BRIDGE: '0' GTK_A11Y: atspi - GTK_MODULES: "gail:atk-bridge" + GTK_MODULES: 'gail:atk-bridge' steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 79a840c185..888f24de08 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -3,13 +3,13 @@ name: macOS on: pull_request: paths-ignore: - - "docs/**" - - "website/**" - - "README.md" - - ".github/actions/build-docs/action.yml" - - ".github/workflows/deploy.yml" - - ".github/workflows/pr-preview.yml" - - ".github/workflows/pr-preview-cleanup.yml" + - 'docs/**' + - 'website/**' + - 'README.md' + - '.github/actions/build-docs/action.yml' + - '.github/workflows/deploy.yml' + - '.github/workflows/pr-preview.yml' + - '.github/workflows/pr-preview-cleanup.yml' push: branches: - main diff --git a/.github/workflows/mutation-affected.yml b/.github/workflows/mutation-affected.yml index c12a3e28eb..1adff6eada 100644 --- a/.github/workflows/mutation-affected.yml +++ b/.github/workflows/mutation-affected.yml @@ -25,17 +25,17 @@ on: # Kernel sources, every src test (ownership is derived, so any test may own # a kernel — `select` decides, not this filter), and the lane's own tooling. # scripts/mutation/workflow.test.ts asserts this covers the registry. - - "src/kernel/errors.ts" - - "src/daemon/ref-frame.ts" - - "src/commands/interaction/runtime/settle.ts" - - "src/utils/scroll-edge-state.ts" - - "src/selectors/**" - - "src/**/*.test.ts" - - "scripts/mutation/**" - - "scripts/lib/**" - - "stryker.config.json" - - "mutation-baselines/**" - - ".github/workflows/mutation-affected.yml" + - 'src/kernel/errors.ts' + - 'src/daemon/ref-frame.ts' + - 'src/commands/interaction/runtime/settle.ts' + - 'src/utils/scroll-edge-state.ts' + - 'src/selectors/**' + - 'src/**/*.test.ts' + - 'scripts/mutation/**' + - 'scripts/lib/**' + - 'stryker.config.json' + - 'mutation-baselines/**' + - '.github/workflows/mutation-affected.yml' permissions: contents: read diff --git a/.github/workflows/mutation-weekly.yml b/.github/workflows/mutation-weekly.yml index 99df7f3290..1d3135157d 100644 --- a/.github/workflows/mutation-weekly.yml +++ b/.github/workflows/mutation-weekly.yml @@ -19,7 +19,7 @@ name: Mutation Weekly on: schedule: # Sundays 05:00 UTC — after the nightly lanes, before the working week. - - cron: "0 5 * * 0" + - cron: '0 5 * * 0' workflow_dispatch: permissions: diff --git a/.github/workflows/perf-nightly.yml b/.github/workflows/perf-nightly.yml index e9c0f037ae..643236463b 100644 --- a/.github/workflows/perf-nightly.yml +++ b/.github/workflows/perf-nightly.yml @@ -8,13 +8,13 @@ name: Perf Nightly on: schedule: - - cron: "0 4 * * *" + - cron: '0 4 * * *' workflow_dispatch: inputs: rounds: - description: "Measured rounds per command (samples)" + description: 'Measured rounds per command (samples)' required: false - default: "5" + default: '5' permissions: contents: read @@ -24,7 +24,7 @@ concurrency: cancel-in-progress: true env: - AGENT_DEVICE_PERF_CLI: "--experimental-strip-types src/bin.ts" + AGENT_DEVICE_PERF_CLI: '--experimental-strip-types src/bin.ts' PERF_ROUNDS: ${{ github.event.inputs.rounds || '5' }} jobs: @@ -33,10 +33,10 @@ jobs: runs-on: macos-26 timeout-minutes: 80 env: - IOS_RUNTIME_VERSION: "26.2" + IOS_RUNTIME_VERSION: '26.2' AGENT_DEVICE_STATE_DIR: ${{ github.workspace }}/.tmp/agent-device-state AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH: ${{ github.workspace }}/.tmp/ios-runner-derived - AGENT_DEVICE_IOS_PREPARE_TIMEOUT_MS: "420000" + AGENT_DEVICE_IOS_PREPARE_TIMEOUT_MS: '420000' steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml index 188e619d4c..85153d8b7c 100644 --- a/.github/workflows/pr-preview.yml +++ b/.github/workflows/pr-preview.yml @@ -7,13 +7,13 @@ on: - reopened - synchronize paths: - - "website/**" - - ".github/actions/build-docs/action.yml" - - ".github/actions/setup-node-pnpm/action.yml" - - ".github/workflows/pr-preview.yml" - - "package.json" - - "pnpm-lock.yaml" - - "pnpm-workspace.yaml" + - 'website/**' + - '.github/actions/build-docs/action.yml' + - '.github/actions/setup-node-pnpm/action.yml' + - '.github/workflows/pr-preview.yml' + - 'package.json' + - 'pnpm-lock.yaml' + - 'pnpm-workspace.yaml' concurrency: preview-${{ github.ref }} diff --git a/.github/workflows/size.yml b/.github/workflows/size.yml index ab8054a02c..974161e736 100644 --- a/.github/workflows/size.yml +++ b/.github/workflows/size.yml @@ -3,13 +3,13 @@ name: Size on: pull_request: paths-ignore: - - "docs/**" - - "website/**" - - "README.md" - - ".github/actions/build-docs/action.yml" - - ".github/workflows/deploy.yml" - - ".github/workflows/pr-preview.yml" - - ".github/workflows/pr-preview-cleanup.yml" + - 'docs/**' + - 'website/**' + - 'README.md' + - '.github/actions/build-docs/action.yml' + - '.github/workflows/deploy.yml' + - '.github/workflows/pr-preview.yml' + - '.github/workflows/pr-preview-cleanup.yml' permissions: contents: read diff --git a/.github/workflows/test-app-build-cache.yml b/.github/workflows/test-app-build-cache.yml index 2c19476d9e..76425ce1d4 100644 --- a/.github/workflows/test-app-build-cache.yml +++ b/.github/workflows/test-app-build-cache.yml @@ -20,9 +20,9 @@ on: - main pull_request: paths: - - "examples/test-app/**" - - ".github/workflows/test-app-build-cache.yml" - - ".github/actions/setup-fixture-app/**" + - 'examples/test-app/**' + - '.github/workflows/test-app-build-cache.yml' + - '.github/actions/setup-fixture-app/**' workflow_dispatch: permissions: diff --git a/.oxfmtrc.json b/.oxfmtrc.json index 0a32e688a5..0ae6cd6e0f 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -4,5 +4,13 @@ "trailingComma": "all", "printWidth": 100, "sortPackageJson": false, - "ignorePatterns": ["dist/**", "node_modules/**", "**/.skillgym-results/**"] + "ignorePatterns": [ + "dist/**", + "node_modules/**", + "**/.skillgym-results/**", + "**/*.md", + "scripts/maestro-conformance/corpus/**", + "fallow-baselines/**", + "mutation-baselines/**" + ] } diff --git a/AGENTS.md b/AGENTS.md index 3cfa3e49aa..3bee709457 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -155,9 +155,10 @@ one question so `rg` → read-whole-file stays one cheap bounded read. `tsconfig.lib.json` (it needs an explicit `rootDir: "./src"`) and `tsdown.config.ts` first, and run `pnpm check:tooling` for any build-tooling edit. - Prefer the aggregate `package.json` scripts; they encode the expected validation bundles better - than ad hoc command lists — including which paths get formatted: run `pnpm format`, never - `oxfmt `, or you will reformat files the repo deliberately leaves alone (`scripts/`, - every `.md`). + than ad hoc command lists. `pnpm format` formats the whole repo (`oxfmt` with no path list), and + the only exclusion list is `.oxfmtrc.json` `ignorePatterns` — Markdown, the Maestro conformance + corpus, and generated baselines. Run `pnpm format`, never `oxfmt `: a path argument + reformats a subset and hides whatever else drifted. - Before pushing, the aggregate is **`pnpm check`** (`check:tooling && check:fallow && check:unit`). `pnpm check:tooling` is a *subset*: it stops before the Fallow audit, so dead exports and complexity findings your diff introduces still fail CI after it passes clean. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 38a9c8b721..6eaf4d82c1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,16 +7,22 @@ Thanks for your interest in contributing to agent-device. Requirements: - Node.js 22+ -- pnpm +- pnpm, activated from `package.json` `packageManager` (`corepack enable pnpm`) - Android SDK tools (`adb`) for Android support - Xcode (`simctl`/`devicectl`) for iOS support Setup: ```bash +corepack enable pnpm pnpm install ``` +`package.json` `packageManager` is the single source of truth for the pnpm version. Corepack +activates that exact version locally, and `.github/actions/setup-node-pnpm` reads the same field +and fails the job when the installed `pnpm --version` disagrees — so the CI and local package +managers cannot drift apart silently, and the "Ignoring...pnpm" drift warning stays quiet locally. + Build all CLIs and Xcode projects: ```bash @@ -162,6 +168,21 @@ Examples: // root. Revisit only if external artifacts get an ownership marker that makes cleanup safe. ``` +## Dependency Updates + +Renovate (`.github/renovate.json`) proposes dependency updates: weekly lockfile maintenance, one +grouped PR for devDependencies, one PR per runtime dependency, and digest bumps for GitHub Actions. +Security updates are enabled explicitly (`vulnerabilityAlerts` plus `osvVulnerabilityAlerts`) and +are the only updates exempt from the 7-day minimum release age. + +Renovate PRs are gated exactly like human PRs: `automerge` is off everywhere, so every branch needs +a green CI run and a human review before merge. Review one the way you would review any dependency +change — read the release notes in the PR body, and check that the affected-check plan for the diff +(`pnpm check:affected --base origin/main --run`, which fails open to the full set for lockfile and +workflow changes) is green. A green Renovate PR is a merge candidate, not a merge: no rubber-stamp +automerge path exists, and none should be added without also deciding which gate is trusted to +replace the reviewer. + ## Issue Labels Issue labels describe workflow state, not who will do the work. See diff --git a/examples/test-app/src/screens/AudioScreen.tsx b/examples/test-app/src/screens/AudioScreen.tsx index 3cb908d93f..dd75ddfeec 100644 --- a/examples/test-app/src/screens/AudioScreen.tsx +++ b/examples/test-app/src/screens/AudioScreen.tsx @@ -86,7 +86,10 @@ export function AudioScreen() { function playNativeSample() { stopNativeSample(); try { - const player = createAudioPlayer({ uri: createNativeBeepDataUri(), name: 'Agent Device beep' }); + const player = createAudioPlayer({ + uri: createNativeBeepDataUri(), + name: 'Agent Device beep', + }); nativePlayerRef.current = player; player.play(); setPlaybackState('playing'); @@ -304,7 +307,8 @@ function base64Encode(bytes: Uint8Array): string { const third = bytes[index + 2]; output += alphabet[first >> 2]; output += alphabet[((first & 0x03) << 4) | ((second ?? 0) >> 4)]; - output += index + 1 < bytes.length ? alphabet[((second & 0x0f) << 2) | ((third ?? 0) >> 6)] : '='; + output += + index + 1 < bytes.length ? alphabet[((second & 0x0f) << 2) | ((third ?? 0) >> 6)] : '='; output += index + 2 < bytes.length ? alphabet[(third ?? 0) & 0x3f] : '='; } return output; diff --git a/examples/test-app/src/screens/InertScreen.tsx b/examples/test-app/src/screens/InertScreen.tsx index 1103d6ecbe..70fdb987b1 100644 --- a/examples/test-app/src/screens/InertScreen.tsx +++ b/examples/test-app/src/screens/InertScreen.tsx @@ -36,8 +36,8 @@ export function InertScreen() { Tapping this text changes nothing on screen. - Nothing here reacts to touch, loads, or animates, so a tap leaves the - hierarchy byte-identical and retryTapIfNoChange must re-tap. + Nothing here reacts to touch, loads, or animates, so a tap leaves the hierarchy + byte-identical and retryTapIfNoChange must re-tap. ); diff --git a/package.json b/package.json index fdd3383f06..f64ddda8cb 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "url": "https://github.com/callstack/agent-device/issues" }, "type": "module", - "packageManager": "pnpm@11.1.2", + "packageManager": "pnpm@11.17.0", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", "exports": { @@ -114,8 +114,8 @@ "mutation:affected": "node --experimental-strip-types scripts/mutation/run.ts --affected", "mutation:test": "node --experimental-strip-types --test scripts/mutation/*.test.ts", "lint": "oxlint . --deny-warnings", - "format": "node ./node_modules/oxfmt/bin/oxfmt --write src test skills scripts/fuzz scripts/mutation scripts/lib scripts/help-conformance-bench.mjs scripts/help-conformance-case-checks.mjs scripts/help-conformance-command-validator.ts scripts/help-conformance-expectations.mjs scripts/help-conformance-plan-validator.mjs scripts/help-conformance-runner-output.mjs scripts/help-conformance-summary.mjs scripts/help-conformance-cases.mjs scripts/help-conformance-sample-outputs.mjs scripts/__tests__/help-conformance-bench.test.ts scripts/__tests__/help-conformance-sample-outputs.test.ts scripts/__tests__/help-conformance-topic-coverage.test.ts package.json tsconfig.json tsconfig.lib.json tsdown.config.ts vitest.config.ts vitest.mutation.config.ts .github/actions/setup-node-pnpm/action.yml .oxlintrc.json .oxfmtrc.json '!test/skillgym/.skillgym-results/**'", - "format:check": "node ./node_modules/oxfmt/bin/oxfmt --check src test skills scripts/fuzz scripts/mutation scripts/lib scripts/help-conformance-bench.mjs scripts/help-conformance-case-checks.mjs scripts/help-conformance-command-validator.ts scripts/help-conformance-expectations.mjs scripts/help-conformance-plan-validator.mjs scripts/help-conformance-runner-output.mjs scripts/help-conformance-summary.mjs scripts/help-conformance-cases.mjs scripts/help-conformance-sample-outputs.mjs scripts/__tests__/help-conformance-bench.test.ts scripts/__tests__/help-conformance-sample-outputs.test.ts scripts/__tests__/help-conformance-topic-coverage.test.ts package.json tsconfig.json tsconfig.lib.json tsdown.config.ts vitest.config.ts vitest.mutation.config.ts .github/actions/setup-node-pnpm/action.yml .oxlintrc.json .oxfmtrc.json '!test/skillgym/.skillgym-results/**'", + "format": "node ./node_modules/oxfmt/bin/oxfmt --write", + "format:check": "node ./node_modules/oxfmt/bin/oxfmt --check", "fuzz:parsers": "node --experimental-strip-types scripts/fuzz/run.ts", "fallow": "fallow audit --base origin/main", "fallow:all": "fallow --summary", diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2bce251d8b..d45982f8a0 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,5 @@ packages: - - "website" + - 'website' # Keep non-interactive checks from downloading tooling or reinstalling dependencies implicitly. pmOnFail: warn verifyDepsBeforeRun: warn diff --git a/scripts/check-replay-compat-provenance.ts b/scripts/check-replay-compat-provenance.ts index c38c7eb304..df4dcd8fda 100644 --- a/scripts/check-replay-compat-provenance.ts +++ b/scripts/check-replay-compat-provenance.ts @@ -47,7 +47,9 @@ for (const tag of REPLAY_COMPAT_RELEASED_TAGS) { for (const entry of REPLAY_COMPAT_CORPUS) { if (!REPLAY_COMPAT_RELEASED_TAGS.includes(entry.recordedBy)) { - failures.push(`${entry.id}: recordedBy ${entry.recordedBy} is not in REPLAY_COMPAT_RELEASED_TAGS`); + failures.push( + `${entry.id}: recordedBy ${entry.recordedBy} is not in REPLAY_COMPAT_RELEASED_TAGS`, + ); continue; } if (entry.provenance.kind !== 'mined') continue; diff --git a/scripts/depgraph/build.ts b/scripts/depgraph/build.ts index 291c28848f..044811b7d6 100644 --- a/scripts/depgraph/build.ts +++ b/scripts/depgraph/build.ts @@ -37,7 +37,7 @@ type Payload = { lvl: number; cyc: number; }[]; -/** + /** * `[fromIndex, toIndex, kind, flags]`; kind 0=value 1=type 2=dynamic, * flags bit0=R5 back-edge, bit1=target also reachable at distance >= 2, bit2=R6 type inversion. */ @@ -71,9 +71,7 @@ function edgeKindCode(kind: GraphData['edges'][number]['kind']): number { */ function edgeFlags(edge: GraphData['edges'][number]): number { return ( - (edge.backEdge ? 1 : 0) | - (edge.transitivelyReachable ? 2 : 0) | - (edge.typeInversion ? 4 : 0) + (edge.backEdge ? 1 : 0) | (edge.transitivelyReachable ? 2 : 0) | (edge.typeInversion ? 4 : 0) ); } diff --git a/scripts/depgraph/model.test.ts b/scripts/depgraph/model.test.ts index f9c8d20567..c7186f3672 100644 --- a/scripts/depgraph/model.test.ts +++ b/scripts/depgraph/model.test.ts @@ -213,7 +213,11 @@ test('no tracked TypeScript source contains a raw NUL byte', () => { // output path, JSON shape or summary without anything failing. These run it as a subprocess, which // is the only way to cover argument handling and the file it actually writes. -function runBuild(args: readonly string[]): { status: number | null; stdout: string; stderr: string } { +function runBuild(args: readonly string[]): { + status: number | null; + stdout: string; + stderr: string; +} { const result = spawnSync( process.execPath, ['--experimental-strip-types', 'scripts/depgraph/build.ts', ...args], @@ -243,7 +247,10 @@ test('build.ts writes the default path and a summary consistent with the JSON', // The printed summary must agree with the payload it was derived from. const inversions = Object.values(payload.typeInversions).reduce((sum, n) => sum + n, 0); - assert.match(stdout, new RegExp(`${payload.generated.files} files, ${payload.generated.edges} edges`)); + assert.match( + stdout, + new RegExp(`${payload.generated.files} files, ${payload.generated.edges} edges`), + ); assert.match(stdout, new RegExp(`type-only spine inversions \\(R6\\): ${inversions}`)); const reachable = payload.edges.filter(([, , , flags]) => (flags & 2) !== 0).length; assert.match(stdout, new RegExp(`reachable at distance >= 2: ${reachable}`)); diff --git a/scripts/depgraph/model.ts b/scripts/depgraph/model.ts index 790fea2c20..591abf6fd8 100644 --- a/scripts/depgraph/model.ts +++ b/scripts/depgraph/model.ts @@ -66,7 +66,6 @@ export type GraphData = { typeInversions: Record; }; - function countLines(source: string): number { let lines = 1; for (let index = 0; index < source.length; index++) { @@ -317,9 +316,7 @@ function aggregateZones(nodes: ReadonlyMap): GraphData['zones * but the count feeding a CI equality check must not be able to drift for a reason unrelated to * layering. */ -export function typeInversionsByPair( - edges: readonly ResolvedImportEdge[], -): Record { +export function typeInversionsByPair(edges: readonly ResolvedImportEdge[]): Record { const seen = new Set(); const byPair = new Map(); for (const edge of edges) { diff --git a/scripts/layering/check.ts b/scripts/layering/check.ts index 15ae7d81b2..1d6733d9f9 100644 --- a/scripts/layering/check.ts +++ b/scripts/layering/check.ts @@ -83,7 +83,9 @@ function readSources(files: readonly string[]): Map { } function fileExists(file: string): boolean { - return fs.existsSync(path.join(repoRoot, file)) && fs.statSync(path.join(repoRoot, file)).isFile(); + return ( + fs.existsSync(path.join(repoRoot, file)) && fs.statSync(path.join(repoRoot, file)).isFile() + ); } function readSourceOrNull(file: string): string | null { @@ -443,7 +445,10 @@ function checkSessionStateOwnership(sources: ReadonlyMap): Viola // `node_modules` is always present — which is why this needs a gate rather than a convention. function checkZeroDepJobs(): Violation[] { const workflows = readSources( - execFileSync('git', ['ls-files', '.github/workflows/*.yml'], { cwd: repoRoot, encoding: 'utf8' }) + execFileSync('git', ['ls-files', '.github/workflows/*.yml'], { + cwd: repoRoot, + encoding: 'utf8', + }) .split('\n') .filter(Boolean), ); @@ -480,9 +485,7 @@ function checkZeroDepJobs(): Violation[] { } function sessionStateFieldCount(): number { - return ( - Object.keys(SESSION_STATE_FIELD_OWNERS).length + STORE_OWNED_SESSION_STATE_FIELDS.size - ); + return Object.keys(SESSION_STATE_FIELD_OWNERS).length + STORE_OWNED_SESSION_STATE_FIELDS.size; } /** R9 is growth-only, so a shrunk tree is reported rather than failed — see checkTypeCycleGrowth. */ diff --git a/scripts/layering/model.test.ts b/scripts/layering/model.test.ts index 84508d2317..6d10b243b7 100644 --- a/scripts/layering/model.test.ts +++ b/scripts/layering/model.test.ts @@ -395,11 +395,18 @@ test("the repo's own zero-dep jobs resolve without node_modules", () => { }; const exists = (file: string): boolean => read(file) !== null; - const jobs = zeroDepJobs(new Map([['.github/workflows/ci.yml', read('.github/workflows/ci.yml')!]]), exists); + const jobs = zeroDepJobs( + new Map([['.github/workflows/ci.yml', read('.github/workflows/ci.yml')!]]), + exists, + ); assert.ok(jobs.length > 0, 'expected ci.yml to still declare at least one zero-dep job'); for (const job of jobs) { assert.ok(job.entries.length > 0, `${job.job} must name an entry script`); - assert.deepEqual(uninstallableImports(job, read, exists), [], `${job.job} must reach no package`); + assert.deepEqual( + uninstallableImports(job, read, exists), + [], + `${job.job} must reach no package`, + ); } }); @@ -475,20 +482,24 @@ test('classification drift is reported in all three directions', () => { test('largestTypeCycleSize counts type-only cycles and ignores dynamic ones', () => { // Acyclic: every component is a single file, so the largest is 1 (not 0). const acyclic = resolveImportEdges( - new Map(Object.entries({ - 'src/core/a.ts': "import type { B } from '../contracts/b.ts';", - 'src/contracts/b.ts': 'export type B = 1;', - })), + new Map( + Object.entries({ + 'src/core/a.ts': "import type { B } from '../contracts/b.ts';", + 'src/contracts/b.ts': 'export type B = 1;', + }), + ), ); assert.equal(largestTypeCycleSize(acyclic), 1); // A three-file loop closed by type-only imports is exactly what R4 permits and R9 measures. const typeCycle = resolveImportEdges( - new Map(Object.entries({ - 'src/core/a.ts': "import type { B } from './b.ts';", - 'src/core/b.ts': "import type { C } from './c.ts';\nexport type B = 1;", - 'src/core/c.ts': "import type { A } from './a.ts';\nexport type C = 1;", - })), + new Map( + Object.entries({ + 'src/core/a.ts': "import type { B } from './b.ts';", + 'src/core/b.ts': "import type { C } from './c.ts';\nexport type B = 1;", + 'src/core/c.ts': "import type { A } from './a.ts';\nexport type C = 1;", + }), + ), ); assert.equal(largestTypeCycleSize(typeCycle), 3); @@ -496,20 +507,24 @@ test('largestTypeCycleSize counts type-only cycles and ignores dynamic ones', () // comprehension barrier, and R3 relies on dynamic imports existing. With no non-dynamic edge at // all no file enters the walk, so the floor here is 0 rather than 1 — specified, not incidental. const dynamicCycle = resolveImportEdges( - new Map(Object.entries({ - 'src/core/a.ts': "void import('./b.ts');", - 'src/core/b.ts': "void import('./a.ts');", - })), + new Map( + Object.entries({ + 'src/core/a.ts': "void import('./b.ts');", + 'src/core/b.ts': "void import('./a.ts');", + }), + ), ); assert.equal(largestTypeCycleSize(dynamicCycle), 0); // A value cycle counts too — R4 rejects it separately, so R9 must not be the thing that // notices, but it must not under-report either. const valueCycle = resolveImportEdges( - new Map(Object.entries({ - 'src/core/a.ts': "export { b } from './b.ts';", - 'src/core/b.ts': "export { a } from './a.ts';", - })), + new Map( + Object.entries({ + 'src/core/a.ts': "export { b } from './b.ts';", + 'src/core/b.ts': "export { a } from './a.ts';", + }), + ), ); assert.equal(largestTypeCycleSize(valueCycle), 2); }); diff --git a/scripts/layering/session-state.ts b/scripts/layering/session-state.ts index fdc268133e..0e6fd499fd 100644 --- a/scripts/layering/session-state.ts +++ b/scripts/layering/session-state.ts @@ -134,9 +134,7 @@ export type FieldClassificationDrift = { * Where the two ownership tables disagree with `SessionState` itself. Empty means every declared * field is classified exactly once and neither table names a field that no longer exists. */ -export function fieldClassificationDrift( - fields: readonly string[], -): FieldClassificationDrift[] { +export function fieldClassificationDrift(fields: readonly string[]): FieldClassificationDrift[] { const declared = new Set(fields); const owned = new Set(Object.keys(SESSION_STATE_FIELD_OWNERS)); const drift: FieldClassificationDrift[] = []; diff --git a/scripts/layering/zero-dep-jobs.ts b/scripts/layering/zero-dep-jobs.ts index 2e1efb6030..1edf6b2e2f 100644 --- a/scripts/layering/zero-dep-jobs.ts +++ b/scripts/layering/zero-dep-jobs.ts @@ -54,7 +54,9 @@ function runBlockEntries(run: string): string[] { function stepsOf(job: unknown): Record[] { if (job === null || typeof job !== 'object') return []; const steps = (job as Record)['steps']; - return Array.isArray(steps) ? steps.filter((step) => step !== null && typeof step === 'object') : []; + return Array.isArray(steps) + ? steps.filter((step) => step !== null && typeof step === 'object') + : []; } function skipsInstall(step: Record): boolean { @@ -96,7 +98,8 @@ export function zeroDepJobs( } } return jobs.sort( - (left, right) => left.workflow.localeCompare(right.workflow) || left.job.localeCompare(right.job), + (left, right) => + left.workflow.localeCompare(right.workflow) || left.job.localeCompare(right.job), ); } diff --git a/scripts/maestro-conformance/build-manifest.mjs b/scripts/maestro-conformance/build-manifest.mjs index 3786bfb5a7..1b3f08fa1b 100644 --- a/scripts/maestro-conformance/build-manifest.mjs +++ b/scripts/maestro-conformance/build-manifest.mjs @@ -17,23 +17,37 @@ const UPSTREAM_REPO_DIR = 'maestro-test/src/test/resources'; const NOTES = { 'bug-classes/percent-decimal-swipe': 'Bug class 1: decimal percentage rejection at parse.', - 'bug-classes/target-swipe-missing-direction': 'Bug class 2: target swipe requires an explicit direction.', - 'bug-classes/retry-over-cap': 'Bug class 3: retry maxRetries parses verbatim; the clamp is a layer-2 vector.', - 'bug-classes/settle-after-tap': 'Bug class 4: plain tap; settle ordering is a layer-3 differential.', + 'bug-classes/target-swipe-missing-direction': + 'Bug class 2: target swipe requires an explicit direction.', + 'bug-classes/retry-over-cap': + 'Bug class 3: retry maxRetries parses verbatim; the clamp is a layer-2 vector.', + 'bug-classes/settle-after-tap': + 'Bug class 4: plain tap; settle ordering is a layer-3 differential.', 'authored/runscript': 'Coverage: runScript file command (no self-contained upstream flow).', 'authored/runflow-main': 'Coverage: runFlow file include + provenance.', 'authored/runflow-child': 'Include target for runflow-main (not parsed as a top-level flow).', - 'authored/doubletap': 'Coverage: doubleTapOn (upstream 101 uses the unsupported retryTapIfNoChange option).', - 'authored/scroll-until-visible': 'Coverage: scrollUntilVisible (upstream 079 uses unsupported speed/visibilityPercentage).', - 'authored/extended-wait': 'Coverage: extendedWaitUntil (upstream 042 interpolates ${TIMEOUT} from a flow env block).', - 'authored/repeat': 'Coverage: repeat.times (upstream 053 also uses the unsupported evalScript command).', - 'authored/presskey': 'Coverage: pressKey supported keys (upstream 034 exercises many unsupported keycodes).', - 'authored/numeric-variable-tap': 'Coverage: tapOn repeat/delay/index numeric option fields accept ${VAR} tokens (upstream rejects repeat/delay as integer-typed; agent-device is lenient).', - 'authored/numeric-variable-doubletap': 'Coverage: doubleTapOn delay accepts a ${VAR} token (upstream rejects as integer-typed; agent-device is lenient).', - 'authored/numeric-variable-swipe': 'Coverage: swipe duration accepts a ${VAR} token (upstream rejects as integer-typed; agent-device is lenient).', - 'authored/numeric-variable-erase': 'Coverage: eraseText charactersToErase accepts a ${VAR} token (upstream rejects as integer-typed; agent-device is lenient).', - 'authored/numeric-variable-tap-index': 'Coverage: tapOn index accepts a ${VAR} token and projects identically through the canonical model.', - 'authored/numeric-variable-wait': 'Coverage: waitForAnimationToEnd timeout accepts a ${VAR} token and projects identically through the canonical model.', + 'authored/doubletap': + 'Coverage: doubleTapOn (upstream 101 uses the unsupported retryTapIfNoChange option).', + 'authored/scroll-until-visible': + 'Coverage: scrollUntilVisible (upstream 079 uses unsupported speed/visibilityPercentage).', + 'authored/extended-wait': + 'Coverage: extendedWaitUntil (upstream 042 interpolates ${TIMEOUT} from a flow env block).', + 'authored/repeat': + 'Coverage: repeat.times (upstream 053 also uses the unsupported evalScript command).', + 'authored/presskey': + 'Coverage: pressKey supported keys (upstream 034 exercises many unsupported keycodes).', + 'authored/numeric-variable-tap': + 'Coverage: tapOn repeat/delay/index numeric option fields accept ${VAR} tokens (upstream rejects repeat/delay as integer-typed; agent-device is lenient).', + 'authored/numeric-variable-doubletap': + 'Coverage: doubleTapOn delay accepts a ${VAR} token (upstream rejects as integer-typed; agent-device is lenient).', + 'authored/numeric-variable-swipe': + 'Coverage: swipe duration accepts a ${VAR} token (upstream rejects as integer-typed; agent-device is lenient).', + 'authored/numeric-variable-erase': + 'Coverage: eraseText charactersToErase accepts a ${VAR} token (upstream rejects as integer-typed; agent-device is lenient).', + 'authored/numeric-variable-tap-index': + 'Coverage: tapOn index accepts a ${VAR} token and projects identically through the canonical model.', + 'authored/numeric-variable-wait': + 'Coverage: waitForAnimationToEnd timeout accepts a ${VAR} token and projects identically through the canonical model.', 'invalid/bad-swipe-direction': 'Lenient-guard: unknown SwipeDirection enum value.', 'invalid/unknown-command': 'Lenient-guard: unknown command name (tapOn typo).', 'invalid/malformed-selector': 'Lenient-guard: selector given as a sequence.', @@ -46,7 +60,10 @@ const sha256 = (file) => createHash('sha256').update(fs.readFileSync(file)).dige const yamlIn = (dir) => fs.existsSync(path.join(CORPUS_DIR, dir)) - ? fs.readdirSync(path.join(CORPUS_DIR, dir)).filter((n) => n.endsWith('.yaml')).sort() + ? fs + .readdirSync(path.join(CORPUS_DIR, dir)) + .filter((n) => n.endsWith('.yaml')) + .sort() : []; function entriesFor(dir, kind) { diff --git a/scripts/maestro-conformance/differential/invariants.test.ts b/scripts/maestro-conformance/differential/invariants.test.ts index 35827ea9ec..3181ed287c 100644 --- a/scripts/maestro-conformance/differential/invariants.test.ts +++ b/scripts/maestro-conformance/differential/invariants.test.ts @@ -41,7 +41,10 @@ test('a tap that burns the full settle budget violates the invariant (bug class }); test('the invariant reports the slowest matching step, not the first', () => { - const result = evaluateInvariant([stop('tapOn', 300, 1), stop('tapOn', 2117, 2)], SETTLE_INVARIANT); + const result = evaluateInvariant( + [stop('tapOn', 300, 1), stop('tapOn', 2117, 2)], + SETTLE_INVARIANT, + ); assert.equal(result.status, 'violated'); assert.match(result.detail, /2117ms/); }); diff --git a/scripts/maestro-conformance/differential/run.test.ts b/scripts/maestro-conformance/differential/run.test.ts index 715fb3548c..e556db844b 100644 --- a/scripts/maestro-conformance/differential/run.test.ts +++ b/scripts/maestro-conformance/differential/run.test.ts @@ -22,7 +22,10 @@ test('every differential scenario references an existing corpus flow with a uniq test('the settle-loop bug class (4) is covered by a differential scenario', () => { const settle = DIFFERENTIAL_SCENARIOS.find((scenario) => scenario.bugClass === 4); - assert.ok(settle, 'bug class 4 (settle ordering) has no reflectable constant; it must be a differential scenario'); + assert.ok( + settle, + 'bug class 4 (settle ordering) has no reflectable constant; it must be a differential scenario', + ); assert.equal(settle?.id, 'settle-after-tap'); }); @@ -75,14 +78,22 @@ test('every knownDivergence carries a tracking issue', () => { // swallow upstream regressing or a different invariant breaking — hiding the // next bug behind the last one. describe('knownDivergence signature matching', () => { - const sig: DivergenceSignature = { maestro: 'pass', agentDevice: 'fail', invariants: ['no-data'] }; + const sig: DivergenceSignature = { + maestro: 'pass', + agentDevice: 'fail', + invariants: ['no-data'], + }; const engine = (outcome: 'pass' | 'fail') => ({ engine: 'maestro' as const, outcome, exitCode: outcome === 'pass' ? 0 : 1, }); const inv = (status: 'held' | 'violated' | 'no-data') => - ({ invariant: { kind: 'stepDurationBelow', command: 'tapOn', maxMs: 1, because: 'x' }, status, detail: '' }) as never; + ({ + invariant: { kind: 'stepDurationBelow', command: 'tapOn', maxMs: 1, because: 'x' }, + status, + detail: '', + }) as never; test('matches the declared failure exactly', () => { assert.equal(matchesSignature(sig, engine('pass'), engine('fail'), [inv('no-data')]), true); @@ -113,7 +124,10 @@ describe('knownDivergence signature matching', () => { for (const scenario of DIFFERENTIAL_SCENARIOS) { const declared = scenario.knownDivergence; if (!declared) continue; - assert.ok(declared.expected, `${scenario.id}: knownDivergence must declare an expected signature`); + assert.ok( + declared.expected, + `${scenario.id}: knownDivergence must declare an expected signature`, + ); assert.ok( ['pass', 'fail'].includes(declared.expected.maestro) && ['pass', 'fail'].includes(declared.expected.agentDevice), diff --git a/scripts/maestro-conformance/differential/run.ts b/scripts/maestro-conformance/differential/run.ts index 5ec8984208..4677236161 100644 --- a/scripts/maestro-conformance/differential/run.ts +++ b/scripts/maestro-conformance/differential/run.ts @@ -68,17 +68,18 @@ export function validateScenarios(): void { if (ids.has(scenario.id)) throw new Error(`Duplicate scenario id: ${scenario.id}`); ids.add(scenario.id); const flowPath = path.join(CONFORMANCE_DIR, scenario.flow); - if (!fs.existsSync(flowPath)) throw new Error(`Scenario ${scenario.id} flow not found: ${scenario.flow}`); + if (!fs.existsSync(flowPath)) + throw new Error(`Scenario ${scenario.id} flow not found: ${scenario.flow}`); } } -type EngineResult = { engine: 'maestro' | 'agent-device'; outcome: 'pass' | 'fail'; exitCode: number }; +type EngineResult = { + engine: 'maestro' | 'agent-device'; + outcome: 'pass' | 'fail'; + exitCode: number; +}; -function runEngine( - engine: EngineResult['engine'], - command: string, - args: string[], -): EngineResult { +function runEngine(engine: EngineResult['engine'], command: string, args: string[]): EngineResult { const [bin = '', ...rest] = command.split(' ').filter(Boolean); const result = spawnSync(bin, [...rest, ...args], { stdio: 'inherit', cwd: process.cwd() }); const exitCode = result.status ?? 1; @@ -255,7 +256,9 @@ function main(argv: readonly string[]): void { // Keep declared gaps visible: a green run must still say what it is not proving. const known = reports.filter((report) => report.status === 'known-divergence'); if (known.length > 0) { - console.log(`\n${known.length} declared divergence(s), not enforced: ${known.map((r) => r.id).join(', ')}`); + console.log( + `\n${known.length} declared divergence(s), not enforced: ${known.map((r) => r.id).join(', ')}`, + ); } const failed = reports.filter((report) => report.failed); diff --git a/scripts/maestro-conformance/expected-divergence.ts b/scripts/maestro-conformance/expected-divergence.ts index 7ea4668309..f0ffbdbc97 100644 --- a/scripts/maestro-conformance/expected-divergence.ts +++ b/scripts/maestro-conformance/expected-divergence.ts @@ -32,7 +32,8 @@ export const FLOW_DIVERGENCES: Record = { }, 'upstream/062_copy_paste_text': { classification: 'we-reject', - reason: 'Clipboard commands are unsupported; pasteText is de-advertised (takes clipboard, not inline text).', + reason: + 'Clipboard commands are unsupported; pasteText is de-advertised (takes clipboard, not inline text).', unsupported: ['copyTextFrom', 'pasteText'], }, 'upstream/067_assertTrue_pass': { @@ -58,7 +59,8 @@ export const FLOW_DIVERGENCES: Record = { }, 'upstream/053_repeat_times': { classification: 'we-reject', - reason: 'repeat is supported, but the flow also uses evalScript and a ${output.list.length} times expression.', + reason: + 'repeat is supported, but the flow also uses evalScript and a ${output.list.length} times expression.', unsupported: ['evalScript'], }, // --- Deliberately stricter than upstream --- @@ -83,23 +85,27 @@ export const FLOW_DIVERGENCES: Record = { }, 'upstream/076_optional_assertion': { classification: 'we-reject', - reason: 'assertTrue is outside the supported subset; optional is now supported on scrollUntilVisible and extendedWaitUntil.', + reason: + 'assertTrue is outside the supported subset; optional is now supported on scrollUntilVisible and extendedWaitUntil.', unsupported: ['assertTrue'], tracking: 'https://github.com/callstack/agent-device/issues/1295', }, 'upstream/079_scroll_until_visible': { classification: 'we-reject', - reason: 'scrollUntilVisible is supported; the flow sets the unsupported speed and visibilityPercentage options.', + reason: + 'scrollUntilVisible is supported; the flow sets the unsupported speed and visibilityPercentage options.', unsupported: ['scrollUntilVisible.speed', 'scrollUntilVisible.visibilityPercentage'], }, 'upstream/101_doubleTapOn': { classification: 'we-reject', - reason: 'doubleTapOn is supported; the flow sets the unsupported retryTapIfNoChange option on it.', + reason: + 'doubleTapOn is supported; the flow sets the unsupported retryTapIfNoChange option on it.', unsupported: ['doubleTapOn.retryTapIfNoChange'], }, 'upstream/119_retry_commands': { classification: 'we-reject', - reason: 'retry is supported; the flow uses the unsupported per-command waitToSettleTimeoutMs option on a nested tapOn.', + reason: + 'retry is supported; the flow uses the unsupported per-command waitToSettleTimeoutMs option on a nested tapOn.', unsupported: ['tapOn.waitToSettleTimeoutMs'], }, // --- agent-device supports ${VAR} in integer-typed numeric option fields that the pinned upstream parser rejects --- diff --git a/scripts/maestro-conformance/normalize.ts b/scripts/maestro-conformance/normalize.ts index d906ef07e5..5d90aa5a74 100644 --- a/scripts/maestro-conformance/normalize.ts +++ b/scripts/maestro-conformance/normalize.ts @@ -33,7 +33,12 @@ export type CanonicalTarget = { export type CanonicalGesture = | { mode: 'direction'; direction: string; duration?: number | string } - | { mode: 'coordinates'; start?: CanonicalPoint; end?: CanonicalPoint; duration?: number | string } + | { + mode: 'coordinates'; + start?: CanonicalPoint; + end?: CanonicalPoint; + duration?: number | string; + } | { mode: 'element'; from: CanonicalSelector; direction?: string; duration?: number | string }; export type CanonicalCommand = @@ -42,14 +47,31 @@ export type CanonicalCommand = // COUNT is the canonical field on both sides rather than a `double` variant on // one — that keeps our distinct tapOn/doubleTapOn kinds comparable to upstream // and preserves conformance signal for tapOn.repeat/delay. - | { kind: 'tap'; longPress: boolean; repeat: number | string; delay?: number | string; target: CanonicalTarget } - | { kind: 'assert'; mode: 'visible' | 'notVisible'; timed: boolean; timeout?: number | string; selector?: CanonicalSelector } + | { + kind: 'tap'; + longPress: boolean; + repeat: number | string; + delay?: number | string; + target: CanonicalTarget; + } + | { + kind: 'assert'; + mode: 'visible' | 'notVisible'; + timed: boolean; + timeout?: number | string; + selector?: CanonicalSelector; + } | { kind: 'swipe'; gesture: CanonicalGesture } | { kind: 'inputText'; text?: string } | { kind: 'eraseText'; count?: number | string } | { kind: 'openLink'; link?: string } | { kind: 'scroll' } - | { kind: 'scrollUntilVisible'; direction?: string; selector?: CanonicalSelector; timeout?: number | string } + | { + kind: 'scrollUntilVisible'; + direction?: string; + selector?: CanonicalSelector; + timeout?: number | string; + } | { kind: 'pressKey'; key: string } | { kind: 'back' } | { kind: 'hideKeyboard' } @@ -141,7 +163,10 @@ function canonicalizeUpstreamCommand(command: UpstreamCommand): CanonicalCommand case 'InputTextCommand': return dropUndefined({ kind: 'inputText', text: str(f.text) }); case 'EraseTextCommand': - return dropUndefined({ kind: 'eraseText', count: numLike(f.charactersToErase) ?? str(f.charactersToErase) }); + return dropUndefined({ + kind: 'eraseText', + count: numLike(f.charactersToErase) ?? str(f.charactersToErase), + }); case 'OpenLinkCommand': return dropUndefined({ kind: 'openLink', link: str(f.link) }); case 'PressKeyCommand': @@ -153,13 +178,19 @@ function canonicalizeUpstreamCommand(command: UpstreamCommand): CanonicalCommand case 'TakeScreenshotCommand': return { kind: 'takeScreenshot' }; case 'WaitForAnimationToEndCommand': - return dropUndefined({ kind: 'waitForAnimationToEnd', timeout: numLike(f.timeout) ?? str(f.timeout) }); + return dropUndefined({ + kind: 'waitForAnimationToEnd', + timeout: numLike(f.timeout) ?? str(f.timeout), + }); case 'StopAppCommand': return { kind: 'stopApp' }; case 'RepeatCommand': return { kind: 'repeat', times: numLike(f.times) ?? str(f.times) ?? '' }; case 'RetryCommand': - return dropUndefined({ kind: 'retry', maxRetries: numLike(f.maxRetries) ?? str(f.maxRetries) }); + return dropUndefined({ + kind: 'retry', + maxRetries: numLike(f.maxRetries) ?? str(f.maxRetries), + }); case 'RunFlowCommand': return { kind: 'runFlow', source: f.sourceDescription != null ? 'file' : 'commands' }; case 'RunScriptCommand': @@ -335,7 +366,9 @@ function canonicalizeAgentCommand( return canonicalTap({ longPress: false, repeat, - delay: repeatIsNumber ? (numLike(command.delay) ?? AGENT_REPEAT_DELAY_MS) : numLike(command.delay), + delay: repeatIsNumber + ? (numLike(command.delay) ?? AGENT_REPEAT_DELAY_MS) + : numLike(command.delay), target: agentTarget(command.target, numLike(command.index), command.childOf), }); } @@ -403,7 +436,10 @@ function canonicalizeAgentCommand( case 'repeat': return { kind: 'repeat', times: numLike(command.times) ?? str(command.times) }; case 'retry': - return dropUndefined({ kind: 'retry', maxRetries: numLike(command.maxRetries) ?? str(command.maxRetries) }); + return dropUndefined({ + kind: 'retry', + maxRetries: numLike(command.maxRetries) ?? str(command.maxRetries), + }); case 'runFlow': return { kind: 'runFlow', source: command.include.kind === 'file' ? 'file' : 'commands' }; case 'runScript': @@ -429,7 +465,9 @@ function agentTarget( }), }; } - return { point: { x: target.x, y: target.y, unit: target.space === 'percent' ? 'percent' : 'px' } }; + return { + point: { x: target.x, y: target.y, unit: target.space === 'percent' ? 'percent' : 'px' }, + }; } function agentSelector( @@ -468,6 +506,14 @@ function agentGesture(gesture: MaestroSwipeGesture): CanonicalGesture { } } -function agentPoint(coordinate: { space: 'absolute' | 'percent'; x: number; y: number }): CanonicalPoint { - return { x: coordinate.x, y: coordinate.y, unit: coordinate.space === 'percent' ? 'percent' : 'px' }; +function agentPoint(coordinate: { + space: 'absolute' | 'percent'; + x: number; + y: number; +}): CanonicalPoint { + return { + x: coordinate.x, + y: coordinate.y, + unit: coordinate.space === 'percent' ? 'percent' : 'px', + }; } diff --git a/scripts/maestro-conformance/regenerate.mjs b/scripts/maestro-conformance/regenerate.mjs index b6fca17867..592d432eb1 100644 --- a/scripts/maestro-conformance/regenerate.mjs +++ b/scripts/maestro-conformance/regenerate.mjs @@ -37,12 +37,16 @@ function gradle(args, options = {}) { const [cmd, baseArgs] = override ? [override, []] : [process.platform === 'win32' ? 'gradlew.bat' : './gradlew', []]; - return execFileSync(cmd, [...baseArgs, '-p', HARNESS_DIR, '--no-daemon', '--console=plain', '-q', ...args], { - cwd: HARNESS_DIR, - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'inherit'], - ...options, - }); + return execFileSync( + cmd, + [...baseArgs, '-p', HARNESS_DIR, '--no-daemon', '--console=plain', '-q', ...args], + { + cwd: HARNESS_DIR, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'inherit'], + ...options, + }, + ); } function sha256(filePath) { @@ -58,7 +62,8 @@ function verifyArtifacts(pin) { } for (const artifact of pin.artifacts) { const jarPath = resolved.get(artifact.coordinate); - if (!jarPath) throw new Error(`Pinned artifact ${artifact.coordinate} was not resolved by Gradle.`); + if (!jarPath) + throw new Error(`Pinned artifact ${artifact.coordinate} was not resolved by Gradle.`); const actual = sha256(jarPath); if (actual !== artifact.sha256) { throw new Error( @@ -92,7 +97,9 @@ function writeFixture(name, pin, content) { function main() { const pin = readPin(); - console.log(`Regenerating Maestro ${pin.version} conformance fixtures (${pin.commit.slice(0, 12)}).`); + console.log( + `Regenerating Maestro ${pin.version} conformance fixtures (${pin.commit.slice(0, 12)}).`, + ); // Refresh corpus provenance first so a newly added flow is picked up. const manifest = writeManifest(pin); @@ -110,7 +117,9 @@ function main() { } finally { fs.rmSync(outDir, { recursive: true, force: true }); } - console.log('Done. Review the diff, then run `node --experimental-strip-types scripts/maestro-conformance/verify.test.ts`.'); + console.log( + 'Done. Review the diff, then run `node --experimental-strip-types scripts/maestro-conformance/verify.test.ts`.', + ); } main(); diff --git a/scripts/maestro-conformance/verify.test.ts b/scripts/maestro-conformance/verify.test.ts index 046d8301b9..105699f8f6 100644 --- a/scripts/maestro-conformance/verify.test.ts +++ b/scripts/maestro-conformance/verify.test.ts @@ -63,11 +63,16 @@ test('the seal rejects an edited capture (proof the check has teeth)', () => { test('fixtures pin the reviewed upstream Maestro artifacts', () => { for (const fixture of [loadLayer1(), loadLayer2()] as Array<{ upstream?: unknown }>) { - const upstream = (fixture as { upstream: { version: string; commit: string; artifacts: unknown } }) - .upstream; + const upstream = ( + fixture as { upstream: { version: string; commit: string; artifacts: unknown } } + ).upstream; assert.equal(upstream.version, PIN.version, 'fixture must pin the reviewed version'); assert.equal(upstream.commit, PIN.commit, 'fixture must pin the reviewed commit'); - assert.deepEqual(upstream.artifacts, PIN.artifacts, 'fixture jar SHAs must match pinned-upstream.json'); + assert.deepEqual( + upstream.artifacts, + PIN.artifacts, + 'fixture jar SHAs must match pinned-upstream.json', + ); } }); @@ -104,21 +109,33 @@ test('layer 1: every divergence is declared (no silent drift)', () => { for (const flow of flows) { const declared = FLOW_DIVERGENCES[flow.id]; if (flow.classification === 'identical' || flow.classification === 'both-reject') { - if (declared) problems.push(`${flow.id}: declared divergence but classified ${flow.classification}`); + if (declared) + problems.push(`${flow.id}: declared divergence but classified ${flow.classification}`); continue; } if (!declared) { - problems.push(`${flow.id}: undeclared ${flow.classification}${flow.detail ? `\n ${flow.detail}` : ''}`); + problems.push( + `${flow.id}: undeclared ${flow.classification}${flow.detail ? `\n ${flow.detail}` : ''}`, + ); continue; } if (declared.classification !== flow.classification) { - problems.push(`${flow.id}: declared ${declared.classification} but classified ${flow.classification}`); + problems.push( + `${flow.id}: declared ${declared.classification} but classified ${flow.classification}`, + ); } - if (flow.classification === 'we-reject' && !(declared.unsupported && declared.unsupported.length > 0)) { + if ( + flow.classification === 'we-reject' && + !(declared.unsupported && declared.unsupported.length > 0) + ) { problems.push(`${flow.id}: we-reject entries must list the unsupported command(s)/option(s)`); } } - assert.deepEqual(problems, [], `Undeclared or mismatched divergences:\n ${problems.join('\n ')}`); + assert.deepEqual( + problems, + [], + `Undeclared or mismatched divergences:\n ${problems.join('\n ')}`, + ); }); test('layer 1: no stale divergence declarations', () => { @@ -138,7 +155,9 @@ test('layer 2: generated semantic vectors match live engine constants', () => { // Every reference-only vector must be an on-the-record deviation. for (const result of results) { if (result.status === 'reference-only') { - const documented = DOCUMENTED_DEVIATIONS.some((d) => d.description.includes(result.id) || LAYER2_REFERENCE_ONLY.has(result.id)); + const documented = DOCUMENTED_DEVIATIONS.some( + (d) => d.description.includes(result.id) || LAYER2_REFERENCE_ONLY.has(result.id), + ); assert.ok(documented, `reference-only vector ${result.id} must be a documented deviation`); } } @@ -181,7 +200,9 @@ test('bug class 4: settle default parses identically; ordering is a layer-3 diff // sleep-after-capture ordering is verified by the layer-3 differential scenario. const layer2 = loadLayer2(); assert.ok( - !layer2.constants.some((constant) => /settle/i.test(constant.id) && constant.id !== 'iosScreenSettleTimeoutMs'), + !layer2.constants.some( + (constant) => /settle/i.test(constant.id) && constant.id !== 'iosScreenSettleTimeoutMs', + ), 'no upstream settle-loop constant exists to cross-check; keep this as layer 3', ); }); diff --git a/scripts/maestro-conformance/verify.ts b/scripts/maestro-conformance/verify.ts index edaae87f87..a6e01aab05 100644 --- a/scripts/maestro-conformance/verify.ts +++ b/scripts/maestro-conformance/verify.ts @@ -112,7 +112,10 @@ function agentParseProgram(file: string): MaestroProgram | null { } } -function agentParse(file: string): { status: 'parsed' | 'rejected'; commands?: CanonicalCommand[] } { +function agentParse(file: string): { + status: 'parsed' | 'rejected'; + commands?: CanonicalCommand[]; +} { const program = agentParseProgram(file); if (!program) return { status: 'rejected' }; return { status: 'parsed', commands: canonicalizeAgentCommands(program) }; @@ -121,7 +124,12 @@ function agentParse(file: string): { status: 'parsed' | 'rejected'; commands?: C function classifyFlow(fixtureFlow: Layer1Fixture['flows'][number]): FlowResult { const agent = agentParse(fixtureFlow.file); const upstreamStatus = fixtureFlow.status; - const base = { id: fixtureFlow.id, file: fixtureFlow.file, upstreamStatus, agentStatus: agent.status }; + const base = { + id: fixtureFlow.id, + file: fixtureFlow.file, + upstreamStatus, + agentStatus: agent.status, + }; if (upstreamStatus === 'rejected') { return { @@ -208,7 +216,10 @@ export function agentKindsByCorpus(): Set { return kinds; } -function collectKinds(commands: Array<{ kind: string; commands?: unknown }>, into: Set): void { +function collectKinds( + commands: Array<{ kind: string; commands?: unknown }>, + into: Set, +): void { for (const command of commands) { into.add(command.kind); const nested = (command as { commands?: Array<{ kind: string }> }).commands; @@ -246,7 +257,9 @@ function report(): void { } console.log('\n### layer 2'); for (const result of checkLayer2()) { - console.log(` ${result.status.padEnd(14)} ${result.id} upstream=${result.upstream} agent=${result.agent ?? '-'}`); + console.log( + ` ${result.status.padEnd(14)} ${result.id} upstream=${result.upstream} agent=${result.agent ?? '-'}`, + ); } console.log('\n### coverage gaps'); for (const result of checkCoverage()) { diff --git a/scripts/mutation/workflow.test.ts b/scripts/mutation/workflow.test.ts index 2cbeb27887..28322a9417 100644 --- a/scripts/mutation/workflow.test.ts +++ b/scripts/mutation/workflow.test.ts @@ -56,9 +56,11 @@ test('no mutation shard is allowed to exceed the 30-minute budget', () => { }); test('every kernel path a PR can touch selects the affected mutation job', () => { - const paths = [...workflow('mutation-affected.yml').matchAll(/^ {6}- "(?[^"]+)"$/gm)].map( - (match) => match.groups!.glob, - ); + // Quote style is the formatter's business (oxfmt formats the workflow tree), so + // accept either spelling of the same scalar rather than pinning this gate to it. + const paths = [ + ...workflow('mutation-affected.yml').matchAll(/^ {6}- (?['"])(?[^'"]+)\k$/gm), + ].map((match) => match.groups!.glob); for (const module of KERNEL_MODULES) { for (const owned of module.owns) { const selected = paths.some( diff --git a/scripts/perf/report.ts b/scripts/perf/report.ts index 3d863a5ff8..f02763db62 100644 --- a/scripts/perf/report.ts +++ b/scripts/perf/report.ts @@ -33,12 +33,16 @@ function toMarkdown(run: RunResult): string { lines.push(`- **Finished**: ${run.finishedAt}`); lines.push(''); lines.push('All times in milliseconds. `wall-clock` includes process spawn + socket overhead;'); - lines.push('`daemon` is the batch step round-trip (spawn overhead ≈ wall-median − daemon-median).'); + lines.push( + '`daemon` is the batch step round-trip (spawn overhead ≈ wall-median − daemon-median).', + ); lines.push('`elements` = node count in the snapshot payload (tree-size proxy).'); lines.push('An untimed warmup interaction runs after each open/relaunch, so measured commands'); lines.push('do not pay the one-time iOS-runner startup or post-relaunch first-AX-query cost.'); lines.push(''); - lines.push('| command | cli | mode | n | wall min | wall median | wall p95 | wall max | daemon median | elements | notes |'); + lines.push( + '| command | cli | mode | n | wall min | wall median | wall p95 | wall max | daemon median | elements | notes |', + ); lines.push('|---|---|---|---|---|---|---|---|---|---|---|'); for (const m of run.measurements) lines.push(measurementRow(m)); lines.push(''); @@ -49,7 +53,9 @@ function toMarkdown(run: RunResult): string { lines.push(''); for (const m of failed) { const sample = m.samples.find((s) => !s.ok); - lines.push(`- **${m.label}** — ${m.notes.join('; ')}${sample?.errorMessage ? ` — ${sample.errorMessage}` : ''}`); + lines.push( + `- **${m.label}** — ${m.notes.join('; ')}${sample?.errorMessage ? ` — ${sample.errorMessage}` : ''}`, + ); } lines.push(''); } diff --git a/scripts/perf/run.ts b/scripts/perf/run.ts index 9a00105368..330d618a38 100644 --- a/scripts/perf/run.ts +++ b/scripts/perf/run.ts @@ -1,7 +1,12 @@ import fs from 'node:fs'; import path from 'node:path'; import { parseConfig, REPO_ROOT, usesSourceCli } from './config.ts'; -import { runScenario, setupIsolation, teardownIsolation, type IsolationContext } from './harness.ts'; +import { + runScenario, + setupIsolation, + teardownIsolation, + type IsolationContext, +} from './harness.ts'; import { writeReports } from './report.ts'; import type { RunResult } from './types.ts'; diff --git a/scripts/sync-mcp-metadata.mjs b/scripts/sync-mcp-metadata.mjs index 2b43661e73..856b51372a 100644 --- a/scripts/sync-mcp-metadata.mjs +++ b/scripts/sync-mcp-metadata.mjs @@ -19,7 +19,10 @@ if (typeof expectedName !== 'string' || expectedName.length === 0) { if (typeof expectedVersion !== 'string' || expectedVersion.length === 0) { fail('package.json must define version.'); } -if (typeof server.description === 'string' && server.description.length > registryDescriptionMaxLength) { +if ( + typeof server.description === 'string' && + server.description.length > registryDescriptionMaxLength +) { fail(`server.json description must be ${registryDescriptionMaxLength} characters or fewer.`); } diff --git a/scripts/vitest-slow-test-reporter.ts b/scripts/vitest-slow-test-reporter.ts index 34aa6bbc7c..c55b0ad621 100644 --- a/scripts/vitest-slow-test-reporter.ts +++ b/scripts/vitest-slow-test-reporter.ts @@ -53,10 +53,7 @@ export function classifySlowTest(params: { } /** Render the gate outcome; returns true when the run must fail. */ -export function reportSlowTests( - offenders: Offender[], - write: (message: string) => void, -): boolean { +export function reportSlowTests(offenders: Offender[], write: (message: string) => void): boolean { if (offenders.length === 0) return false; const sorted = [...offenders].sort((a, b) => b.durationMs - a.durationMs); const line = (o: Offender): string => @@ -71,7 +68,7 @@ export function reportSlowTests( } if (failing.length === 0) return false; write( - `\nSlow-test gate: ${failing.length} test(s) exceeded ${ENFORCE_FACTOR}x the wall-clock budget.\n` + + `\nSlow-test gate: ${failing.length} test(s) exceeded ${ENFORCE_FACTOR}x the wall-clock budget.\n` + `Tests must not wait real time — inject the timeout/poll budget or assert the budget is\n` + `wired instead of waiting it out (docs/agents/testing.md). If the runtime cost is genuinely\n` + `irreducible, document the reason in the owning test or move it out of the unit lane.\n` + diff --git a/smithery.yaml b/smithery.yaml index 322550ef7b..8c93739c2b 100644 --- a/smithery.yaml +++ b/smithery.yaml @@ -1 +1 @@ -runtime: "typescript" +runtime: 'typescript' diff --git a/website/rspress.config.ts b/website/rspress.config.ts index dfa58d4937..b35d27843d 100644 --- a/website/rspress.config.ts +++ b/website/rspress.config.ts @@ -23,8 +23,9 @@ export default withCallstackPreset( theme: { content: { outlineCTAHeadline: 'Curious about developing mobile apps with AI agents?', - outlineCTADescription: 'We can help you take your agentic workflows to the next level and ship faster.', - outlineCTAButtonText: "Book a call", + outlineCTADescription: + 'We can help you take your agentic workflows to the next level and ship faster.', + outlineCTAButtonText: 'Book a call', }, }, },