stim 1.18.0 → 1.20.0

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 (178) hide show
  1. package/README.md +21 -4
  2. package/dist/{activity-BxnkDZDi.mjs → activity-BrGh9EAI.mjs} +3 -3
  3. package/dist/{agent-device-usage-output-CZ66YA4T.mjs → agent-device-usage-output-DVyv_hWj.mjs} +7 -7
  4. package/dist/android-0vv-OruW.mjs +6 -0
  5. package/dist/android-Brx6EKvJ.mjs +1258 -0
  6. package/dist/{android-DC3ZLuw0.mjs → android-BsYNR81W.mjs} +103 -45
  7. package/dist/{android-CBwCNicO.mjs → android-CK3fpEyZ.mjs} +2 -2
  8. package/dist/{android-cas-BBFyr9CA.mjs → android-cas-DCAuXryb.mjs} +9 -5
  9. package/dist/android-cas-compiler.mjs +1 -1
  10. package/dist/api-run.mjs +159 -8
  11. package/dist/{api-D0VtFhqk.mjs → api-zuX3ynYN.mjs} +9 -7
  12. package/dist/api.d.mts +104 -4
  13. package/dist/api.mjs +1 -1
  14. package/dist/{app-install-D-PUENug.mjs → app-install-0LgaQRT7.mjs} +39 -18
  15. package/dist/artifact-CnC0mfov.mjs +807 -0
  16. package/dist/artifact-DClXJNWx.mjs +772 -0
  17. package/dist/artifact-lifecycle-DzPpBYvK.mjs +30 -0
  18. package/dist/{web-DTU0uiWj.mjs → browser-web-CzBmhmgs.mjs} +136 -174
  19. package/dist/{budget-DIhL2BKx.mjs → budget-BONq9vXY.mjs} +3096 -3244
  20. package/dist/build-BixRI6sB.mjs +462 -0
  21. package/dist/build-plan-CP0tckNU.mjs +553 -0
  22. package/dist/{cache-manifest-BI7ozfmH.mjs → cache-manifest-qA3dT2x2.mjs} +1 -1
  23. package/dist/cache-manifest.mjs +1 -1
  24. package/dist/ccache-CjHQ5F8g.mjs +142 -0
  25. package/dist/{chrome-DJnmnS5q.mjs → chrome-Dp2lMLBe.mjs} +1 -1
  26. package/dist/{cli-DNJMwADx.mjs → cli-B2R8wH1F.mjs} +20 -20
  27. package/dist/cli.mjs +1 -1
  28. package/dist/{client-DzhapoMB.mjs → client-BLy1Pw2V.mjs} +172 -165
  29. package/dist/collector-run.d.mts +10 -0
  30. package/dist/collector-run.mjs +7 -7
  31. package/dist/{command-output-CBtzyIgF.mjs → command-output-BjPN99ZB.mjs} +2 -2
  32. package/dist/{config-CgYMahz7.mjs → config-D0LhTY3S.mjs} +13 -13
  33. package/dist/{created-devices-DKbeWsiK.mjs → created-devices-HU_DAb_Q.mjs} +7 -6
  34. package/dist/{dependency-state-xKoOypBm.mjs → dependency-state-B2m8eqsS.mjs} +2 -2
  35. package/dist/{detached-entry-BLfcgc4O.mjs → detached-entry-BXq4fIl-.mjs} +1 -1
  36. package/dist/{dev-client-CLnIH02x.mjs → dev-client-ClATaJoH.mjs} +2 -2
  37. package/dist/{device-D7oCHGWi.mjs → device-B7lsC0K1.mjs} +8 -8
  38. package/dist/device-DVCwAFlX.mjs +727 -0
  39. package/dist/{device-capacity-EUOOYr8T.mjs → device-capacity-Ca_ke1LE.mjs} +25 -15
  40. package/dist/device-host-worker.mjs +38 -18
  41. package/dist/{device-ios-6MewKKnc.mjs → device-ios-CRUqPxKw.mjs} +12 -11
  42. package/dist/{device-lease-Co-EwifR.mjs → device-lease-DHOBXGgS.mjs} +135 -22
  43. package/dist/{device-lease-run-w1xhCqIW.mjs → device-lease-run-CvDeHTkG.mjs} +4 -4
  44. package/dist/{device-pool-F5WvRPtD.mjs → device-pool-L2-5GxMy.mjs} +3 -3
  45. package/dist/{device-remote-tHbd1fKy.mjs → device-remote-Bmg42063.mjs} +17 -69
  46. package/dist/doctor-C2MSuMMg.mjs +515 -0
  47. package/dist/{doctor-B9ckYUoU.mjs → doctor-DPAqhVCK.mjs} +334 -721
  48. package/dist/doctor-watchman-CB4NnVZm.mjs +260 -0
  49. package/dist/{simslim-EHBQ2R4G.mjs → eas-build-Dw_ec2Km.mjs} +6 -152
  50. package/dist/{stim-installations-aR_WsNOA.mjs → eas-session-ledger-DZlC-Fq3.mjs} +57 -4
  51. package/dist/{error-diagnostics-BRXOqDzG.mjs → error-diagnostics-Du7I_GuY.mjs} +17 -12
  52. package/dist/errors-DzkMYh8Q.mjs +205 -0
  53. package/dist/{exec-DWhCZDGm.mjs → exec-B81Qodjn.mjs} +69 -15
  54. package/dist/{gc-Dh6mXOKC.mjs → gc-PWp34zGb.mjs} +23 -22
  55. package/dist/{gradle-C6GKnunQ.mjs → gradle-C2OEKCCI.mjs} +50 -125
  56. package/dist/{guide-tm6igAaa.mjs → guide-Dai9lphv.mjs} +862 -538
  57. package/dist/{hosted-android-Bv285t_t.mjs → hosted-android-DQ-Agsd8.mjs} +5 -5
  58. package/dist/{hosted-client-C-rMtppF.mjs → hosted-client-Io01s5By.mjs} +9 -3
  59. package/dist/{hosted-ios-D5mNmtj4.mjs → hosted-ios-DIxFnYp-.mjs} +3 -3
  60. package/dist/{hosted-logs-DN8Iz4mJ.mjs → hosted-logs-DP-ABU_M.mjs} +8 -8
  61. package/dist/{hosted-macos-B-wEjlwG.mjs → hosted-macos-Aziw9T_9.mjs} +7 -6
  62. package/dist/{hosted-native-DeeOG8xZ.mjs → hosted-native-BdAQi21M.mjs} +23 -16
  63. package/dist/{idle-shutdown-jjtxAn5C.mjs → idle-shutdown-DdMqoSUB.mjs} +7 -7
  64. package/dist/ios-BZ9dg2Fr.mjs +9 -0
  65. package/dist/{ios-B3SbOWdw.mjs → ios-CXLIUxXJ.mjs} +180 -24
  66. package/dist/ios-CZuv6gRv.mjs +1112 -0
  67. package/dist/{ios-Bvbf0SGY.mjs → ios-VGvNdsv2.mjs} +1 -1
  68. package/dist/{ios-device-Bn7jWDB1.mjs → ios-device-B1B14xND.mjs} +1 -1
  69. package/dist/{ios-device-vOaByMhw.mjs → ios-device-BreZOpM0.mjs} +3 -3
  70. package/dist/ios-project-B9drt_aY.mjs +197 -0
  71. package/dist/{ios-state-B2pES6Vs.mjs → ios-state-CjrL2qv-.mjs} +1 -1
  72. package/dist/js-swap-C8icNYIY.mjs +432 -0
  73. package/dist/launch-DxH0THLP.mjs +745 -0
  74. package/dist/launch-YrbqaIFj.mjs +1530 -0
  75. package/dist/{launch-verify-hskNcWkZ.mjs → launch-verify-BzRdbuO1.mjs} +26 -7
  76. package/dist/{logs-1Ylc1Ri6.mjs → logs-DHaRNfg7.mjs} +11 -11
  77. package/dist/macos-C0iljhsN.mjs +2 -0
  78. package/dist/{macos-DF6C7wUU.mjs → macos-V_3Ui7g4.mjs} +125 -155
  79. package/dist/macos-run.mjs +1 -1
  80. package/dist/maintenance-run.mjs +13 -10
  81. package/dist/{metro-Dh8T4Ytd.mjs → metro-D_j9sN-B.mjs} +209 -43
  82. package/dist/{metro-gateway-CEUw9RMH.mjs → metro-gateway-BBJldOdi.mjs} +2 -2
  83. package/dist/miss-reason-BPAzZS0r.mjs +238 -0
  84. package/dist/{named-ports-BFIz1Shz.mjs → named-ports-C2lvoPsb.mjs} +12 -5
  85. package/dist/native-android-lHzaew7r.mjs +226 -0
  86. package/dist/native-gradle-inputs-BQnIdAlL.mjs +356 -0
  87. package/dist/{native-runtime-D_603JiC.mjs → native-runtime-BRmWL7ru.mjs} +9 -8
  88. package/dist/native-xcode-inputs-eutS19jU.mjs +307 -0
  89. package/dist/native-xcode-ios-DP3cHLwu.mjs +434 -0
  90. package/dist/{ndjson-LGZAMsqa.mjs → ndjson-GOnbyvso.mjs} +6 -6
  91. package/dist/offload-worker.d.mts +12 -6
  92. package/dist/offload-worker.mjs +409 -73
  93. package/dist/{ownership-D6Se7gg_.mjs → ownership-DXzMMSx6.mjs} +2 -2
  94. package/dist/{placement-log-BcPhRwR4.mjs → placement-log-reFbjqEr.mjs} +7 -3
  95. package/dist/plan-BDk2AeY_.mjs +260 -0
  96. package/dist/plan-placement-D_YYlE6N.mjs +73 -0
  97. package/dist/{ports-BLTtfBq-.mjs → ports-jzvAJXXL.mjs} +3 -3
  98. package/dist/{prebuild-juCyreaR.mjs → prebuild-Bw44QxVh.mjs} +4 -4
  99. package/dist/{preview-D5V07Nrl.mjs → preview-BaBJ72Al.mjs} +51 -34
  100. package/dist/project-B8_duAzW.mjs +1067 -0
  101. package/dist/project-plan-BPwbLwQu.mjs +10 -0
  102. package/dist/{pull-request-u4vp7zmK.mjs → pull-request-vOhLNmIb.mjs} +1 -1
  103. package/dist/pull-requests.mjs +1 -1
  104. package/dist/react-native-android-chm1DMlA.mjs +921 -0
  105. package/dist/react-native-build-oAw6gQeW.mjs +239 -0
  106. package/dist/react-native-doctor-DYXoh-6I.mjs +78 -0
  107. package/dist/react-native-ios-SCTK_CT5.mjs +595 -0
  108. package/dist/{recordings-BAWltsVt.mjs → recordings-Bk-VivUV.mjs} +1 -1
  109. package/dist/{reload-DvyWzVjJ.mjs → reload-D34GkY78.mjs} +17 -14
  110. package/dist/remote-BU9UyhCJ.mjs +192 -0
  111. package/dist/{remote-cache-BGg7aGIZ.mjs → remote-cache-Bsts6f6Y.mjs} +5 -5
  112. package/dist/result-BDoRk1k3.mjs +186 -0
  113. package/dist/{run-vhtDgpHO.mjs → run-qvff78hv.mjs} +3 -3
  114. package/dist/{server-bare-B9E7S1aR.mjs → server-bare-dWjAarki.mjs} +6 -6
  115. package/dist/server-command-7XIKlWtO.mjs +69 -0
  116. package/dist/{server-expo-DoBHhuzK.mjs → server-expo-CsYu2aKL.mjs} +107 -82
  117. package/dist/server-expo-DV43vep4.mjs +2 -0
  118. package/dist/settings-Bbztm4oV.mjs +1503 -0
  119. package/dist/{settings-Byp2AQ_E.mjs → settings-CT3E71-i.mjs} +6 -7
  120. package/dist/{state-lKh8shBp.d.mts → settings-vk3mJGSm.d.mts} +4 -2
  121. package/dist/settings.schema.json +156 -6
  122. package/dist/simslim-ChMfe8rg.mjs +149 -0
  123. package/dist/{slot-launch-C1nqh4mu.mjs → slot-launch-B9hn96wd.mjs} +6 -6
  124. package/dist/{stage-g_WqWeaE.mjs → stage-BNKKdDNc.mjs} +6 -3
  125. package/dist/start-CbQCSnXQ.mjs +2 -0
  126. package/dist/{start-D-IeOyoK.mjs → start-CdvcTaXr.mjs} +113 -77
  127. package/dist/{state-xnqpx3AD.mjs → state-Boo0IMJq.mjs} +4 -2
  128. package/dist/{state-BG-aAk4k.mjs → state-DI84A-uu.mjs} +29 -10
  129. package/dist/{stats-DY6ekz8F.mjs → stats-BIIDrjA8.mjs} +6 -6
  130. package/dist/{status-BEeL6IKz.mjs → status-BDGHwkCs.mjs} +3 -1
  131. package/dist/{status-DCjZeE-K.mjs → status-DARMkc9q.mjs} +80 -294
  132. package/dist/status-watch-DvmI3SmN.mjs +296 -0
  133. package/dist/{stim-desktop-Behws_fi.mjs → stim-desktop-8GQX2OV8.mjs} +1 -1
  134. package/dist/{stop-DIJtD2GL.mjs → stop-CCZS-C9m.mjs} +2 -2
  135. package/dist/stop-WQPQHLuy.mjs +72 -0
  136. package/dist/{stop-BTjs3xbc.mjs → stop-iNemHsJC.mjs} +81 -33
  137. package/dist/supervisor-run.d.mts +7 -2
  138. package/dist/supervisor-run.mjs +32 -20
  139. package/dist/{support-Bwvbrq5-.mjs → support-CnE_SIjt.mjs} +17 -7
  140. package/dist/{support-BaayeYCf.mjs → support-DCco5fut.mjs} +61 -8
  141. package/dist/swiftpm-macos-afeNUExW.mjs +119 -0
  142. package/dist/{native-run-B06FkmZx.mjs → tailnet-CoJUb2tB.mjs} +113 -6
  143. package/dist/toolchain-BIupIro1.mjs +2 -0
  144. package/dist/{toolchain-Bnyd_K3A.mjs → toolchain-DqyDXs30.mjs} +225 -315
  145. package/dist/{trigger-CJMhwXe8.mjs → trigger-C0MWBi8O.mjs} +3 -3
  146. package/dist/trigger-SHLOWJ4O.mjs +2 -0
  147. package/dist/{warm-progress-CjB-MP6H.mjs → warm-progress-C3ASD6z5.mjs} +134 -37
  148. package/dist/{status-Dcr0Qexd.mjs → watchman-CQl51YZu.mjs} +75 -7
  149. package/dist/web-B3eldHFH.mjs +123 -0
  150. package/dist/web-BLXpOe6b.mjs +2 -0
  151. package/dist/web-run.mjs +10 -10
  152. package/dist/{workspace-state-C59dqG-Z.mjs → workspace-state-CSIFuuDa.mjs} +33 -33
  153. package/dist/{workspaces-xXItD8Dj.mjs → workspaces-D2xSHBDY.mjs} +1297 -1211
  154. package/dist/{worktree-BSYlofkq.mjs → worktree-BxDqaTev.mjs} +102 -63
  155. package/dist/worktree-q7_OZNY0.mjs +2 -0
  156. package/package.json +8 -6
  157. package/shim/bundle-response.cjs +2 -1
  158. package/shim/bundle-response.d.cts +1 -1
  159. package/shim/expo-metro-config.cjs +1 -1
  160. package/shim/native-android.gradle +101 -0
  161. package/dist/android-BKA16DTf.mjs +0 -4
  162. package/dist/android-BvwuMUJ9.mjs +0 -3572
  163. package/dist/guide-status-Bhf9SJXc.mjs +0 -125
  164. package/dist/ios-CPdkoKaa.mjs +0 -3953
  165. package/dist/ios-DNpdygiv.mjs +0 -7
  166. package/dist/macos-Ck7OBMNY.mjs +0 -2
  167. package/dist/metro-store-BAHFcJm6.mjs +0 -106
  168. package/dist/plan-placement-CtxkCO54.mjs +0 -1859
  169. package/dist/project-C3ChRLZy.mjs +0 -212
  170. package/dist/projects-DD7dBTCj.mjs +0 -92
  171. package/dist/server-expo-Db9HBUoa.mjs +0 -2
  172. package/dist/settings-CQyMElEP.mjs +0 -701
  173. package/dist/start-NTc1Ifqs.mjs +0 -2
  174. package/dist/stop-BGhKMOuH.mjs +0 -48
  175. package/dist/trigger-DWAq9hkw.mjs +0 -2
  176. package/dist/web-B6_bFDn7.mjs +0 -2
  177. package/dist/worktree-BJfnU6r5.mjs +0 -2
  178. package/dist/worktree-CsKYexJM.mjs +0 -753
@@ -1,8 +1,9 @@
1
- import { s as findProjectRoot$1 } from "./project-C3ChRLZy.mjs";
2
- import { t as RECENT_LAUNCH_MS } from "./status-Dcr0Qexd.mjs";
3
- import { t as ANDROID_AVD_CONFIG_HELP } from "./settings-CQyMElEP.mjs";
4
- import { t as guideStatus } from "./guide-status-Bhf9SJXc.mjs";
5
- import { SETTINGS, SETTINGS_SCHEMA_URL } from "@stim-cli/core/state";
1
+ import { t as ANDROID_AVD_CONFIG_HELP } from "./settings-Bbztm4oV.mjs";
2
+ import { r as findProjectRoot } from "./project-B8_duAzW.mjs";
3
+ import { s as RECENT_LAUNCH_MS } from "./watchman-CQl51YZu.mjs";
4
+ import { o as guideStatus, t as WATCHMAN_NESTED_WORKTREES } from "./doctor-watchman-CB4NnVZm.mjs";
5
+ import "./status-watch-DvmI3SmN.mjs";
6
+ import { SETTINGS, SETTINGS_SCHEMA_URL, TUTORIAL_VERSION } from "@stim-cli/core/state";
6
7
  import chalk from "chalk";
