vybekiit 0.7.20 → 0.7.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/dist/bin.js +11 -5
  2. package/dist/global-skills/aws-cloudformation/SKILL.md +173 -7
  3. package/dist/global-skills/aws-cloudformation/references/cloudformation-pre-deploy-validation.script.md +107 -45
  4. package/dist/global-skills/aws-cloudformation/references/lookup-resource-properties.script.md +1 -1
  5. package/dist/global-skills/aws-cloudformation/references/persist-template-context.script.md +310 -0
  6. package/dist/global-skills/aws-cloudformation/references/retrieve-template-context.script.md +357 -0
  7. package/dist/global-skills/aws-cloudformation/references/template-safety-guidance.md +174 -0
  8. package/dist/global-skills/aws-cloudformation/references/validate-cloudformation-template.script.md +20 -2
  9. package/dist/global-skills/aws-serverless/SKILL.md +16 -1
  10. package/dist/global-skills/eas-simulator/references/controllers.md +21 -1
  11. package/dist/global-skills/eas-simulator/references/run-your-app.md +103 -69
  12. package/dist/global-skills/eas-simulator/references/troubleshooting.md +7 -4
  13. package/dist/global-skills/neon/SKILL.md +40 -17
  14. package/dist/global-skills/neon/references/claimable-neon.md +91 -0
  15. package/dist/global-skills/neon-object-storage/SKILL.md +11 -4
  16. package/dist/global-skills/onboarding/SKILL.md +5 -1
  17. package/dist/global-skills/resend/references/api-keys.md +1 -1
  18. package/dist/global-skills/resend/references/segments.md +19 -3
  19. package/package.json +8 -8
  20. package/dist/global-skills/email-best-practices/.github/workflows/sync-skills.yml +0 -30
  21. package/dist/global-skills/email-best-practices/README.md +0 -63
  22. package/dist/global-skills/email-best-practices/tests/README.md +0 -35
  23. package/dist/global-skills/email-best-practices/tests/scenarios/01-spam-deliverability.md +0 -46
  24. package/dist/global-skills/email-best-practices/tests/scenarios/02-multi-region-compliance.md +0 -48
  25. package/dist/global-skills/email-best-practices/tests/scenarios/03-retry-idempotency.md +0 -36
  26. package/dist/global-skills/email-best-practices/tests/scenarios/04-webhook-bounce-handling.md +0 -52
  27. package/dist/global-skills/email-best-practices/tests/scenarios/05-new-saas-email-plan.md +0 -51