7
8
  //#region src/guide/agent.ts
8
9
  var agent_default = {
@@ -14,6 +15,9 @@ device with another workspace. Prefer plain output: it streams each phase and
14
15
  ends with the facts the next step needs. Use --json only when a script must
15
16
  parse a stable payload.
16
17
 
18
+ For a native Xcode or Gradle app without React Native or Expo, read guide
19
+ lifecycle native-ios or guide lifecycle native-android before running it.
20
+
17
21
  TWO WORKFLOWS
18
22
 
19
23
  WORKTREE is the default. Take SINGLE CHECKOUT only when the user asks to work in
@@ -91,7 +95,10 @@ the printed remedy and keep concurrent runs on the same STIM_HOME.
91
95
  Before native work, run doctor for the platform in scope when the STATUS block
92
96
  at the top of this topic says it is due. No block means doctor is current and
93
97
  Stim is up to date. In the worktree workflow, run it from the linked worktree
94
- so it also checks the source checkout. Follow each finding's printed remedy,
98
+ so it also checks the source checkout. Doctor never loads fingerprint.config.js
99
+ or runs an eas from the project tree; its fingerprint checks can still run
100
+ installed Expo packages, including hoisted ones, as a build does. warm,
101
+ start, ios and android run project code. Follow each finding's printed remedy,
95
102
  and read the routed topic below before acting on one you do not understand. If
96
103
  the stim resolved from PATH is older than another installation, fix PATH or the
97
104
  installation before continuing so commands and guidance match. Under host
@@ -405,7 +412,7 @@ FULL TOPIC LIST
405
412
  //#endregion
406
413
  //#region src/guide/api.ts
407
414
  var api_default = {
408
- summary: "Typed lifecycle API: createStim, run, stop, diagnostics, and cancellation",
415
+ summary: "Typed lifecycle API: createStim, build, run, stop, diagnostics, and cancellation",
409
416
  body: () => `PROGRAMMATIC API
410
417
 
411
418
  Install stim as a project dependency: npm install --save-dev stim.
@@ -435,8 +442,27 @@ run takes platform plus its options:
435
442
  macos: remoteBuild
436
443
  web: headed (default false)
437
444
  iOS and Android also accept slot, metroCheck, buildCache, and remoteBuild.
438
- All methods accept signal. run builds, installs and launches; build-only is
439
- not available. Web requires a running server, just like stim web.
445
+ All methods accept signal. run builds, installs and launches.
446
+ Web requires a running server, just like stim web.
447
+
448
+ build produces an artifact without acquiring a device, starting a runtime, or
449
+ stopping an existing session. Its result is inferred from platform:
450
+ const result = await stim.build({ platform: 'ios' });
451
+ console.log(result.facts.appPath);
452
+
453
+ Build options:
454
+ ios: scheme, configuration, arch (arm64, x86_64, all), buildCache, remoteBuild
455
+ android: variant, abi (arm64-v8a, armeabi-v7a, x86, x86_64, all), buildCache, remoteBuild
456
+ macos: remoteBuild
457
+ An iOS build targets the simulator. A Debug build defaults to the host
458
+ architecture (Android only while optimizations.android.targetAbiOnly is on, the
459
+ default); Release defaults to all. macOS builds the configured Debug SwiftPM app.
460
+ Build results retain an owned appPath (iOS), apkPath (Android) or bundle (macOS)
461
+ inside this workspace until worktree removal. Later builds keep earlier copies.
462
+ A build leaves a running app's last build record and build log untouched; it
463
+ writes its own build-artifact-<platform>.ndjson. No distribution archive or web build
464
+ pipeline is inferred. Use @stim-cli/ci's buildCI to export a portable artifact and
465
+ diagnostics for your CI provider.
440
466
 
441
467
  run returns { platform, facts }. iOS facts include udid; Android includes
442
468
  serial; both include bundleId, appPath, metroPort, cacheHit and launched.
@@ -518,7 +544,9 @@ worktree, carries git:
518
544
  null when git fails or does not answer within 3 s, or the
519
545
  worktree has no environment and sits in ~/Desktop, ~/Documents,
520
546
  ~/Downloads, iCloud Drive, ~/Library/CloudStorage or /Volumes,
521
- which status does not open on macOS
547
+ which status does not open on macOS; one-shot status also
548
+ leaves it null for a worktree with no live environment and
549
+ for every unprovisioned worktree
522
550
  changed tracked paths with staged or unstaged changes, conflicts included
523
551
  untracked untracked entries as git status lists them; a new directory
524
552
  counts once
@@ -528,16 +556,17 @@ worktree, carries git:
528
556
  mergedInto "origin/<default>" when gc would call the branch merged, judged
529
557
  from local refs without fetching, else null
530
558
 
531
- status runs \`git status --porcelain=v2 --branch\` in every worktree in
532
- parallel, and caches the merge verdict by HEAD and default-branch commit
559
+ status runs \`git status --porcelain=v2 --branch\` in the worktrees it reads,
560
+ six git calls at a time, and caches the merge verdict by HEAD and default-branch commit
533
561
  under $STIM_HOME/git-merge. One call starts no new merge check 250 ms after
534
562
  its first; later calls judge the rest, and until then a verdict for the same
535
563
  HEAD at an older default-branch commit stands in; a check that timed out
536
564
  is retried after 5 minutes. \`status --watch\` reuses a worktree's git read
537
565
  until its index, HEAD, reflog or the branch, upstream or default-branch refs
538
566
  change, and for at most 60 s, so a file edit, creation or deletion that is
539
- not staged can take up to a minute to show. Plain status prints "git: 2 changed, 1 untracked, ahead 3" under
540
- each environment, and the same after each worktree with no environment.
567
+ not staged can take up to a minute to show. One-shot status reads git only
568
+ for live environments; \`status --watch\` reads every worktree. Plain status prints "git: 2 changed, 1 untracked, ahead 3" under
569
+ each environment it read, and the same after each worktree with no environment under \`--watch\`.
541
570
 
542
571
  The paired phone's Work sheet can open changed and untracked files when the
543
572
  server advertises workspace-diff. File lists and selected patches load on
@@ -632,7 +661,8 @@ leased until <time>" for each one.`,
632
661
  port the Metro port RESERVED for this workspace
633
662
  supervisorPid the detached supervisor's pid, or NULL when a dev server was
634
663
  already answering that Stim did not start
635
- mode "bare-inproc" | "expo-child" | null (see \`guide metro\`)
664
+ mode "bare-inproc" | "expo-child" | "command-child" | null
665
+ (see \`guide metro\`)
636
666
  logsDir where the NDJSON timeline is written
637
667
  agentDevice { stateDir }: absolute workspace agent-device state path;
638
668
  set AGENT_DEVICE_STATE_DIR to it (see guide logs)
@@ -696,8 +726,9 @@ leased until <time>" for each one.`,
696
726
  configuration the Xcode configuration that was built ("Release" from
697
727
  --configuration or the ios.configuration setting); null for
698
728
  the default Debug
699
- scheme the explicit shared Xcode scheme selected by --scheme;
700
- absent for automatic selection; not the app URL scheme
729
+ scheme the explicit shared Xcode scheme selected by --scheme or
730
+ the ios.scheme setting; absent for automatic selection;
731
+ not the app URL scheme
701
732
  cacheKey the shared-build-cache key derived from it (the
702
733
  configuration is part of it: -release-sim vs
703
734
  -debug-sim-arm64). A single-architecture simulator build
@@ -757,8 +788,11 @@ leased until <time>" for each one.`,
757
788
  the bundle would cost more than installing it
758
789
  launched true, "bundling", or "unverified". THE THREE ARE DIFFERENT
759
790
  FACTS and only the last one is a problem.
760
- true Metro finished the bundle response, then the app stayed
761
- alive through a three-second stability window.
791
+ true Metro finished the bundle response, or an app
792
+ without Metro (a release build, or a native
793
+ Xcode or Gradle app) has a live process; then
794
+ the app stayed alive through a three-second
795
+ stability window.
762
796
  The command checks process liveness when the
763
797
  platform exposes it. Errors from that window
764
798
  are printed even when the app stays alive,
@@ -1013,6 +1047,9 @@ leased until <time>" for each one.`,
1013
1047
  optional fields
1014
1048
  findings the diagnostic findings; a lower resolved Stim is a
1015
1049
  costs-time finding with a PATH or installation remedy
1050
+ memory-culprit is a costs-time finding naming a process
1051
+ with an abnormally large footprint and, when it is safe to
1052
+ restart, the command
1016
1053
  offload-candidate is a note after 3+ successful local cold builds in 7 days average over 3 min, with no remote.machines and an online tailnet Mac; open Stim Desktop Settings > Remote Macs > Add
1017
1054
 
1018
1055
  ON FAILURE
@@ -1432,8 +1469,9 @@ RULES
1432
1469
  body: () => ` stim status --json
1433
1470
 
1434
1471
  logs { dir, errorsSinceMarker }, or null without a log directory
1435
- agentDevice { stateDir }: absolute workspace agent-device state path on
1436
- every environment, shared by its slots. Reporting it creates
1472
+ agentDevice { stateDir, installed }: absolute workspace agent-device state
1473
+ path on every environment, shared by its slots, and whether
1474
+ the agent-device executable is on PATH. Reporting it creates
1437
1475
  no directory; agent-device creates it when used.
1438
1476
 
1439
1477
  Each environment carries phase, where the workspace is in its lifecycle:
@@ -1463,6 +1501,9 @@ RULES
1463
1501
  of the Stim tutorial app (stim guide tutorial). Status
1464
1502
  reports whatever version it finds; Desktop decides which
1465
1503
  versions it supports. Static app.json only.
1504
+ doctorRuns { ios?: { at }, android?: { at } }: when stim doctor last
1505
+ ran in the workspace, per platform, as ISO timestamps;
1506
+ absent until it has run.
1466
1507
  recording { enabled }: whether stim-server may record the workspace's
1467
1508
  device screens for replay, from recording.enabled
1468
1509
 
@@ -1732,8 +1773,8 @@ RULES
1732
1773
  build { platform, slot, state, phase, startedAt, phaseStartedAt,
1733
1774
  outcome, outcomeKnown, cacheLookupOutcome?, expectedMs, expectedPhaseMs,
1734
1775
  completedPhaseMs?, basis,
1735
- plannedPhases, missReason?, missProvisional?, detail?, placement,
1736
- waitingOn?, waitingFor? }
1776
+ plannedPhases, missReason?, missProvisional?, detail?, activity?,
1777
+ placement, waitingOn?, waitingFor? }
1737
1778
 
1738
1779
  state "running" while the run's own native-run claim is live;
1739
1780
  "stale" when that claim was released or its process is
@@ -1749,7 +1790,10 @@ RULES
1749
1790
  once the app is ready and covers waiting for the
1750
1791
  device: its boot, adoption cleanup, or a physical
1751
1792
  device's lease and connection check. A boot that
1752
- finishes during the build adds no device time. An
1793
+ finishes during the build adds no device time. For a
1794
+ device hosted on another Mac, device covers reserving
1795
+ and preparing it there, install covers delivering the
1796
+ app, and launch starts when the host launches it. An
1753
1797
  --eas-profile run has no cache lookup, so its outcome
1754
1798
  stays the project's most recent one until install.
1755
1799
  A stim macos run enters only prepare, compile (SwiftPM,
@@ -1822,6 +1866,16 @@ RULES
1822
1866
  to file names, at most 160 characters
1823
1867
  updatedAt when the build last wrote it; the run writes it at most
1824
1868
  every 2 seconds
1869
+ activity during device or launch, once Stim observed what the run
1870
+ waits on: { name, startedAt, percent? }. name is
1871
+ "booting" while device waits for the owned simulator or
1872
+ emulator the run is booting; during launch, "bundling"
1873
+ from the app's bundle request to this workspace's Metro
1874
+ until Metro delivered it, or "waiting-ready" while the
1875
+ app reports its readiness pending; absent in between.
1876
+ startedAt is the evidence's time. percent is Metro's own
1877
+ progress for that request, present only once Metro
1878
+ reported one. Activity never changes launched.
1825
1879
  placement where the build runs: "local", or while it is offloaded
1826
1880
  { host, phase, startedAt, phaseStartedAt }. host is the
1827
1881
  remote.machines entry; phase is the step there: sync,
@@ -1842,6 +1896,7 @@ RULES
1842
1896
 
1843
1897
  build: ios compile, 1m10s elapsed -- about 3 min left (median of 4 cold runs)
1844
1898
  build: ios compile on janics-mac-mini (build, 2m10s), 3m05s elapsed
1899
+ build: ios launch (bundling JS 45%, 12s), 58s elapsed
1845
1900
 
1846
1901
  An environment with a recorded run also carries lastBuilds, each
1847
1902
  platform's most recent ios or android run, finished or failed:
@@ -1942,7 +1997,8 @@ RULES
1942
1997
  disk (environment) { worktreeBytes, nodeModulesBytes, buildBytes,
1943
1998
  measuredAt }
1944
1999
  worktreeBytes the linked worktree, or else the checkout holding the
1945
- workspace, node_modules included
2000
+ workspace, node_modules included; the linked
2001
+ worktrees nested inside a checkout are not counted
1946
2002
  nodeModulesBytes node_modules at that root and at the workspace path;
1947
2003
  part of worktreeBytes
1948
2004
  buildBytes Stim's folder for the workspace: Xcode derived data,
@@ -1952,11 +2008,13 @@ RULES
1952
2008
  disk (device) { bytes, measuredAt }: the simulator's data folder
1953
2009
  under CoreSimulator/Devices, or the AVD's .avd folder
1954
2010
 
1955
- \`status --watch\` runs one du at a time off its refresh path, and measures
1956
- a folder at most every 5 minutes while its environment is active and every
1957
- hour otherwise. It caches each size under $STIM_HOME/disk-usage, which
1958
- one-shot status only reads, so the fields appear once a watcher, such as
1959
- stim-server or Stim Desktop, has measured.
2011
+ \`status --watch\` runs one du at a time on the whole machine, at low
2012
+ priority, off its refresh path, and measures a folder at most every 5
2013
+ minutes while its environment is active and every hour otherwise. A walk
2014
+ that fails or times out is not repeated by any watcher for that period. It
2015
+ caches each size under $STIM_HOME/disk-usage, which one-shot status only
2016
+ reads, so the fields appear once a watcher, such as stim-server or Stim
2017
+ Desktop, has measured.
1960
2018
 
1961
2019
  An environment carries agents when coding-agent sessions work in it, most
1962
2020
  recently active first:
@@ -2018,7 +2076,9 @@ RULES
2018
2076
  builds included, when machine.memorySource is footprint. Otherwise it is
2019
2077
  the estimate, with memorySource estimate.
2020
2078
 
2021
- capacity.committedMb sums memoryMb. The memory budget plans before a boot
2079
+ capacity.committedMb sums memoryMb; in a \`status --watch --json\` line each
2080
+ figure, including an environment's memoryMb, is held on its own (see
2081
+ \`guide lifecycle\`), so the sums can differ from what they sum. The memory budget plans before a boot
2022
2082
  and always uses the estimate. What is using CPU and memory now is the
2023
2083
  top-level machine section:
2024
2084
 
@@ -2252,8 +2312,9 @@ HOW A RUN IS COUNTED (\`stats\`)
2252
2312
 
2253
2313
  DEVICE PLACEMENT (\`ios|android --remote auto\`)
2254
2314
  Auto runs include devicePlacement: { decision, reason, machine? } in the run
2255
- facts, lastBuilds and build history. decision is "local", "hosted" or
2256
- "waited-locally" (the local run actually waited for a device slot). The same
2315
+ facts, lastBuilds and build history. decision is "local", "hosted",
2316
+ "waited-locally" (the local run actually waited for a device slot) or "eas"
2317
+ (remote.easFallback put it on an EAS Simulator, reported like --remote eas). The same
2257
2318
  optional devicePlacement appears on the status device entry for each slot.
2258
2319
  Hosted host facts include selected: "auto" or the named machine, and reason
2259
2320
  for automatic placement. Plain status prints (auto: <reason>) after the host.
@@ -2433,8 +2494,9 @@ WHAT THE SUPERVISOR IS
2433
2494
  to install, and no cross-project state. It hosts the dev server, writes its
2434
2495
  output as NDJSON into the global workspace logs directory
2435
2496
  ($STIM_HOME/workspaces/<project>--<digest>/logs; see \`guide logs\`), and
2436
- records itself in that workspace's state.json before it starts serving. Two modes,
2437
- chosen by ecosystem detection:
2497
+ records itself in that workspace's state.json before it starts serving. Three
2498
+ modes: metro.command selects command-child, otherwise ecosystem detection
2499
+ picks one of the first two:
2438
2500
 
2439
2501
  bare-inproc bare React Native: Metro is hosted INSIDE the supervisor,
2440
2502
  from the project's own node_modules, with Stim's reporter
@@ -2443,6 +2505,15 @@ WHAT THE SUPERVISOR IS
2443
2505
  expo-child Expo: the project's own \`expo start --port <port>\` runs as
2444
2506
  a child and its stdout is parsed into records. Levels are
2445
2507
  INFERRED from each line, so those records carry raw: true.
2508
+ command-child metro.command (\`guide settings\`): that argv runs as a
2509
+ child in its own process group, with {port} replaced by the
2510
+ reserved port, and its output is parsed like expo-child's
2511
+ under command_stdout and command_stderr events. Stopping the
2512
+ dev server signals the whole group, so a wrapper such as
2513
+ yarn takes its Metro with it. Stim adds no reporter, warmup
2514
+ observer or shared transform store, so ios and android
2515
+ launches stay UNVERIFIED: read \`stim logs --source metro\`
2516
+ for the bundle lines.
2446
2517
 
2447
2518
  In expo-child mode, remote intent plus metro.tunnel "expo" makes \`start\` pass
2448
2519
  \`--tunnel\` and EXPO_UNSTABLE_TUNNEL_V2=1 (the legacy ws-tunnel path is
@@ -2461,7 +2532,8 @@ WHAT THE SUPERVISOR IS
2461
2532
 
2462
2533
  IDLE STOP: the supervisor stops its dev server after metro.idleStopMinutes
2463
2534
  (default 60; 0 never stops) with no bundle request, no client log record
2464
- (in-app console logs; for Expo, any stdout line of the Expo CLI) and no Stim
2535
+ (in-app console logs; for Expo or metro.command, any stdout line of the
2536
+ dev server) and no Stim
2465
2537
  command in the workspace (start, ios, android, reload, worktree warm). It
2466
2538
  checks once a minute. It keeps running while a bundle response is in
2467
2539
  flight, while a build in the workspace is in progress, while a stim ios,
@@ -2600,9 +2672,15 @@ then 8081, without probing or reserving; an invalid pin still refuses.
2600
2672
 
2601
2673
  New allocations scan TCP ports 8900-8999. They skip registry reservations
2602
2674
  and existing listeners, announcing occupied ports and upward retries on
2603
- stderr. Listener checks require lsof, or netstat on Windows. All 100 ports
2604
- occupied or reserved is a refusal; stop or release unused allocations in
2605
- their owning workspaces.
2675
+ stderr. All 100 ports occupied or reserved is a refusal; stop or release
2676
+ unused allocations in their owning workspaces.
2677
+
2678
+ Metro allocation (from 8082 up) and named allocation find listeners in the
2679
+ native TCP table: netstat on macOS and Windows, /proc/net on Linux. When that
2680
+ table is denied, empty or unreadable, as in some sandboxes, each candidate is
2681
+ checked with an lsof listener scan and connects to 127.0.0.1 and ::1 instead.
2682
+ Allocation refuses only when none of them can answer: start refuses with
2683
+ STIM_PORT_INSPECTION_FAILED, and ports get prints the same message.
2606
2684
 
2607
2685
  The machine registry, under STIM_HOME, serializes allocation and cleanup.
2608
2686
  The workspace is the nearest package.json directory, resolved through
@@ -2625,6 +2703,7 @@ ports lists named labels and ports, plus Metro marked managed.
2625
2703
  ports stop [label] kills TCP listeners on those named ports and releases
2626
2704
  the allocations. It sends SIGTERM, waits two seconds, then SIGKILL if needed;
2627
2705
  on Windows it terminates the listener's process tree with taskkill.
2706
+ Stopping listeners requires lsof on macOS and Linux, or netstat on Windows.
2628
2707
  It prints the PID and command (the image name on Windows) for each stopped
2629
2708
  process. The listener's cwd can be anywhere: the named reservation is
2630
2709
  permission to stop that listener.
@@ -2635,11 +2714,12 @@ ports release [label] releases without signalling a process.
2635
2714
  Omitting the label selects every named port, never Metro. stim stop leaves
2636
2715
  named ports alone.
2637
2716
 
2638
- worktree remove stops named listeners and releases their allocations.
2639
- gc reports allocations whose workspace no longer exists; gc --delete stops
2640
- and releases them. Unmounted or unresolved workspace paths are retained.
2641
- Failed stops keep the registry entry. Use the same current Stim version for
2642
- cleanup: versions without ports do not know about named allocations.
2717
+ worktree remove, gc --delete and automatic maintenance release named
2718
+ allocations without signalling their listeners; run ports stop first to stop
2719
+ a server this workspace started. gc reports allocations whose workspace no
2720
+ longer exists; gc --delete releases them. Unmounted or unresolved workspace
2721
+ paths are retained. Use the same current Stim version for cleanup: versions
2722
+ without ports do not know about named allocations.
2643
2723
 
2644
2724
  A shared API does not get a shared reservation. Pass its port by environment
2645
2725
  instead of allocating a separate label in every worktree.`
@@ -2864,7 +2944,8 @@ THE RECORD
2864
2944
  clockOffsetMs the offset added to deviceTs; absent if the query failed.
2865
2945
  A collector_clock warning then says timestamps retain device time.
2866
2946
  raw true when the level was inferred from a line of text rather than
2867
- reported by the producer (every expo-child record)
2947
+ reported by the producer (every expo-child and command-child
2948
+ record)
2868
2949
  context --errors --json only: the code frame and stack lines Expo
2869
2950
  printed after this error, as an array of strings. Absent when
2870
2951
  there are none. Stim adds it at query time; it is not in the
@@ -2915,27 +2996,31 @@ remote Mac and a named --remote target write none:
2915
2996
  stim logs --source placement --json
2916
2997
  Fields: kind (build or device), platform, settings [{ key, value, from }] with
2917
2998
  from flag, env, setting or default (remote.build and remote.buildMode for a
2918
- build, ios.remote or android.remote for a device), candidates [{ machine, code,
2999
+ build, ios.remote or android.remote, plus remote.easFallback when it is on, for
3000
+ a device), candidates [{ machine, code,
2919
3001
  msg, detail? }], choice { machine, code, msg } with machine local when the run
2920
3002
  stays on this Mac, and fallback { code, msg, machine? } when a remote Mac that
2921
3003
  was meant to take the run did not (level warn). The msg is the same text the
2922
3004
  placement: and build: phase lines print.
2923
3005
  Candidate codes: accepted, unreachable, busy, disk, load, version-mismatch
2924
3006
  (detail lists the toolchain parts: xcode, arch, jdk, ...), no-matching-device,
2925
- declined, memory, no-capacity. Choice codes for a build: placed, named,
3007
+ declined, memory, no-capacity; with machine eas, why an EAS Simulator was not
3008
+ used: eas-named-slot, eas-local-flags, eas-no-agent-device, eas-no-cli,
3009
+ eas-cli-too-old, eas-session-busy, eas-metro-unreachable, eas-logged-out, eas-not-enabled,
3010
+ eas-unavailable. Choice codes for a build: placed, named,
2926
3011
  forced, this-mac-busy, this-mac-free, mode-off, local-selected, no-remote-mac,
2927
3012
  unsupported; fallback codes: no-remote-mac-took-it, offload-failed, fallback.
2928
3013
  Choice codes for a device: placed, sticky, this-mac-free, no-remote-mac,
2929
- device-count-unknown, no-host-admits. Machine names appear in these records;
3014
+ device-count-unknown, no-host-admits, eas-fallback (machine eas). Machine names appear in these records;
2930
3015
  they stay in the local logs. These records carry no tokens.
2931
3016
 
2932
3017
  WHAT WRITES WHAT
2933
3018
  maintenance.ndjson workspace maintenance actions, failures and explaining skips
2934
- metro.ndjson the bundler, in both supervisor modes
3019
+ metro.ndjson the bundler, in every supervisor mode
2935
3020
  client.ndjson in-app console logs and redboxes -- BARE PROJECTS ONLY.
2936
- In expo-child mode everything Expo prints lands in
2937
- metro.ndjson with raw: true, so \`--source client\`
2938
- returns nothing there.
3021
+ In expo-child and command-child mode everything the
3022
+ dev server prints lands in metro.ndjson with raw: true,
3023
+ so \`--source client\` returns nothing there.
2939
3024
  device.ndjson the device-log collector uses \`simctl log stream\`
2940
3025
  predicated on the app, or \`adb logcat\` filtered to
2941
3026
  the app's pid. Local iOS simulator capture starts
@@ -3098,7 +3183,9 @@ WHAT WRITES WHAT
3098
3183
  build-android.ndjson extracted diagnostics at level error, and the launch as
3099
3184
  a marker record. One RUN's worth: each build starts the
3100
3185
  file over, so the first error in it always belongs to
3101
- the run that pointed you at it.
3186
+ the run that pointed you at it. A build from the API's
3187
+ build() writes build-artifact-<platform>.ndjson
3188
+ instead, so it never replaces a run's transcript.
3102
3189
 
3103
3190
  Only a dev server Stim hosted is captured. If you started the bundler
3104
3191
  yourself, the metro and client sources stay empty -- which is not a sign of a
@@ -3440,9 +3527,14 @@ Branch on the code, never on the message.`,
3440
3527
  works. See stim guide macos.`
3441
3528
  },
3442
3529
  STIM_OFFLOAD_REFUSED: {
3443
- summary: "the selected remote Mac cannot build this app",
3530
+ summary: "the selected remote Mac or automatic build pool cannot build this app",
3444
3531
  body: () => `STIM_OFFLOAD_REFUSED
3445
3532
 
3533
+ Automatic placement also refuses when local is excluded in remote.buildPoolDisabled
3534
+ and no enabled remote can finish this build, or when the build requires a local
3535
+ compiler. Enable local or choose an explicit --remote-build placement. Existing
3536
+ builds are not interrupted and a cache hit needs no compiler.
3537
+
3446
3538
  A named --remote-build selection is strict. The message names the machine
3447
3539
  and why it cannot take or finish the build: not configured or paired, approval
3448
3540
  pending or denied, unreachable or changed pinned identity, incompatible
@@ -3555,7 +3647,8 @@ Rerun with --remote-build auto for normal placement and local fallback, or
3555
3647
  body: () => `STIM_NO_SCHEME
3556
3648
  Stim could not list or select an app scheme in ios/. Share the intended app
3557
3649
  scheme so xcodebuild can see it. Select an available exact name with
3558
- \`stim ios --scheme <name>\`. An unknown explicit name prints available choices.
3650
+ \`stim ios --scheme <name>\`, or set it once with the ios.scheme setting. An
3651
+ unknown explicit name prints available choices.
3559
3652
  Without an explicit selector, a workspace
3560
3653
  name match wins; otherwise Stim accepts a sole non-test scheme, or a listed
3561
3654
  scheme matching app.json. Unmatched ambiguous schemes are refused.`
@@ -3888,7 +3981,9 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
3888
3981
  physical, hosted, remote, parked and other homes' devices and never deletes.
3889
3982
  A failed reclaim is logged and skipped; the run keeps waiting.
3890
3983
  Stop an environment (\`stim stop\`), pass a longer \`--wait <seconds>\`,
3891
- or raise concurrency.maxDevices. Waiting prints holder names and elapsed
3984
+ or raise concurrency.maxDevices. With --remote auto, remote.machines and
3985
+ the opt-in remote.easFallback (billed EAS Simulator) are tried before this
3986
+ wait; \`stim logs --source placement\` says why neither took the run. Waiting prints holder names and elapsed
3892
3987
  time; status JSON exposes build.waitingFor independently of phase.
3893
3988
  Stats records capacityWaits for waits and capacityRefusals for this code.
3894
3989
  See \`guide lifecycle concurrency\`.`
@@ -4175,6 +4270,17 @@ captured" (in metro.ndjson, bare RN)
4175
4270
  printed the last lines of the global workspace logs/supervisor.log above this -- read
4176
4271
  them. A cold Metro on a large graph can genuinely need more than the default
4177
4272
  60s: re-run with \`--wait 180\`. Otherwise \`stim stop\`, then \`start\`.`
4273
+ },
4274
+ STIM_PORT_INSPECTION_FAILED: {
4275
+ summary: "no free Metro port could be confirmed: netstat, lsof and loopback connects all failed",
4276
+ body: () => `STIM_PORT_INSPECTION_FAILED
4277
+ Stim reads the native TCP listener table (netstat on macOS and Windows,
4278
+ /proc/net on Linux) to find a free Metro port. When that table is denied,
4279
+ empty or unreadable, it checks each candidate with an lsof listener scan and
4280
+ connects to 127.0.0.1 and ::1. This refusal means none of them could answer,
4281
+ usually because a sandbox denies them. Allow Stim to run netstat or lsof, or
4282
+ to connect to loopback, then retry. A metro.port pin skips the scan for
4283
+ Metro. \`stim ports get\` prints the same message.`
4178
4284
  },
4179
4285
  STIM_SUPERVISOR_EXITED: {
4180
4286
  summary: "the dev server failed outright; the quoted supervisor.log tail is the real error",
@@ -4218,10 +4324,10 @@ captured" (in metro.ndjson, bare RN)
4218
4324
  \`--device-profile\` on an eas/proxy run (\`--remote\` or android.remote), a
4219
4325
  \`android --remote auto\`, a changed hosted placement, or hosted Debug
4220
4326
  with \`--no-metro-check\` (see lifecycle hosted-android), a working directory
4221
- with no package.json above it, or one whose nearest package.json does not
4222
- parse or depends on neither react-native nor expo, so the directory is not
4223
- an app (the refusal names that package.json and says which of the two it
4224
- is; \`doctor\` reports the same directory as a finding), a \`logs\` query in
4327
+ with no supported app project, or a project whose integration refuses this
4328
+ operation. React Native and Expo require a readable package.json that
4329
+ declares react-native or expo; the refusal names the invalid file.
4330
+ \`doctor\` reports project admission problems as findings. A \`logs\` query in
4225
4331
  a workspace that has never produced a log timeline (the refusal names the
4226
4332
  nearest registered descendant app with logs when one exists), an
4227
4333
  android/app/build.gradle that declares product flavors with
@@ -4248,7 +4354,7 @@ captured" (in metro.ndjson, bare RN)
4248
4354
  \`gc --json --cache <name>\` refuses with STIM_BAD_ARG when no shared cache
4249
4355
  carries the name; the remedy names the caches on this machine. \`gc\`
4250
4356
  refuses --cache together with --worktrees the same way.
4251
- A working directory with no package.json above it gets the same
4357
+ A working directory with no supported project above it gets the same
4252
4358
  STIM_NO_PROJECT refusal from \`start\`, \`ios\`, \`android\`, \`stop\`,
4253
4359
  \`reload\`, \`logs\`, \`doctor\` and \`device lock|unlock\`. With \`--json\` the { code, message, remedy } object
4254
4360
  is on stdout, except \`logs --json\`, whose stdout stays empty NDJSON; the
@@ -4709,6 +4815,10 @@ const lifecycle = {
4709
4815
  summary: "The full worktree -> start -> ios/android -> logs -> teardown flow, with sections for builds, devices and flags",
4710
4816
  preamble: () => `ENVIRONMENT LIFECYCLE
4711
4817
 
4818
+ For a native Xcode or Gradle app without React Native or Expo, read
4819
+ \`stim guide lifecycle native-ios\` or \`stim guide lifecycle native-android\`.
4820
+ The Metro and JavaScript steps below apply to React Native and Expo.
4821
+
4712
4822
  Two workflows share steps 2 through 6.
4713
4823
 
4714
4824
  SINGLE CHECKOUT: work in place, on a branch, in one directory. There is no
@@ -4784,7 +4894,7 @@ a seed belongs to this workflow.
4784
4894
  # unwarmed worktrees with no Stim registry entry. Git-created branches stay.
4785
4895
  stim worktree remove
4786
4896
 
4787
- A Debug \`ios\` or \`android\` run checks the reserved port before the device
4897
+ A React Native or Expo Debug \`ios\` or \`android\` run checks the reserved port before the device
4788
4898
  or the build. When no healthy dev server of this workspace answers there, it
4789
4899
  runs the same start as step 2: a dev server started outside Stim is reused, a
4790
4900
  reservation held by a foreign process moves to a free port, and the budgets
@@ -4948,7 +5058,10 @@ WAITING FOR A CHANGE
4948
5058
  To wait for a device, a build or a dev server instead of polling, run
4949
5059
  \`stim status --watch --json\`. It keeps running and prints one complete
4950
5060
  status payload per line: one at once, then one each time the payload
4951
- changes, never two identical ones in a row. It reacts to Stim state files,
5061
+ changes, never two identical ones in a row. In a watch line, a usage
5062
+ figure repeats its value in the previous line until it moves by a step: an owner's cpuPercent 5 points, an
5063
+ owner's or environment's memoryMb and capacity.committedMb 16 MB. An owner's row
5064
+ changes as a whole, so its residentMb and processes update with it. It reacts to Stim state files,
4952
5065
  the EAS session ledger, adb device arrivals and departures, and simulator
4953
5066
  state, and recomputes every 30 seconds as a fallback. A log append updates
4954
5067
  only the log error count and device activity, no sooner than 15 seconds
@@ -4959,6 +5072,161 @@ WAITING FOR A CHANGE
4959
5072
  still running, which it notices at the next change.
4960
5073
  Without --json it reprints the human view on change.`,
4961
5074
  sections: {
5075
+ "native-ios": {
5076
+ summary: "native Xcode project selection, process launch, cache behavior and current limits",
5077
+ body: () => `NATIVE XCODE APPS
5078
+ Run from the directory containing the .xcodeproj or .xcworkspace. A Node
5079
+ package.json is not required. React Native and Expo apps keep their existing
5080
+ integration.
5081
+
5082
+ stim doctor --platform ios
5083
+ stim ios --scheme MyApp --configuration Debug --json
5084
+ stim ios --scheme MyApp --configuration Release --json
5085
+ stim logs --errors
5086
+ stim stop
5087
+
5088
+ Use the project's actual scheme and configuration names. Shared Run schemes
5089
+ select application targets; a unique application target can also use its
5090
+ automatic scheme. A workspace owns its referenced projects. --scheme selects
5091
+ an exact name; duplicate container/scheme matches refuse instead of guessing.
5092
+ --configuration overrides ios.configuration in .stim.json, then defaults to
5093
+ Debug. Custom configurations such as Staging use the same path.
5094
+
5095
+ Native Debug, Release and custom configurations do not start or require Metro,
5096
+ embed JavaScript, or swap a JavaScript bundle. The app is launched as a native
5097
+ process and reports metroPort: null. launched: true establishes a live process;
5098
+ verify the expected screen and interaction on the reported device separately.
5099
+ After a native source edit, rerun stim ios. stim reload is for JavaScript.
5100
+ The existing owned simulator, device lease, signing, log and teardown services
5101
+ still apply. Stim does not change signing accounts or provisioning profiles.
5102
+
5103
+ A reusable artifact key includes the selected build configuration, source and
5104
+ known dependency inputs, toolchain and build options. In a git checkout,
5105
+ ignored files the project does not reference are not part of the key. Native
5106
+ Release cache reuse does not depend on releaseBundleSwap. Xcode compilation
5107
+ uses the shared compilation cache when the toolchain and settings support it.
5108
+ --plan predicts this same local artifact without preparing dependencies or
5109
+ choosing a device.
5110
+
5111
+ Inputs whose complete build closure cannot be established build locally with
5112
+ artifact caching skipped. This includes shell build phases, custom build rules,
5113
+ C-family header graphs, Swift package graphs and unresolved external input
5114
+ paths or compiler overrides. The miss reason explains the exclusion. --plan
5115
+ refuses such a prediction; omit --plan to prepare and build the app.
5116
+
5117
+ A compatible approved build Mac can compile a native Xcode app with
5118
+ --remote-build auto or --remote-build <name>. Both peers must support
5119
+ native-xcode-build. This first worker path requires a verified artifact identity
5120
+ and all inputs contained in the repository and visible to git. Ignored or
5121
+ external inputs keep the build here, and placement names the first one; their
5122
+ bytes are never silently omitted. The common case is a Finder .DS_Store that a
5123
+ global gitignore hides: delete it, or stop ignoring it, to offload. The worker
5124
+ compares its own toolchain and compiler environment identity before compiling
5125
+ and refuses, naming the differing parameter, so the build falls back here.
5126
+ Worker source mirrors remove stale inputs, while Xcode compilation caches remain
5127
+ in the worker's private Stim home. Physical builds stay local.
5128
+
5129
+ Native Xcode apps can install on an already approved hosting Mac with
5130
+ process-mode support:
5131
+ stim ios --scheme MyApp --configuration Debug --remote janics-mac-mini --json
5132
+
5133
+ The host must advertise hosted-ios-process; older hosts refuse with an update
5134
+ remedy before reservation or upload. --remote auto skips incompatible hosts,
5135
+ but an existing session stays on its recorded host until stim stop. Debug,
5136
+ Release and custom configurations use the offered simulator architecture,
5137
+ report metroPort: null, and close any previous session Metro bridge. Logs,
5138
+ view/control, agent access and scoped stop use the existing hosted-ios services.
5139
+ Verify UI and interaction separately from the host's live process evidence.
5140
+
5141
+ Unbounded native build offload, hosted --plan, eas/proxy devices and EAS artifact
5142
+ profiles remain unsupported for native Xcode apps. These limits do not change React Native or
5143
+ Expo support.
5144
+
5145
+ Copyable agent request:
5146
+ Run this native Xcode app with stim ios --scheme MyApp --configuration Debug
5147
+ --json. Verify its expected screen and interaction on the reported device,
5148
+ inspect stim logs --errors, then stop only this workspace with stim stop.`
5149
+ },
5150
+ "native-android": {
5151
+ summary: "native Gradle app selection, hosted emulators, build-worker inputs and current limits",
5152
+ body: () => `NATIVE ANDROID GRADLE APPS
5153
+ Native Android Gradle apps run directly with \`stim android [--variant freeDebug]\`:
5154
+ no Metro, npm install or React Native dependency is required. The directory must
5155
+ contain a Gradle wrapper and settings.gradle or settings.gradle.kts. AGP resolves
5156
+ one application module and the exact variant (debug by default). Stim installs
5157
+ one signed universal or matching-ABI APK on an owned emulator or leased phone.
5158
+ Use \`--remote <approved-mac>\` or \`--remote auto\` for a hosted emulator; the host
5159
+ must support native Android process mode. Multiple application modules,
5160
+ density/split APK sets and EAS/proxy targets are unsupported. Set
5161
+ org.gradle.configureondemand=false; configuration cache remains supported.
5162
+ Stim verifies the existing APK signature; it does not sign it.
5163
+
5164
+ Stim artifact caching is unavailable because arbitrary Gradle inputs are not fully
5165
+ tracked. Build workers require a project-scoped \`android.offloadInputs\` declaration:
5166
+ \`{"complete":true,"ignored":["app/src/main/assets/generated.json"],"outputs":["build","app/build"]}\`.
5167
+ Use \`stim android --remote-build <approved-mac>\` after reviewing this declaration.
5168
+ Paths are exact repository-relative paths. complete affirms that Git-visible
5169
+ source plus the listed ignored files suffice, including optional files; Gradle
5170
+ DSL can read undeclared input without failing. Never list secrets or user Gradle
5171
+ homes. External inputs, directory links, submodules and custom local.properties
5172
+ refuse; an SDK-only local.properties stays local. Only declared directories
5173
+ reported by AGP and previously produced by this worker remain warm, along with
5174
+ worker project .gradle/.kotlin state. Undeclared bytes are removed. Gradle owns
5175
+ incremental, task-cache and configuration-cache reuse; transfer digests never
5176
+ become reusable APK cache keys. Older hosts refuse before source upload.
5177
+
5178
+ outputs must list every build directory of this build's projects that the build
5179
+ creates: the root build directory and each module's, library modules included.
5180
+ A remote build that creates an undeclared one fails with its name, and Stim
5181
+ builds locally; add it to outputs. A buildSrc directory refuses before upload.
5182
+ Included builds (includeBuild, such as a build-logic plugin build) and
5183
+ externalNativeBuild staging (.cxx) are not reported by AGP and cannot be
5184
+ declared, so a remote build that creates them fails and Stim builds locally.
5185
+
5186
+ Native Android \`--plan\` refuses without executing Gradle; \`doctor\` reports native
5187
+ prerequisites. \`reload\` refuses because the app has no Metro runtime. Re-run
5188
+ \`stim android\` after an edit and use \`stim stop\` for scoped cleanup.`
5189
+ },
5190
+ ci: {
5191
+ summary: "Run an app and tests through @stim-cli/ci with diagnostics and scoped cleanup",
5192
+ body: () => `CONTINUOUS INTEGRATION
5193
+
5194
+ Install app dependencies and platform tools first. Use a dedicated checkout:
5195
+ cleanup stops that workspace, including resources created before a build fails.
5196
+
5197
+ npx --yes --package @stim-cli/ci stim-ci run --platform ios --project ./app --timeout 1800 -- pnpm test:e2e
5198
+
5199
+ Or install npm install --global @stim-cli/ci and use stim-ci directly.
5200
+ Platforms: ios, android, macos, web. Everything after -- is argv, not a shell.
5201
+ --artifacts selects the result directory (a fresh temporary directory by default).
5202
+ An explicit artifacts directory must be empty; use a new directory per run.
5203
+ Stdout is one JSON result; progress and test output go to stderr.
5204
+
5205
+ Tests receive STIM_CI_PLATFORM, STIM_CI_DEVICE_ID, STIM_CI_APP_ID,
5206
+ STIM_CI_METRO_PORT, STIM_CI_ARTIFACTS_DIR and STIM_CI_RUN_RESULT. The last is
5207
+ run.json with the exact public API result. Use that target and check app
5208
+ readiness; launched can still be bundling or unverified.
5209
+
5210
+ result.json preserves the test exit code even if diagnostics or cleanup fail.
5211
+ Passing tests with failed cleanup return 1; timeout returns 124; cancellation
5212
+ returns 130. Leave 70 seconds before the job's hard timeout for diagnostics and
5213
+ stop. SIGKILL and runner loss cannot run cleanup. Stop shuts down owned devices;
5214
+ it never deletes them.
5215
+
5216
+ GitHub-hosted jobs default to $RUNNER_TEMP/stim-ci/home and
5217
+ $RUNNER_TEMP/stim-ci/build-cache, reused between steps in the job. Detection
5218
+ requires GITHUB_ACTIONS=true, RUNNER_ENVIRONMENT=github-hosted and RUNNER_TEMP.
5219
+ An explicit --home or STIM_HOME keeps the selected home and its normal cache
5220
+ configuration; --build-cache or STIM_BUILD_CACHE overrides the cache path.
5221
+ Self-hosted runners and local runs keep normal Stim configuration. Other
5222
+ exclusive disposable providers can set their job-local paths explicitly.
5223
+ CI=true alone does not change defaults, ownership or coordination. Persist only
5224
+ cache artifacts, not state, claims or device ledgers. Independent homes must
5225
+ not share a writable filesystem cache because its claims live in the home.
5226
+
5227
+ The programmatic runner is import { runCI } from '@stim-cli/ci'. See its package
5228
+ README for result types, cancellation, cache policy, and coordination findings.`
5229
+ },
4962
5230
  "hosted-android": {
4963
5231
  summary: "Android on a named approved Mac: strict placement, private Metro, status and stop",
4964
5232
  body: () => `ANDROID ON A HOSTING MAC
@@ -4982,7 +5250,7 @@ policy. No flag or setting runs on this Mac.
4982
5250
  A named Mac is strict: refusal, unreachable, declined or failed preparation
4983
5251
  returns STIM_HOSTING_REFUSED without fallback. --device, a different recorded
4984
5252
  machine, a running local owned emulator in the slot, and --no-metro-check for
4985
- hosted Debug refuse STIM_BAD_ARG. Run stim stop before switching placement.
5253
+ hosted Metro development apps refuse STIM_BAD_ARG. Run stim stop before switching placement.
4986
5254
  Unreadable android.host blocks only its slot; restore its recorded machine and
4987
5255
  session from the host before stopping that slot.
4988
5256
 
@@ -4992,7 +5260,7 @@ compatible build worker. Stim builds before reserving, records the session as
4992
5260
  soon as it exists, then delivers one App.apk. The emulator boots headless;
4993
5261
  androidEmulatorApp is ignored with a note.
4994
5262
 
4995
- Debug requires the local Metro supervisor. metro.publicUrl and metro.tunnel
5263
+ React Native Debug requires the local Metro supervisor. metro.publicUrl and metro.tunnel
4996
5264
  are ignored with a note. A private tailnet gateway reaches the host loopback
4997
5265
  bridge; the host reverses the client's Metro port into that bridge on its exact
4998
5266
  ledger-owned serial and sets debug_http_host to localhost:<clientMetroPort>.
@@ -5002,6 +5270,14 @@ adb against the host serial on this Mac. Release variants skip Metro. Launch
5002
5270
  is true only with client bundle evidence or a live host release process;
5003
5271
  bundling requires a bundle request and unverified has no launch evidence.
5004
5272
 
5273
+ Native Gradle apps use process mode for every variant. The host must advertise
5274
+ hosted-android-process; older hosts refuse before admission or APK upload.
5275
+ Native runs close any previous Metro bridge, launch the signed APK and require
5276
+ a live app process for launched=true. --no-metro-check is allowed, and reload
5277
+ refuses because there is no Metro runtime. Re-run stim android after an edit.
5278
+ Native EAS/proxy targets remain unsupported. Build offload requires the complete
5279
+ android.offloadInputs declaration; see stim guide lifecycle native-android.
5280
+
5005
5281
  Status adds android.host { machine, session, selected, agent, device: { name,
5006
5282
  systemImage, api }, state } per slot. The public name is the profile and API
5007
5283
  level, never the host serial or AVD name. ready, stopped and unverified are
@@ -5125,7 +5401,10 @@ there is a free concurrency.maxDevices slot (0 means unlimited), no device
5125
5401
  waiter ahead, normal host memory pressure, no budget shortfall,
5126
5402
  and 5-minute load per core below server.maxLoadPerCore. Without remote.machines,
5127
5403
  auto is local. An unknown local device count also stays local; boot admission
5128
- still decides. The default without a flag or setting stays local.
5404
+ still decides. The default without a flag or setting stays local. remote.devicePoolDisabled excludes
5405
+ members from new automatic placement only; an excluded local member is never a
5406
+ fallback. Existing sessions keep their owner and named placement bypasses the pool.
5407
+ See stim guide settings for separate build/device membership controls.
5129
5408
 
5130
5409
  Otherwise Stim asks every approved remote.machines host in parallel, with a
5131
5410
  3-second probe timeout and this run's model/runtime or image/profile selectors.
@@ -5136,9 +5415,24 @@ Hosts rank by the build preference (flag > STIM_REMOTE_BUILD > remote.build)
5136
5415
  when it names a machine, then lowest load, most free memory and remote.machines order. Automatic build
5137
5416
  offload resolves later, after the device architecture is known.
5138
5417
 
5139
- When no host admits, auto runs here and may wait in the existing FIFO device
5418
+ When no host admits and this Mac is at its concurrency.maxDevices cap (or runs
5419
+ are queued ahead), remote.easFallback (default false; EAS Simulator is billed)
5420
+ runs the device on an EAS Simulator exactly as --remote eas would: the build
5421
+ stays here, nothing starts an EAS cloud build, stop ends the session, and
5422
+ status reports it under remoteDevices. A busy Mac with a free slot never uses
5423
+ it. Before choosing it Stim checks, without starting a session, that eas-cli
5424
+ has simulator commands (and 22.2.0+ for --device-type), eas
5425
+ simulator:availability says this project's account can use it, agent-device
5426
+ is on PATH, the slot is default, no --runtime, --system-image or
5427
+ --device-profile flag is given, no EAS Simulator session of this workspace runs
5428
+ another platform or model, and a Debug run's Metro is reachable (not
5429
+ metro.tunnel off; on an Expo tunnel, run stim start --remote first). A recorded
5430
+ EAS session is not sticky: once this Mac has room, auto runs here and the
5431
+ session bills until stim stop. If any check fails, or the
5432
+ setting is off, auto runs here and may wait in the existing FIFO device
5140
5433
  slot queue; --no-wait and --wait 0 refuse with STIM_AT_CAPACITY. The placement
5141
- line explains the decision and the skipped hosts. JSON progress goes to stderr.
5434
+ line explains the decision, the skipped hosts and why EAS was not used
5435
+ (machine "eas"). JSON progress goes to stderr.
5142
5436
  The same decision, with a reason code per host, is a src: placement record in
5143
5437
  stim logs (stim guide logs).
5144
5438
  A recorded hosted session wins over load; a live local owned slot stays here.
@@ -5180,7 +5474,7 @@ sessions.
5180
5474
 
5181
5475
 
5182
5476
  Stim builds or fetches before reserving, records the session immediately, then
5183
- uploads the app on every run. Debug keeps Metro here; its supervisor owns a
5477
+ uploads the app on every run. React Native and Expo Debug keep Metro here; the supervisor owns a
5184
5478
  private gateway bound to this Mac's Tailscale address, pinned to the host peer
5185
5479
  and protected by a per-session secret. The host exposes only a loopback bridge.
5186
5480
  No start --remote is needed; metro.tunnel and metro.publicUrl are ignored.
@@ -5188,7 +5482,8 @@ Debug requires a running supervisor with private gateway support before reservin
5188
5482
  a simulator. --no-metro-check refuses with STIM_BAD_ARG for hosted Debug runs.
5189
5483
  If its supervisor is missing or older, run stim stop; stim start, then retry.
5190
5484
  Non-Debug runs skip Metro. Launch is unverified until this workspace's Metro
5191
- provides bundle evidence, or the host proves a live release process.
5485
+ provides bundle evidence, or the host proves a live release process. Native Xcode
5486
+ apps use process mode in every configuration and require no Metro; see native-ios.
5192
5487
 
5193
5488
  A rerun on the same named Mac reattaches and delivers a new app attempt. A
5194
5489
  stopped or missing session is replaced; an unreachable or unknown owner refuses
@@ -5861,6 +6156,11 @@ PREDICTING THE NEXT BUILD (--plan)
5861
6156
  installing, or starting Metro. They take no workspace lock and write no
5862
6157
  Stim state, cache entry or statistic, so they run beside a build.
5863
6158
 
6159
+ The selected project integration supplies the read-only plan and uses its
6160
+ build recipe's identity and cache policy. If it has no planner, --plan
6161
+ refuses with STIM_BAD_ARG; run the command without --plan. The refusal
6162
+ reports no cache hit. React Native and Expo planning behaves as follows.
6163
+
5864
6164
  stim ios --plan
5865
6165
  plan ios 1b625d.. -> local cache hit
5866
6166
  expect ~2.7s (median of 1 hit run)
@@ -5899,8 +6199,9 @@ PREDICTING THE NEXT BUILD (--plan)
5899
6199
  this Mac and a listed Mac would build for another architecture, placement
5900
6200
  says so, since that Mac's key differs. The plan only predicts the key: a
5901
6201
  run reads the architecture of the device it actually gets. A Mac that does
5902
- not answer, or ios.remote or android.remote set to eas or proxy, refuses
5903
- with STIM_BAD_ARG. Without --eas-profile an Android plan also
6202
+ not answer, ios.remote or android.remote set to eas or proxy, or auto that
6203
+ would use an EAS Simulator now (remote.easFallback), refuses with
6204
+ STIM_BAD_ARG. Without --eas-profile an Android plan also
5904
6205
  refuses the experimental compiler CAS with STIM_BAD_ARG, and refuses with
5905
6206
  STIM_NO_DEVICE when no system image is installed.
5906
6207
 
@@ -5910,13 +6211,16 @@ IOS SCHEME SELECTION
5910
6211
  Xcode scheme, not the app's URL scheme. Combine it with --configuration
5911
6212
  when choosing both an app scheme and a build configuration.
5912
6213
 
5913
- Without --scheme, automatic selection is unchanged:
6214
+ The ios.scheme setting names the scheme once for the app (guide settings);
6215
+ --scheme overrides it per run, and both give the same keys below.
6216
+
6217
+ Without --scheme or ios.scheme, automatic selection is unchanged:
5914
6218
  Stim keeps a scheme matching the workspace/project name, or the sole non-test
5915
6219
  scheme. When neither identifies one, it also checks the static top-level name
5916
6220
  in the app directory's app.json against Xcode's listed schemes. It does not
5917
6221
  execute app config or choose an arbitrary scheme from an ambiguous list.
5918
- If selection fails, pass --scheme with an available name, or share the app
5919
- scheme in Xcode first. See guide errors STIM_NO_SCHEME.
6222
+ If selection fails, pass --scheme with an available name, set ios.scheme, or
6223
+ share the app scheme in Xcode first. See guide errors STIM_NO_SCHEME.
5920
6224
 
5921
6225
  Explicit schemes have separate artifact keys, shared-build locks, and Xcode
5922
6226
  build directories. Stim identifies the resulting application from Xcode's
@@ -5925,7 +6229,8 @@ IOS SCHEME SELECTION
5925
6229
  configured Stim cache providers use the scheme-specific key. The older Expo
5926
6230
  buildCacheProvider tier is skipped for explicit schemes because a provider
5927
6231
  may key only on the fingerprint and return another scheme's app. Omitting
5928
- --scheme retains the existing cache keys and provider behavior.
6232
+ both --scheme and ios.scheme retains the existing cache keys and provider
6233
+ behavior.
5929
6234
 
5930
6235
  AN ARTIFACT THE DEVICE ALREADY HOLDS IS NOT INSTALLED AGAIN
5931
6236
  Both platforms store the artifact verbatim, so its hash is its identity.
@@ -5967,7 +6272,9 @@ A RUNNING APP IS RESTARTED, LIKE XCODE'S RUN
5967
6272
  launch com.example.app restarted running app (was pid 4242) (0.9s)
5968
6273
 
5969
6274
  A stop that fails refuses the launch rather than reusing the old process.
5970
- When the process list cannot be read, Stim launches without stopping
6275
+ When a simulator's process list cannot be read, Stim launches with
6276
+ \`simctl launch --terminate-running-process\`, which replaces any running
6277
+ copy. When an emulator's cannot be read, Stim launches without stopping
5971
6278
  anything and verification reports what it observes. An iPhone run
5972
6279
  (\`ios --device\`) needs no extra step: its collector launches with
5973
6280
  devicectl's \`--terminate-existing\`. Remote targets launch through
@@ -6886,7 +7193,7 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
6886
7193
  MUTATE: the cache entry stays the pristine, shareable artifact, and the
6887
7194
  per-run address lives only in the copy that is installed and then deleted.
6888
7195
 
6889
- A RELEASE device run builds fresh every time for now. A cached Release app
7196
+ A React Native or Expo RELEASE device run builds fresh every time for now. A cached Release app
6890
7197
  carries its BUILDER's JS, and the device JS swap (which has to re-seal what
6891
7198
  it injects) lands with phase 6 of appandflow/stim#178, so the cache hit is
6892
7199
  refused rather than installed with someone else's JavaScript.
@@ -7101,8 +7408,8 @@ THE POOL: WHICH DEVICE AN ID-LESS \`--device\` PICKS
7101
7408
  distribution stay out of scope.
7102
7409
 
7103
7410
  \`ios --configuration <name>\` selects the Xcode configuration --
7104
- \`--configuration Release\` builds a SIMULATOR Release app with the JS
7105
- bundle embedded. It overrides the ios.configuration setting (the app-level
7411
+ for React Native and Expo, \`--configuration Release\` builds a SIMULATOR
7412
+ Release app with the JS bundle embedded. It overrides the ios.configuration setting (the app-level
7106
7413
  default); unset, the Debug flow is unchanged. A non-Debug configuration
7107
7414
  skips Metro ENTIRELY: no gate, no port wiring, no dev-client deep link (a
7108
7415
  plain \`simctl launch\`), and the payload says \`metroPort: null\` --
@@ -7191,6 +7498,19 @@ HOST MEMORY PRESSURE AND STALLED SIMULATORS
7191
7498
  a failed query remains unknown. Existing swap or low free RAM alone is not
7192
7499
  enough to diagnose pressure.
7193
7500
 
7501
+ Doctor, and boot messages under warning or critical pressure, also name any
7502
+ process whose physical footprint is abnormal, read from macOS top (it counts
7503
+ compressed and swapped memory, which ps RSS leaves out, and reads root-owned
7504
+ daemons). Abnormal is at least a quarter of physical memory and 8 GiB, or
7505
+ 2 GiB for fseventsd and Watchman, which are normally far smaller. A known
7506
+ safe-to-restart process comes with its command; Stim never runs it:
7507
+ fseventsd sudo killall fseventsd (launchd restarts it;
7508
+ file watchers rescan once)
7509
+ Watchman stim gc --delete --cache watchman
7510
+ Gradle or Kotlin daemon stim gc --delete --cache gradle-daemons
7511
+ Any other process is named with its size only; ask before quitting it.
7512
+ Doctor skips this check when STIM_HOME is set: processes are machine-global.
7513
+
7194
7514
  If pressure is elevated, free host memory before retrying. Use \`stim stop\`
7195
7515
  only in workspaces you own and have finished using; ask before closing other
7196
7516
  agents' simulators or heavy apps. Never quit Device Hub or Simulator.app to
@@ -7239,7 +7559,9 @@ Pressure checks read free disk on the Stim home, projects and worker root
7239
7559
  volumes. On macOS the memory signal is the sysctl pressure level, with no
7240
7560
  signal if sysctl fails. Other platforms use os.freemem(); macOS never falls
7241
7561
  back to it. Memory pressure is recorded only; stopping devices, dev servers
7242
- and helpers is not automatic.
7562
+ and helpers is not automatic. While the level is warning or critical, the
7563
+ check also names abnormally large processes (guide lifecycle simslim) in
7564
+ status's maintenance block and the maintenance log.
7243
7565
 
7244
7566
  In on mode a pass runs, cheapest to rebuild first:
7245
7567
  1. orphaned workspace directories whose project is gone, and registered
@@ -7620,11 +7942,11 @@ STALE STATUS CACHE ENTRIES
7620
7942
  if it comes back.
7621
7943
 
7622
7944
  NAMED SERVER PORTS
7623
- worktree remove stops TCP listeners on each named allocation and releases
7624
- the ports. gc reports named allocations for missing workspaces; gc --delete
7625
- stops their listeners and releases them. Unmounted or unresolved paths stay
7626
- registered. Failed stops retain their allocations for a later retry.
7627
- stim stop does not touch named ports. See guide ports.
7945
+ worktree remove, gc --delete and automatic maintenance release named
7946
+ allocations without signalling their listeners. gc reports named
7947
+ allocations for missing workspaces; gc --delete releases them. Unmounted or
7948
+ unresolved paths stay registered. Only ports stop stops a named listener,
7949
+ and stim stop does not touch named ports. See guide ports.
7628
7950
 
7629
7951
  ON THE SOURCE CHECKOUT
7630
7952
  git cannot remove a repository's main working tree, and deleting the source
@@ -7873,7 +8195,23 @@ THE ONE CASE GC WILL NOT REAP
7873
8195
 
7874
8196
  --older-than does not apply to these kinds and is refused with
7875
8197
  STIM_BAD_ARG. With STIM_HOME set, gc skips them: they are machine-global.
7876
- \`stim doctor\` notes a watchman footprint over 2 GiB.`
8198
+ \`stim doctor\` notes a watchman footprint over 2 GiB.
8199
+
8200
+ A checkout that holds its linked worktrees inside it, such as
8201
+ .worktrees/<name>, makes a watchman root there crawl every worktree's files,
8202
+ node_modules and build output. \`stim doctor\` reports each such checkout
8203
+ whose .watchmanconfig ignore_dirs does not exclude them, as
8204
+ ${WATCHMAN_NESTED_WORKTREES}, and says when watchman watches it now.
8205
+ \`stim doctor --fix\` merges the worktrees' shared parent (.worktrees) when
8206
+ git tracks nothing in it, otherwise each worktree path, into that
8207
+ checkout's .watchmanconfig and keeps every other key; it refuses a file
8208
+ that is not a JSON object or whose ignore_dirs is not an array. Watchman
8209
+ matches entries literally: \`.worktrees/\` or \`./.worktrees\` ignores
8210
+ nothing. Commit the file. Watchman reads ignore_dirs only from a root's
8211
+ .watchmanconfig, never from a global config, and only when it adds the
8212
+ root, so an existing root needs \`watchman watch-del <root>\` before the
8213
+ ignore applies. Doctor never runs watch-del or shutdown-server; a watch-del
8214
+ plus a watchman restart frees the memory now.`
7877
8215
  },
7878
8216
  disk: {
7879
8217
  summary: "disk usage, workspace build outputs, logs and device recordings, AVD and build-log sizes, the data partition, trimming the shared caches",
@@ -8120,6 +8458,15 @@ KEYS STIM READS
8120
8458
  The \`--runtime\` flag overrides this per invocation,
8121
8459
  and an uninstalled version refuses the same way,
8122
8460
  naming the layer the value came from
8461
+ ios.scheme e.g. "RNTester" -- the shared Xcode scheme to build
8462
+ when the workspace lists several and none matches
8463
+ its name, which otherwise refuses with
8464
+ STIM_NO_SCHEME. The \`--scheme\` flag overrides it
8465
+ per invocation; an \`--eas-profile\` build reads only
8466
+ the flag. Part of the cache key, like the flag, so
8467
+ it also skips the older Expo buildCacheProvider tier,
8468
+ which may key only on the fingerprint. Workspace or
8469
+ committed scope.
8123
8470
  ios.configuration e.g. "Release" -- the Xcode configuration to build
8124
8471
  (simulator only). Committing
8125
8472
  { "ios": { "configuration": "Release" } } makes every
@@ -8130,7 +8477,21 @@ KEYS STIM READS
8130
8477
  ios.remote "proxy", "eas", or a named approved Mac from
8131
8478
  remote.machines, with the same meaning as --remote.
8132
8479
  "auto" places on an approved Mac when this Mac is full or
8133
- busy. Unset runs here. See lifecycle hosted-ios.
8480
+ busy. Unset runs here; "local" runs here even when a
8481
+ lower layer says otherwise. See lifecycle hosted-ios.
8482
+ ios.projectPath directory under the app holding a bare React Native
8483
+ app's Xcode project and Podfile; default "ios". "."
8484
+ is the app directory itself (RNTester's layout).
8485
+ Detection, pod install, xcodebuild, the bundle id,
8486
+ doctor, worktree warm and remove, and remote builds
8487
+ all use it. A subdirectory is fingerprinted the way
8488
+ ios/ is; "." hashes every top-level entry except
8489
+ node_modules, Pods, build, android, .stim.json and
8490
+ anything git ignores, so a JS edit misses the build
8491
+ cache instead of risking a stale build. Workspace or committed scope. ios refuses
8492
+ with STIM_BAD_ARG a path that escapes the app or
8493
+ holds no .xcworkspace or .xcodeproj, and any value
8494
+ on an Expo app, whose prebuild writes ios/.
8134
8495
  ios.simslimProfile a SimSlim JSON profile under the app directory,
8135
8496
  at most 64 KiB. Install the
8136
8497
  external tool once with
@@ -8241,6 +8602,26 @@ ${ANDROID_AVD_CONFIG_HELP.map((line) => ` ${line}`).joi
8241
8602
  and hw.audioOutput for that headless launch. With
8242
8603
  androidEmulatorApp "stim-desktop" on macOS, -gpu host
8243
8604
  overrides hw.gpu.mode the same way.
8605
+ android.gradleRoot directory holding gradlew and settings.gradle(.kts),
8606
+ relative to the app; default "android". It may sit
8607
+ outside the app but not outside the repository:
8608
+ RNTester in the React Native monorepo uses "../..".
8609
+ android.module the app's Gradle project path; default ":app", e.g.
8610
+ ":packages:rn-tester:android:app". Its directory is
8611
+ Gradle's default mapping from the root and must hold
8612
+ a build.gradle(.kts). With either set, Gradle runs
8613
+ from the root with qualified tasks such as
8614
+ <module>:assembleDebug; both values join the cache
8615
+ key and the remote build job, and the root's
8616
+ settings, build script, gradle.properties and
8617
+ gradle/ are fingerprinted. Native sources Gradle
8618
+ builds from elsewhere in the repository are not:
8619
+ list them in fingerprint.config.js extraSources.
8620
+ android refuses with STIM_BAD_ARG a root outside the
8621
+ repository or without settings.gradle, a module
8622
+ without a build script, and either value on an Expo
8623
+ app, whose prebuild writes android/. Workspace or
8624
+ committed scope.
8244
8625
  android.variant e.g. "productionDebug" -- the gradle variant to
8245
8626
  assemble and install on a project with product
8246
8627
  flavors. A repo like tlon-mobile with
@@ -8256,24 +8637,55 @@ ${ANDROID_AVD_CONFIG_HELP.map((line) => ` ${line}`).joi
8256
8637
  build: embedded JS, no Metro, cache keyed on the
8257
8638
  variant, and an APK re-pack on cache hits. See
8258
8639
  \`guide lifecycle release\`.
8640
+ android.offloadInputs a native Gradle build-worker declaration:
8641
+ {"complete":true,"ignored":[],"outputs":["build","app/build"]}.
8642
+ complete confirms Git-visible source plus the exact
8643
+ repository-relative ignored files suffice for this
8644
+ build. outputs names generated directories reported
8645
+ by AGP; recorded outputs can survive worker sync.
8646
+ Omitted optional inputs can change build results.
8647
+ Stim artifact caching remains unavailable. See
8648
+ \`stim guide lifecycle native-android\` for input,
8649
+ privacy and worker requirements.
8259
8650
  android.keystore the keystore a RE-PACKED release APK is signed with,
8260
8651
  absolute or relative to the project root. Unset means
8261
8652
  android/app/debug.keystore, which every RN and Expo
8262
8653
  android project carries -- the right default, because
8263
8654
  what this signs is a local emulator install and never
8264
8655
  anything distributed. Set it only when the release
8265
- variant must be signed with the repo's own key.
8656
+ variant must be signed with the repo's own key. A
8657
+ value in the committed .stim.json must resolve
8658
+ inside the repository (symlinks included); an
8659
+ absolute path outside it needs --scope workspace
8660
+ or repo.
8266
8661
  android.keystorePassword
8267
8662
  the password for it. apksigner's SCHEMED form is
8268
8663
  passed through unchanged (\`env:MY_KS_PASS\`,
8269
8664
  \`file:/keys/pw.txt\`, \`stdin\`), which is how a
8270
8665
  committed .stim.json avoids carrying a secret; a
8271
- bare string is used as the literal password. Unset
8272
- means the debug keystore's fixed "android".
8666
+ bare string (or \`pass:<password>\`) is the literal
8667
+ password, handed to apksigner through its
8668
+ environment, never its command line. Unset means
8669
+ the debug keystore's fixed "android".
8273
8670
  android.remote "proxy", "eas", or a named approved Mac in
8274
8671
  remote.machines; "auto" places on an approved Mac when
8275
- this Mac is full or busy. Unset runs here.
8672
+ this Mac is full or busy. Unset runs here; "local"
8673
+ runs here even when a lower layer says otherwise.
8276
8674
  See lifecycle hosted-android.
8675
+ remote.easFallback true lets "auto" (ios.remote, android.remote or
8676
+ --remote auto) run the simulator or emulator on a
8677
+ billed EAS Simulator when this Mac is at its
8678
+ concurrency.maxDevices cap (or has runs queued) and no
8679
+ remote.machines Mac takes the run, instead of waiting
8680
+ or refusing with STIM_AT_CAPACITY. Default false. A
8681
+ busy Mac with a free slot never uses it. Stim checks
8682
+ first that the run could use --remote eas (eas-cli
8683
+ with simulator commands, eas simulator:availability,
8684
+ agent-device, the default slot, a reachable Metro);
8685
+ otherwise it waits or refuses as before. Machine or
8686
+ project scope; a committed value opts in everyone who
8687
+ runs the app. Only the user enables it. See lifecycle
8688
+ hosted-ios.
8277
8689
  metro.tunnel selects how a remote device reaches this workspace's
8278
8690
  Metro after remote intent exists. Plain \`start\` stays
8279
8691
  local. For Expo and bare React Native, "auto" (default)
@@ -8288,6 +8700,21 @@ ${ANDROID_AVD_CONFIG_HELP.map((line) => ` ${line}`).joi
8288
8700
  foreground serve process, never Funnel; "auto" never
8289
8701
  selects it. The device must share the tailnet. See
8290
8702
  \`guide metro\`. Any other value is refused as invalid.
8703
+ metro.command argv array that starts this app's dev server, for an
8704
+ app whose Metro only runs through its own command,
8705
+ e.g. ["node", "../react-native/cli.js", "start",
8706
+ "--port", "{port}"]. No shell; it runs from the app
8707
+ directory, and {port} becomes the reserved Metro port,
8708
+ which the command must pass. It replaces bare-inproc
8709
+ and expo-child with command-child (see \`guide
8710
+ metro\`). Workspace or committed scope.
8711
+ Metro must keep running from inside the app, which
8712
+ is how Stim proves it is this app's. Without Stim's
8713
+ Metro reporter, launches stay UNVERIFIED. start
8714
+ refuses with STIM_BAD_ARG a value that is not such an
8715
+ array or has no {port}, --reset-cache (put the
8716
+ command's own reset flag in metro.command instead),
8717
+ and any use on Windows.
8291
8718
  metro.ngrokUrl the stable managed ngrok URL. It requires metro.tunnel
8292
8719
  "ngrok" and passes --url to ngrok http. Stim owns
8293
8720
  this process.
@@ -8522,8 +8949,9 @@ To show the booted simulator in Stim Desktop and open no simulator window:
8522
8949
 
8523
8950
  { "iosSimulatorApp": "stim-desktop" }
8524
8951
 
8525
- Stim Desktop selects the workspace that owns the simulator and focuses that
8526
- device. It only displays the simulator; it never boots or shuts it down.
8952
+ Stim Desktop keeps the current page and shows a launch card. Clicking Show
8953
+ opens the workspace that owns the simulator and focuses that device.
8954
+ It only displays the simulator; it never boots or shuts it down.
8527
8955
  When Stim Desktop is not running, Stim starts it without the command's
8528
8956
  \`STIM_HOME\`, so it reads the same Stim home as when you open it yourself.
8529
8957
  It shows only devices from that home: under another \`STIM_HOME\`, pick
@@ -8552,7 +8980,8 @@ headlessly and show it in Stim Desktop:
8552
8980
  { "androidEmulatorApp": "stim-desktop" }
8553
8981
 
8554
8982
  Stim then starts the emulator with \`-no-window -gpu host\` and opens
8555
- \`stim-desktop://open?serial=<serial>\` in the background. Stim Desktop reads
8983
+ \`stim-desktop://open?serial=<serial>\` in the background. Stim Desktop shows a
8984
+ launch card and opens the emulator only when Show is clicked. It reads
8556
8985
  frames and sends input over the emulator's gRPC endpoint. The setting applies
8557
8986
  only when Stim boots the emulator: one that is already running keeps its
8558
8987
  current display until it next boots, and physical devices are unaffected.
@@ -8635,6 +9064,32 @@ simulator sessions on, by MagicDNS name with an optional serve port (default
8635
9064
  stim settings set remote.machines '["janics-mac-mini"]'
8636
9065
  stim doctor --fix
8637
9066
 
9067
+ Automatic membership is separate from approval. In Desktop Settings > Remote Macs,
9068
+ use Builds enabled and Simulators enabled for this Mac or a configured remote.
9069
+ The equivalent machine settings list the excluded members; both default to []:
9070
+
9071
+ stim settings set remote.buildPoolDisabled '["local"]'
9072
+ stim settings set remote.devicePoolDisabled '["janics-mac-mini"]'
9073
+ stim settings unset remote.buildPoolDisabled
9074
+
9075
+ Use local for this Mac and exact remote.machines entries for remotes, including
9076
+ case and port. Names are not trimmed; unmatched entries exclude nothing. Copy the
9077
+ configured name or use Desktop's switches. Each pool
9078
+ must retain local or at least one configured remote already approved for that role.
9079
+ Offline approved members count as configured members, but placement still requires
9080
+ an available compatible host. Settings refuses removing the last member, including
9081
+ removing it from remote.machines. Disabling does not unpair a machine or stop a run.
9082
+
9083
+ These settings apply only to new automatic work requested by this Mac. They do not
9084
+ change which work other requesters send to a host. Named placement and --remote-build
9085
+ local bypass membership; the default device placement without --remote auto remains
9086
+ local. Existing local and hosted device sessions keep their owner. Cache hits remain
9087
+ usable without compiling. If local is excluded, automatic placement cannot fall back
9088
+ to a local compile or boot; unavailable hosts and unsupported offloads refuse. Excluding
9089
+ local alone does not trigger billed EAS fallback; an existing explicit EAS opt-in still
9090
+ requires the physical device cap or queue condition. Restore membership before retrying or choose
9091
+ an explicit placement. Build and simulator memberships are independent.
9092
+
8638
9093
  A remote Mac is used for a capability only after it grants that approval. Build
8639
9094
  and device-host approvals are separate: a Mac in the list that never granted
8640
9095
  one is not an error, it is not used for that capability, and doctor reports
@@ -8726,7 +9181,7 @@ value is unset. Trimmed auto/local are case-insensitive. Machine names match
8726
9181
  configured names case-insensitively, with port 7443 when omitted; reports use
8727
9182
  the configured entry.
8728
9183
 
8729
- auto follows remote.buildMode and keeps its local fallback behavior
9184
+ auto follows remote.buildMode, considering only enabled automatic pool members
8730
9185
  local builds only on this Mac for this invocation
8731
9186
  name requires a matching entry in remote.machines, already paired and
8732
9187
  approved for builds; ignores remote.buildMode and this Mac's load/slot gating
@@ -8765,7 +9220,7 @@ emulator debug build or a stim macos SwiftPM Debug build compiles:
8765
9220
  Mac's. A Mac too old to report its load counts only while every
8766
9221
  slot here is busy.
8767
9222
  force on a remote Mac whenever one accepts it
8768
- off always here
9223
+ off here when local remains enabled in the automatic build pool
8769
9224
 
8770
9225
  Load per core is the 5-minute load average divided by the CPU count; a Mac's
8771
9226
  native builds are its Stim runs in prebuild, pods or compile on that Mac, not
@@ -8793,7 +9248,7 @@ iPhone simulator on the target runtime. Its project-selected CocoaPods must matc
8793
9248
  the app's Gemfile.lock pins CocoaPods: both Macs then run that version through
8794
9249
  bundler, so the machine needs only Bundler on its stim-server PATH and
8795
9250
  installs the pinned gems itself on the first build. The comparison selects
8796
- the app's .ruby-version when installed, with pod install's UTF-8 locale defaults.
9251
+ the app's .ruby-version when installed, and otherwise the GEM_HOME, GEM_PATH and Ruby-related PATH entries of the Mac's login shell (read once with $SHELL -lic), unless the caller already sets GEM_HOME, with pod install's UTF-8 locale defaults. pod install, including the Bundler run, uses the same environment.
8797
9252
  For Android its JDK major
8798
9253
  version must match, and its Android SDK must hold the NDK, build-tools and
8799
9254
  compile platform that the project's React Native version names in
@@ -9225,7 +9680,7 @@ the same as for any web project: start the dev server, then run stim web.
9225
9680
  stim logs --errors
9226
9681
  stim reload web
9227
9682
  stim stop # closes Chrome; stim ports stop web stops Vite
9228
- stim worktree remove # in a linked worktree: Chrome, Vite and profile
9683
+ stim worktree remove # in a linked worktree: Chrome and profile, not Vite
9229
9684
 
9230
9685
  The repo layer is shared by every worktree of the repository, so a new
9231
9686
  worktree skips the two settings lines; the certificate and scheme remedies
@@ -9416,18 +9871,39 @@ executables, including Stim Desktop's sim-fold helper, are not built.
9416
9871
 
9417
9872
  stim macos # fixed SwiftPM Debug build, then launch
9418
9873
  stim macos --json # one launch record; progress goes to stderr
9874
+ stim macos --plan --json # validate the next build without running it
9419
9875
  stim status --json # environments[].macos and build state
9420
9876
  stim logs --source build
9421
9877
  stim logs --errors
9422
9878
  stim stop # stop this workspace's owned app and supervisor
9423
9879
 
9424
- Each invocation stops its previous owned app, rebuilds and launches. SwiftPM
9880
+ Each invocation validates the project settings, development plist and resource
9881
+ entries before stopping its previous owned app, rebuilding and launching. SwiftPM
9425
9882
  keeps incremental outputs in the workspace's runtime directory under STIM_HOME.
9883
+ Every process running an app bundle from that directory is the workspace's
9884
+ owned app, including copies opened through LaunchServices (open, agent-device
9885
+ open). stim macos, stim stop, stim worktree remove and stim gc --delete stop all
9886
+ of them: SIGTERM, up to 5 s, then SIGKILL, and success only once each has
9887
+ exited. When an app cannot be verified or does not exit, stop reports
9888
+ STIM_MACOS_OWNER_UNVERIFIED or a failure, and removal keeps the workspace.
9426
9889
  Local stim macos starts the app in the background without activating it or
9427
9890
  changing focus: it sets STIM_BACKGROUND_LAUNCH=1 in the app's environment, which
9428
- Stim Desktop honors. An app that activates itself at launch still takes focus.
9891
+ Stim Desktop honors, including for reopen events from open -g. An app that
9892
+ activates itself at launch or on reopen still takes focus.
9429
9893
  Hosted launches (macos --remote) do not set it.
9430
- macOS artifacts are not cached. This prototype has no --plan, --slot, or reload command.
9894
+ macOS artifacts are not cached. This prototype has no --slot or reload command.
9895
+ --plan validates the Swift Package directory, macos settings, development plist
9896
+ and declared resources without building, signing, staging, stopping or launching
9897
+ an app, writing workspace state or claiming a build slot. It does not execute
9898
+ Package.swift, so it cannot validate the executable product or package dependencies.
9899
+ --remote-build is respected, including named-machine setup refusals; no worker
9900
+ is contacted and live worker availability is unknown. --remote is launch-only
9901
+ and refuses with --plan. The JSON plan has platform "macos", product and
9902
+ buildMachine; fingerprint, cacheKey, provider, prebuild, outcome and expectedMs
9903
+ are null, cacheHit and cacheSkipped are false, and basis is 0. SwiftPM decides
9904
+ incremental compile work when a build runs; the plan predicts no cache outcome
9905
+ or duration. Desktop checks automatically while build details are visible,
9906
+ reusing a completed build or check for 60 seconds and skipping running builds.
9431
9907
  A failed build records its error and compiler output without launching an app.
9432
9908
  While it runs, stim status --json reports it as environments[].build with
9433
9909
  platform "macos": phase prepare, compile, install, then launch, and during
@@ -9453,7 +9929,10 @@ refuse before stopping the app or changing its build record. See stim guide erro
9453
9929
 
9454
9930
  With remote.build auto, remote.buildMode places these SwiftPM Debug builds:
9455
9931
  auto builds here while this Mac has capacity, force uses an approved build
9456
- machine when one accepts, and off always builds here. Configure remote.machines
9932
+ machine when one accepts, and off builds here when local remains in the automatic
9933
+ build pool. remote.buildPoolDisabled excludes members from automatic placement,
9934
+ including its local fallback. Explicit local or named placement bypasses membership.
9935
+ Configure remote.machines
9457
9936
  and approve build access as described in stim guide settings. The worker needs
9458
9937
  matching Stim, CPU architecture, Xcode and macOS SDK, plus network access to
9459
9938
  fetch package dependencies the first time. It keeps SwiftPM dependencies in a
@@ -9771,290 +10250,178 @@ this workspace's app. Do not change permissions or use custom build scripts."
9771
10250
  };
9772
10251
  //#endregion
9773
10252
  //#region src/guide/tutorial-data.ts
9774
- const TUTORIAL_PINS = {
9775
- createExpoApp: "5.0.0",
9776
- template: "expo-template-blank@58.0.15"
9777
- };
9778
- const TUTORIAL_FILES = {
9779
- "App.js": `import { useEffect, useState } from 'react';
9780
- import { Pressable, StyleSheet, Text, View } from 'react-native';
9781
- import { StatusBar } from 'expo-status-bar';
9782
- import { TITLE_COLOR } from './theme';
9783
-
9784
- const TAG = '[stim:tutorial]';
9785
-
9786
- function Button({ label, onPress }) {
9787
- return (
9788
- <Pressable accessibilityRole="button" onPress={onPress} style={styles.button}>
9789
- <Text style={styles.buttonText}>{label}</Text>
9790
- </Pressable>
9791
- );
9792
- }
9793
-
9794
- export default function App() {
9795
- const [note, setNote] = useState('');
9796
-
9797
- useEffect(() => {
9798
- console.log(\`\${TAG} title color=\${TITLE_COLOR}\`);
9799
- }, [TITLE_COLOR]);
9800
-
9801
- const logError = () => {
9802
- console.error(\`\${TAG} error-button test error\`);
9803
- setNote('Logged an error.');
9804
- };
9805
-
9806
- const crash = () => {
9807
- setTimeout(() => {
9808
- throw new Error(\`\${TAG} crash-button uncaught test error\`);
9809
- }, 0);
9810
- };
9811
-
9812
- const slowRequest = async () => {
9813
- const started = Date.now();
9814
- setNote('Waiting 3 seconds...');
9815
- await new Promise((resolve) => setTimeout(resolve, 3000));
9816
- const elapsed = Date.now() - started;
9817
- console.warn(\`\${TAG} slow-request \${elapsed}ms\`);
9818
- setNote(\`Slow request took \${elapsed}ms.\`);
9819
- };
9820
-
9821
- return (
9822
- <View style={styles.container}>
9823
- <Text style={[styles.title, { color: TITLE_COLOR }]}>Stim Tutorial</Text>
9824
- <Text style={styles.body}>Tap a button, then look at Stim Desktop &gt; Logs.</Text>
9825
- <Button label="Log an error" onPress={logError} />
9826
- <Button label="Crash me" onPress={crash} />
9827
- <Button label="Slow request" onPress={slowRequest} />
9828
- <Text style={styles.note}>{note}</Text>
9829
- <StatusBar style="auto" />
9830
- </View>
9831
- );
9832
- }
9833
-
9834
- const styles = StyleSheet.create({
9835
- container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', padding: 24 },
9836
- title: { fontSize: 32, fontWeight: '700', marginBottom: 8 },
9837
- body: { fontSize: 16, color: '#4b5563', textAlign: 'center', marginBottom: 24 },
9838
- button: { backgroundColor: '#111827', borderRadius: 10, paddingVertical: 14, paddingHorizontal: 28, marginBottom: 12 },
9839
- buttonText: { color: '#fff', fontSize: 17, fontWeight: '600' },
9840
- note: { marginTop: 12, fontSize: 15, color: '#374151' },
9841
- });
9842
- `,
9843
- "theme.js": `export const TITLE_COLOR = '#1f2937';
9844
- `,
9845
- "app.json": `{
9846
- "expo": {
9847
- "name": "Stim Tutorial",
9848
- "slug": "stim-tutorial",
9849
- "version": "1.0.0",
9850
- "orientation": "portrait",
9851
- "icon": "./assets/icon.png",
9852
- "userInterfaceStyle": "light",
9853
- "ios": {
9854
- "supportsTablet": true,
9855
- "bundleIdentifier": "dev.stim.tutorial"
9856
- },
9857
- "android": {
9858
- "adaptiveIcon": {
9859
- "backgroundColor": "#E6F4FE",
9860
- "foregroundImage": "./assets/android-icon-foreground.png",
9861
- "backgroundImage": "./assets/android-icon-background.png",
9862
- "monochromeImage": "./assets/android-icon-monochrome.png"
9863
- },
9864
- "package": "dev.stim.tutorial"
9865
- },
9866
- "web": {
9867
- "favicon": "./assets/favicon.png"
9868
- },
9869
- "extra": {
9870
- "stimTutorial": 1
9871
- }
9872
- }
9873
- }
9874
- `,
9875
- ".gitignore": `/ios
9876
- /android
9877
- /tutorial*.ad
9878
- /tutorial*.png
9879
- `
9880
- };
9881
- const TUTORIAL_PROMPTS = {
9882
- begin: "Run the Stim tutorial.",
9883
- rebuild: "Continue the Stim tutorial: rebuild",
9884
- agent: "Continue the Stim tutorial: agent",
9885
- refresh: "Continue the Stim tutorial: refresh",
9886
- machine: "Continue the Stim tutorial: machine",
9887
- finish: "Continue the Stim tutorial: finish"
9888
- };
10253
+ const TUTORIAL_REPO = "appandflow/stim-tutorial";
10254
+ const TUTORIAL_BUNDLE_ID = "dev.stim.tutorial";
9889
10255
  const TUTORIAL_RESTART_PROMPT = "Restart the Stim tutorial.";
10256
+ /**
10257
+ * The requests Stim Desktop offers to copy, written the way a developer would ask. {base}, {tour} and {worktrees}
10258
+ * are filled in by Desktop. A step with a section names the guide section that holds its safety rules.
10259
+ */
10260
+ const TUTORIAL_ASKS = {
10261
+ begin: `Clone ${TUTORIAL_REPO} into {base} and follow stim guide tutorial run.`,
10262
+ build: "Make the title purple in the Stim tutorial app and check it on the simulator.",
10263
+ agent: "Open the app on the iOS simulator, take a screenshot and confirm the title color.",
10264
+ finish: "I'm done with these experiments in {base} and don't need the changes. Stop the apps and remove the worktrees {worktrees}, and keep the clone. Follow stim guide tutorial finish.",
10265
+ delete: "Remove the Stim tutorial: remove its worktrees and the clone at {base} with stim worktree remove, then delete {base}. Follow stim guide tutorial delete.",
10266
+ share: `Open a pull request to ${TUTORIAL_REPO} with my title color change, with before and after screenshots from the simulator. See stim guide tutorial share.`,
10267
+ retry: "The first iOS build of the tutorial app in {tour} failed. Find out why and run it on iOS again."
10268
+ };
9890
10269
  const TUTORIAL_STEPS = [
9891
10270
  {
9892
10271
  id: "begin",
9893
- title: "Create the Tutorial",
10272
+ title: "Get the Test App",
9894
10273
  who: "agent",
9895
10274
  optional: false,
9896
- prompt: TUTORIAL_PROMPTS.begin,
10275
+ ask: TUTORIAL_ASKS.begin,
9897
10276
  section: "run",
9898
- manual: [
9899
- "base=\"{base}\"",
9900
- "mkdir -p \"${base%/*}\"",
9901
- "if [ ! -e \"$base\" ]; then",
9902
- "cd \"${base%/*}\"",
9903
- "if git -C \"${base%/*}\" rev-parse --is-inside-work-tree >/dev/null 2>&1; then echo \"Stop: inside another repository. Ask for another folder; never git add in the user repository.\"; exit 1; fi",
9904
- `npx --yes create-expo-app@${TUTORIAL_PINS.createExpoApp} stim-tutorial --template ${TUTORIAL_PINS.template} --no-install --no-agents-md --yes`,
9905
- "cd \"$base\"",
9906
- "stim guide tutorial app",
9907
- ...Object.entries(TUTORIAL_FILES).flatMap(([name, content]) => [`cat ${name === ".gitignore" ? ">>" : ">"} ${name} <<'STIM_TUTORIAL_EOF'`].concat(content.trimEnd().split("\n"), "STIM_TUTORIAL_EOF")),
9908
- "npm install --prefer-offline",
9909
- "npm pkg set scripts.ios=\"expo run:ios\" scripts.android=\"expo run:android\"",
9910
- "git init",
9911
- "git add -A",
9912
- "git -c user.name=Stim -c user.email=stim@localhost -c commit.gpgsign=false commit -m \"Stim tutorial\"",
9913
- "else",
9914
- "cd \"$base\"",
9915
- `node -e 'const fs = require("node:fs"); const app = fs.existsSync("app.json") ? JSON.parse(fs.readFileSync("app.json", "utf8")) : null; if (app?.expo?.extra?.stimTutorial !== ${JSON.parse(TUTORIAL_FILES["app.json"]).expo.extra.stimTutorial}) { console.error("Stop: existing non-tutorial folder or another version. Ask for another folder; never overwrite or delete it."); process.exit(1); }'`,
9916
- `git rev-parse --show-toplevel | node -e 'const fs = require("node:fs"); const root = fs.readFileSync(0, "utf8").trim(); if (fs.realpathSync(root) !== fs.realpathSync(process.cwd())) { console.error("Stop: inside another repository. Ask for another folder; never git add in the user repository."); process.exit(1); }'`,
9917
- "fi"
9918
- ]
10277
+ commands: []
9919
10278
  },
9920
10279
  {
9921
- id: "sidebar",
9922
- title: "Workspace in Sidebar",
10280
+ id: "build",
10281
+ title: "Make a Change",
9923
10282
  who: "you",
9924
10283
  optional: false,
9925
- prompt: null,
10284
+ ask: TUTORIAL_ASKS.build,
9926
10285
  section: null,
9927
- manual: []
10286
+ commands: []
9928
10287
  },
9929
10288
  {
9930
- id: "build",
9931
- title: "First iOS Build",
10289
+ id: "parallel",
10290
+ title: "Change It Again in Parallel",
9932
10291
  who: "you",
9933
10292
  optional: false,
9934
- prompt: null,
10293
+ ask: null,
9935
10294
  section: null,
9936
- manual: [
9937
- "cd \"{base}\"",
9938
- "git worktree add -B stim-tutorial/tour \"{tour}\" HEAD",
9939
- "cd \"{tour}\"",
9940
- "stim worktree warm",
9941
- "stim guide agent",
9942
- "stim doctor --platform ios",
9943
- "stim start",
9944
- "stim ios"
9945
- ]
9946
- },
9947
- {
9948
- id: "rebuild",
9949
- title: "Rebuild from Cache",
9950
- who: "agent",
9951
- optional: false,
9952
- prompt: TUTORIAL_PROMPTS.rebuild,
9953
- section: "rebuild",
9954
- manual: [
9955
- "cd \"{tour}\"",
9956
- "stim ios",
9957
- "stim status --json"
9958
- ]
10295
+ commands: []
9959
10296
  },
9960
10297
  {
9961
10298
  id: "device",
9962
10299
  title: "Live View and Control",
9963
10300
  who: "you",
9964
- optional: false,
9965
- prompt: null,
9966
- section: null,
9967
- manual: ["stim status"]
9968
- },
9969
- {
9970
- id: "logs",
9971
- title: "App Logs",
9972
- who: "you",
9973
- optional: false,
9974
- prompt: null,
10301
+ optional: true,
10302
+ ask: null,
9975
10303
  section: null,
9976
- manual: ["stim logs --errors", "stim logs --grep '\\[stim:tutorial\\]'"]
10304
+ commands: ["stim status"]
9977
10305
  },
9978
10306
  {
9979
10307
  id: "agent",
9980
10308
  title: "Agent Actions and Replay",
9981
10309
  who: "agent",
9982
- optional: false,
9983
- prompt: TUTORIAL_PROMPTS.agent,
9984
- section: "agent",
9985
- manual: [
10310
+ optional: true,
10311
+ ask: TUTORIAL_ASKS.agent,
10312
+ section: null,
10313
+ commands: [
9986
10314
  "cd \"{tour}\"",
9987
10315
  "export AGENT_DEVICE_STATE_DIR=\"{stateDir}\"",
9988
- "stim status --json",
9989
- "read -r iosUdid",
9990
- "agent-device open dev.stim.tutorial --platform ios --udid \"$iosUdid\" --save-script=tutorial.ad",
9991
- "agent-device react-native dismiss-overlay || true",
9992
- `agent-device press 'label="Log an error"' --settle`,
10316
+ `agent-device open ${TUTORIAL_BUNDLE_ID} --platform ios --udid {udid}`,
9993
10317
  "agent-device screenshot tutorial.png",
9994
10318
  "agent-device close",
9995
- "grep -v -e 'target-v1' -e 'dismiss-overlay' tutorial.ad > tutorial-replay.ad",
9996
- "agent-device replay tutorial-replay.ad --platform ios --udid \"$iosUdid\"",
9997
10319
  "stim logs --source agent --tail 10"
9998
10320
  ]
9999
10321
  },
10000
10322
  {
10001
- id: "refresh",
10002
- title: "Fast Refresh",
10003
- who: "agent",
10004
- optional: false,
10005
- prompt: TUTORIAL_PROMPTS.refresh,
10006
- section: "refresh",
10007
- manual: [
10008
- "cd \"{tour}\"",
10009
- "cat > theme.js <<'STIM_TUTORIAL_EOF'",
10010
- "export const TITLE_COLOR = '#7c3aed';",
10011
- "STIM_TUTORIAL_EOF",
10012
- "sleep 6",
10013
- "stim logs --errors",
10014
- "stim logs --grep 'title color'"
10015
- ]
10323
+ id: "logs",
10324
+ title: "App Logs",
10325
+ who: "you",
10326
+ optional: true,
10327
+ ask: null,
10328
+ section: null,
10329
+ commands: ["stim logs --errors", "stim logs --grep stim:tutorial"]
10016
10330
  },
10017
10331
  {
10018
10332
  id: "phone",
10019
10333
  title: "Watch on Your Phone",
10020
10334
  who: "you",
10021
10335
  optional: true,
10022
- prompt: null,
10336
+ ask: null,
10023
10337
  section: null,
10024
- manual: []
10338
+ commands: []
10025
10339
  },
10026
10340
  {
10027
- id: "machine",
10028
- title: "Build on Another Mac",
10029
- who: "both",
10341
+ id: "share",
10342
+ title: "Share Your Finish",
10343
+ who: "you",
10030
10344
  optional: true,
10031
- prompt: TUTORIAL_PROMPTS.machine,
10032
- section: "machine",
10033
- manual: ["cd \"{tour}\"", "stim ios --remote-build \"{machine}\" --no-build-cache"]
10345
+ ask: TUTORIAL_ASKS.share,
10346
+ section: "share",
10347
+ commands: [
10348
+ "cd \"{tour}\"",
10349
+ "git commit -am \"<one-line summary>\"",
10350
+ "mkdir -p .expo/screenshots",
10351
+ "git checkout origin/main -- theme.js",
10352
+ "xcrun simctl io {udid} screenshot .expo/screenshots/before.png",
10353
+ "git checkout HEAD -- theme.js",
10354
+ "xcrun simctl io {udid} screenshot .expo/screenshots/after.png",
10355
+ "cp .github/pull_request_template.md .expo/screenshots/body.md",
10356
+ `gh repo fork ${TUTORIAL_REPO} --remote --remote-name fork`,
10357
+ "git push -u fork HEAD",
10358
+ `gh pr create --repo ${TUTORIAL_REPO} --title "<one-line summary>" --body-file .expo/screenshots/body.md --attach ".expo/screenshots/before.png#Before" --attach ".expo/screenshots/after.png#After"`
10359
+ ]
10034
10360
  },
10035
10361
  {
10036
10362
  id: "finish",
10037
10363
  title: "Finish and Archive",
10038
10364
  who: "agent",
10039
10365
  optional: false,
10040
- prompt: TUTORIAL_PROMPTS.finish,
10366
+ ask: TUTORIAL_ASKS.finish,
10041
10367
  section: "finish",
10042
- manual: [
10043
- "git -C \"{tour}\" checkout -- theme.js",
10368
+ commands: [
10044
10369
  "cd \"{tour}\"",
10045
10370
  "stim stop",
10371
+ "cd \"{second}\"",
10372
+ "stim stop",
10046
10373
  "cd \"{base}\"",
10047
- "stim worktree remove \"{tour}\""
10374
+ "stim worktree remove \"{tour}\"",
10375
+ "stim worktree remove \"{second}\""
10376
+ ]
10377
+ },
10378
+ {
10379
+ id: "delete",
10380
+ title: "Delete the Test App",
10381
+ who: "agent",
10382
+ optional: true,
10383
+ ask: TUTORIAL_ASKS.delete,
10384
+ section: "delete",
10385
+ commands: [
10386
+ "cd \"{base}\"",
10387
+ "grep stimTutorial app.json",
10388
+ "git worktree list",
10389
+ "stim stop",
10390
+ "stim worktree remove \"{base}\"",
10391
+ "cd ..",
10392
+ "rm -rf \"{base}\""
10048
10393
  ]
10049
10394
  }
10050
10395
  ];
10051
10396
  //#endregion
10052
10397
  //#region src/guide/tutorial.ts
10053
- const paths = `Use {base} = ~/stim-tutorial and {tour} = ~/stim-tutorial-tour unless the
10054
- user named another parent folder. Expand ~ to the absolute home path when
10055
- substituting inside quotes. Keep the tutorial outside the user's project.`;
10398
+ const paths = `Use {base} = ~/stim-tutorial unless the user named another folder. Expand ~
10399
+ to the absolute home path when substituting inside quotes. Keep the tutorial
10400
+ outside the user's own projects.`;
10401
+ const local = `The tutorial runs on this Mac. Every time you run the tutorial app on iOS
10402
+ from a worktree of {base}, use stim ios --remote local --remote-build local,
10403
+ even when the user's settings would place the device or the build on another
10404
+ Mac. Only an explicit request to build on another Mac changes the build, with
10405
+ stim ios --remote local --remote-build "<machine>"; the device stays here.`;
10406
+ const screenshots = `Before and after screenshots, for a pull request: in each change's worktree,
10407
+ run the app on iOS before you edit anything, open it with agent-device and save
10408
+ the screen, then make the change and, once Fast Refresh applies it, save the
10409
+ same screen on the same device:
10410
+
10411
+ mkdir -p .expo/screenshots
10412
+ export AGENT_DEVICE_STATE_DIR="<agentDevice.stateDir from stim status --json>"
10413
+ agent-device open ${TUTORIAL_BUNDLE_ID} --platform ios --udid <ios.udid>
10414
+ agent-device screenshot .expo/screenshots/before.png
10415
+ (make the change)
10416
+ agent-device screenshot .expo/screenshots/after.png
10417
+
10418
+ Without agent-device, use xcrun simctl io <ios.udid> screenshot
10419
+ .expo/screenshots/before.png, using that worktree's udid from stim status --json,
10420
+ never booted. Take both with the same tool so they match in size. .expo/ is
10421
+ ignored and outside the native fingerprint, so the files are never committed,
10422
+ never change the build, and never block stim worktree remove. Keep each PNG under 1 MB.`;
10056
10423
  function commands(id) {
10057
- return TUTORIAL_STEPS.find((step) => step.id === id).manual.join("\n");
10424
+ return TUTORIAL_STEPS.find((step) => step.id === id).commands.join("\n");
10058
10425
  }
10059
10426
  //#endregion
10060
10427
  //#region src/guide/index.ts
@@ -10072,260 +10439,217 @@ const TOPICS = {
10072
10439
  web: web_default,
10073
10440
  macos: macos_default,
10074
10441
  tutorial: {
10075
- summary: "An iOS tutorial: isolated worktree, builds, devices, logs, agent actions, and cleanup",
10442
+ summary: "A cloned test app: a first build, a second change in parallel with a cache hit, and cleanup",
10076
10443
  sectionHint: "run",
10077
10444
  preamble: () => `STIM TUTORIAL
10078
10445
 
10079
- Create a small Expo app in its own repository and tour worktree. Follow the
10080
- normal Stim flow on iOS, then inspect builds, cache reuse, device control,
10081
- logs, agent actions, Fast Refresh, and cleanup.
10446
+ The tutorial clones ${TUTORIAL_REPO}, a tiny Expo app, into {base}; the clone is
10447
+ never run, and only the optional delete step removes it. The user then asks for a visual change, and for another
10448
+ change while the first builds. Each change runs in its own linked worktree of
10449
+ {base}, so each has its own simulator and Metro port, and the second worktree's
10450
+ first iOS build reuses the first one's native build. Only the clone, the
10451
+ optional share and the cleanup need this guide: the changes are ordinary
10452
+ requests, so follow stim guide agent for them, and check each change on the
10453
+ device.
10082
10454
 
10083
- Run one section per user request. Then end the turn, say what to look at in
10084
- Stim Desktop, and give the next prompt. Pause never means stim stop.
10085
- "${TUTORIAL_PROMPTS.begin}" means stim guide tutorial run.
10086
- "Continue the Stim tutorial: <section>" means stim guide tutorial <section>.
10087
- "${TUTORIAL_RESTART_PROMPT}" means stim guide tutorial restart.
10455
+ ${local}
10088
10456
 
10089
- Without Stim Desktop, use the simulator, stim status for the workspace and
10090
- device, stim logs --errors for errors, and stim stats for build performance.
10091
- Follow stim guide agent for doctor, errors, consent, and cleanup. Never pair
10092
- phones, approve machines, or grant access on the user's behalf.
10457
+ "Follow stim guide tutorial run" means stim guide tutorial run.
10458
+ "Follow stim guide tutorial finish" means stim guide tutorial finish.
10459
+ "Follow stim guide tutorial delete" means stim guide tutorial delete.
10460
+ "${TUTORIAL_RESTART_PROMPT}" means stim guide tutorial restart.
10093
10461
 
10094
10462
  ${paths}
10095
10463
 
10096
- Start with stim guide tutorial run. For commands to type yourself, read
10097
- stim guide tutorial manual. The first run needs network access unless the
10098
- required npm and native dependencies are already cached.`,
10464
+ Never pair phones, approve machines, or grant access on the user's behalf.
10465
+ For commands to type yourself, read stim guide tutorial manual.`,
10099
10466
  sections: {
10100
10467
  run: {
10101
- summary: "Create or reuse the app, warm a tour worktree, and build on iOS",
10468
+ summary: "Clone the test app into a fresh folder and install its dependencies",
10102
10469
  body: () => `RUN THE TUTORIAL
10103
10470
 
10104
10471
  ${paths}
10105
10472
 
10106
- Folder safety: create the base folder only if absent. Reuse an existing
10107
- folder only if app.json's expo.extra.stimTutorial equals this guide's version,
10108
- ${JSON.parse(TUTORIAL_FILES["app.json"]).expo.extra.stimTutorial}; skip creation and file writes when reusing it. For an existing non-tutorial
10109
- folder or another tutorial version, stop and ask the user for another folder.
10110
- Never overwrite or delete it.
10111
-
10112
- For a new app, first check the selected parent folder: create it if absent,
10113
- then run git -C <parent> rev-parse --is-inside-work-tree. If it succeeds, the
10114
- folder is inside another repository: stop and ask the user for another folder.
10115
- Never git add in the user's repo. Then, from the parent folder, run:
10116
-
10117
- npx --yes create-expo-app@${TUTORIAL_PINS.createExpoApp} stim-tutorial --template ${TUTORIAL_PINS.template} --no-install --no-agents-md --yes
10118
-
10119
- Enter the base folder. Run stim guide tutorial app, write its three app files
10120
- verbatim, and append its .gitignore lines. Then run:
10121
-
10122
- npm install --prefer-offline
10123
- npm pkg set scripts.ios="expo run:ios" scripts.android="expo run:android"
10124
- git init
10125
- git add -A
10126
- git -c user.name=Stim -c user.email=stim@localhost -c commit.gpgsign=false commit -m "Stim tutorial"
10127
-
10128
- On reuse, run git rev-parse --show-toplevel in the base folder before adding
10129
- a worktree; if it does not equal the base folder, the folder is inside another
10130
- repository: stop and ask the user for another folder.
10131
-
10132
- Set the scripts before the commit because Expo prebuild rewrites them to
10133
- expo run:ios and expo run:android; otherwise the dirty worktree blocks removal.
10134
- On npm or network failure, report stderr and stop.
10135
-
10136
- From the base folder, add the tour worktree and run:
10137
-
10138
- git worktree add -B stim-tutorial/tour "{tour}" HEAD
10139
- cd "{tour}"
10140
- stim worktree warm
10141
- stim guide agent
10142
-
10143
- Apply the guide agent doctor rule before native work: if its STATUS block
10144
- says doctor is due, run stim doctor --platform ios from the tour worktree and
10145
- resolve its findings. No STATUS block means doctor is current. Then run:
10146
-
10147
- stim start
10148
- stim ios
10149
-
10150
- Tell the user the first build can take about four minutes on a cold cache.
10151
- Relay the Open in Stim Desktop link printed by stim ios once.
10152
-
10153
- PAUSE: end the turn. Ask the user to look at the Build section, then the
10154
- device, then Logs in Stim Desktop. Without Desktop, use stim status,
10155
- stim stats, and stim logs --errors. Give the next prompt:
10156
- "${TUTORIAL_PROMPTS.rebuild}". Leave the workspace running.`
10473
+ Folder safety: if {base} does not exist, clone into it. If it exists, reuse it
10474
+ only when all of these hold: app.json has expo.extra.stimTutorial equal to
10475
+ ${TUTORIAL_VERSION}, git remote get-url origin names ${TUTORIAL_REPO}, git
10476
+ rev-parse --show-toplevel equals {base}, and git status --porcelain prints
10477
+ nothing. Then skip the clone, install its dependencies (npm ci), run stim doctor
10478
+ --platform ios there, and continue at PAUSE. For any other existing folder (another
10479
+ repository, local changes, no tutorial marker), stop and ask the user for
10480
+ another folder; never overwrite or delete it. Before cloning, check the parent folder:
10481
+ create it if absent, then run git -C <parent> rev-parse --is-inside-work-tree.
10482
+ If that succeeds, the folder is inside another repository: stop and ask the
10483
+ user for another folder. Never git add in the user's repo.
10484
+
10485
+ Then, from the parent folder, run:
10486
+
10487
+ git clone https://github.com/${TUTORIAL_REPO}.git stim-tutorial
10488
+
10489
+ Check that app.json in the clone has expo.extra.stimTutorial equal to
10490
+ ${TUTORIAL_VERSION}. If it does not, report the mismatch and stop; the user needs a newer Stim.
10491
+ Enter the clone and install its dependencies (npm ci) so the worktrees made for
10492
+ the changes inherit them, then run stim doctor --platform ios there: it registers
10493
+ the clone with Stim so Stim Desktop sees it, and builds or boots nothing. Do not
10494
+ run the app: the clone is only the base for the user's changes; only the optional
10495
+ delete step removes it.
10496
+ Report the findings of stim doctor and do not act on them: no SimSlim install, no
10497
+ --fix, nothing that changes the machine during the tutorial. On npm or network failure, report stderr
10498
+ and stop.
10499
+
10500
+ ${local}
10501
+
10502
+ ${screenshots}
10503
+
10504
+ PAUSE: end the turn. Tell the user to ask for a visual change next, such as
10505
+ making the title purple, in their own words. Each change runs in a new linked
10506
+ worktree of {base} (stim guide agent), and its first iOS build takes a few
10507
+ minutes.`
10157
10508
  },
10158
- app: {
10159
- summary: "Pinned template, verbatim app files, and .gitignore additions",
10160
- body: () => `TUTORIAL APP
10161
-
10162
- create-expo-app: ${TUTORIAL_PINS.createExpoApp}
10163
- Template: ${TUTORIAL_PINS.template}
10164
- Write the app files verbatim. Append the .gitignore lines to the template's
10165
- existing file. App log lines start with [stim:tutorial]. Crash me raises an
10166
- uncaught JavaScript error, and Slow request times a local three-second timer;
10167
- Stim does not capture native network requests.
10168
-
10169
- ${Object.entries(TUTORIAL_FILES).map(([name, content]) => `${name}\n\n\`\`\`${name.endsWith(".json") ? "json" : name.endsWith(".js") ? "js" : "text"}\n${content}\`\`\``).join("\n\n")}`
10170
- },
10171
- rebuild: {
10172
- summary: "Repeat the iOS build and explain the cache result",
10173
- body: () => `REBUILD
10174
-
10175
- ${paths}
10176
-
10177
- Run the same build again, never with --no-build-cache:
10178
-
10179
- ${commands("rebuild")}
10180
-
10181
- Read this workspace's environments[].lastBuilds.ios.cacheHit from
10182
- stim status --json, and the miss reason printed by stim ios. Explain a local or remote hit, or the actual miss
10183
- reason when cacheHit is false. A repeat run can miss; report the evidence.
10184
-
10185
- PAUSE: end the turn. Point at the Build section and cache badge, or stim stats.
10186
- Ask the user to open the device's live view and tap Log an error; without
10187
- Desktop use the simulator. Then inspect Logs or stim logs --errors. Crash me
10188
- and Slow request are optional: the former shows a red box, the latter prints
10189
- a timing line. Give the next prompt: "${TUTORIAL_PROMPTS.agent}".`
10190
- },
10191
- agent: {
10192
- summary: "Record a tap and screenshot, replay it, and inspect agent logs",
10193
- body: () => `AGENT ACTIONS
10194
-
10195
- ${paths}
10196
-
10197
- Set {stateDir} to this workspace's agentDevice.stateDir from stim ios or
10198
- stim status --json. Replace <ios.udid> with this workspace's ios.udid from
10199
- stim status --json. Use that exact owned simulator, not a guessed one.
10200
- Run from the tour worktree:
10201
-
10202
- ${commands("agent").replace("read -r iosUdid", "iosUdid=\"<ios.udid>\"")}
10203
-
10204
- Use --save-script=tutorial.ad with the equals sign: agent-device treats a
10205
- separate path as a URL and refuses. The dismiss-overlay step clears a red box that would cover the buttons.
10206
- Replay with the same --platform and --udid so its steps reach the agent log.
10207
- Remove the target-v1 evidence and dismiss-overlay lines before replay: replaying
10208
- them can fail with REPLAY_DIVERGENCE on the recorded button identity.
10209
- The scripts and screenshot stay in the tour worktree and are git-ignored.
10210
-
10211
- PAUSE: end the turn. Point at Agent Actions, or the agent log records just
10212
- printed. Expect another error-button line after replay. When screen recording
10213
- is enabled, the user can scrub Replay in Desktop. Give the next prompt:
10214
- "${TUTORIAL_PROMPTS.refresh}".`
10215
- },
10216
- refresh: {
10217
- summary: "Change the title to purple and verify Fast Refresh through logs",
10218
- body: () => `FAST REFRESH
10509
+ finish: {
10510
+ summary: "Stop and remove the two tutorial worktrees, keeping the clone",
10511
+ body: () => `FINISH
10219
10512
 
10220
10513
  ${paths}
10221
10514
 
10222
- Set TITLE_COLOR in theme.js to '#7c3aed' (purple). Wait a few seconds for
10223
- Fast Refresh, without reloading the app:
10224
-
10225
- ${commands("refresh")}
10515
+ The worktrees to remove are the two the tutorial tracked: the linked
10516
+ worktrees of {base} made for the user's changes. The finish request names them
10517
+ as {tour} and {second}. Never touch any other worktree, an earlier tutorial
10518
+ clone, or the clone itself. For each, stop from its path, then remove it from
10519
+ the clone:
10226
10520
 
10227
- The line [stim:tutorial] title color=#7c3aed is the proof that the edit reached
10228
- the app. The intentional button errors may still be in the error log; check
10229
- whether the edit introduced a new error.
10521
+ ${commands("finish")}
10230
10522
 
10231
- PAUSE: end the turn. Point at the purple title in the device view or simulator
10232
- and the title color log line. Phone viewing and an approved remote Mac are
10233
- optional user steps; skip them if unwanted. Never pair, approve, or grant
10234
- anything. If the user names an approved machine, the next prompt is
10235
- "${TUTORIAL_PROMPTS.machine}". Otherwise give
10236
- "${TUTORIAL_PROMPTS.finish}".`
10523
+ Use a plain remove first. The user's finish request says they do not need the
10524
+ changes, which is the consent stim guide agent asks for before worktree remove
10525
+ --force, for exactly those two paths: if the plain remove refuses one of them
10526
+ only because of uncommitted changes or commits found nowhere else, remove that
10527
+ worktree with --force. Never use --force on the clone or on any other
10528
+ worktree, and on any other refusal report it and stop. Keep the clone: the
10529
+ optional delete step removes it only when the user asks (stim guide tutorial
10530
+ delete).
10531
+
10532
+ If archive is enabled, tell the user the worktrees appear under Archived in
10533
+ Stim Desktop. With archive disabled, report removal without promising an
10534
+ archive. Without Desktop, inspect stim status --json for the removed
10535
+ environments. End the turn.`
10237
10536
  },
10238
- machine: {
10239
- summary: "Optionally build using a machine the user names and has approved",
10240
- body: () => `REMOTE MAC (OPTIONAL)
10537
+ delete: {
10538
+ summary: "Remove the tutorial worktrees and the clone through Stim, then delete the folder",
10539
+ body: () => `DELETE THE TEST APP (OPTIONAL)
10241
10540
 
10242
10541
  ${paths}
10243
10542
 
10244
- Proceed only when the user names an approved remote Mac. Substitute that
10245
- name for {machine}. Never approve, pair, or grant anything. If none is named,
10246
- ask for the name or let the user skip this step.
10247
-
10248
- ${commands("machine")}
10249
-
10250
- This step bypasses the artifact cache so the build can use the named machine.
10251
- A named machine refuses without a local fallback. If it refuses, report the
10252
- refusal and offer --remote-build auto or local; do not retry silently.
10253
-
10254
- PAUSE: end the turn. Point at the build's machine in Desktop or its report in
10255
- stim status --json. Give the next prompt: "${TUTORIAL_PROMPTS.finish}".`
10256
- },
10257
- finish: {
10258
- summary: "Revert the tutorial edit, stop, and remove only the tour worktree",
10259
- body: () => `FINISH
10260
-
10261
- ${paths}
10543
+ Only on the user's explicit request, which the delete prompt is. It covers
10544
+ {base} and its linked worktrees, nothing else. Remove them through Stim so their
10545
+ simulators, Metro ports and Stim records are torn down before the files go.
10262
10546
 
10263
- The user's finish request authorizes removing this tour worktree only.
10264
- Revert the refresh edit before removal; worktree remove refuses dirty trees.
10265
- Stop from the tour path, then remove from the base checkout:
10547
+ First check that {base}/app.json has expo.extra.stimTutorial. If it does not,
10548
+ {base} is not the tutorial clone: report it and stop.
10266
10549
 
10267
- ${commands("finish")}
10550
+ List the worktrees with git -C "{base}" worktree list. For each linked worktree
10551
+ (every entry except {base} itself), run stim stop from its path, then
10552
+ stim worktree remove "<path>" from {base}. Never use --force here: the delete
10553
+ request does not say which changes the user can lose. If a remove refuses, report
10554
+ it and stop without deleting anything, so the user can decide about that
10555
+ worktree.
10268
10556
 
10269
- Never use --force. On a refusal, report it and stop. Keep the base folder
10270
- and branch. Print these optional cleanup commands for the user; do not run. Deleting
10271
- the base folder also removes the branch, so they are alternatives:
10557
+ Then, from {base}, run stim stop and stim worktree remove "{base}". On the
10558
+ source checkout it reclaims the Stim environment and leaves the files. If it
10559
+ refuses, report it and stop. Only after it succeeds, from the parent folder,
10560
+ delete the clone:
10272
10561
 
10273
10562
  rm -rf "{base}"
10274
- git -C "{base}" branch -D stim-tutorial/tour
10275
10563
 
10276
- If archive is enabled, tell the user the tour appears under Archived in Stim
10277
- Desktop. With archive disabled, report removal without promising an archive.
10278
- Without Desktop, inspect stim status --json for the removed environment and,
10279
- when enabled, its archived entry. End the turn.`
10564
+ Never use --force in this step, and never delete any path other than {base}. Report
10565
+ what you removed and end the turn.`
10566
+ },
10567
+ share: {
10568
+ summary: "Optionally open a public pull request with before and after screenshots of the change",
10569
+ body: () => `SHARE YOUR FINISH (OPTIONAL)
10570
+
10571
+ Only on the user's explicit request, which the share prompt is, and before the
10572
+ finish step removes the worktrees. The pull request is public: the user's GitHub
10573
+ name and change appear on ${TUTORIAL_REPO}, and a bot replies and closes it. It
10574
+ needs gh signed in. Never open it unprompted or from any other step.
10575
+
10576
+ Work from the worktree holding the user's change, {tour}. Commit only the
10577
+ change's files there, never .expo/screenshots/. Fork the repository and push the
10578
+ branch to the fork, since the user has no write access:
10579
+
10580
+ gh repo fork ${TUTORIAL_REPO} --remote --remote-name fork
10581
+ git push -u fork HEAD
10582
+
10583
+ Screenshots: use .expo/screenshots/before.png and .expo/screenshots/after.png from that
10584
+ worktree (stim guide tutorial run). If either is missing, take it now on the same
10585
+ device, that worktree's ios.udid from stim status --json, never booted, with the
10586
+ app running from the branch (run it again first if needed). For
10587
+ a missing before, show the original screen with
10588
+ git checkout origin/main -- <changed files>, capture it once Fast Refresh applies,
10589
+ then restore the change with git checkout HEAD -- <changed files> and capture the
10590
+ after again with the same tool, so both match in size.
10591
+
10592
+ Body: write .expo/screenshots/body.md from the clone's
10593
+ .github/pull_request_template.md, keeping its headings, table and Stim link and
10594
+ dropping its <!-- --> comments. An older clone without the template gets the
10595
+ same parts, in this order:
10596
+ - the first line: one sentence saying what changed and why;
10597
+ - the table, with ![Before](./.expo/screenshots/before.png) and
10598
+ ![After](./.expo/screenshots/after.png) in its cells;
10599
+ - a How it was verified section, with only what you observed: Device is ios.name from
10600
+ stim status --json; Readiness is the "app reported ready" time stim ios
10601
+ printed; Build is lastBuilds.ios.durationMs and whether lastBuilds.ios.cacheHit
10602
+ was local or remote (a cache hit) or false (a full build), plus, when you know
10603
+ it, whether the second worktree's first iOS build was a cache hit; Logs is the
10604
+ result of stim logs --errors. Drop a line you did not observe rather than
10605
+ guess it;
10606
+ - the closing line: Built and verified with [Stim](https://github.com/appandflow/stim).
10607
+
10608
+ Open the pull request from {tour}:
10609
+
10610
+ gh pr create --repo ${TUTORIAL_REPO} --title "<the one-line summary>" \\
10611
+ --body-file .expo/screenshots/body.md \\
10612
+ --attach ".expo/screenshots/before.png#Before" --attach ".expo/screenshots/after.png#After"
10613
+
10614
+ --attach uploads each image and rewrites the matching ./.expo/screenshots/ reference in
10615
+ the body to the uploaded file, so the table shows both images. gh 2.99.0
10616
+ (2026-09-01) has --attach; confirm with gh pr create --help rather than guessing
10617
+ a version. When gh has no --attach, open the pull request with --body-file alone
10618
+ and tell the user to drag the two images into the table on GitHub. Give the user
10619
+ the pull request URL.`
10280
10620
  },
10281
10621
  restart: {
10282
- summary: "Remove the existing tour safely and repeat from the worktree step",
10622
+ summary: "Start the tutorial again without removing anything",
10283
10623
  body: () => `RESTART
10284
10624
 
10285
10625
  ${paths}
10286
10626
 
10287
- For "${TUTORIAL_RESTART_PROMPT}", if the tour worktree is present, revert
10288
- the refresh edit, stop, and remove it using the finish section's commands:
10289
-
10290
- ${commands("finish")}
10291
-
10292
- Never use --force. On a refusal, report it and stop. Keep the base repository.
10293
- Read stim guide tutorial run, recheck its folder and repository safety rules,
10294
- and redo run from the worktree step. Pause after the build as run instructs.`
10627
+ For "${TUTORIAL_RESTART_PROMPT}", remove nothing: the user may still want the
10628
+ earlier worktrees. Finish only removes the pair named in its request, so leave
10629
+ any earlier worktrees and tell the user they stay until they ask for each by
10630
+ path; never use --force for them. Reuse the clone at {base} only if its
10631
+ stimTutorial marker equals ${TUTORIAL_VERSION}; otherwise follow stim guide
10632
+ tutorial run into a fresh folder. Pause as run instructs. Stim Desktop starts
10633
+ over: only worktrees and builds after the restart count.`
10295
10634
  },
10296
10635
  manual: {
10297
- summary: "Commands for every step, including heredocs for the app files",
10636
+ summary: "The commands behind each step, for typing yourself",
10298
10637
  body: () => `MANUAL TUTORIAL
10299
10638
 
10300
10639
  ${paths}
10301
10640
 
10302
- These commands are for a person typing them, one step at a time,
10303
- as scripts: save a block to a file and run it with sh -e, so it stops on the
10304
- first failure. Read stderr and do not continue to later commands. Follow stim guide tutorial run for folder and repository
10305
- safety. Reuse only this version's tutorial app, never overwrite another folder.
10306
- The creation block skips writes on reuse and checks the repository root.
10307
-
10308
- Replace {base} and {tour} with absolute paths; {base} must end in stim-tutorial. For Agent Actions, replace
10309
- {stateDir} with agentDevice.stateDir from stim ios or stim status --json;
10310
- when read -r iosUdid waits, type this workspace's ios.udid from that status.
10311
- For the optional machine step,
10312
- replace {machine} with a machine you have already approved, or skip it.
10313
-
10314
- Look at the sidebar during warm and Build during the first build (about four
10315
- minutes on a cold cache). Compare cacheHit and missReason on rebuild. At Live
10316
- view and control, open the device viewer and tap Log an error; without Desktop
10317
- use the simulator. At App Logs, try Crash me or Slow request if wanted. A JS
10318
- crash shows a red box; the slow request is a local timer, not network capture.
10319
- At Watch on Your Phone, optionally open an already paired Stim phone to see
10320
- the tour workspace; phone setup and machine approval stay with you.
10321
- Use stim status, stim logs --errors, and stim stats without Desktop.
10322
-
10323
- ${TUTORIAL_STEPS.map((step) => `${step.title}${step.optional ? " (optional)" : ""}\n\n${step.manual.length ? `\`\`\`sh\n${step.manual.join("\n")}\n\`\`\`` : "Observe this step in Stim Desktop, or skip it without Desktop."}`).join("\n\n")}
10324
-
10325
- Finish removes only the tour worktree. Never use --force; report a refusal.
10326
- With archive enabled, find the tour under Archived in Desktop or archived[]
10327
- in stim status --json. Keep the base repository and branch. To delete them,
10328
- read stim guide tutorial finish for commands to review and run yourself.`
10641
+ Replace {base}, {tour} and {second} with absolute paths: {base} is the clone,
10642
+ {tour} the first worktree and {second} the second. For Agent Actions, replace
10643
+ {stateDir} with agentDevice.stateDir from stim ios or stim status --json and
10644
+ {udid} with the workspace's ios.udid. Clone with git
10645
+ clone https://github.com/${TUTORIAL_REPO}.git into a fresh folder outside any
10646
+ repository. Each change runs in its own worktree of that clone (stim guide
10647
+ agent); the clone itself is never run. Run each worktree on iOS with
10648
+ stim ios --remote local --remote-build local so it stays on this Mac.
10649
+
10650
+ ${TUTORIAL_STEPS.map((step) => `${step.title}${step.optional ? " (optional)" : ""}\n\n${step.commands.length ? `\`\`\`sh\n${step.commands.join("\n")}\n\`\`\`` : "Ask your agent in your own words, or observe this step in Stim Desktop."}`).join("\n\n")}
10651
+
10652
+ Finish removes only the two tutorial worktrees, never the clone; stim guide tutorial finish says when --force is allowed for them.`
10329
10653
  }
10330
10654
  }
10331
10655
  }
@@ -10409,7 +10733,7 @@ function renderIndex(version) {
10409
10733
  return lines.join("\n");
10410
10734
  }
10411
10735
  function guideCommand(program, version, status = (running) => guideStatus({
10412
- projectRoot: findProjectRoot$1(process.cwd()),
10736
+ projectRoot: findProjectRoot(process.cwd()),
10413
10737
  running
10414
10738
  })) {
10415
10739
  program.command("guide [topic] [section]").description("Print reference documentation for THIS version of Stim (topics: " + topicNames().join(", ") + "). A topic with sections prints its section index; name a section to print only it, e.g. `stim guide errors STIM_NO_METRO` or `stim guide lifecycle builds`. Generated by the binary, so it cannot drift from the installed CLI.").action(async (topic, section) => {