@@ -0,0 +1,174 @@
1
+ # Template Safety Guidance
2
+
3
+ - [Cross-Stack Reference Safety](#cross-stack-reference-safety)
4
+ - [Conditional Resource Coupling](#conditional-resource-coupling)
5
+ - [Security Group Blast Radius](#security-group-blast-radius)
6
+ - [DeletionPolicy Preservation for Stateful
7
+ Resources](#deletionpolicy-preservation-for-stateful-resources)
8
+ - [Parameter Propagation for New
9
+ Resources](#parameter-propagation-for-new-resources)
10
+ - [Template Size Limits](#template-size-limits)
11
+
12
+ ## Cross-Stack Reference Safety
13
+
14
+ **Never rename or remove an exported Output without checking for Fn::ImportValue
15
+ consumers.**
16
+
17
+ When a template has `Outputs` with `Export.Name`, other stacks may depend on
18
+ that export via `Fn::ImportValue`. Renaming or removing the export will cause
19
+ immediate deployment failures in all consuming stacks.
20
+
21
+ Before modifying any exported output:
22
+
23
+ 1. Check `Metadata."com.aws.cloudformation.Context"` for documented consumers
24
+ 2. If no context exists, warn the user that downstream stacks may break
25
+ 3. If proceeding with a rename, update the `com.aws.cloudformation.Context`
26
+ context to reflect the new export name
27
+ 4. Recommend coordinating the rename with all importing stacks (deploy consumers
28
+ first with the new name, then rename the export)
29
+
30
+ **Key principle:** Exported outputs are a public API contract. Treat renames as
31
+ breaking changes.
32
+
33
+ ## Conditional Resource Coupling
34
+
35
+ **Resources sharing a Condition form an atomic feature toggle group.**
36
+
37
+ When multiple resources use the same `Condition`, they are intentionally coupled
38
+ — they must all be created or none created. Removing the Condition from one
39
+ resource in the group breaks the atomicity.
40
+
41
+ Before modifying or removing a Condition from a resource:
42
+
43
+ 1. Check `Metadata."com.aws.cloudformation.Context"` for feature toggle group
44
+ documentation
45
+ 2. Identify all other resources that share the same Condition
46
+ 3. Warn the user that breaking the coupling may cause deployment failures (e.g.,
47
+ a resource created without its required subnet group or security group)
48
+ 4. If the user intends to break the coupling, recommend removing the Condition
49
+ from ALL resources in the group, or explain why selective removal is safe
50
+
51
+ ## Security Group Blast Radius
52
+
53
+ **Assess the blast radius before modifying shared security groups.**
54
+
55
+ A single security group may be referenced by EC2 instances, RDS databases,
56
+ Lambda VPC configs, and other resources. Adding an ingress rule affects ALL
57
+ resources using that group.
58
+
59
+ Before modifying a security group:
60
+
61
+ 1. Check `Metadata."com.aws.cloudformation.Context"` for documented references
62
+ and blast radius
63
+ 2. Enumerate which resources use the security group
64
+ 3. Warn the user about the full impact (e.g., "opening port 443 from 0.0.0.0/0
65
+ will also expose the RDS instance, not just the web server")
66
+ 4. Recommend creating a separate, scoped security group if the ingress rule
67
+ should only apply to a subset of resources
68
+
69
+ **Key principle:** Public ingress (0.0.0.0/0) on a shared security group is
70
+ almost always wrong — it exposes databases and internal services, not just the
71
+ intended target.
72
+
73
+ ## DeletionPolicy Preservation for Stateful Resources
74
+
75
+ **Never remove or downgrade a DeletionPolicy on stateful resources without
76
+ explicit user confirmation.**
77
+
78
+ Resources with `DeletionPolicy: Retain` (DynamoDB tables, RDS instances, S3
79
+ buckets) contain data that cannot be recreated. When asked to remove such a
80
+ resource:
81
+
82
+ 1. Check `Metadata."com.aws.cloudformation.Context"` for data criticality
83
+ documentation
84
+ 2. Warn about data loss risk — even with Retain, removing from the template
85
+ orphans the resource from CloudFormation management
86
+ 3. Confirm the user understands: the physical resource survives (Retain), but it
87
+ is no longer managed by the stack
88
+ 4. If removing, update the template Description and remaining resources'
89
+ `com.aws.cloudformation.Context` context to document the orphaned resource
90
+ 5. Never change DeletionPolicy from Retain to Delete without explicit user
91
+ confirmation and documented backup verification
92
+
93
+ **Key principle:** `DeletionPolicy: Retain` exists for a reason. Respect it,
94
+ document it, and warn loudly before any operation that could result in data
95
+ loss.
96
+
97
+ ## Parameter Propagation for New Resources
98
+
99
+ **When adding resources to a template with naming conventions, propagate
100
+ existing parameters.**
101
+
102
+ Many templates use Parameters (e.g., `Environment`, `Project`, `Team`) to drive
103
+ resource naming for multi-environment deployment. New resources must follow the
104
+ same convention.
105
+
106
+ When adding a resource to a template with parameterized names:
107
+
108
+ 1. Check `Metadata."com.aws.cloudformation.Context"` for naming convention
109
+ documentation
110
+ 2. Examine existing resources for naming patterns (e.g., `!Sub
111
+ "${Environment}-..."`)
112
+ 3. Apply the same pattern to the new resource's name
113
+ 4. Add `Metadata."com.aws.cloudformation.Context"` to the new resource,
114
+ documenting its purpose and constraints
115
+ 5. If the template has a documented convention (e.g., "all resources must use
116
+ Environment prefix"), follow it even if not explicitly requested
117
+
118
+ **Key principle:** Consistency in naming enables multi-environment deployment. A
119
+ resource that breaks the naming convention becomes an obstacle to promotion
120
+ across environments.
121
+
122
+ ## Template Size Limits
123
+
124
+ **Check the template body size before adding resources to an already-large
125
+ template.** CloudFormation enforces hard limits: a template body passed inline
126
+ (`TemplateBody`) is capped at 51,200 bytes, a template uploaded via S3
127
+ (`TemplateURL`) at 1,048,576 bytes (1 MB), and any single template at 500
128
+ resources. A template that already carries many resources or rich
129
+ `Metadata."com.aws.cloudformation.Context"` may be close to these limits, so the
130
+ addition you are about to make may not fit.
131
+
132
+ Service Quotas reports the current values for two of these — `Template Size`
133
+ (1 MB) and `Template Resources` (500), both non-adjustable — and also
134
+ `Template Description Length` (1,024 bytes), which the persist procedure
135
+ relies on. The 51,200-byte inline `TemplateBody` cap is not published as a
136
+ service quota; take it from the [CloudFormation quotas
137
+ documentation][cloudformation-quotas]. When the margin matters, confirm with:
138
+
139
+ ```shell
140
+ aws service-quotas list-service-quotas --service-code cloudformation \
141
+ --query "Quotas[?starts_with(QuotaName, 'Template')].[QuotaName,Value,Unit]"
142
+ ```
143
+
144
+ [cloudformation-quotas]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cloudformation-limits.html
145
+
146
+ When adding or modifying resources — especially in a large template:
147
+
148
+ 1. Measure the current template body size in bytes (e.g., `wc -c <template>` on
149
+ Unix/macOS or Git Bash, or `(Get-Item <template>).Length` in PowerShell) and
150
+ compare it against the 1,048,576-byte limit; note the remaining headroom and
151
+ the resource count against the 500 cap.
152
+ 2. Estimate the size of what you are about to add, INCLUDING the
153
+ `Metadata."com.aws.cloudformation.Context"` you are required to attach. If
154
+ the addition would push the template over the limit, do NOT blindly append.
155
+ 3. When headroom is tight, intelligently adjust context to fit rather than
156
+ dropping it:
157
+ - Condense and consolidate verbose existing
158
+ `Metadata."com.aws.cloudformation.Context"` (collapse long `why`/rationale
159
+ prose into terse caveman shorthand; keep `must` constraints intact).
160
+ - Prioritize the highest-value context and write concise context for the new
161
+ resources.
162
+ 4. If condensing is not enough, split the stack: move a cohesive group of
163
+ resources into a nested stack (`AWS::CloudFormation::Stack`) or a
164
+ CloudFormation module, or relocate bulky static content (e.g., large inline
165
+ code) to S3. Preserve `Metadata."com.aws.cloudformation.Context"` on the
166
+ extracted resources.
167
+ 5. Never silently drop required context or exceed the limit — a template over
168
+ the limit fails at `CreateStack`/`UpdateStack` (e.g., "Template body is too
169
+ long" / "Template format error: number of resources exceeds maximum").
170
+
171
+ **Key principle:** Context is mandatory, but so is staying under the size limit.
172
+ When both cannot fit, *intelligently adjust* existing and new context (condense,
173
+ prioritize, or relocate) — never choose between blindly adding and dropping
174
+ context.
@@ -31,7 +31,18 @@ Check which validation mechanism is available.
31
31
  - You MUST check in this order of preference:
32
32
  1. `cfn-lint` CLI available on the user's system (verify with `which cfn-lint` or `cfn-lint --version`)
33
33
  2. Python `cfnlint` library (verify by attempting `import cfnlint` in a throwaway Python command)
34
- - If cfn-lint is not installed, You MUST ask the user: "I can install `cfn-lint` via `pip install cfn-lint`. Do you want me to install it, or would you prefer to install it manually?"
34
+ - If cfn-lint is not installed, You MUST ask the user: "I can install
35
+ `cfn-lint` from PyPI via `pip install 'cfn-lint>=1,<2'`. Do you want me to
36
+ install it, or would you prefer to install it manually?"
37
+ - You MUST install ONLY the `cfn-lint` package from PyPI, with no extra
38
+ packages. If installation is not possible — pip missing, PyPI unreachable, or
39
+ the user declines — You MUST NOT attempt an alternative installation
40
+ mechanism. Tell the user that cfn-lint cannot be installed and note the
41
+ reduced validation coverage. If the environment still has AWS connectivity,
42
+ continue with the validation steps that do not need cfn-lint: the
43
+ `aws cloudformation validate-template` and change-set paths. If AWS API calls
44
+ are also unreachable, no validation layer can run — say so and stop rather
45
+ than reporting an unvalidated template as validated.
35
46
  - You MUST NOT execute validation or run any install command without the user's explicit approval because this changes the user's environment
36
47
  - If no mechanism is available and the user declines installation, You MUST ask whether to abort or proceed anyway (knowing the SOP cannot complete)
37
48
  - You MUST respect the user's decision to proceed, install, or abort
@@ -132,4 +143,11 @@ If cfn-lint reports errors you believe are incorrect, suppress specific rules us
132
143
  Some resource properties are only valid in certain regions. If you see region-related errors, pass the target deployment region in the `regions` parameter to get accurate validation.
133
144
 
134
145
  ### cfn-lint not installed
135
- Install with `pip install cfn-lint`. The tool is maintained at https://github.com/aws-cloudformation/cfn-lint.
146
+ Install ONLY the `cfn-lint` package from PyPI with
147
+ `pip install 'cfn-lint>=1,<2'`; do not install extra packages. If installation
148
+ is not possible (pip missing, PyPI unreachable, or the user declines), do not
149
+ try another installation mechanism. Tell the user cfn-lint cannot be installed
150
+ and note the reduced coverage. With AWS connectivity still available, continue
151
+ with `aws cloudformation validate-template` and the change-set path; if AWS API
152
+ calls are also unreachable, no validation layer can run — report that instead of
153
+ treating the template as validated.
@@ -2,7 +2,7 @@
2
2
  name: aws-serverless
3
3
  description: Builds, deploys, manages, debugs, configures, and optimizes serverless applications on AWS using Lambda, API Gateway, Step Functions, EventBridge, and SAM/CDK. Covers cold starts, CORS debugging, event source mappings, troubleshooting, concurrency, SnapStart, Powertools, function URLs, EventBridge Scheduler, Lambda layers, and production readiness. Triggers on mentions of Lambda, API Gateway, Step Functions, SAM templates, CDK serverless stacks, DynamoDB stream triggers, SQS event sources, cold starts, timeouts, 502/504 errors, throttling, concurrency, CORS, Powertools, or any event-driven architecture on AWS, even without the word "serverless." Does not apply to EC2, ECS/Fargate containers, or Amplify hosting.
4
4
  metadata:
5
- version: "1"
5
+ version: "2"
6
6
  ---
7
7
 
8
8
  # AWS Serverless
@@ -23,6 +23,21 @@ These cover capabilities and procedures the general references below do **not**.
23
23
  | **aws-lambda-durable-functions** | Durable execution, checkpoint-and-replay, long-running multi-step workflows written as plain code (TS/Python/Java), automatic state persistence, saga pattern in code, human-in-the-loop callbacks, executions up to 1 year, `context.step`/`context.wait`/`context.invoke`, `withDurableExecution`, `durable-execution-sdk` |
24
24
  | **aws-lambda-managed-instances** | Lambda Managed Instances (LMI), capacity providers, EC2-backed Lambda, steady high-volume traffic (50M+ req/mo) wanting Savings Plans / Reserved Instance pricing, `PerExecutionEnvironmentMaxConcurrency`, `CapacityProviderConfig`, multi-concurrent execution environments |
25
25
 
26
+ ### Workflow orchestration
27
+
28
+ Route here when the user wants to coordinate multiple steps, services, or functions. Triggers include "orchestration", "workflow", "state machine", "multi-step coordination", "coordinate Lambda functions", "durable execution", "pipeline with retries", or intent to build saga/compensation, human-in-the-loop approval, fan-out, or long-running async coordination.
29
+
30
+ When starting a new orchestration or multi-step workflow, you MUST surface the choice between AWS Step Functions and AWS Lambda Durable Functions before implementing — do not silently pick one. Route on the signals below. When the request names only a generic pattern (saga/compensation, human-in-the-loop, fan-out, or "workflow orchestration") with no technology, present both options and the one-line tradeoff, then let the user decide. Do not lead with the tradeoff caveats when the signals already point to one service.
31
+
32
+ | Use this skill | When the workload involves |
33
+ |---|---|
34
+ | **aws-step-functions** | Orchestration whose primary work is calling AWS services directly; coordinating non-Lambda compute (ECS/Fargate, Glue, SageMaker, Batch) through native managed integrations; a visual, auditable workflow definition required for compliance, cross-team operational observability, or as a shared contract between teams that do not share a codebase (ASL is the specification, not application code); authoring or editing state machines and Amazon States Language (ASL) — state types, JSONata data transformation, Retry/Catch error handling, `.sync`/`waitForTaskToken` service integrations, Distributed Map, TestState unit testing, JSONPath-to-JSONata migration |
35
+ | **aws-lambda-durable-functions** | Code-first orchestration in-process when already building on Lambda (`context.step`/`context.wait`/`context.invoke`, `withDurableExecution`); many fine-grained steps per execution where cumulative Step Functions Standard state-transition cost may be significant — compare Step Functions pricing (Standard vs Express) against Lambda invocation cost at the expected volume before choosing; orchestration steps written in a general-purpose language within the same application codebase (share modules, data types, and test suites with application code); teams applying standard software-engineering practices (unit tests, code review, type checking) to orchestration logic without learning a declarative workflow language |
36
+
37
+ **Tradeoff (use when either fits):** Durable Functions keeps orchestration in your Lambda codebase; Step Functions externalizes it into a managed, visual state machine with built-in service integrations.
38
+
39
+ **Security:** Both services persist workflow state and payloads — Step Functions records full input/output in execution history (viewable in the console and, if logging is enabled, CloudWatch Logs). As a baseline, enable execution logging (CloudTrail) and CloudWatch alarms on execution failures, and use least-privilege per-workflow execution roles. Do not pass secrets, tokens, or PII through workflow state; reference them by Secrets Manager/ARN pointer, and apply a customer-managed KMS key to encrypt state when the data is sensitive.
40
+
26
41
  ### Step-by-step task procedures (tested CLI SOPs)
27
42
 
28
43
  | Use this skill | For the task |
@@ -28,7 +28,7 @@ EAS-specific notes:
28
28
 
29
29
  `npx --yes eas-cli@latest simulator:start --type argent` provisions an argent remote session. The connection config it returns is different (`ARGENT_TOOLS_URL` / `ARGENT_AUTH_TOKEN`).
30
30
 
31
- **Invoking argent — run its tools directly.** Drive argent by running its tools directly — `argent run <tool> --udid <udid> …` (with `argent link` or the env-var config below) where flags work (the examples here use this path); or via its MCP server, which passes structured params. Heads-up (flagged elsewhere, not reproduced in our own runs): routing an `argent run` call through `npx --yes eas-cli@latest simulator:exec` can **strip the `--flag` arguments**, so the tool runs with no options and fails confusingly. If you must go through `simulator:exec`, wrap it in `sh -c` and pass one `--args` JSON blob instead of flags: `npx --yes eas-cli@latest simulator:exec sh -c 'argent run <tool> --args "{\"udid\":\"<udid>\", …}"'`. argent's gesture tools take **normalized 0.0–1.0** coordinates, not pixels — check its help for the exact input shape.
31
+ **Invoking argent — run its tools directly.** Drive argent with `npx --yes eas-cli@latest simulator:exec argent run <tool> --udid <udid> …`. `simulator:exec` is `strict = false` and hands the command its args verbatim (it `spawnAsync(command, args)` with the session env loaded), so argent's `--flags` pass straight through no `sh -c` wrapper and no `--args` JSON blob needed. (You can also drive argent via its MCP server, which passes structured params.) argent's gesture tools take **normalized 0.0–1.0** coordinates, not pixels — check its help for the exact input shape.
32
32
 
33
33
  **Installing apps in an argent session.** `--type argent` provisions only an argent daemon on the VM — there is no agent-device daemon, so agent-device install verbs don't apply. Install a local build with argent's own `reinstall-app` (tar-upload):
34
34
 
@@ -40,6 +40,26 @@ Whenever the tools client is routed to a remote tool-server, it tars the local b
40
40
 
41
41
  Needs argent ≥ 0.16.0 (the release that adds tar-upload) — verify with `argent --version`. On older versions `reinstall-app` resolves `--appPath` on the VM only, so a local path fails; drive an app already on the sim instead.
42
42
 
43
+ **Mode C (dev client) on argent.** Easiest is the native launch (eas-cli ≥ 22.4.0): `simulator:start --type argent --build-id <id> --launch-arg … --open-url "<scheme>://expo-development-client/?url=<metro-url>"` installs, launches, and connects the dev client with the launch-args applied and the "Open in?" dialog auto-handled — same as agent-device Method 1 (see run-your-app.md). No manual `open-url` or coordinate tap. argent needs no `open --foreground` attach either; `argent run screenshot` works against the running app, but pass an explicit `--udid` from `list-devices` (the `Booted` one — there's no default), and it saves to a LOCAL temp path.
44
+
45
+ To drive the connect yourself on a bare argent session (no launch flags), argent has `open-url`, which opens a scheme / deep link directly, so you can point a dev client at Metro without tapping through the launcher. Use the dev-client **custom scheme** (not `https://`, which can fall through to Safari):
46
+
47
+ ```bash
48
+ # load the dev client from Metro via its deep link
49
+ npx --yes eas-cli@latest simulator:exec argent run open-url --udid <udid> --url "<scheme>://expo-development-client/?url=<metro-url>"
50
+ # open-url raises the "Open in '<app>'?" system dialog — argent has NO alert-accept, so screenshot to
51
+ # locate "Open", then coordinate-tap it (its describe may not see the dialog — see "System dialogs" below)
52
+ npx --yes eas-cli@latest simulator:exec argent run screenshot --udid <udid>
53
+ npx --yes eas-cli@latest simulator:exec argent run gesture-tap --udid <udid> --x <0..1> --y <0..1>
54
+ # then attach to Metro's debugger / reload the bundle
55
+ npx --yes eas-cli@latest simulator:exec argent run debugger-connect --udid <udid>
56
+ ```
57
+
58
+ Where argent is weaker than agent-device Mode C — so it's **capable, not as fast**:
59
+ - **No launch-args.** `launch-app` takes only `--bundleId`; argent can't pre-seed `-EXDevMenuIsOnboardingFinished` / `-EXDevMenuShowsAtLaunch` the way agent-device's `open --launch-args` does. If the onboarding popup or dev menu blocks the screen, tap through it by **normalized 0.0–1.0 coordinates** (`gesture-tap`, positions from `describe` / `native-describe-screen`) — there's no element/ref tap.
60
+ - **No Metro bind on launch.** No `--metro-host` / `--bundle-url` seed; point the client at Metro with the `open-url` deep link above, then `debugger-connect` / `debugger-reload-metro`.
61
+ - **Whole-string text entry:** use `keyboard --text "<string>"` — it types the entire string in one call. Never type character by character.
62
+
43
63
  **System dialogs on argent (e.g. the first-time deep-link "Open in '<app>'?").** argent's UI queries (`describe` / `await-ui-element`) may not see system dialogs / native modals — a screenshot shows the dialog, but element lookups time out. When that happens, argent surfaces a hint with the fix (today that's a `boot-device --force` to switch its AX backend); follow the hint, then locate and tap "Open". There's no single press-with-timeout — you wait for the element, then tap it. Use argent's own command help for the exact tools and flags.
44
64
 
45
65
  **Recording video on argent (`screen-recording-start`/`stop`).** The gotcha to know: argent **trims static stretches by default**, which drops the very frames you're measuring — turn that off when you care about cadence or timing (see argent's help for the flag). Recordings also carry a burned-in "Argent" watermark that can't be disabled on a hosted session — fine for diagnosis, mind it before sharing publicly. The stop call returns a video already downloaded locally; extract frames with `ffmpeg` (may need installing) to inspect motion frame by frame. The capture samples at ~30fps, so it shows visible jank but can't prove or disprove sub-frame hitches on 60/120Hz content.
@@ -33,6 +33,34 @@ done
33
33
 
34
34
  If you need the id explicitly, it's `EAS_SIMULATOR_SESSION_ID` in `.env.eas-simulator`. `start` also prints a `webPreviewUrl` (iOS-only browser preview — surface it per the SKILL.md "watch it live" rules) and a job-run URL. Once live, the session env is in `.env.eas-simulator`, so `simulator:exec` works.
35
35
 
36
+ ## Targeting a device — iPad, or several at once
37
+
38
+ **Boot a specific device at session start** with `eas simulator:start --device "<name|UDID>"` (eas-cli ≥ 22.4.0) — this is how you run on an iPad instead of the default iPhone:
39
+
40
+ ```bash
41
+ npx --yes eas-cli@latest simulator:start --platform ios --device "iPad Pro 13-inch (M5)" \
42
+ --non-interactive --name "iPad run"
43
+ # then install / launch / screenshot as usual — the iPad renders larger (e.g. 1032x1376).
44
+ ```
45
+
46
+ The value must be a device the **remote runner** offers (NOT your local Xcode set), by name **or** UDID. List them from a live session:
47
+
48
+ ```bash
49
+ npx --yes eas-cli@latest simulator:exec npx agent-device@latest devices --json
50
+ ```
51
+
52
+ Available iOS devices today: iPhone 17 / 17 Pro / 17 Pro Max / 17e / Air, and iPad (A16), iPad Air 11"/13" (M4), iPad mini (A17 Pro), iPad Pro 11"/13" (M5).
53
+
54
+ **Switch devices mid-session:** a session exposes ~16 sims but boots only one at start. Pass the **controller's** global `--device "<name>"` on `open` (and other verbs) to boot + target another; it stays booted alongside the first, so pass `--device` on each verb to say which it hits.
55
+
56
+ ```bash
57
+ npx --yes eas-cli@latest simulator:exec npx agent-device@latest open <bundleId> "<devClientURL>" \
58
+ --platform ios --device "iPad Pro 13-inch (M5)" --relaunch
59
+ ```
60
+
61
+ - ⚠️ The **controller** `--device` resolves by **NAME only** — a udid returns `DEVICE_NOT_FOUND`. (The start-time CLI `--device` above takes either.)
62
+ - `devices` reports each device's name, kind, and booted state, but **not** its iOS version.
63
+
36
64
  ---
37
65
 
38
66
  ## Mode A — Local release build (embedded JS, no Metro)
@@ -103,88 +131,94 @@ npx --yes eas-cli@latest simulator:stop # omit --id → stops the doten
103
131
 
104
132
  ---
105
133
 
106
- ## Mode C — Local dev build + tunnel (live edits via Fast Refresh)
134
+ ## Mode C — Dev build + tunnel (live edits via Fast Refresh)
107
135
 
108
- This is the agentic edit-and-see loop: a **dev (Debug) build** loads JS from your local **Metro** over **tunnel v2**, so code edits appear on the remote sim via Fast Refresh. It has the most steps each is necessary.
136
+ The agentic edit-and-see loop: a **dev (Debug) build** loads JS from your **Metro** over a tunnel, so edits appear on the remote sim via Fast Refresh. Two ways to connect the dev client to Metro:
109
137
 
110
- ⚠️ **Don't install a release build as a "quick interim" and screenshot it** that interim shows stale, build-time code (the "outdated screenshot" trap). Go straight to the dev build + Metro; screenshot only after the dev client is connected to Metro.
138
+ - **Method 1 (recommended, eas-cli 22.4.0):** launch at session start`simulator:start` installs the build, applies launch-args, and opens the Metro URL in one command, and the "Open in?" dialog is auto-handled. Needs a **remote** build source (`--build-id`, `--application-archive-url`, or `--expo-go`); a local `.app` can't be passed here.
139
+ - **Method 2 (fallback):** drive the connect with the controller — for a **local `.app`** build, or eas-cli < 22.4.0.
111
140
 
112
- **No local Mac toolchain?** (the common cloud/Linux case) Build the dev client on **EAS** instead of step 1 below. ⚠️ Same order-matters rule as Mode B: build first, start the session after you have the artifact URL.
141
+ ⚠️ **Don't install a release build as a "quick interim" and screenshot it** it shows stale, build-time code. Use a dev build + Metro; screenshot only after the dev client is connected.
113
142
 
114
- ```bash
115
- # ── Non-Mac path: replace step 1 with these ──────────────────────────────────
143
+ ### Get a dev-client build (either method needs one)
116
144
 
117
- # Find or create a dev-client simulator build profile in eas.json.
118
- # Read eas.json if it exists and look for a build profile with developmentClient: true + ios.simulator: true.
119
- # If one exists, note its name and skip to the build step.
120
- # If not, add one named "dev-sim" — use node, python3, jq, or a direct JSON edit, whichever
121
- # is available. Preserve all other profiles. Minimum: { "developmentClient": true, "ios": { "simulator": true } }
145
+ - **Local (Mac):** `npx expo install expo-dev-client`; `npx expo prebuild --platform ios --clean` (set `ios.bundleIdentifier` first); `( cd ios && LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 pod install )`; then `xcodebuild -workspace ios/<App>.xcworkspace -scheme <App> -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build-debug build` → `ios/build-debug/Build/Products/Debug-iphonesimulator/<App>.app`. A local `.app` → **Method 2 only**.
146
+ - **EAS (no Mac, or to use Method 1):** ensure a profile with `developmentClient: true` + `ios.simulator: true`, then `npx --yes eas-cli@latest build --platform ios --profile <dev-sim> --non-interactive`. Note the **build id** (Method 1's `--build-id`) or the artifact URL. Reuse a fingerprint-matched build to skip the ~15-20 min.
122
147
 
123
- # Build (~15-20 min). Prints an artifact URL when done.
124
- npx --yes eas-cli@latest build --platform ios --profile dev-sim --non-interactive
125
- # → https://expo.dev/artifacts/eas/<hash>.tar.gz
148
+ ### Method 1 launch at session start (recommended)
126
149
 
127
- # Start a session AFTER the build finishes (don't start early idle sessions time out).
128
- # Then in step 3 below, use install-from-source (VM downloads the artifact) instead of local install:
129
- ART="https://expo.dev/artifacts/eas/<hash>.tar.gz"
130
- npx --yes eas-cli@latest simulator:exec npx agent-device@latest install-from-source "$ART" --platform ios
131
- # Continue from step 3a (open the dev client, enter Metro URL) onward — identical to the Mac path.
150
+ ⚠️ **Metro FIRST, then `simulator:start`** the runner opens `--open-url` during startup, with no retry.
151
+
152
+ ```bash
153
+ # 1. Start Metro with a tunnel on your own free port (tunnel-backend details at the end of this mode).
154
+ EXPO_UNSTABLE_TUNNEL_V2=1 npx expo start --tunnel --port <your-free-port> # background it durably
155
+ # Capture the manifest host. Headless runs won't print it — read ngrok's API (curl -s 127.0.0.1:4040/api/tunnels)
156
+ # or the manifest (curl -s -H "expo-platform: ios" localhost:<port>/ → launchAsset.url).
157
+
158
+ # 2. Start the session AND install+launch+open the app in one command (--launch-arg = one token per flag):
159
+ # Dev client: --build-id <id>, --open-url <scheme>://expo-development-client/?url=https://<manifest-host>
160
+ # (scheme = app.json `scheme`, NOT the slug; URL-encode the inner url if it has a path/query)
161
+ # Expo Go: --expo-go instead of --build-id, and --open-url exp://<manifest-host> (no port; https opens Safari)
162
+ npx --yes eas-cli@latest simulator:start --platform ios --build-id <BUILD_ID> \
163
+ --launch-arg "-EXDevMenuIsOnboardingFinished" --launch-arg "1" \
164
+ --launch-arg "-EXDevMenuShowsAtLaunch" --launch-arg "0" \
165
+ --launch-arg "-EXDevMenuShowFloatingActionButton" --launch-arg "0" \
166
+ --open-url "<scheme>://expo-development-client/?url=https://<manifest-host>" \
167
+ --non-interactive --name "Coin flip live edits"
168
+ # The app installs, launches with the launch-args (onboarding/dev-menu/gear suppressed), and opens the URL.
169
+ # The "Open in '<app>'?" dialog is auto-bypassed (the CLI writes the scheme approval) and the approval
170
+ # persists session-wide — so NO `alert accept` is needed, here or for later controller opens.
171
+ # `start` prints NOTHING about the install/launch — confirm from Metro's `iOS Bundled …` line.
172
+
173
+ # 3. To screenshot/drive, ATTACH the controller once — the CLI launch makes NO agent-device session, so a bare
174
+ # `screenshot` fails `SESSION_NOT_FOUND`. `open --foreground` attaches without relaunching:
175
+ npx --yes eas-cli@latest simulator:exec npx agent-device@latest open <bundleId> --foreground --platform ios
176
+ npx --yes eas-cli@latest simulator:exec npx agent-device@latest screenshot ./live.png
177
+ # VERIFY it's the REMOTE sim, not a silent local-sim fallback (agent-device falls back to a LOCAL sim with no
178
+ # error when the dotenv lacks remote config → believable but WRONG screenshots). Decisive tells: `simulator:get
179
+ # --json` returns the id `start` printed, AND the attach's "Session state:" path is under /Users/expo/ (remote
180
+ # VM), not /Users/gabe/ (your Mac). The `sessions/` vs `remote-diagnostics/` directory name is NOT reliable.
181
+
182
+ # 4. Fast Refresh: edit a source file → it hits the remote sim with no reload. Screenshot again to confirm.
183
+ # 5. Stop: npx --yes eas-cli@latest simulator:stop # then kill the Metro process
132
184
  ```
133
185
 
186
+ ### Method 2 — drive the connect with the controller (fallback)
187
+
188
+ For a **local `.app`** (can't be passed to `--build-id`) or **eas-cli < 22.4.0**. Start a plain session (see "Starting a session"), install the build, then deep-link the dev client:
189
+
134
190
  ```bash
135
- # 1. Add expo-dev-client and build a Debug (dev-client) simulator .app
136
- npx expo install expo-dev-client
137
- npx expo prebuild --platform ios --clean # set ios.bundleIdentifier first (as in Mode A) to avoid prompts
138
- ( cd ios && LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 pod install )
139
- LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 xcodebuild \
140
- -workspace ios/<App>.xcworkspace -scheme <App> \
141
- -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build-debug build
142
- DEVAPP=ios/build-debug/Build/Products/Debug-iphonesimulator/<App>.app
143
-
144
- # 2. Start Metro with tunnel v2 on its OWN port — don't force :8081 or kill anything to reclaim it.
145
- # Each `expo start --tunnel` gets its own unique tunnel URL, so a second Metro never has to fight
146
- # for the first one's port. Pick a free high port with `--port <N>` (e.g. 8082). Only reuse a
147
- # running Metro if YOU started it this session there's no command to prove ownership, so when
148
- # unsure just start a new one on another port; never kill someone else's server. A bare `&` won't
149
- # survive across agent shell calls use a long-lived/background run or a separate terminal. tunnel
150
- # v2 (durable-object, not ngrok) works from robot/cloud agents where plain --tunnel is blocked.
151
- EXPO_UNSTABLE_TUNNEL_V2=1 npx expo start --tunnel --port <N>
152
- # → capture the tunnel/manifest URL + deep link it prints (format like https://<host>.on.expo.app).
153
- # The port is NOT in the URL, so read it from stdout — `--port` only identifies the local process.
154
-
155
- # 3. Start a session, install the dev build, then connect it to Metro.
156
- # RELIABLE path = "open the dev client, then Enter URL manually". The deep-link + system "Open in
157
- # '<app>'?" dialog is flaky: the dialog may not appear, and `press 'label="Open"'` can hang ~90s
158
- # against a slow daemon. Don't make the loop depend on it.
159
- # The button labels below ("Enter URL manually"/"Connect"/"Reload"/"Go back", and the system "Open")
160
- # are expo-dev-client / iOS / expo-router UI — the same across ANY Expo app (not app-specific), but
161
- # UI text that can shift across versions. Treat them as illustrative: if a label doesn't match,
162
- # `snapshot -i` and press the current ref. The flow matters, not the exact strings.
163
- npx --yes eas-cli@latest simulator:exec npx agent-device@latest install dev.example.app "$DEVAPP" --platform ios
164
-
165
- # a) launch the dev client (it opens its launcher, which only auto-discovers LAN Metro — ours is remote):
166
- npx --yes eas-cli@latest simulator:exec npx agent-device@latest open dev.example.app --platform ios
191
+ # install: a local .app uploads over the tunnel; an EAS artifact uses install-from-source (VM downloads it):
192
+ npx --yes eas-cli@latest simulator:exec npx agent-device@latest install <bundleId> "$DEVAPP" --platform ios
193
+ # (EAS artifact instead: install-from-source "https://expo.dev/artifacts/eas/<hash>.tar.gz" --platform ios)
194
+
195
+ # connect: `open <bundleId> <devClientURL>` deep-links into the bundle, skipping the launcher UI. Assemble
196
+ # <devClientURL> from the app's SCHEME (app.json `scheme`, NOT the slug): <scheme>://expo-development-client/?url=https://<manifest-host>
197
+ npx --yes eas-cli@latest simulator:exec npx agent-device@latest open <bundleId> "<devClientURL>" --platform ios --relaunch \
198
+ --launch-args "-EXDevMenuIsOnboardingFinished" --launch-args "1" \
199
+ --launch-args "-EXDevMenuShowsAtLaunch" --launch-args "0" \
200
+ --launch-args "-EXDevMenuShowFloatingActionButton" --launch-args "0"
201
+ # a controller open on a BARE session (no Method-1 launch to pre-approve the scheme) can raise the
202
+ # "Open in '<app>'?" dialog accept it (no-op if absent; not needed after a Method-1 launch):
203
+ npx --yes eas-cli@latest simulator:exec npx agent-device@latest alert accept 2500 --platform ios
204
+ # then screenshot; if it shows the launcher not the app, the deep link didn't take manual fallback:
205
+ # press 'label="Enter URL manually"' snapshot -i fill @<field> "<manifest URL>" press 'label="Connect"'
206
+ # press 'label="Reload"'; press 'label="Go back"' if expo-router shows "Unmatched Route".
207
+ ```
167
208
 
168
- # b) point it at your remote Metro via "Enter URL manually":
169
- npx --yes eas-cli@latest simulator:exec npx agent-device@latest press 'label="Enter URL manually"'
170
- npx --yes eas-cli@latest simulator:exec npx agent-device@latest snapshot -i # get the text-field ref
171
- npx --yes eas-cli@latest simulator:exec npx agent-device@latest fill @<field> "<manifest URL Metro printed in step 2>" # e.g. https://<host>.on.expo.app
172
- npx --yes eas-cli@latest simulator:exec npx agent-device@latest press 'label="Connect"'
209
+ ### Dev-menu launch flags (both methods)
173
210
 
174
- # c) first-run dev menu Reload to fetch the bundle (first build+transfer over the tunnel ~40-60s):
175
- npx --yes eas-cli@latest simulator:exec npx agent-device@latest press 'label="Reload"'
211
+ The launch-args are iOS UserDefaults (`-Key Value`), verified in expo/expo `packages/expo-dev-menu`. By default the onboarding popup, auto-opened dev menu, and floating gear all show and clutter screenshots; these suppress them:
212
+ - `-EXDevMenuIsOnboardingFinished 1` skip the first-run onboarding popup (dev client **and** Expo Go)
213
+ - `-EXDevMenuShowsAtLaunch 0` — don't auto-open the dev menu at launch (dev client)
214
+ - `-EXDevMenuShowFloatingActionButton 0` — hide the floating gear (defaults visible on both)
176
215
 
177
- # d) expo-router may show "Unmatched Route" (the connect URL was parsed as a path) go to home:
178
- npx --yes eas-cli@latest simulator:exec npx agent-device@latest press 'label="Go back"'
216
+ Method 1 passes each as two flags: `--launch-arg "<key>" --launch-arg "<value>"`. Method 2 passes them as `--launch-args`.
179
217
 
180
- # 4. Edit a source file locally → Fast Refresh pushes it to the remote sim with NO reload. Screenshot to confirm.
181
- npx --yes eas-cli@latest simulator:exec npx agent-device@latest screenshot ./live.png
218
+ ### Metro tunnel backends (both methods)
182
219
 
183
- # 5. Stop the session AND Metro
184
- npx --yes eas-cli@latest simulator:stop # omit --id stops the dotenv session
185
- # kill the `expo start --tunnel` process
186
- ```
220
+ Start Metro on your OWN free port — each run gets its own tunnel URL, so never fight for or kill :8081 (#133's rule). BOTH backends accept ANY `--port`:
221
+ - **ws-tunnel v2 (account-signed):** `EXPO_UNSTABLE_TUNNEL_V2=1` signed URL for your EAS account, `on.expo.app` host, and the path for robot/EXPO_TOKEN/cloud agents (plain ngrok is blocked for them). Needs login / an EAS-linked project; if the signed URL fails, the CLI says to unset the flag and use ngrok.
222
+ - **ngrok (plain `--tunnel`, no flag):** `<host>.exp.direct` host; blocked for robot/EXPO_TOKEN users.
187
223
 
188
- Notes:
189
- - The launcher's auto-discovery only scans the LAN, so a remote Metro must be entered via "Enter URL manually" — that's why this is the connect step.
190
- - **This "Enter URL manually" + public tunnel URL flow is the ONLY connect path.** If it fails, don't switch mechanisms or reconnect in a loop — reset to baseline and redo Mode C once (SKILL.md principle 1). (`agent-device`'s `metro prepare --proxy-base-url` bridge exists but is not part of this loop.)
224
+ The ONLY 8081 lock is the LEGACY ws-tunnel path — hit WITHOUT the v2 account URL (an older CLI where the flag no-ops, or `EXPO_FORCE_WEBCONTAINER_ENV=1`). Do NOT set `EXPO_FORCE_WEBCONTAINER_ENV` to "fix" a port — it forces that legacy path and locks you to 8081. On an older CLI (e.g. expo 56) the v2 flag no-ops and you get ngrok on your chosen port (verified: expo 56.0.3 → ngrok on :8083).
@@ -16,19 +16,22 @@ Concrete errors seen while validating this flow, and the fix.
16
16
  | Two sessions running / orphaned session / surprise double billing | A second `start` (e.g. to "retry" a slow boot) creates a second billed session and overwrites the dotenv id, orphaning the first | Never `start` again to retry — poll the existing session instead. Find orphans with `simulator:list --status in-progress` and stop each with `simulator:stop --id <id>`. |
17
17
  | A device verb hangs (no return for a minute+) | Slow daemon; `press`/`screenshot` can block ~90s | Bound it with agent-device's own `--timeout <ms>` (e.g. `--timeout 120000`) — **not** a shell `timeout` wrapper (macOS has no `timeout` binary, so `timeout 120 …` fails with `command not found` and skips the verb). On timeout `snapshot -i` to see if the action landed before retrying (taps can double-fire). Don't blind-retry. |
18
18
  | `install requires an active session or an explicit device selector` | `install` can't infer the device | Pass `--platform ios` (or `open` something first to establish a session). |
19
+ | `DEVICE_NOT_FOUND: No device named <udid>` when targeting a non-default device (iPad, second sim) | In a remote session agent-device's `--device` resolves by **name**, not udid (despite the CLI docs) | Pass the device **name** from `agent-device devices` (e.g. `--device "iPad Pro 13-inch (M5)"`), not the udid. |
19
20
  | `Unknown command: tap` | The tap verb is `press` | Use `press <ref\|selector>` (e.g. `press @e2` or `press 'label="Open"'`). |
20
- | `SESSION_NOT_FOUND: No active session. Run open first.` | A verb (e.g. `screenshot`) ran before any app/session was opened | `open <app\|url>` first (or pass `--platform ios`). |
21
+ | `SESSION_NOT_FOUND: No active session. Run open first.` | A verb (e.g. `screenshot`) ran before any app/session was opened — **or** you used Method 1 (`simulator:start --open-url`), which launches the app but creates NO agent-device session | `open <app\|url>` first (or pass `--platform ios`). After a Method-1 launch, attach without relaunching: `agent-device open <bundleId> --foreground --platform ios` (pass the bundle id — `--foreground` alone fails `AMBIGUOUS_MATCH`), then screenshot. |
22
+ | Screenshot looks plausible but the session/UI is wrong (e.g. Safari, an iPhone shot when you booted an iPad, or "incompatible Expo Go SDK") | agent-device **silently falls back to a LOCAL simulator** when `.env.eas-simulator` has no remote config — no error, believable-but-wrong output. Common cause: a **concurrent `simulator:start`** on the same account/machine overwrote the shared dotenv with its own id (the dotenv is a single file, NOT concurrency-safe). | Confirm you're on the REMOTE VM: `simulator:get --json` returns the id `start` printed, AND the verb's "Session state:" path is under **`/Users/expo/`** (remote), not `/Users/<you>/` (local); `devices --json` host is a `turtle-worker-*`. The `sessions/` vs `remote-diagnostics/` directory name is NOT a reliable tell. If concurrency is possible, drive by explicit id — load the daemon vars from `simulator:get --id <id> --json` — instead of trusting the dotenv. |
21
23
  | `simulator:exec` / `build` / `simulator:stop`: "Run this command inside a project directory." | Run from the wrong cwd | Run from the Expo project directory (where `app.json`/`eas.json` live). |
22
24
  | New session's id shows as the *previous* one; "Overwriting previous simulator session (id: …)" | The stale `.env.eas-simulator` had an old `EAS_SIMULATOR_SESSION_ID`; the warning line masks the new id | Reset the dotenv before `start`: `printf '# managed by eas-cli\n' > .env.eas-simulator`. |
23
25
  | No `.env.eas-simulator` written after `start` | `--json` suppresses the dotenv | Run `start` *without* `--json` for the `exec` flow; with `--json` you must read `remoteConfig` from stdout and set the env yourself. |
24
26
  | `pod install` fails: `Unicode Normalization not appropriate for ASCII-8BIT` | Ruby 4 + CocoaPods with a non-UTF-8 locale | Re-run with `LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 pod install`. |
25
- | (Mode C) Deep-link `open` lands on the dev-client launcher, not the app | Opening the deep link triggers a system "Open in '<app>'?" dialog; and the launcher only auto-discovers Metro on the LAN | `press 'label="Open"'` to dismiss the dialog, then "Enter URL manually" → `fill` the `https://<host>.on.expo.app` manifest URL → "Connect". |
27
+ | (Mode C) Deep-link `open` lands on the dev-client launcher, not the app | The "Open in '<app>'?" system dialog wasn't accepted, so the deep link didn't take | Accept the dialog with `agent-device alert accept 2500 --platform ios` (not a UI tap). If it still lands on the launcher, fall back to "Enter URL manually" → `fill` the `https://<host>.on.expo.app` manifest URL → "Connect" (see run-your-app.md Mode C). |
26
28
  | (Mode C) App shows expo-router "Unmatched Route" | The connect URL was parsed as a route path | `press 'label="Go back"'` (or navigate to `/`). |
27
29
  | (Mode C) Dev client shows a `?` placeholder / blank after connect | Bundle not fetched yet | `press 'label="Reload"'` and wait ~40-60s for the first build+transfer over the tunnel. |
28
- | (Mode C) `expo start` fails: "port 8081 already in use" | Something already owns that port (often another Metro) | Don't kill it start on another port (`--port <N>`, e.g. 8082). Each `expo start --tunnel` gets its own tunnel URL, so ports coexist. Only reuse a running Metro if you started it this session. |
30
+ | (Mode C) `expo start` fails: "port 8081 already in use" | Another Metro owns 8081 | Don't kill it. Start on your own `--port` **both** `EXPO_UNSTABLE_TUNNEL_V2=1` (account-signed) and plain ngrok accept any port. Only the LEGACY ws-tunnel path is 8081-locked (see the `WS_TUNNEL_PORT` row). Reuse a Metro only if you started it this session. |
29
31
  | (Mode C) `expo start` / `node` killed with **exit 137** | 137 = SIGKILL — almost always the **OOM killer** (memory pressure, common in constrained cloud sandboxes, esp. a native build + Metro at once). **Not** a port clash. | Reduce memory pressure: don't run a native build and Metro concurrently; give the sandbox more memory; retry. |
30
32
  | (Mode C) Edits won't live-reload no matter how often you reconnect | A **release** build is installed — its JS is baked in, so it ignores Metro | Stop reconnecting: **install the dev (Debug) build**, connect it to Metro, reload. Reconnecting a release build to Metro is a no-op. |
31
- | `expo start --tunnel` errors for a robot/`EXPO_TOKEN` user | The ngrok robot-user guard | Use tunnel v2: `EXPO_UNSTABLE_TUNNEL_V2=1 expo start --tunnel`. |
33
+ | `expo start --tunnel` errors for a robot/`EXPO_TOKEN` user | The ngrok robot-user guard blocks plain (ngrok) tunnels | Use ws-tunnel v2 (account-signed, **any** port): `EXPO_UNSTABLE_TUNNEL_V2=1 expo start --tunnel --port <any>` — needs login / an EAS-linked project. Do NOT use `EXPO_FORCE_WEBCONTAINER_ENV`; that forces the legacy path, which is 8081-locked. |
34
+ | `CommandError: WS-tunnel only supports tunneling over port 8081` | You're on the **legacy** ws-tunnel path — no v2 account URL (older CLI where `EXPO_UNSTABLE_TUNNEL_V2` is a no-op, not logged in, or `EXPO_FORCE_WEBCONTAINER_ENV` set) | Get onto the account-signed v2 path: set `EXPO_UNSTABLE_TUNNEL_V2=1` and log in / link the project — then any `--port` works. Otherwise use `--port 8081`, or the ngrok path (drop the flag; non-robot only). |
32
35
  | Unexpected charges / a session you forgot | `start --non-interactive` does NOT auto-stop | Always `npx --yes eas-cli@latest simulator:stop --id <id>`. List leftovers with `npx --yes eas-cli@latest simulator:list`. |
33
36
  | Screenshot shows **old content** / my recent edits don't appear | Running a **release build (Mode A/B)** whose JS was baked in *before* your edits — typically a reused/stale build | A/B reflect code at build time, not now. **Rebuild** (ensure the build's fingerprint matches current source), or use **Mode C** (dev + Metro) so live edits show via Fast Refresh. The screenshot itself is fresh — it's the build that's stale. (`9:41` in the status bar is the sim default, not staleness.) |
34
37
  | (argent) Every `argent run`/`tools` call returns `401 Unauthorized` right after linking | `argent link` without `--yes` no-ops on an already-linked URL ("Already linked. No changes."), keeping a stale token from a previous session | Re-link with `--yes` so the new token is written — see the link command in [controllers.md](./controllers.md). |