stim 1.13.0 → 1.15.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 (133) hide show
  1. package/README.md +2 -0
  2. package/dist/{activity-DfVKZTEB.mjs → activity-B2-kb7k0.mjs} +281 -41
  3. package/dist/{android-BHfAu2mf.mjs → android-CYmq0jNi.mjs} +2 -2
  4. package/dist/{android-CD8pr8Q2.mjs → android-Cu_L4RpL.mjs} +524 -412
  5. package/dist/{android-qx_31wJo.mjs → android-Db2eEMaf.mjs} +152 -65
  6. package/dist/android-cas-PnKfAdB2.mjs +167 -0
  7. package/dist/android-cas-compiler.mjs +2 -2
  8. package/dist/{app-install-CDFY2nUL.mjs → app-install-CAAplXAt.mjs} +162 -173
  9. package/dist/{budget-E5o91NR1.mjs → budget-CMiqghig.mjs} +1048 -77
  10. package/dist/{build-plan-BTa93o_r.mjs → build-plan-aeGMFX7e.mjs} +193 -650
  11. package/dist/build-progress-C2Jv1AtA.mjs +872 -0
  12. package/dist/build-slots-Df1ApSuH.mjs +127 -0
  13. package/dist/{cache-manifest-DWPF4RKJ.mjs → cache-manifest-DnUHBJaU.mjs} +2 -2
  14. package/dist/cache-manifest.mjs +1 -1
  15. package/dist/{chrome-B5hWgQJS.mjs → chrome-BrcVWQyD.mjs} +4 -4
  16. package/dist/cli-0cX5s4NM.mjs +45 -0
  17. package/dist/cli.mjs +6 -42
  18. package/dist/client-C2IS36Qf.mjs +1333 -0
  19. package/dist/collector-run.mjs +8 -7
  20. package/dist/{command-output-BIL7rlX7.mjs → command-output-CliHHn7a.mjs} +1 -1
  21. package/dist/{config-0jCcMM0E.mjs → config-D0DbpZtM.mjs} +29 -29
  22. package/dist/{created-devices-Cd558h31.mjs → created-devices-BwVb3liH.mjs} +2 -2
  23. package/dist/{dependency-state-DNN0yfx6.mjs → dependency-state-BmsjKZ2H.mjs} +2 -2
  24. package/dist/{deps-2Wo81np9.mjs → deps-BFlKbOIp.mjs} +3 -3
  25. package/dist/{detached-entry-DJHoca49.mjs → detached-entry-CiTEgMR1.mjs} +1 -1
  26. package/dist/dev-client-CjzLoNs0.mjs +80 -0
  27. package/dist/{device-Cf7vl-so.mjs → device-BZ1IyS5A.mjs} +7 -7
  28. package/dist/device-capacity-DMuKC7nx.mjs +136 -0
  29. package/dist/device-host-worker.d.mts +1 -0
  30. package/dist/device-host-worker.mjs +769 -0
  31. package/dist/device-ios-DMQBUPus.mjs +381 -0
  32. package/dist/{device-lease-CL9dZf73.mjs → device-lease-W5Pi1q38.mjs} +43 -36
  33. package/dist/{device-pool-MuoiyRcu.mjs → device-pool-B2r38YjS.mjs} +4 -4
  34. package/dist/{device-remote-DT9ngZiD.mjs → device-remote-9ulPsdhY.mjs} +68 -162
  35. package/dist/{doctor-D2iysNNH.mjs → doctor-BcE5xsBn.mjs} +191 -99
  36. package/dist/{error-diagnostics-BoiOhck8.mjs → error-diagnostics-Cb0P1WLS.mjs} +12 -11
  37. package/dist/{exec-DK8KaVHK.mjs → exec-CaErVv7t.mjs} +5 -1
  38. package/dist/{gc-eA-PRBX8.mjs → gc-BuL1Genr.mjs} +430 -102
  39. package/dist/{android-cas-B9Kv9aIy.mjs → gradle-CmdOBq1d.mjs} +8 -294
  40. package/dist/{guide-BfZ0OmiP.mjs → guide-cCSGRFd3.mjs} +2052 -240
  41. package/dist/{guide-status-BaMYu55M.mjs → guide-status-CIoWK6mG.mjs} +3 -3
  42. package/dist/hosted-macos-DwowNU7W.mjs +447 -0
  43. package/dist/idle-shutdown-B5LPaNyY.mjs +122 -0
  44. package/dist/in-use-CndPMilN.mjs +110 -0
  45. package/dist/{ios-CmEzZiOj.mjs → ios-3CtCRhwr.mjs} +201 -10
  46. package/dist/{ios-CcfCeZ19.mjs → ios-Chob8ny7.mjs} +560 -259
  47. package/dist/{ios-device-BW-t_Lkv.mjs → ios-device-DERmMmk-.mjs} +12 -7
  48. package/dist/{ios-device-BiOsSVzd.mjs → ios-device-Ee7sDXfp.mjs} +1 -1
  49. package/dist/{launch-verify-CuJaBpEa.mjs → launch-verify-CLlofxRt.mjs} +42 -11
  50. package/dist/{logs-DcSRbC2t.mjs → logs-h9h6MUvE.mjs} +95 -13
  51. package/dist/machine-usage-D_YYMzfR.mjs +283 -0
  52. package/dist/machines-CYtOCqcj.mjs +259 -0
  53. package/dist/macos-BwtbUtTN.mjs +468 -0
  54. package/dist/macos-run.d.mts +5 -0
  55. package/dist/macos-run.mjs +2 -0
  56. package/dist/{merge-state-ytFQCeh4.mjs → merge-state-BnxLNQiM.mjs} +22 -12
  57. package/dist/{metro-BPeY1YMh.mjs → metro-Di8EN2Tb.mjs} +20 -120
  58. package/dist/{named-ports-n7tnJsI8.mjs → named-ports-Dj_XBNaL.mjs} +8 -64
  59. package/dist/native-run-DFDoIRbY.mjs +147 -0
  60. package/dist/{native-runtime-CiiFHt0e.mjs → native-runtime-i6OkVg-w.mjs} +11 -6
  61. package/dist/offload-worker.d.mts +121 -0
  62. package/dist/offload-worker.mjs +601 -0
  63. package/dist/{ownership-Bz2kETAS.mjs → ownership-BkYxUtuj.mjs} +3 -2
  64. package/dist/{page-BTu7I60K.mjs → page-QF_Ke10s.mjs} +7 -2
  65. package/dist/{ports-C5UutqpJ.mjs → ports-DddwttKM.mjs} +3 -3
  66. package/dist/prebuild-BvxDlD6C.mjs +213 -0
  67. package/dist/process-identity-BmRm1qV0.mjs +12 -0
  68. package/dist/{project-DGeclhvO.mjs → project-CV3tDFHl.mjs} +9 -18
  69. package/dist/pull-request-pxnDCQzn.mjs +322 -0
  70. package/dist/pull-requests.d.mts +120 -0
  71. package/dist/pull-requests.mjs +2 -0
  72. package/dist/recordings-GEXp4iBT.mjs +37 -0
  73. package/dist/{reload-C-cV5yy_.mjs → reload-ChPiwvot.mjs} +70 -22
  74. package/dist/{remote-cache--aehr4Pr.mjs → remote-cache-CO2elm_b.mjs} +4 -4
  75. package/dist/run-B-xqRrsg.mjs +119 -0
  76. package/dist/{server-bare-Bq8BpSIo.mjs → server-bare-X7PUgizL.mjs} +60 -13
  77. package/dist/{server-expo-DUCg52-K.mjs → server-expo-DhtX6bom.mjs} +6 -10
  78. package/dist/server-expo-KJeD109I.mjs +2 -0
  79. package/dist/{settings-xB0Frkg6.mjs → settings-CduVcoxj.mjs} +77 -26
  80. package/dist/{settings-ssVBToDR.mjs → settings-DbL6uhH8.mjs} +16 -10
  81. package/dist/settings.schema.json +357 -2
  82. package/dist/{simslim-Bbg9UWrt.mjs → simslim-Ciluxlq4.mjs} +8 -132
  83. package/dist/slot-launch-BY0BrOLh.mjs +65 -0
  84. package/dist/{spawn-claims-PT8aQpCa.mjs → spawn-claims-D0XfSe8u.mjs} +1 -1
  85. package/dist/spawn-entry-DncpJvWL.mjs +16 -0
  86. package/dist/stage-CzaiwFsI.mjs +143 -0
  87. package/dist/start-CPO4wRg7.mjs +2 -0
  88. package/dist/{start-BZhItkiL.mjs → start-CUD9nasA.mjs} +108 -61
  89. package/dist/state-B4PQFXMW.d.mts +12 -0
  90. package/dist/state-BxCgeTQu.mjs +41 -0
  91. package/dist/{state-DwAU2_-D.mjs → state-CaeaHbDA.mjs} +7 -4
  92. package/dist/{state-l1pYwOre.mjs → state-DFm77wRn.mjs} +67 -5
  93. package/dist/stats-DBLLFmKd.mjs +66 -0
  94. package/dist/{status-TJlL0HWo.mjs → status-DYXxNOT0.mjs} +425 -260
  95. package/dist/{status-sszP4XKU.mjs → status-l84Gbvp2.mjs} +122 -20
  96. package/dist/stdout-reader-C8yjre1x.mjs +23 -0
  97. package/dist/stim-desktop-Cnz8CPnl.mjs +63 -0
  98. package/dist/stim-footprint.swift +32 -0
  99. package/dist/{stim-installations-CINXy_su.mjs → stim-installations-QMEI32v1.mjs} +1 -1
  100. package/dist/{stop-BZ2qMiBP.mjs → stop-Bad61z_2.mjs} +128 -19
  101. package/dist/stop-Bvf1MflS.mjs +48 -0
  102. package/dist/{stop-DskDKNTU.mjs → stop-CceYh1yG.mjs} +2 -2
  103. package/dist/stop-cause-CZ_Mr-v9.mjs +665 -0
  104. package/dist/supervisor-run.d.mts +44 -12
  105. package/dist/supervisor-run.mjs +276 -33
  106. package/dist/support-CDQbDkL5.mjs +315 -0
  107. package/dist/support-WLOzVKUv.mjs +176 -0
  108. package/dist/teardown-BYuQafiA.mjs +718 -0
  109. package/dist/{xcode-LM6A8cNZ.mjs → toolchain-DmeYjXD9.mjs} +363 -13
  110. package/dist/warm-progress-DVHLN4uC.mjs +463 -0
  111. package/dist/{idle-DxLi5OkN.mjs → watchman-Bx0wceoF.mjs} +55 -8
  112. package/dist/{web-sWXd5iek.mjs → web-k6w8-HnE.mjs} +39 -22
  113. package/dist/web-run.mjs +326 -13
  114. package/dist/workspace-process-lock-BL7Q0HCO.mjs +57 -0
  115. package/dist/workspace-state-CL2V2Ons.mjs +371 -0
  116. package/dist/worktree-BT_uULFx.mjs +2 -0
  117. package/dist/{worktree-DeBQSVtR.mjs → worktree-C0c5Udwa.mjs} +55 -54
  118. package/dist/{worktree-D4Do_XiQ.mjs → worktree-qXKzADSC.mjs} +161 -255
  119. package/package.json +10 -5
  120. package/shim/bundle-response.cjs +55 -10
  121. package/shim/bundle-response.d.cts +1 -0
  122. package/shim/expo-metro-config.cjs +10 -2
  123. package/dist/build-progress-l7d-X-ZP.mjs +0 -247
  124. package/dist/in-use-wYEaKQM9.mjs +0 -249
  125. package/dist/ownership-PUP4Dwai.mjs +0 -1138
  126. package/dist/server-expo-BeYwP_Xy.mjs +0 -2
  127. package/dist/slot-launch-BIWEW-wI.mjs +0 -19
  128. package/dist/start-Rw2GD3nV.mjs +0 -2
  129. package/dist/stats-CaSwmsSZ.mjs +0 -62
  130. package/dist/stats-D8rUTPHd.mjs +0 -368
  131. package/dist/stim-desktop-CXt8-Z3J.mjs +0 -40
  132. package/dist/worktree-3cBa-bQt.mjs +0 -2
  133. package/dist/{metro-store-DzeWkfqW.mjs → metro-store-CDDZHtPl.mjs} +1 -1
@@ -1,7 +1,7 @@
1
- import { a as findProjectRoot } from "./project-DGeclhvO.mjs";
2
- import { t as ANDROID_AVD_CONFIG_HELP } from "./settings-xB0Frkg6.mjs";
3
- import { t as RECENT_LAUNCH_MS } from "./status-sszP4XKU.mjs";
4
- import { t as guideStatus } from "./guide-status-BaMYu55M.mjs";
1
+ import { a as findProjectRoot$1 } from "./project-CV3tDFHl.mjs";
2
+ import { t as ANDROID_AVD_CONFIG_HELP } from "./settings-CduVcoxj.mjs";
3
+ import { t as RECENT_LAUNCH_MS } from "./status-l84Gbvp2.mjs";
4
+ import { t as guideStatus } from "./guide-status-CIoWK6mG.mjs";
5
5
  import { SETTINGS_SCHEMA_URL } from "@stim-cli/core/state";
6
6
  import chalk from "chalk";
7
7
  //#endregion
@@ -18,6 +18,10 @@ parse a stable payload.
18
18
 
19
19
  TWO WORKFLOWS
20
20
 
21
+ WORKTREE is the default. Take SINGLE CHECKOUT only when the user asks to work in
22
+ place, or the task depends on the user's uncommitted changes in the current
23
+ checkout; say why.
24
+
21
25
  SINGLE CHECKOUT: work in place, on whatever branch the task needs, in one
22
26
  directory. start, ios, android, logs, stop, and never a linked worktree. That
23
27
  directory is your workspace, and no rule below about keeping the source checkout
@@ -34,18 +38,17 @@ MULTIPLE DEVICES
34
38
  Use ios/android --slot <name> to retain multiple devices in one workspace,
35
39
  including several of the same model. Reuse the same slot name on subsequent
36
40
  runs. Read guide lifecycle options for the complete slot workflow. Use the
37
- reported device ID for UI interaction. All slots share Metro; reload can reach
38
- multiple devices, and a shared bundle request does not prove a slot launched.
41
+ reported device ID for UI interaction. All slots share Metro and reload can
42
+ reach multiple devices. A launch counts a bundle delivery only when it can
43
+ attribute it to its own device; read guide lifecycle options for the rule.
39
44
  Use stop --slot <name> for one slot, or plain stop for the whole workspace.
40
45
 
41
46
  NORMAL WORKFLOW
42
47
 
43
- Work in the current checkout by default. When the task needs another branch or
44
- an isolated environment, take the worktree workflow: create a linked worktree
45
- with Git and warm its ignored state. If a harness already created this linked
46
- worktree, run stim worktree warm here instead of creating another one. It
47
- copies missing ignored paths from the source checkout, including eligible .env
48
- and local configuration files. It preserves the branch, tracked files, and
48
+ Create a linked worktree with Git and warm its ignored state. If a harness
49
+ already created this linked worktree, run stim worktree warm here instead of
50
+ creating another one. It copies missing ignored paths from the source checkout,
51
+ including eligible .env and local configuration files. It preserves the branch, tracked files, and
49
52
  every existing destination entry; existing ignored directories are skipped
50
53
  whole, not filled in. Add --refresh to fast-forward the source checkout and
51
54
  install what moved there before the copy; it refuses a source checkout with local
@@ -75,7 +78,8 @@ other apps or other workspaces' devices.
75
78
 
76
79
  stim doctor --platform ios # or: --platform android
77
80
 
78
- # Skip Git creation if the harness already created this linked worktree.
81
+ # Worktree workflow. Skip Git creation if the harness already created this
82
+ # linked worktree.
79
83
  git worktree add -b <branch> <worktree-path> HEAD
80
84
  cd <worktree-path>
81
85
  stim worktree warm
@@ -83,6 +87,12 @@ other apps or other workspaces' devices.
83
87
  stim start
84
88
  stim ios # or: stim android, or: stim web
85
89
 
90
+ When Stim Desktop is installed, worktree warm, start, ios, android and web
91
+ print "Open in Stim Desktop: <link>" on stderr (links.desktop in JSON). The
92
+ first time one prints it in a workspace, tell the user once: "You can watch
93
+ this workspace in Stim Desktop: <link>". Do not repeat it after later
94
+ commands. When no command prints it, do not mention Stim Desktop.
95
+
86
96
  For the web target, stim web opens the page in a Stim-owned headless Chrome
87
97
  and captures its console and errors in stim logs. It never starts a web
88
98
  server: start the dev server first, then run stim web. For Expo web the dev
@@ -104,6 +114,9 @@ To learn whether the next build is a cache hit and how long it should take,
104
114
  without building, run stim ios --plan (or stim android --plan); see stim guide
105
115
  lifecycle builds.
106
116
 
117
+ Read stim guide settings to select a build machine. Keep a requested named
118
+ placement when recovering STIM_OFFLOAD_REFUSED: report its reason and remedy.
119
+
107
120
  Read stim guide lifecycle concurrency when a build waits on another workspace
108
121
  or a build call times out. A native build can outlive a shell timeout; if the
109
122
  tool call timed out, retry the same command and follow its printed remedy if
@@ -160,10 +173,11 @@ RULES DURING THE LOOP
160
173
  the error screen remains, follow the printed reload remedy instead of
161
174
  running ios or android again. If launch says FATAL because the app process exited,
162
175
  fix the crash and run the platform command again; Metro cannot restart it.
163
- - ios and android install the app, launch it, and check readiness. Trust the
164
- exact device, app, Metro, and launch facts in the final summary. Use the full
165
- reported device ID. Never assume a simulator named booted belongs to this
166
- workspace.
176
+ - ios and android install the app, launch it, and check readiness. A running
177
+ copy of the app is stopped and launched fresh, so in-app state does not
178
+ survive the run. Trust the exact device, app, Metro, and launch facts in the
179
+ final summary. Use the full reported device ID. Never assume a simulator
180
+ named booted belongs to this workspace.
167
181
  - After each ios or android run, give the user one compact result: exact device,
168
182
  app id, launch state, cache result, total duration, and whether stim logs
169
183
  --errors passed. Include a remedy only when action remains. Do not repeat the
@@ -195,10 +209,11 @@ the stim-<label> (<model> <runtime>) name. Never point Stim at a user-created
195
209
  emulator or simulator.
196
210
 
197
211
  worktree remove parks the workspace's simulator or emulator for later adoption.
198
- Before owned-device teardown, Stim best-effort closes local agent-device sessions
212
+ During owned-device teardown, Stim best-effort closes local agent-device sessions
199
213
  on that exact device; failures warn and teardown continues (guide cleanup).
200
- A parked device is Stim-owned: never delete one by hand. gc --delete clears verified
201
- entries and keeps failures; see guide lifecycle pool. First launch on a
214
+ A parked device is Stim-owned: never delete or erase one by hand. gc --delete clears
215
+ verified entries and keeps failures; gc --cache parked --delete erases them and keeps
216
+ them parked; see guide lifecycle pool. First launch on a
202
217
  physical iPhone can need the one-time taps named by the remedy.
203
218
 
204
219
  stim android --device [serial] and stim ios --device [udid] install on a
@@ -283,6 +298,7 @@ Read the matching guide before acting in these situations:
283
298
  | Web app in an owned Chrome (stim web) | stim guide web |
284
299
  | Web or API server ports | stim guide ports |
285
300
  | Remote device, custom Metro, or tunnel | stim guide metro |
301
+ | Metro stopped and no one ran stim stop | stim guide metro |
286
302
  | Cache miss, bypass, or fingerprint exclusions | stim guide lifecycle builds |
287
303
  | Capacity limits | stim guide lifecycle concurrency |
288
304
  | budget lines, reclaimed field, or low disk | stim guide lifecycle budget |
@@ -295,8 +311,11 @@ Read the matching guide before acting in these situations:
295
311
  | Parallel iOS, simulator stall, or memory pressure | stim guide lifecycle simslim |
296
312
  | Android boot timeout | stim guide errors STIM_NO_DEVICE |
297
313
  | gc or orphaned resources | stim guide cleanup gc |
314
+ | watchman or Gradle daemon memory | stim guide cleanup memory |
298
315
  | worktree remove refusal or --force | stim guide errors remove |
299
316
  | Cleanup failure or unverified cleanup ownership | stim guide errors teardown |
317
+ | Swift Package macOS development | stim guide macos |
318
+ | macOS app on another Mac (stim macos --remote) | stim guide macos |
300
319
  | Unfamiliar state or JSON field | stim guide facts payloads |
301
320
  | Refusal without a code | stim guide errors |
302
321
 
@@ -325,12 +344,14 @@ FULL TOPIC LIST
325
344
  stim guide lifecycle release # Release configurations and ...Release variants
326
345
  stim guide facts # the --json payloads
327
346
  stim guide facts devmenu # the Expo dev menu or Tools button over the app
347
+ stim guide macos # Swift Package Debug apps, logs, local preview, --remote on another Mac
328
348
  stim guide web # stim web: owned Chrome, page logs, launched, teardown
329
349
  stim guide ports # named ports for web and API servers
330
350
  stim guide metro # supervisor, custom Metro, tunnels, and remote devices
331
351
  stim guide logs # filters, record shape, and capture limits
332
352
  stim guide cleanup # what reclaims a device, and what deletes
333
353
  stim guide cleanup collector # an unproven collector pid; why the app on a phone closed
354
+ stim guide cleanup memory # watchman and Gradle daemon memory; gc --cache watchman
334
355
  stim guide settings # configuration files and supported keys`
335
356
  },
336
357
  facts: {
@@ -357,9 +378,13 @@ environment when one is registered at it or inside it. Each entry is
357
378
  directory of a bare repository. status lists worktrees from git's own worktree
358
379
  records, and on macOS runs no git in a worktree with no environment under a
359
380
  protected folder such as ~/Documents, so listing it raises no privacy prompt.
360
- \`worktree warm\` does not create an environment; \`start\`, \`ios\`,
361
- \`android\` and \`doctor\` register it. An environment's worktree is the
362
- linked worktree it is registered at or inside.
381
+ \`worktree warm\` registers the app it prepares, before it copies: the
382
+ nearest React Native or Expo app above where it runs when that is below the
383
+ worktree root; else the one app the source checkout has registered, at the
384
+ same path in this worktree; else the worktree root when it is the app. A warm
385
+ that finds none registers nothing and says so on stderr, and \`start\`, \`ios\`,
386
+ \`android\` and \`doctor\` register the app later. An environment's worktree is the linked worktree it is
387
+ registered at or inside.
363
388
 
364
389
  Each worktree entry, in unprovisionedWorktrees and in an environment's
365
390
  worktree, carries git:
@@ -389,6 +414,46 @@ change, and for at most 60 s, so a file edit, creation or deletion that is
389
414
  not staged can take up to a minute to show. Plain status prints "git: 2 changed, 1 untracked, ahead 3" under
390
415
  each environment, and the same after each worktree with no environment.
391
416
 
417
+ The paired phone's Work sheet can open changed and untracked files when the
418
+ server advertises workspace-diff. File lists and selected patches load on
419
+ demand, not through status. The read-only preview is capped at 200 files per
420
+ list and 256 KiB per patch response; binary and unavailable files are labeled. Active Git clean/process filters
421
+ (including Git LFS) refuse without running those commands.
422
+
423
+ A worktree entry also carries pullRequest, the GitHub pull request of its
424
+ branch and HEAD, once \`status --watch\` has looked it up:
425
+
426
+ pullRequest { number, url, title, state, checks, reviewDecision,
427
+ checkedAt }, null when GitHub has none for the branch and
428
+ HEAD, absent when never looked up: gh missing or signed
429
+ out, no GitHub remote, or status could not read the
430
+ worktree's git. A later gh failure keeps the last answer.
431
+ state "open", "draft", "merged" or "closed"
432
+ checks { passing, failing, pending } counts of the head commit's
433
+ check runs and commit statuses, or null when it has none
434
+ reviewDecision "approved", "changes-requested", "review-required" or null
435
+ checkedAt when Stim last asked GitHub
436
+
437
+ A worktree entry with git also carries gitChip, the git chip Stim Desktop and
438
+ the phone app show for it:
439
+
440
+ gitChip { parts, ci }
441
+ parts in order: { kind: "arrows", ahead, behind } when either is above
442
+ 0; { kind: "changed", count } of changed plus untracked entries;
443
+ { kind: "merged", into } when mergedInto is set and the pull
444
+ request is not merged; { kind: "no-upstream" } when the branch is
445
+ not merged and upstream is null
446
+ ci "failing" when a check fails, else "pending" when one is pending,
447
+ else "passing" when one passes; null without a pull request or
448
+ checks
449
+
450
+ \`status --watch\` asks GitHub off its refresh path, through the same gh api
451
+ graphql lookup gc uses, one call per repository, for a worktree whose lookup
452
+ is over 5 minutes old or whose branch or HEAD moved. It runs git in the
453
+ repository, never in the worktree. It caches each answer
454
+ under $STIM_HOME/pull-requests, which one-shot status only reads, and never
455
+ warns when gh is unavailable.
456
+
392
457
  status's remoteDevices lists each environment's recorded EAS Simulator
393
458
  session. The session is billable while it runs, and it makes the environment
394
459
  live. status reads only local records; it does not ask EAS whether the session
@@ -406,7 +471,34 @@ is still running.
406
471
  Stim created the session, or null
407
472
 
408
473
  Plain status prints one "remote <platform>: EAS session <id> billable" line
409
- per session, with the preview URL.`,
474
+ per session, with the preview URL.
475
+
476
+ status's physicalDevices lists each physical phone or tablet the environment
477
+ holds an unexpired lease on, from \`ios --device\`, \`android --device\` or
478
+ \`device lock\`, in every slot. A run lease ends with its run, so a phone
479
+ stays listed after the run only while \`device lock\` holds it. A
480
+ simulator or emulator lease is not listed. The field is absent when the
481
+ workspace leases no physical device.
482
+
483
+ physicalDevices [{ platform, slot, id, name, model, owned, physical,
484
+ connection, lease }]
485
+ id the UDID or adb serial
486
+ name the device's own name, else the name the lease recorded
487
+ model devicectl's marketing name ("iPhone 12 Pro"), null when
488
+ unread, or the Android model the lease recorded
489
+ owned always false: Stim uses a physical device, never owns it
490
+ physical always true
491
+ connection "connected" devicectl can reach the phone, or adb lists
492
+ the serial as device
493
+ "disconnected" the tool answered without it
494
+ "unknown" the tool could not be read in time
495
+ status reuses one devicectl or adb listing for 30 s
496
+ under \`status --watch\`
497
+ lease { holder, kind, grantedAt, expiresAt }: holder is the
498
+ workspace path, kind "declared" (device lock) or "run"
499
+
500
+ Plain status prints "ios: Old iPhone (physical, iPhone 12 Pro) connected --
501
+ leased until <time>" for each one.`,
410
502
  sections: {
411
503
  payloads: {
412
504
  summary: "every field of the start, ios, android, web and reload payloads, the error contract, the device rules",
@@ -418,6 +510,11 @@ per session, with the preview URL.`,
418
510
  mode "bare-inproc" | "expo-child" | null (see \`guide metro\`)
419
511
  logsDir where the NDJSON timeline is written
420
512
  alreadyRunning true when nothing needed starting
513
+ links { desktop }: a stim-desktop://workspace?path=<workspace>
514
+ link that shows this workspace in Stim Desktop. Absent when
515
+ no app on this Mac opens stim-desktop links. Plain output
516
+ prints it on stderr as "Open in Stim Desktop: <link>"; so
517
+ does worktree warm, which has no JSON payload
421
518
 
422
519
  stim ios --json
423
520
 
@@ -468,7 +565,12 @@ per session, with the preview URL.`,
468
565
  scheme the explicit shared Xcode scheme selected by --scheme;
469
566
  absent for automatic selection; not the app URL scheme
470
567
  cacheKey the shared-build-cache key derived from it (the
471
- configuration is part of it: -release-sim vs -debug-sim)
568
+ configuration is part of it: -release-sim vs
569
+ -debug-sim-arm64). A single-architecture simulator build
570
+ ends in its architecture (-arm64 or -x86-64): a local Debug
571
+ build compiles this Mac's, a --remote build the remote
572
+ host's. A local non-Debug simulator build compiles every
573
+ architecture the project lists and has no suffix
472
574
  With --eas-profile, fingerprint is computed by EAS CLI using the selected
473
575
  profile/environment, and cacheKey identifies the EAS project and build ID
474
576
  separately from local native builds. cacheHit is "remote" for the EAS
@@ -487,6 +589,15 @@ per session, with the preview URL.`,
487
589
  the device; it is a page, not a deep link.
488
590
  cacheSkipped true only when --no-build-cache was passed: "nothing was
489
591
  looked up", which is a different fact from "nothing was found"
592
+ buildMachine selected auto, local or machine name, including cache hits
593
+ builtOn here or machine name when a build ran; absent otherwise
594
+ offloadedTo only on an app a build machine compiled: its
595
+ offload.machines entry (see \`guide settings\`). cacheHit is
596
+ false for it. The Android payload carries it too
597
+ offloadFallback only on an app compiled here after the run considered
598
+ offloading it: why it built here -- one reason per
599
+ machine when none took the build, or why the offload
600
+ stopped. The Android payload carries it too
490
601
  compilationCache
491
602
  Xcode compilation-cache activity for a compiled iOS app:
492
603
  { status: "reported", hits, cacheableTasks, hitRatePercent }
@@ -585,7 +696,8 @@ per session, with the preview URL.`,
585
696
  hw.device.name. Null where systemImage is null, and on an
586
697
  AVD of the old generic profile, which has no hw.device.name
587
698
  fingerprint / cacheKey / cacheHit / cacheSkipped / waitedForBuild /
588
- appPath / installSkipped / launched
699
+ appPath / installSkipped / launched / buildMachine / builtOn /
700
+ offloadedTo / offloadFallback
589
701
  as above -- cacheKey keys on the VARIANT here
590
702
  (<fingerprint>-productionrelease-sim). A Debug artifact for
591
703
  a proven target ABI also ends in that ABI
@@ -627,7 +739,8 @@ per session, with the preview URL.`,
627
739
  through ccache -- ccache absent from PATH, a project that
628
740
  sets its own CMake compiler launcher, or a Gradle run whose
629
741
  native work was all up to date. None of the three is an
630
- error, and this field is separate from cacheHit
742
+ error, and this field is separate from cacheHit. On an
743
+ offloaded APK it is the build machine's ccache
631
744
  logs the workspace log directory
632
745
  durationMs wall time for the whole run
633
746
  reclaimed present only when the run was over its disk or memory
@@ -644,6 +757,8 @@ per session, with the preview URL.`,
644
757
  { started: true, reason }
645
758
  reason is "not running" or "stopped (idle)" when the
646
759
  supervisor had stopped it after metro.idleStopMinutes
760
+ links as in start, with &platform= and, for a named slot, &slot=,
761
+ so Stim Desktop can focus that device
647
762
 
648
763
  stim reload [ios|android|web] --json
649
764
 
@@ -656,7 +771,8 @@ per session, with the preview URL.`,
656
771
  deviceId the exact owned simulator UDID or emulator serial targeted;
657
772
  for web, the owned Chrome's DevTools endpoint
658
773
  deviceName the owned simulator or AVD name; for web, the Chrome version
659
- appId the live bundle id or Android package; for web, the page URL
774
+ appId the live bundle id or Android package; for web, the URL the
775
+ owned page was on when Stim sent the reload
660
776
  metroPort the workspace's verified Metro port; for web, the reserved
661
777
  Metro port or null
662
778
  strategy how the reload was addressed.
@@ -674,6 +790,9 @@ per session, with the preview URL.`,
674
790
  broadcast. Greater than 1 means several devices are running
675
791
  this app on that Metro and the request addressed all of
676
792
  them, not only deviceId. Completion is not observed
793
+ reverseRestored the Android serials whose adb reverse for metroPort was
794
+ missing and that Stim re-applied before reloading; [] when
795
+ none was, and always [] for iOS and web
677
796
 
678
797
  stim web --json
679
798
 
@@ -689,6 +808,8 @@ per session, with the preview URL.`,
689
808
  profile the Stim-owned Chrome user data directory
690
809
  cdpEndpoint http://127.0.0.1:<port>, the reserved DevTools endpoint, or
691
810
  null when not running
811
+ targetId the DevTools target id of the owned page, or null when not
812
+ running
692
813
  reused true when the running Chrome navigated again instead of
693
814
  starting: same --headed, viewport and certificate options
694
815
  launched true | "bundling" | "unverified" (see \`guide web\`)
@@ -696,6 +817,7 @@ per session, with the preview URL.`,
696
817
  that does not use {port:metro}
697
818
  logs { dir }: web.ndjson holds the page records
698
819
  durationMs wall time of the run
820
+ links as in start, with &platform=web
699
821
 
700
822
  stim doctor --json
701
823
 
@@ -716,6 +838,36 @@ per session, with the preview URL.`,
716
838
  null when off; plan lists what the next start, ios or
717
839
  android would reclaim, in the \`reclaimed\` shape without
718
840
  freedMb, and is empty while under budget
841
+ deviceHosts one { machine, state, dnsName?, deviceId?, requestedAt? }
842
+ per hosting.machines entry. state is "approved", "pending",
843
+ "not-asked", "revoked", "node-changed", "not-on-tailnet",
844
+ "tailscale-off", "unreachable", "invalid",
845
+ "credentials-unavailable" or "busy". Tokens stay private.
846
+ Only --fix asks for access or forgets removed names.
847
+ An approved hosting machine does not imply build approval
848
+ or change ios/android placement; see \`guide settings\`.
849
+ buildMachines one { machine, state, dnsName?, deviceId?, requestedAt?,
850
+ offloadable?, reasons?, problems?, capacity? } per
851
+ offload.machines entry; state is "approved", "pending", "not-asked",
852
+ "revoked" (revoked, or the request lapsed), "node-changed",
853
+ "not-on-tailnet", "tailscale-off", "unreachable" or
854
+ "invalid". An approved machine also carries offloadable,
855
+ true when it would take this app's builds now (iOS
856
+ simulator unless --platform android, Android emulator when
857
+ --platform android or the app has android/ or uses Expo,
858
+ plus macOS SwiftPM when Package.swift exists and no platform
859
+ is selected on a Mac), and reasons, each reason it would not,
860
+ prefixed "iOS: ", "Android: " or "macOS: " when only one platform
861
+ has it, with a finding
862
+ per reason (code build-machine-<reason>: unreachable,
863
+ checkout, stim-build, arch, xcode, simulator-sdk, macos-sdk,
864
+ cocoapods, bundler, runtime, jdk, android-sdk, ndk,
865
+ build-tools, compile-sdk, disk or busy). problems lists the same
866
+ reasons as { code, reason } with that code. capacity
867
+ is the machine's offer: { running, max, diskFreeBytes,
868
+ minDiskFreeBytes, cpus?, loadPerCore?, builds?, maxBuilds?,
869
+ maxLoadPerCore?, declined? }; an older stim-server omits the
870
+ optional fields
719
871
  findings the diagnostic findings; a lower resolved Stim is a
720
872
  costs-time finding with a PATH or installation remedy
721
873
 
@@ -738,7 +890,8 @@ ON FAILURE
738
890
  lease, Ctrl-C with no build tool running) exits 130 with no payload.
739
891
 
740
892
  \`stop --json\` prints { root, ok, supervisor, collectors, metro, device,
741
- port, metroTunnel, releasedLeases }. When it cannot end the \`ios\` or
893
+ port, metroTunnel, releasedLeases, macos? }. macos is { status: stopped | failed,
894
+ reason? } when a macOS app is recorded. When it cannot end the \`ios\` or
742
895
  \`android\` run holding the workspace, it prints the error contract with
743
896
  root and ok: false instead, plus device.remote when it ended a recorded EAS
744
897
  session first, and exits 1:
@@ -788,12 +941,44 @@ RULES
788
941
  preapproves CoreSimulatorBridge for exactly the installed
789
942
  bundle id and discovered scheme on its owned simulator. That
790
943
  suppresses iOS's first-launch confirmation;
791
- unrelated schemes remain unapproved. It also writes
944
+ unrelated schemes remain unapproved. The approvals persist
945
+ until the simulator is erased, so Stim records the ones it
946
+ wrote on the workspace's device record and writes only the
947
+ ones missing from it. Parking drops the record, so an
948
+ adopted simulator gets them again once.
949
+ THE DEV MENU AND ITS BUTTON stay off a simulator two ways,
950
+ which the onboarding flag does NOT cover. Stim's own
951
+ \`simctl launch --initialUrl\` ends in
952
+ \`-EXDevMenuShowsAtLaunch 0 -EXDevMenuShowFloatingActionButton 0\`
953
+ as trailing argv, the same mechanism the phone launch uses:
954
+ NSUserDefaults reads the argument domain ahead of the
955
+ persisted one, so Stim's own launch comes up on the app, not
956
+ the menu, without writing to the simulator. Stim also writes
792
957
  EXDevMenuShowsAtLaunch=false and
793
- EXDevMenuShowFloatingActionButton=false, which the flag does
794
- NOT cover, and those together are what keep the menu and its
795
- button off a simulator entirely, so device automation opens
796
- on the app. The
958
+ EXDevMenuShowFloatingActionButton=false to the app's
959
+ persisted defaults, but only on a real install -- one that
960
+ is not skipped as byte-identical -- or when its own launch
961
+ falls back to \`simctl openurl\` (no console capture in flight),
962
+ which carries no launch arguments at all and so writes them
963
+ itself before opening the URL. Adoption clears the app's
964
+ data by replacing its defaults with those two keys (one
965
+ \`defaults import\`), so an install that adoption then skips
966
+ as byte-identical keeps them too. Those persisted values
967
+ cover launches Stim did not make: a home-screen tap, an
968
+ agent relaunch, or a crash restart. The \`__expo_\` deep-link
969
+ params below last one process and do not persist.
970
+ When the project's installed expo-dev-launcher (found
971
+ through expo-dev-client, no network) is 58.0.0 or later
972
+ (expo/expo#49651), Stim's own deep link also carries
973
+ \`disableFab=1&disableAutoLaunch=1\` on the outer link and
974
+ on the project url, plus
975
+ \`__expo_disable_fab=1&__expo_disable_auto_launch=1\` on the
976
+ project url, which is the only url \`simctl launch
977
+ --initialUrl\` hands the launcher and the one 58.0.7 reads
978
+ the \`__expo_\` spellings from, for that launch only. So
979
+ device automation opens on the app.
980
+ The \`install dev client prepared (...)\` line appears
981
+ only when one of those simulator writes ran. The
797
982
  unverified warning therefore leads with the picker, then
798
983
  prints the openurl
799
984
  retry. ON LOCAL ANDROID the same deep link also carries the
@@ -805,8 +990,8 @@ RULES
805
990
  Remote Android opens only the URL, so that intent-extra
806
991
  suppression does not apply there.
807
992
  Every Stim deep link also carries an outer \`disableFab=1\`
808
- query parameter. Versions with expo/expo#49651 use that as a
809
- session-only override; earlier versions ignore it. Stim does
993
+ query parameter. Versions with expo/expo#49651 read it and
994
+ hide the button; earlier versions ignore it. Stim does
810
995
  not rewrite expo-dev-menu's private SharedPreferences XML:
811
996
  that internal file is not a supported API, and changing it
812
997
  would persist over the user's own Tools-button setting. The
@@ -853,7 +1038,7 @@ RULES
853
1038
  writes and the Local Network grant both survive an
854
1039
  UPGRADE install. Android's intent extra prevents the menu's
855
1040
  automatic launch; versions with expo/expo#49651 also honor
856
- the session-only FAB flag in Stim's deep link.
1041
+ the outer FAB flag in Stim's deep link.
857
1042
  The phone's unverified remedy is also ROUTED, not a fixed
858
1043
  list. When this launch's device records carry the Local
859
1044
  Network path reason, the remedy leads with that evidence and
@@ -868,7 +1053,7 @@ RULES
868
1053
  },
869
1054
  gc: {
870
1055
  summary: "the gc report payload: mode, sections, reasons, failures, results, inventory, and the gc refusals",
871
- body: () => ` stim gc [--delete] [--older-than <days>] [--cache <name|all|workspaces>]
1056
+ body: () => ` stim gc [--delete] [--older-than <days>] [--cache <name|all|workspaces|recordings|parked>]
872
1057
  [--worktrees] [--idle <duration>] --json
873
1058
 
874
1059
  The report the text prints, as one payload. Show the user its sections
@@ -881,7 +1066,8 @@ RULES
881
1066
  idle the --idle duration in milliseconds, or null
882
1067
  cacheScope the --cache name, or null. When set, devices, project
883
1068
  entries and locks were not inspected and their sections
884
- are empty
1069
+ are empty, except that --cache parked fills
1070
+ parkedSimulators and parkedEmulators
885
1071
  olderThan the --older-than days, or null
886
1072
  worktreeSweep null without --worktrees; otherwise { olderThan, defaulted }:
887
1073
  the idle days a linked worktree needs, and whether that is
@@ -895,10 +1081,13 @@ RULES
895
1081
  run. status is "done", "kept" (left alone, detail says
896
1082
  why) or "failed" (detail says why and what to retry).
897
1083
  kind: device, parkedDevice, idleDevice, deviceRecord,
898
- workspaceOutputs, workspaceDirectory, project, buildLock,
899
- buildSlot, deviceLease, easSession, worktree, cache.
1084
+ workspaceOutputs, recording, workspaceDirectory, project,
1085
+ buildLock, buildSlot, deviceLease, easSession, worktree,
1086
+ cache.
900
1087
  label is a device, path or cache name; id is the UDID,
901
- AVD name or path behind it, or null
1088
+ AVD name or path behind it, or null. Under --cache
1089
+ parked, a "done" parkedDevice was erased and stays
1090
+ parked; otherwise it was deleted
902
1091
  inventory null except on a dry run without --cache or --idle. Report
903
1092
  only: gc never acts on it, even under --delete.
904
1093
  { devices, runtimes, systemImages, notices }
@@ -952,10 +1141,12 @@ RULES
952
1141
  recent-activity worktree becomes removable,
953
1142
  else null. Without --worktrees, the source
954
1143
  checkout and roots outside git are left out
955
- parkedSimulators { udid, name, model, runtime, parkedAt, bytes,
956
- listed } with --older-than, only those parked
957
- at least that long
958
- parkedEmulators { name, systemImage, deviceProfile, parkedAt, bytes, listed }
1144
+ parkedSimulators { udid, name, model, runtime, parkedAt, app,
1145
+ bytes, listed } app is the bundle id recorded
1146
+ at park, else null; with --older-than, only
1147
+ those parked at least that long
1148
+ parkedEmulators { name, systemImage, deviceProfile, parkedAt, app,
1149
+ bytes, listed } app is the package name;
959
1150
  likewise
960
1151
  orphanedDevices { kind, id, name, bytes, directory }
961
1152
  unverifiedDevices { kind, id, name, command } stim-* devices this
@@ -973,6 +1164,10 @@ RULES
973
1164
  root, and browser profile paths whose
974
1165
  directory is gone; --delete forgets the
975
1166
  ledger entry only
1167
+ staleStatusCaches { kind: "disk-usage" | "pull-request", path,
1168
+ file, bytes } entries of $STIM_HOME/disk-usage
1169
+ and pull-requests whose path is gone from a
1170
+ mounted volume; --delete removes the file
976
1171
  idleDevices { kind, id, name, project, slot, lastActivityAt,
977
1172
  idleForMs, buildInProgress } booted owned
978
1173
  devices whose status activity is "idle";
@@ -996,14 +1191,47 @@ RULES
996
1191
  Metro, client and device logs over twice the
997
1192
  8 MiB cap; willTrim marks the ones it would
998
1193
  trim
1194
+ recordings { dir, projectRoot, bytes, deleteBytes,
1195
+ willDelete, withWorkspace, reason, detail }
1196
+ recordings/ of each workspace; deleteBytes is
1197
+ what --delete would remove; withWorkspace
1198
+ marks those of a gone workspace, removed with
1199
+ its workspace directory
999
1200
  workspaceBuildOutputs { dir, projectRoot, bytes, idleDays, willClear,
1000
1201
  reason, detail } derived-data, gradle-build,
1001
- android-cas and cache-provider of each
1202
+ android-cas, cache-provider, macos/build,
1203
+ macos/<Product>.app and macos/staging-* of each
1002
1204
  workspace; willClear marks the ones --delete
1003
- would clear
1205
+ would clear; --cache workspaces omits the
1206
+ directories that --delete removes whole (dead
1207
+ projects and orphanedWorkspaces), and --cache
1208
+ all does not
1004
1209
  caches { name, dir, source, bytes, note, willEmpty,
1005
- emptySkipped } alive, not garbage; willEmpty
1006
- marks the ones --delete would empty
1210
+ emptySkipped, scopedEmpty } alive, not
1211
+ garbage; willEmpty marks the ones --delete
1212
+ would empty. scopedEmpty is { willEmpty,
1213
+ emptySkipped } and says what \`--delete
1214
+ --cache <selector>\` without --older-than
1215
+ would do to the cache; it is present without
1216
+ --cache and null with it, where willEmpty
1217
+ already says so. Caches that share a name
1218
+ carry their directory's last component, as
1219
+ "Metro transform cache: <store>"
1220
+ memory { kind: "watchman" | "gradleDaemon" |
1221
+ "kotlinDaemon", cacheKind: "watchman" |
1222
+ "gradle-daemons", pid, startedAt, bytes,
1223
+ measure: "footprint" | "rss", version,
1224
+ gradleHome, offloadClient, state: "idle" |
1225
+ "busy" | "unknown", reclaimable, reason,
1226
+ detail } without --cache, or with --cache
1227
+ watchman or gradle-daemons; reclaimable marks
1228
+ the ones gc --delete --cache <cacheKind>
1229
+ would stop (\`guide cleanup memory\`)
1230
+ watchmanRoots { path, stale: "missing" | "pruned-worktree" |
1231
+ null, subscriptions, triggers, removable,
1232
+ detail } removable marks the stale roots
1233
+ --cache watchman would watch-del
1234
+ memoryNotices { message } why the memory sweep was skipped
1007
1235
  bytes is null when the size is unknown: not measured, or the measurement
1008
1236
  failed. idleDays is null when the last use is unknown. listed is false for
1009
1237
  a parked device that is no longer on this machine, and null when its
@@ -1015,6 +1243,9 @@ RULES
1015
1243
  workspaceBuildOutputs unresolved | in-use | last-use-unknown |
1016
1244
  recently-used
1017
1245
  workspaceLogs unresolved | in-use | collector
1246
+ recordings unresolved | retained | recently-recorded
1247
+ memory in-use | busy | unknown | build-running |
1248
+ stim-server
1018
1249
  linkedWorktrees not-a-worktree | bare-repository |
1019
1250
  source-checkout-unknown | source-checkout |
1020
1251
  locked | in-use | status-unreadable | dirty |
@@ -1029,26 +1260,96 @@ RULES
1029
1260
 
1030
1261
  - a --cache name that no shared cache carries; the remedy names the
1031
1262
  caches on this machine
1032
- - --cache together with --worktrees or --idle`
1263
+ - --cache together with --worktrees or --idle
1264
+ - --cache watchman or gradle-daemons together with --older-than`
1033
1265
  },
1034
1266
  status: {
1035
- summary: "the status payload's issues and their codes, build and device activity fields: a running build, its estimate, each platform's last build, who drives each device, whether the app runs on it, and what uses CPU and memory now",
1267
+ summary: "the status payload's lifecycle phase and stage, issues and their codes, build and device activity fields: a running build, its estimate, each platform's last build, who drives each device, whether the app runs on it, and what uses CPU and memory now",
1036
1268
  body: () => ` stim status --json
1037
1269
 
1270
+ Each environment carries phase, where the workspace is in its lifecycle:
1271
+
1272
+ phase "warming" stim worktree warm runs in it now
1273
+ "ready" its last warm succeeded and nothing has run there
1274
+ since: no start, ios, android, web or reload, for
1275
+ at most 2 hours
1276
+ "live" live is true: Metro, a device, Chrome or a remote
1277
+ session of it runs
1278
+ "idle" none of these
1279
+ phaseSince when the warm started ("warming") or finished ("ready"); null
1280
+ for "live" and "idle"
1281
+ warmStep "refresh" or "copy", the step a warming workspace is in;
1282
+ absent in other phases
1283
+ recording { enabled }: whether stim-server may record the workspace's
1284
+ device screens for replay, from recording.enabled
1285
+
1286
+ Each environment also carries stage, where the workspace is as Stim
1287
+ Desktop and the phone app show it: { kind, since, platform, closedApps }.
1288
+ The first kind that applies wins:
1289
+
1290
+ kind "building" a build runs; platform names it
1291
+ "warming" phase is "warming" and nothing is live
1292
+ "ready" phase is "ready" and nothing is live
1293
+ "build-failed" the newest run of either platform failed;
1294
+ platform names it
1295
+ "running" live is true or a remote session runs
1296
+ "stopped" none of these
1297
+ since when it began: the build's start, phaseSince, the failed
1298
+ run's finishedAt (else startedAt), the supervisor's start or
1299
+ metro.lastStop.at; null when unknown
1300
+ platform "ios" or "android" for "building" and "build-failed", else null
1301
+ closedApps for "running", [{ platform, slot }] of the devices whose
1302
+ appPresence is "closed", the default slot first, then each
1303
+ slot in order, iOS before Android; empty otherwise
1304
+
1305
+ A warm records warming under its own ownership claim, so a warm that was
1306
+ killed or failed reads as idle, never as warming. Plain \`stim status\`
1307
+ marks the workspace [warming: <step>], [ready] or [idle].
1308
+
1038
1309
  Each environment carries issues, the things in that workspace that need
1039
- the user, and warnings, the same issues as text ("<slot>: " when not the
1040
- default slot, then "<message>; run \`<remedy>\`"):
1310
+ the user, and warnings, its error and warning issues as text ("<slot>: "
1311
+ when not the default slot, then "<message>; run \`<remedy>\`"):
1041
1312
 
1042
1313
  issues [{ code, severity, message, remedy, workspace, slot? }]
1043
1314
 
1044
- code port-not-ours something other than this workspace's
1045
- Metro answers its reserved port
1046
- sim-missing the recorded simulator no longer exists
1315
+ severity "error" Stim cannot verify or safely act on something; a
1316
+ person resolves it (stim guide errors teardown)
1317
+ "warning" something is broken now and needs someone to act:
1318
+ an app cannot reach Metro, a port is held, an
1319
+ expected device vanished
1320
+ "info" a fact the next normal stim command handles by
1321
+ itself, so it needs no action
1322
+ The apps show error and warning and hide info; plain stim
1323
+ status prints info as a dim note with no remedy; warnings
1324
+ excludes info. The info codes: sim-missing, sim-without-metro,
1325
+ avd-missing, and port-not-ours while no supervisor runs.
1326
+
1327
+ code port-not-ours another process answers Metro on this
1328
+ workspace's reserved port. "info" with
1329
+ remedy stim start when no supervisor
1330
+ runs: start reserves a free port.
1331
+ "warning" when this workspace's
1332
+ supervisor runs and cannot serve there
1333
+ (remedy stim stop), or its supervisor is
1334
+ unverified. The message names the holder:
1335
+ "Metro for workspace <name>" when it runs
1336
+ inside another workspace, else "a dev
1337
+ server in <last dirs>"; metro.heldBy
1338
+ carries its pid and cwd
1339
+ sim-missing the recorded simulator no longer exists;
1340
+ always "info", stim ios creates a new one
1047
1341
  sim-without-metro the simulator is booted and no Metro
1048
- serves the workspace
1342
+ serves the workspace; always "info"
1049
1343
  avd-serial-changed the owned emulator came back on another
1050
1344
  serial, so Metro forwarding is lost
1051
- avd-missing the recorded AVD no longer exists
1345
+ avd-missing the recorded AVD no longer exists; always
1346
+ "info", stim android creates a new one
1347
+ android-reverse-missing the owned emulator runs this
1348
+ workspace's debug app against its live
1349
+ Metro, but the adb reverse for that Metro
1350
+ port is gone, so the app cannot reach
1351
+ Metro; the remedy stim reload android
1352
+ restores it
1052
1353
  avd-not-detected adb does not see the owned emulator while
1053
1354
  the workspace expects it: it holds an
1054
1355
  unexpired lease on its serial, or it
@@ -1068,8 +1369,8 @@ RULES
1068
1369
  browser-orphaned the owned Chrome runs but its supervisor
1069
1370
  exited, so page logs are not captured;
1070
1371
  stim stop closes it
1071
- severity "error" when stop or start refuses until it is resolved, else
1072
- "warning"
1372
+ severity the rule above; "error" is also what makes stop or start
1373
+ refuse until it is resolved
1073
1374
  remedy a command to run from workspace, such as "stim android --slot
1074
1375
  fold"
1075
1376
  slot absent for the default slot
@@ -1082,20 +1383,45 @@ RULES
1082
1383
  An environment where \`stim web\` ran carries web, its Stim-owned Chrome:
1083
1384
 
1084
1385
  web { browser, version, running, pid, supervisorPid, url, headless,
1085
- viewport, profile, cdpEndpoint }
1386
+ viewport, profile, cdpEndpoint, targetId, page?, activity? }
1086
1387
 
1087
1388
  running the browser supervisor and Chrome are both verified live;
1088
- pid, supervisorPid and cdpEndpoint are null when false
1389
+ pid, supervisorPid, cdpEndpoint and targetId are null when
1390
+ false, and page and activity are absent
1391
+ url the page stim web opened; page.url is the document the page
1392
+ loaded last, and page.route the URL an in-app route change
1393
+ (history API or fragment) moved it to since
1089
1394
  cdpEndpoint http://127.0.0.1:<port>, the reserved loopback DevTools
1090
1395
  endpoint of that Chrome. Attach Playwright MCP
1091
1396
  (--cdp-endpoint) or agent-browser (--cdp <port>) to it; it
1092
1397
  reaches only the Stim profile, never your own browser
1398
+ targetId the DevTools target id of the owned page, the one Stim
1399
+ Desktop and the phone stream; other tabs are not the page
1093
1400
  profile the Stim-owned user data directory under STIM_HOME
1401
+ page { url, state, error?, route? }: the page's latest load, from
1402
+ the newest page-load marker in web.ndjson; null before the
1403
+ first. route is absent until an in-app route change after
1404
+ that load
1405
+ "loading" the document was requested; no load event yet
1406
+ "loaded" the load event fired
1407
+ "failed" the document failed (connection refused, HTTP
1408
+ error, certificate) or the page crashed; error
1409
+ is the log message
1410
+ activity like a device's activity below. driver.tool names a
1411
+ DevTools client connected to cdpEndpoint (agent-browser,
1412
+ agent-device, argent, playwright, puppeteer,
1413
+ chrome-devtools-mcp, or the executable's name); Stim's own
1414
+ connections (the browser supervisor, stim-server, Stim
1415
+ Desktop, the stim CLI) are not drivers. basis: cdp-client
1416
+ (one lsof of the DevTools port); agent-action (the newest
1417
+ agent input the browser supervisor recorded in web.ndjson)
1418
+ and page-log (the newest other
1419
+ web.ndjson record) for recency
1094
1420
 
1095
1421
  Each booted simulator and detected emulator in environments (and in
1096
1422
  slots) carries activity; a shut-down or physical device has none:
1097
1423
 
1098
- activity { state, driver?, lastActivityAt?, basis }
1424
+ activity { state, driver?, lastActivityAt?, recent?, basis }
1099
1425
 
1100
1426
  state "driven" a live claim or driver holds the device now
1101
1427
  "active" no driver, but activity in the last 10 minutes
@@ -1103,25 +1429,46 @@ RULES
1103
1429
  "unknown" a claim or driver check could not be read;
1104
1430
  never treated as idle
1105
1431
  driver { tool, pid, since } for "driven": agent-device, stim device
1106
- lock, xcodebuild, idb, maestro, appium, simctl,
1432
+ lock, argent, xcodebuild, idb, maestro, appium, simctl,
1107
1433
  uiautomator or instrumentation; pid and since are null when
1108
1434
  the claim does not record them
1109
1435
  lastActivityAt the newest of this device's app log records, this
1110
- platform's Metro bundle requests and the workspace's last
1111
- Stim run, rounded down to the minute; absent when none is
1436
+ platform's Metro bundle requests, the workspace's last
1437
+ Stim run, while agent-device drives it the agent's last
1438
+ recorded action, and now while a stim-server client views
1439
+ it, rounded down to the minute; absent when none is
1112
1440
  recorded
1441
+ recent the newest time of each kind of evidence behind
1442
+ lastActivityAt: agent-action, device-log, metro-bundle,
1443
+ workspace-use, supervisor-start, page-log or viewer, each
1444
+ rounded down to the minute. viewer is now while a
1445
+ stim-server client streams the device's frames
1113
1446
  basis the evidence behind state, strongest first:
1114
1447
  agent-device-claim, agent-device-lease agent-device state,
1115
1448
  read only; live only when every recorded process is alive
1116
- with its recorded start time, so a reused pid is dead
1449
+ with its recorded start time, so a reused pid is dead.
1450
+ A runner lease that declares agent-device's device
1451
+ claims is never a driver: without a claim, the runner
1452
+ is one agent-device kept warm after close or handed
1453
+ off when its daemon exited
1117
1454
  device-lock an unexpired \`stim device lock\` lease
1118
- driver-process a host process naming the UDID or serial
1119
- instrumentation an on-device uiautomator or androidx.test
1120
- process (one adb shell ps per emulator)
1121
- device-log, metro-bundle, workspace-use recency
1455
+ driver-process a host process naming the UDID or
1456
+ serial, other than such a runner
1457
+ instrumentation an on-device uiautomator, androidx.test or
1458
+ argent helper process (one adb shell ps
1459
+ per emulator)
1460
+ device-log, metro-bundle, workspace-use, agent-action,
1461
+ viewer recency
1122
1462
 
1123
1463
  Plain \`stim status\` appends it to each device line: "driven by
1124
1464
  agent-device for 12m", "active", "idle 3h", or "activity unknown (...)".
1465
+
1466
+ An owned device that is not booted after its supervisor shut it down for
1467
+ devices.idleShutdownMinutes carries idleShutdown, and plain \`status\`
1468
+ appends "shut down after 30m idle at <at>" to its line; the next \`ios\` or
1469
+ \`android\` run for that slot clears it (\`guide lifecycle budget\`):
1470
+
1471
+ idleShutdown { at, idleMinutes }
1125
1472
  \`gc --idle <duration>\` shuts down owned devices idle that long
1126
1473
  (\`guide cleanup gc\`).
1127
1474
 
@@ -1140,6 +1487,16 @@ RULES
1140
1487
  app is absent when the device is not owned or no app id is known: the
1141
1488
  process was not checked.
1142
1489
 
1490
+ Each ios and android device record, in every slot, also carries
1491
+ appPresence, whether the device lacks the app:
1492
+
1493
+ appPresence "none" the device runs, its platform's latest run failed
1494
+ and builds carries the platform with no
1495
+ succeeded run
1496
+ "closed" the device runs and app.state is "stopped"
1497
+ null otherwise, including a device that is not running
1498
+ and a physical device
1499
+
1143
1500
  This is current process state, read from one host ps (simulator apps are
1144
1501
  host processes) and the same adb shell ps as activity, so a status --watch
1145
1502
  refresh notices an exit within its 30-second fallback. It is not launch
@@ -1148,15 +1505,48 @@ RULES
1148
1505
  status appends "<id> running", "<id> not running" or "<id> process
1149
1506
  unknown" to the device line.
1150
1507
 
1508
+ An environment's metro carries tunnel { provider, url } for a recorded
1509
+ managed tunnel on its reserved port: ngrok, cloudflared or tailscale.
1510
+ Plain status prints the provider and URL, with "tailnet-only" for tailscale.
1511
+ This describes the record, not a reachability check (\`guide metro\`).
1512
+
1151
1513
  An environment's metro carries idleStop { reason: "idle", at, idleMinutes }
1152
1514
  when its supervisor stopped the dev server for idleness and nothing serves
1153
1515
  the port since; plain \`status\` prints "stopped (idle)" (\`guide metro\`).
1516
+ Whenever nothing serves the port, metro also carries lastStop when the cause
1517
+ is known, one of:
1518
+ { reason: "idle", at, idleMinutes }
1519
+ { reason: "requested", at, by, byPid, byWorkspace? } a Stim command
1520
+ { reason: "signal", at, signal } a signal no Stim command sent
1521
+ { reason: "server-exited", at, mode, code, signal }
1522
+ { reason: "vanished", pid, startedAt } the recorded supervisor is gone
1523
+ and recorded no cause
1524
+ (\`guide metro\`, WHY IT STOPPED).
1525
+ When another process answers Metro on the port, metro carries
1526
+ heldBy { pid, cwd }, cwd null when its directory could not be read.
1527
+
1528
+ metro also carries bundle, from the bundle requests Stim's middleware
1529
+ records in the workspace's metro log: an app's, and Stim's own prefetch
1530
+ before a launch. It is absent when the log's tail holds none.
1531
+
1532
+ bundle { bundling, platform?, startedAt?, percent?, last? }
1533
+ bundling true while a request is in flight: Metro runs, and the request
1534
+ started under the current dev server less than 10 minutes ago
1535
+ platform with bundling, the in-flight request's "ios" or "android";
1536
+ startedAt when it started
1537
+ percent 0-100, only when Metro reported progress for that request,
1538
+ which it does for clients that accept multipart responses
1539
+ last { platform, status, durationMs, finishedAt }: the newest
1540
+ finished request, status "ok" or "failed", durationMs from the
1541
+ request to the end of the response
1154
1542
 
1155
1543
  Each entry of environments also carries build: null, or the ios or android
1156
1544
  run that holds this workspace's native-run.lock:
1157
1545
 
1158
1546
  build { platform, slot, state, phase, startedAt, phaseStartedAt,
1159
- outcome, expectedMs, expectedPhaseMs, basis }
1547
+ outcome, outcomeKnown, cacheLookupOutcome?, expectedMs, expectedPhaseMs, basis,
1548
+ plannedPhases, missReason?, missProvisional?, detail?, placement,
1549
+ waitingOn? }
1160
1550
 
1161
1551
  state "running" while the run's own native-run claim is live;
1162
1552
  "stale" when that claim was released or its process is
@@ -1164,30 +1554,98 @@ RULES
1164
1554
  record; "unknown" when the claim set cannot be resolved.
1165
1555
  Never treat a stale record as a build in progress.
1166
1556
  phase prepare | cache-lookup | wait | prebuild | pods |
1167
- compile | install | launch. prepare covers settings,
1168
- Metro and the owned device; wait is waiting on another
1169
- workspace's build of the same fingerprint; install
1170
- includes waiting for the device to boot.
1557
+ compile | device | install | launch. prepare covers
1558
+ settings and Metro; wait is waiting on another
1559
+ workspace's build of the same fingerprint. Creating,
1560
+ adopting or booting the owned simulator or emulator
1561
+ before the cache lookup is prepare time. device starts
1562
+ once the app is ready and covers waiting for the
1563
+ device: its boot, adoption cleanup, or a physical
1564
+ device's lease and connection check. A boot that
1565
+ finishes during the build adds no device time. An
1566
+ --eas-profile run has no cache lookup, so its outcome
1567
+ stays the project's most recent one until install.
1171
1568
  startedAt when the run started; phaseStartedAt when its phase did
1172
- outcome "cold" once the run reached prebuild, pods or compile,
1173
- "hit" once it reached install without them. Before
1174
- that, the outcome of this project's most recent run.
1569
+ outcome "cold" after the local/provider lookups resolve a miss;
1570
+ "hit" after a cached artifact is ready to reuse, including
1571
+ a shared-build hit or a recheck after prebuild or pods.
1572
+ A later recheck can replace the first lookup's outcome.
1573
+ Before resolution, the outcome of this project's most
1574
+ recent run.
1575
+ outcomeKnown true once outcome is this run's own, false while it is
1576
+ the project's most recent one
1577
+ cacheLookupOutcome "hit" or "miss" once an actual cache lookup resolves;
1578
+ absent before resolution and on runs that skip lookup,
1579
+ such as --eas-profile
1175
1580
  expectedMs the median duration of this project's last successful
1176
1581
  runs with that outcome on that platform, or null with
1177
- no history
1582
+ no history. The run estimates twice: when it starts,
1583
+ from every run with the project's most recent outcome,
1584
+ and once when outcomeKnown turns true. From then on only
1585
+ runs that created, adopted or cold-booted their device
1586
+ the way this run did count; a run that reuses a booted
1587
+ device also counts runs recorded before Stim tagged them
1588
+ until 3 tagged ones exist. Runs that finish meanwhile do
1589
+ not change the estimate.
1178
1590
  expectedPhaseMs the median duration of this phase in those runs, or null
1179
- basis how many runs the medians come from (at most 10)
1591
+ when none of them entered it
1592
+ basis how many runs the expectedMs median comes from (at most
1593
+ 10 per kind: device-setup runs keep their own 10)
1594
+ plannedPhases [{ phase, expectedMs }] in phase order: the phases at
1595
+ least half of those runs entered, each with its median
1596
+ in them; null with no such run. The current phase can be
1597
+ one it does not list, such as wait. Draw a progress bar
1598
+ from it rather than from builds, which holds only this
1599
+ workspace's runs and includes offloaded ones
1600
+ missReason once the run knows its cache lookup missed, why, in the
1601
+ shape of lastBuilds missReason below
1602
+ missProvisional true while missReason is the first lookup's miss and the
1603
+ run will prebuild or install pods and look the key up
1604
+ again, because those can change the fingerprint. A hit on
1605
+ that second lookup removes missReason and sets outcome to
1606
+ hit; otherwise missReason is replaced with the final one,
1607
+ whose rekeyedBy lists what changed the key, and
1608
+ missProvisional goes away before compile
1609
+ detail during compile, once the build tool printed a line Stim
1610
+ reads: { step, unit, done, total, line, updatedAt }
1611
+ step the tool's step: configure, compile, link, resources,
1612
+ script, dex, package or sign; null before one is known
1613
+ unit "targets" for xcodebuild, "tasks" for Gradle
1614
+ done xcodebuild: targets it finished (a target counts once
1615
+ xcodebuild touches or signs its product, so an
1616
+ incremental build can end below total); Gradle: tasks
1617
+ it reported so far
1618
+ total xcodebuild: targets in its dependency graph; Gradle: null,
1619
+ since its plain output gives no total
1620
+ line the latest compile, link or task line, paths shortened
1621
+ to file names, at most 160 characters
1622
+ updatedAt when the build last wrote it; the run writes it at most
1623
+ every 2 seconds
1624
+ placement where the build runs: "local", or while it is offloaded
1625
+ { host, phase, startedAt, phaseStartedAt }. host is the
1626
+ offload.machines entry; phase is the step there: sync,
1627
+ deps, prebuild, pods, build (xcodebuild or Gradle) or fetch;
1628
+ startedAt is when the offload started and phaseStartedAt
1629
+ when that step did. Meanwhile phase above follows it as
1630
+ prebuild, pods or compile.
1631
+ waitingOn while phase is "wait" and the holder is known: { path },
1632
+ the workspace whose build of the same artifact this run
1633
+ waits for; absent otherwise. The lock is machine-wide, so
1634
+ path is usually an entry of environments but can name a
1635
+ workspace that is not listed.
1180
1636
 
1181
1637
  Plain \`stim status\` prints the same as one line per workspace:
1182
1638
 
1183
1639
  build: ios compile, 1m10s elapsed -- about 3 min left (median of 4 cold runs)
1640
+ build: ios compile on janics-mac-mini (build, 2m10s), 3m05s elapsed
1184
1641
 
1185
1642
  An environment with a recorded run also carries lastBuilds, each
1186
1643
  platform's most recent ios or android run, finished or failed:
1187
1644
 
1188
1645
  lastBuilds { ios?, android? }, each { platform, status, cacheHit,
1189
1646
  cacheSkipped, durationMs, fingerprint, startedAt, finishedAt,
1190
- errorCode?, missReason?, diagnostics? }
1647
+ errorCode?, missReason?, buildMachine?, builtOn?, offloadedTo?, offloadFallback?,
1648
+ diagnostics? }
1191
1649
 
1192
1650
  status "ok" or "failed"
1193
1651
  cacheHit "local" or "remote" for an app from that cache tier; false
@@ -1208,10 +1666,43 @@ RULES
1208
1666
  diagnostics only on a failed run whose compiler reported errors: up to
1209
1667
  5 { file, line, column, message }, null where the compiler
1210
1668
  gave no position.
1669
+ cause only on a failed run: { key, file, line }, the first
1670
+ diagnostic with a file and a line, keyed "<file>:<line>",
1671
+ else the errorCode (or "failed") with null file and line.
1672
+ Failed runs in a row with the same key failed the same way.
1673
+ Invalid, unlisted or unpaired selections refuse during setup, before cache
1674
+ lookup, and create no lastBuild record or failed-run stats.
1675
+ buildMachine selected auto, local or configured machine entry; always on new build records
1676
+ builtOn here or machine name; absent when no build ran (cache/refusal)
1677
+ offloadedTo only on a run a build machine compiled: its name
1678
+ offloadFallback
1679
+ only on a run that considered offloading and built here: one
1680
+ reason per machine when none took the build, or why the
1681
+ offload stopped
1211
1682
 
1212
1683
  Plain status prints "last build: ios local cache in 12s, android compiled
1213
1684
  in 7m02s". To predict the next run instead, see \`guide facts plan\`.
1214
1685
 
1686
+ Swift Package development carries macos, separate from simulator devices:
1687
+ product, bundle, bundleId, executable, launchId, arguments
1688
+ app?, supervisor? { pid, processToken, startedAtMicros }
1689
+ state running | orphaned | stopped | unverified
1690
+ build { state: running | ok | failed, startedAt,
1691
+ finishedAt?, durationMs?, error?,
1692
+ buildMachine?, builtOn?, errorCode?, offloadedTo?, offloadFallback? }
1693
+ host? { machine, session, appSlot, appAttempt, bundleId,
1694
+ agent } when stim macos --remote placed the app on
1695
+ another Mac; it then has no local app or supervisor
1696
+ hostLaunched? true when that host reported a live app, false when
1697
+ stopped, "unverified" when it could not confirm one
1698
+ A live app without its supervisor is orphaned. An identity the system cannot
1699
+ verify is unverified. For hosted placements only, status asks the host for
1700
+ the session state with about a 3 s timeout per connection and request and
1701
+ a 10 s cache. It reports stopped when the host says the session stopped,
1702
+ for example after a stim-server restart there, and unverified when the
1703
+ host is unreachable or cannot confirm. The placement stays recorded.
1704
+ See stim guide macos for local capture, hosting and cleanup.
1705
+
1215
1706
  An environment with a recorded run also carries builds, each platform's
1216
1707
  last 10 runs, newest first. Its newest entry that is not "interrupted" is
1217
1708
  the run lastBuilds reports, once a run has recorded builds.
@@ -1231,23 +1722,108 @@ RULES
1231
1722
  cacheKey the cache key the run looked up or stored under
1232
1723
  phases milliseconds spent in each phase the run entered, among
1233
1724
  prepare, cache-lookup, wait, prebuild, pods, compile,
1234
- install and launch
1725
+ device, install and launch; runs recorded before device
1726
+ existed count their device time in prepare and install
1235
1727
 
1236
1728
  Only runs that record a last build are listed: a run that stops before
1237
1729
  looking up a build, such as a bad flag, is not. Estimates (expectedMs) come from run statistics, not builds.
1238
1730
 
1239
- There is no completion fraction: a compile's log volume depends on what
1240
- is already built, so it does not measure progress.
1241
-
1242
- An environment's memoryMb is an estimate, not a measurement: a fixed amount
1243
- per booted simulator, detected emulator, running Metro and running Chrome.
1244
- capacity.committedMb sums it, and the memory budget uses the same estimate.
1245
- What is using CPU and memory now is the top-level machine section:
1246
-
1247
- machine null, or { owners: [{ kind, name, workspace, slot?, id, owned,
1248
- cpuPercent, residentMb, processes }] }
1249
-
1250
- kind simulator a booted simulator's launchd_sim tree
1731
+ done and total are counts, not a completion fraction: a target or task
1732
+ can take a second or ten minutes, and cached work finishes instantly.
1733
+
1734
+ Each environment also carries disk, and each owned simulator and owned
1735
+ emulator in ios, android and slots carries disk, once \`status --watch\` has
1736
+ measured them:
1737
+
1738
+ disk (environment) { worktreeBytes, nodeModulesBytes, buildBytes,
1739
+ measuredAt }
1740
+ worktreeBytes the linked worktree, or else the checkout holding the
1741
+ workspace, node_modules included
1742
+ nodeModulesBytes node_modules at that root and at the workspace path;
1743
+ part of worktreeBytes
1744
+ buildBytes Stim's folder for the workspace: Xcode derived data,
1745
+ Gradle outputs and logs
1746
+ measuredAt the oldest of the three measurements; a figure not
1747
+ measured yet is null
1748
+ disk (device) { bytes, measuredAt }: the simulator's data folder
1749
+ under CoreSimulator/Devices, or the AVD's .avd folder
1750
+
1751
+ \`status --watch\` runs one du at a time off its refresh path, and measures
1752
+ a folder at most every 5 minutes while its environment is live and every
1753
+ hour otherwise. It caches each size under $STIM_HOME/disk-usage, which
1754
+ one-shot status only reads, so the fields appear once a watcher, such as
1755
+ stim-server or Stim Desktop, has measured.
1756
+
1757
+ An environment carries agents when coding-agent sessions work in it, most
1758
+ recently active first:
1759
+
1760
+ agents [{ tool, sessionId, title?, cwd, startedAt?, lastActiveAt?,
1761
+ pid?, openUrl?, webUrl? }]
1762
+ tool claude-code | codex
1763
+ title the short name the tool keeps for the session; never a
1764
+ prompt or any conversation content
1765
+ cwd the session's working directory, canonical
1766
+ lastActiveAt the tool's last update of the session, or the last Stim
1767
+ command the session ran here when that is newer
1768
+ openUrl opens the session in the Claude desktop app
1769
+ (claude://code/continue) or the Codex app
1770
+ (codex://threads/<id>) when that app is installed on the
1771
+ Mac; absent for a Claude Code session started in a terminal
1772
+ webUrl https://claude.ai/code/<id> for a Claude Code session with
1773
+ Remote Control connected; it opens the session in a browser
1774
+ or the Claude mobile app
1775
+
1776
+ A session works in an environment when its cwd is the environment's
1777
+ path or inside it (the deepest environment wins), or is the environment's
1778
+ git worktree root. \`status --watch\` looks for sessions at most every 15
1779
+ seconds: Claude Code sessions in ~/.claude/sessions whose process runs,
1780
+ and Codex threads in $CODEX_HOME (~/.codex) updated in the last 30
1781
+ minutes. It caches them in $STIM_HOME/agent-sessions.json, which one-shot
1782
+ status reads and ignores once it is 2 minutes old. Both sources are other
1783
+ tools' internal files; a missing or changed one only drops its sessions.
1784
+ \`start\`, \`ios\`, \`android\`, \`web\`, \`reload\` and \`worktree warm\`
1785
+ also record the CLAUDE_CODE_SESSION_ID or CODEX_THREAD_ID of the shell
1786
+ that ran them, so that session is listed for 30 minutes after the command
1787
+ even when status cannot see it, and for as long as it runs.
1788
+
1789
+ When a watcher stops finding a session it found on its previous look, it
1790
+ records the session in each environment it worked in, under
1791
+ $STIM_HOME/workspaces/<id>/ended-agents.json, for 3 days:
1792
+
1793
+ endedAgents [{ tool, sessionId, title?, cwd, startedAt?, lastActiveAt?,
1794
+ openUrl?, webUrl?, endedAt }], most recently ended first
1795
+ endedAt the last time a watcher found the session running; a Codex
1796
+ thread ends 30 minutes after its last update
1797
+
1798
+ The other fields are as the watcher last found them. A session that runs
1799
+ again is listed in agents only.
1800
+
1801
+ memorySource says how a memory figure was obtained:
1802
+
1803
+ footprint physical footprint, the figure Activity Monitor's Memory
1804
+ column shows, read by a small helper Stim compiles with the
1805
+ Xcode command line tools into $STIM_HOME/helpers on first use
1806
+ rss summed resident set size, for machine owners when the helper
1807
+ cannot be built (no Xcode command line tools, Linux). It
1808
+ overstates a simulator many times over.
1809
+ estimate a fixed amount per booted simulator, detected emulator,
1810
+ running Metro and running Chrome
1811
+
1812
+ An environment's memoryMb is what its own processes use now, the sum of
1813
+ memoryMb over the machine owners whose workspace is that environment,
1814
+ builds included, when machine.memorySource is footprint. Otherwise it is
1815
+ the estimate, with memorySource estimate.
1816
+
1817
+ capacity.committedMb sums memoryMb. The memory budget plans before a boot
1818
+ and always uses the estimate. What is using CPU and memory now is the
1819
+ top-level machine section:
1820
+
1821
+ machine null, or { memorySource, owners: [{ kind, name, workspace,
1822
+ slot?, id, owned, cpuPercent, residentMb, memoryMb,
1823
+ processes }] }
1824
+
1825
+ kind macos the owned Swift Package app and supervisor
1826
+ simulator a booted simulator's launchd_sim tree
1251
1827
  emulator an emulator's launcher and qemu tree, by its -avd
1252
1828
  metro a workspace's supervisor and Metro trees
1253
1829
  build a running ios or android run's process tree,
@@ -1269,14 +1845,19 @@ RULES
1269
1845
  residentMb summed resident set size. Pages shared between processes
1270
1846
  count once per process, so a simulator reports well above
1271
1847
  its physical footprint.
1848
+ memoryMb summed physical footprint when machine.memorySource is
1849
+ footprint; a process the helper cannot read, another user's
1850
+ or one that just exited, counts its resident size. Equals
1851
+ residentMb when memorySource is rss.
1272
1852
 
1273
1853
  Each process counts in exactly one owner, the one whose root process is
1274
1854
  its nearest ancestor, so the supervisor a build started counts as Metro,
1275
1855
  not as the build, and a Gradle or Kotlin daemon counts in the build that
1276
1856
  started it until that build exits, then as shared. Processes with no owner
1277
1857
  are left out. machine comes from one host ps, the one status reads for
1278
- device activity, and is null when no simulator is booted, no workspace is
1279
- live and no build runs: status then reads no process table. \`status --watch --json\` rereads it every 15 seconds while
1858
+ device activity, and one run of the footprint helper. It is null when no
1859
+ simulator is booted, no workspace is live and no build runs: status then
1860
+ runs neither. \`status --watch --json\` rereads both every 15 seconds while
1280
1861
  machine is not null, with no other subprocess.`
1281
1862
  },
1282
1863
  plan: {
@@ -1299,7 +1880,9 @@ RULES
1299
1880
  outcome "hit" or "cold"; null when the run would refuse
1300
1881
  expectedMs the median wall time of this project's last successful runs
1301
1882
  with that outcome on that platform, or null with none
1302
- basis how many runs expectedMs comes from (at most 10)
1883
+ basis how many runs expectedMs comes from (at most 20: runs that
1884
+ set up their device and runs that did not are not told apart
1885
+ before the run)
1303
1886
  missReason on a predicted cold build with cache reads on, why the
1304
1887
  cache has no app, in the shape of lastBuilds.<platform>
1305
1888
  .missReason (\`guide facts status\`), compared with the same
@@ -1326,12 +1909,19 @@ RULES
1326
1909
  { "version": 1,
1327
1910
  "project": { "key": "<path>", "ios": <bucket|null>,
1328
1911
  "android": <bucket|null> } | null,
1329
- "machine": { "ios": <bucket|null>, "android": <bucket|null> } }
1912
+ "machine": { "ios": <bucket|null>, "android": <bucket|null> },
1913
+ "offload": { "today": { "here", "offloaded", "fellBack" },
1914
+ "machines": { "<machine>": { "today": <day>,
1915
+ "total": <totals> } },
1916
+ "placements": [<placement>, ...] } }
1330
1917
 
1331
1918
  \`project\` is null outside a project; a platform with no run yet is null.
1332
1919
  A bucket carries runs, failed, hits, misses, coldRuns, coldRunMs, hitRuns,
1333
1920
  hitRunMs, timeSavedMs, firstRunAt and lastRunAt, plus lastColdBuildMs and
1334
- lastPodsMs once the project has compiled or installed pods. Milliseconds are
1921
+ lastPodsMs once the project has compiled or installed pods, and
1922
+ offloadedRuns, offloadedRunMs and lastOffloadHost once a build machine
1923
+ compiled one. An offloaded run is a miss but not a cold run, so the cold
1924
+ average, time saved and build estimates stay local. Milliseconds are
1335
1925
  integers.
1336
1926
 
1337
1927
  HOW A RUN IS COUNTED (\`stats\`)
@@ -1353,7 +1943,8 @@ HOW A RUN IS COUNTED (\`stats\`)
1353
1943
  is therefore an ESTIMATE and is printed as one. The file is
1354
1944
  $STIM_HOME/stats.json (see \`guide lifecycle builds\`). Beside the
1355
1945
  aggregates it keeps the last 10 successful runs per project, platform and
1356
- outcome (hit or cold) that did not wait for another workspace's build: each
1946
+ outcome (hit or cold), plus the last 10 that created, adopted or cold-booted
1947
+ their device, that did not wait for another workspace's build: each
1357
1948
  one's duration and per-phase durations. \`stim status\` estimates a running
1358
1949
  build from them (see \`guide facts status\`); \`stats --json\` does not
1359
1950
  print them.
@@ -1374,7 +1965,30 @@ HOW A RUN IS COUNTED (\`stats\`)
1374
1965
  a hang. A project with no record yet gets \`(1m00s)\`, the elapsed alone,
1375
1966
  and a warm run has no long phase to size. That read takes no lock and
1376
1967
  ignores what it cannot read, so nothing about statistics can change a
1377
- run's outcome.`
1968
+ run's outcome.
1969
+
1970
+ BUILD PLACEMENT (\`offload\`)
1971
+ While at least one build machine is paired, every run that compiles
1972
+ records where it built and why, in a placement: { at, project, platform,
1973
+ decision, reason, machine?, buildMs?, localEstimateMs?, failed? }.
1974
+ decision is "here", "offloaded", or "fell-back" (it tried a build machine
1975
+ and built here). reason is why: the run's \`placement:\` reason, such as
1976
+ "load 0.6/core, 1 of 3 build slots busy here" (auto keeps the build here
1977
+ while this Mac has room) or "offload.mode is off" (which prints no line),
1978
+ or for a fallback the cause, prefixed by the machine it concerns, such as
1979
+ "mini: no less loaded (load 1.2/core there, 0.6/core here)". buildMs is the compile here, or the offloaded build's
1980
+ total (offer, sync, build and fetch); localEstimateMs is this project's
1981
+ last cold build here before the run. A run that hits the cache or waits
1982
+ for another workspace's build compiles nothing and records none.
1983
+ placements lists them newest first: the last 100 from the last 7 days.
1984
+ Per machine, \`total\` keeps { offloaded, offloadedMs, savedMs, fallbacks,
1985
+ lastOffloadAt?, lastFallbackAt? } for good and \`today\` counts { offloaded,
1986
+ offloadedMs, savedMs, fallbacks } from the placements on this Mac's local
1987
+ calendar day. savedMs sums localEstimateMs minus buildMs over offloaded
1988
+ builds that had an estimate, so it is an ESTIMATE and is negative when the
1989
+ machine was slower. \`offload.today\` counts the day's placements by
1990
+ decision. A fallback counts against a machine only when the run knows
1991
+ which one: the machine that failed, or the only one paired.`
1378
1992
  }
1379
1993
  }
1380
1994
  },
@@ -1398,10 +2012,33 @@ another worktree's bundler.
1398
2012
  --wait, it does not wait for a device lease.
1399
2013
  --remote expose Metro for a remote device
1400
2014
 
1401
- Plain \`stim start\` is local and does not create a public tunnel. Remote intent
2015
+ Plain \`stim start\` is local and does not create a tunnel. Remote intent
1402
2016
  comes from \`start --remote\`, \`ios.remote\`, or \`android.remote\`. The
1403
2017
  \`metro.tunnel\` setting selects the provider after remote intent exists.
1404
2018
 
2019
+ TAILNET-ONLY METRO
2020
+ Install Tailscale and sign in on this machine and the remote device or
2021
+ agent-device daemon's host, then select the mode explicitly:
2022
+
2023
+ stim settings set metro.tunnel tailscale --scope workspace
2024
+ stim start --remote
2025
+
2026
+ The device must be on the same tailnet, so the eas backend refuses it. "auto"
2027
+ never selects tailscale.
2028
+ Stim runs a foreground \`tailscale serve --https=<metro port>
2029
+ http://127.0.0.1:<metro port>\` process, never Funnel. It prints
2030
+ https://<tailnet-name>:<metro port> and \`status\` reports the recorded
2031
+ provider and URL. Stim reads the URL from the child's pipes, without a log
2032
+ file, and skips the public bundle probe for this recorded tunnel: this
2033
+ machine may not reach its own tailnet name.
2034
+
2035
+ \`stop\` and \`worktree remove\` stop only the recorded process after
2036
+ verifying its identity; ending that foreground session removes its route.
2037
+ A route Stim did not create is never touched. If the port already has a
2038
+ serve route, Tailscale refuses and Stim prints Tailscale's error message.
2039
+ \`tailscale serve status --json\` shows foreground sessions under the
2040
+ top-level Foreground key; plain \`tailscale serve status\` does not.
2041
+
1405
2042
  BUNDLE WARMUP
1406
2043
  After verifying Metro, \`stim ios\` and \`stim android\` prefetch the
1407
2044
  platform's development bundle while native work continues. Expo supplies the
@@ -1429,7 +2066,7 @@ BUNDLE WARMUP
1429
2066
  REMOTE DEVICE BACKENDS
1430
2067
  Metro exposure and device selection are separate:
1431
2068
 
1432
- stim start --remote prepare public Metro
2069
+ stim start --remote prepare remote Metro
1433
2070
  stim ios --remote proxy use an agent-device daemon
1434
2071
  stim android --remote eas create an EAS Simulator session
1435
2072
 
@@ -1440,6 +2077,12 @@ REMOTE DEVICE BACKENDS
1440
2077
  requires AGENT_DEVICE_DAEMON_BASE_URL and
1441
2078
  AGENT_DEVICE_DAEMON_AUTH_TOKEN. Stim creates no remote session for it.
1442
2079
 
2080
+ An iOS remote build compiles one simulator architecture, since xcodebuild
2081
+ cannot target the remote simulator itself: the proxy daemon's host
2082
+ architecture from its /health hostArch, and arm64 when the daemon does not
2083
+ report one, cannot be reached within 3 seconds, or runs on EAS, whose hosts
2084
+ are Apple silicon. The cache key ends in that architecture.
2085
+
1443
2086
  The EAS backend needs eas-cli 21.6.0 or later and an account with EAS
1444
2087
  Simulator access. An EAS session is billable. Stim prepares the app before
1445
2088
  it creates or reconnects the session, so a failed build starts no session
@@ -1454,7 +2097,17 @@ IDEMPOTENT
1454
2097
  A healthy dev server on the reserved port is a no-op: \`start\` prints the
1455
2098
  facts with alreadyRunning: true and starts nothing. A foreign process holding
1456
2099
  the reserved port moves the RESERVATION instead, so the project is never
1457
- stranded on a port it can never use.
2100
+ stranded on a port it can never use. A port pinned with metro.port or
2101
+ STIM_METRO_PORT never moves: \`start\` refuses with STIM_BAD_ARG and names
2102
+ the holder before anything starts or the pin is reserved. This project's
2103
+ own Metro already on the pin is attached to. Changing the pin while this
2104
+ workspace's dev server runs refuses until stim stop; a supervisor that cannot
2105
+ be verified also refuses, until you stop it with the tool that started it.
2106
+ You can also unset or restore the pin. stim web follows
2107
+ the same rules when its page uses Metro.
2108
+ ios and android with --no-metro-check use the pin, then the recorded port,
2109
+ then 8081, without probing, reserving or writing the registry. An invalid
2110
+ pin still refuses with STIM_BAD_ARG. Release builds still skip Metro.
1458
2111
 
1459
2112
  WHAT THE SUPERVISOR IS
1460
2113
  One detached process per workspace. There is no machine-wide daemon, nothing
@@ -1505,13 +2158,56 @@ WHAT THE SUPERVISOR IS
1505
2158
  connected app alone does not keep the server. It records a
1506
2159
  supervisor_idle_stopped line in metro.ndjson and devServerStop in
1507
2160
  state.json, so \`status\` shows "stopped (idle)" rather than a crash.
1508
- Devices stay booted. The next \`stim start\` starts it again and clears the
2161
+ Devices stay booted, unless devices.idleShutdownMinutes is set: then the
2162
+ idle stop first shuts down the workspace's owned devices idle for the
2163
+ shorter of the two (\`guide lifecycle budget\`). The next \`stim start\`
2164
+ starts it again and clears the
1509
2165
  record, also when it reuses a dev server that another process started. A
1510
2166
  Debug \`ios\` or \`android\` run starts it the same way and reports
1511
2167
  devServer.reason "stopped (idle)". The setting
1512
2168
  is read when \`start\` spawns the supervisor; a value that is not a whole
1513
2169
  number of 0 or more is refused, as \`stim settings set\` refuses it.
1514
2170
 
2171
+ WHY IT STOPPED: a supervisor that stops its dev server writes a
2172
+ supervisor_stopped line (supervisor_idle_stopped for an idle stop) whose
2173
+ stop field and message give the cause, and records the same cause as
2174
+ devServerStop in state.json. A dev server that failed to start is
2175
+ supervisor_failed instead. Plain \`status\` prints it after "not running"
2176
+ while the workspace keeps its port (\`stim stop\` releases the port, so its
2177
+ own stops show only in the log).
2178
+ requested a Stim command stopped it and says which: stim stop,
2179
+ stim worktree remove, stim gc --delete, stim start
2180
+ --reset-cache, or budget reclaim (with the workspace whose
2181
+ ios, android or start run reclaimed it)
2182
+ signal a SIGTERM or SIGINT with no Stim stop request recorded,
2183
+ so it likely came from outside Stim (level warn)
2184
+ server-exited the dev server exited on its own (level error). Expo CLI
2185
+ also exits 0 when it gets SIGTERM, so "exit code 0" with
2186
+ no request usually means something outside Stim signalled
2187
+ the Expo process
2188
+ idle metro.idleStopMinutes, above
2189
+ A supervisor killed with SIGKILL, or one that crashed, cannot write
2190
+ anything; neither can one whose state file was not writable. The next
2191
+ supervisor logs supervisor_vanished (level warn) for it, and until then
2192
+ \`status\` reports metro.lastStop.reason "vanished".
2193
+
2194
+ WATCHMAN ROOTS: a Metro that uses watchman registers a watch root in the
2195
+ shared watchman daemon and never removes it. Bare React Native Metro uses
2196
+ watchman whenever it is installed; Expo SDK 56 and later use it only when
2197
+ metro.config.js sets resolver.useWatchman to true. When the supervisor stops
2198
+ its dev server, it removes with \`watchman watch-del\` a root that appeared
2199
+ after it started, contains the workspace and carried this Metro's
2200
+ subscription, once no other client subscribes to it or has a trigger on it.
2201
+ It logs watchman_root_removed, or watchman_root_kept with the reason, in
2202
+ metro.ndjson. It never starts the daemon and leaves every other root alone,
2203
+ including those jest registers.
2204
+ A supervisor that dies without stopping leaves its root: remove it with
2205
+ \`watchman watch-del <root>\`. Removing a root stops its recrawls; the
2206
+ daemon's memory footprint shrinks only when the daemon restarts, which drops
2207
+ the watches of every other client. \`stim gc --cache watchman\` reports the
2208
+ daemon's footprint, roots and clients, and with --delete shuts it down only
2209
+ when no client is connected (\`guide cleanup memory\`).
2210
+
1515
2211
  ENVIRONMENT: the supervisor -- and through it the dev server, including a
1516
2212
  metro.config.js evaluated inside the expo child -- inherits the environment
1517
2213
  of the \`start\` call that SPAWNED it. A later \`start\` that finds a healthy
@@ -1572,7 +2268,14 @@ If Stim is not installed globally, replace stim with npx stim.
1572
2268
  get <label> allocates on first use and prints only the number to stdout.
1573
2269
  Repeated calls reuse it, even while a server is listening. Labels start with
1574
2270
  a letter and contain up to 64 letters, digits, underscores, or hyphens.
1575
- metro is reserved; use stim start and stim stop for managed Metro.
2271
+ metro is reserved; use stim start and stim stop for managed Metro, and
2272
+ metro.port or STIM_METRO_PORT to choose its number (guide settings).
2273
+ Changing the pin while this workspace's dev server runs refuses until
2274
+ stim stop; an unverified supervisor also blocks the change until you stop it with the tool that started it. A foreign holder
2275
+ of the pin is refused before anything starts; this project's own Metro on
2276
+ it is attached to. stim web follows these rules when its page uses Metro.
2277
+ ios and android with --no-metro-check use the pin, then the recorded port,
2278
+ then 8081, without probing or reserving; an invalid pin still refuses.
1576
2279
 
1577
2280
  New allocations scan TCP ports 8900-8999. They skip registry reservations
1578
2281
  and existing listeners, announcing occupied ports and upward retries on
@@ -1653,10 +2356,15 @@ The only exit-1 paths are a malformed query and no project.
1653
2356
  Device collectors and build records carry their named slot. Use --slot phone
1654
2357
  for that slot's timeline; its launch marker cannot hide a sibling's errors.
1655
2358
  Untagged legacy records belong to default. Metro/client records are shared and
1656
- usually untagged: use an unfiltered workspace query to inspect those errors.
2359
+ usually untagged, so every --slot query includes them, windowed by that slot's
2360
+ own launch and by bundles of the platforms it launched. When another slot's
2361
+ relaunch hides a Metro error from the unfiltered query, --slot <name> for the
2362
+ slot that printed it still shows it.
1657
2363
 
1658
2364
  FLAGS
1659
- --slot <name> only this slot's records (default includes untagged records)
2365
+ --slot <name> only this slot's records plus, once it has launched, the
2366
+ shared untagged Metro and app client records (not the web
2367
+ page's); default also includes untagged legacy records
1660
2368
  --source <s...> metro, client, device, build, agent (one or more), or all.
1661
2369
  An unknown value is REJECTED rather than quietly matching
1662
2370
  nothing.
@@ -1670,7 +2378,12 @@ FLAGS
1670
2378
  owned Chrome's device errors (platform web: failed
1671
2379
  requests, browser errors).
1672
2380
  Capped at 20 printed records.
1673
- --follow keep streaming until interrupted (Ctrl+C is exit 0)
2381
+ --follow keep streaming until interrupted (Ctrl+C is exit 0). Closing
2382
+ its stdout also ends it with exit 0; on Linux, so does the
2383
+ exit of the process that started it when stdout is a pipe.
2384
+ It notices within a few seconds even when nothing new
2385
+ arrives, except a closed stdout pipe on Linux whose starter
2386
+ is still running, which it notices at the next record.
1674
2387
  --json the raw records, one per line, so stdout is valid NDJSON.
1675
2388
  ZERO matches is ZERO bytes on stdout (an empty NDJSON
1676
2389
  stream), exit 0 -- parse stdout line by line, never as one
@@ -1720,9 +2433,19 @@ FLAGS
1720
2433
  errors are reported -- and nothing else. A failed attempt's own summary
1721
2434
  and details land at or after its marker, so they stay reported.
1722
2435
  a LAUNCH marker (src build, written before \`ios\` / \`android\` attempts
1723
- launch) resets EVERYTHING except the web page's records. It precedes the
1724
- tool call so an immediate native crash is not hidden by a marker written
1725
- after the process died.
2436
+ launch) resets EVERYTHING in its slot except the web page's records. It
2437
+ precedes the tool call so an immediate native crash is not hidden by a
2438
+ marker written after the process died. Metro and client output names no
2439
+ device, so in the unfiltered query the newest launch of any slot resets
2440
+ it, and the newest bundle of any platform resets Metro errors; with
2441
+ --slot <name>, that slot's newest launch and the newest bundle of a
2442
+ platform it launched do. Metro records that name their platform (bare
2443
+ React Native bundle records; the "iOS Bundled" / "Android Bundled" /
2444
+ "RSC(iOS) Bundled" tag Expo prints for bundle markers, so a "DOM
2445
+ Bundled" line does not close a native slot's window) follow only that
2446
+ platform, and a slot query leaves out iOS or Android ones when it never
2447
+ launched that platform. A single slot that runs both iOS and Android still shares one
2448
+ window for untagged Metro errors.
1726
2449
  a PAGE-LOAD marker (the web_navigation record \`stim web\` writes for each
1727
2450
  load of the page's top-level document) resets only the page's records,
1728
2451
  those with platform web. It is written when the load starts, so a
@@ -1765,9 +2488,12 @@ FLAGS
1765
2488
  In --follow mode the marker window is dropped -- every error arriving from
1766
2489
  then on is by definition after the last marker seen.
1767
2490
 
1768
- \`stim status\` reports the same count per workspace, as
1769
- logs.errorsSinceMarker: the same query, the same scope, so the two can never
1770
- disagree about whether this workspace is failing.
2491
+ \`stim status\` reports the errors of the same query and scope per
2492
+ workspace, as logs.errorsSinceMarker, so the two never disagree about
2493
+ whether this workspace is failing. It counts failures as the Stim Desktop and
2494
+ phone logs list them: a Metro bundle failure is one error, although it is a
2495
+ marker line, the error line and a failed bundle response in the log, so the
2496
+ count can be lower than the records this command prints.
1771
2497
 
1772
2498
  THE RECORD
1773
2499
  { ts, src, level, msg } always. ts is epoch milliseconds; src is one of
@@ -1876,9 +2602,9 @@ WHAT WRITES WHAT
1876
2602
  console-only; missing reports or symbols are not proof
1877
2603
  of a healthy app. No symbol downloads are performed.
1878
2604
  Android may keep a crashed Java PID alive behind its
1879
- crash dialog. Follow the app-scoped force-stop remedy
1880
- printed by android after fixing the crash, then rerun
1881
- stim android. Metro reload alone cannot recover it.
2605
+ crash dialog. Fix the crash, then rerun stim android,
2606
+ which force-stops that process before launching. Metro
2607
+ reload alone cannot recover it.
1882
2608
 
1883
2609
  The connection refusal \`TCP Conn ... Failed :
1884
2610
  error 0:61 [61]\` (61 is ECONNREFUSED) is not even
@@ -1925,13 +2651,17 @@ WHAT WRITES WHAT
1925
2651
  archive rather than a stream. Streaming with full fidelity needs
1926
2652
  libimobiledevice or pymobiledevice3, which are third-party installs Stim
1927
2653
  does not require. See appandflow/stim#179.
1928
- agent no file. Each query reads agent-device's session
1929
- records (~/.agent-device/sessions, or
2654
+ agent agent input on the owned Chrome page is in
2655
+ web.ndjson (\`guide web\`). For simulators and
2656
+ emulators, no file: each query reads agent-device's
2657
+ session records (~/.agent-device/sessions, or
1930
2658
  AGENT_DEVICE_STATE_DIR) for this workspace's owned
1931
2659
  simulators and emulators, read-only. An action is
1932
2660
  info (msg is agent-device's summary, e.g. "Tapped
1933
2661
  (201, 731)"; event agent_action with command, session,
1934
- deviceId and details); a failed command is error
2662
+ deviceId and details, and startedAt when agent-device
2663
+ logged the command's start: ts is when it finished,
2664
+ after any --settle wait); a failed command is error
1935
2665
  (event agent_failed). A plain query includes them;
1936
2666
  --errors only with --source agent or all. iOS sessions
1937
2667
  match a simulator through their runner.log, Android
@@ -2128,8 +2858,12 @@ Branch on the code, never on the message.`,
2128
2858
  On a release cache hit Stim regenerates this workspace's JS bundle into a
2129
2859
  COPY of the cached artifact before installing it -- \`ios --configuration
2130
2860
  Release\` into a copy of the .app, \`android --variant ...Release\` into a
2131
- copy of the APK. When any step of that swap fails (the bundle command,
2132
- hermesc, the re-sign, zipalign, apksigner), the run does NOT install the
2861
+ copy of the APK. An iOS app that sets React Native's RCTUseAssetCatalog
2862
+ Info.plist key also gets its RNAssets.bundle image catalog recompiled with
2863
+ actool from the new bundle. When any step of that swap fails (reading the
2864
+ app's Info.plist, the bundle command, hermesc, actool, the expo-updates
2865
+ manifest refresh, the re-sign, zipalign, apksigner), the run does NOT
2866
+ install the
2133
2867
  cached artifact -- its baked-in JS is the builder's, not yours -- and does
2134
2868
  NOT fail: it prints a \`swap failed at <step>: ... --
2135
2869
  building fresh instead\` note on stderr and falls back to a full build. If
@@ -2243,6 +2977,37 @@ Branch on the code, never on the message.`,
2243
2977
  Legacy inline \`deletionClaim\` fields in config.json have no process
2244
2978
  identity and require separate manual inspection; \`stim guide lifecycle pool\`
2245
2979
  describes their field-only recovery.`
2980
+ },
2981
+ STIM_MACOS_OWNER_UNVERIFIED: {
2982
+ summary: "the macOS app owner cannot be verified; no signal is sent",
2983
+ body: () => `STIM_MACOS_OWNER_UNVERIFIED
2984
+ The recorded macOS process identity is unavailable or its workspace record
2985
+ is malformed. Stim sends no signal to an owner it cannot verify. Inspect the
2986
+ named process and workspace record before repairing it; do not replace a PID
2987
+ or token with another running app. Retry stim stop once identity inspection
2988
+ works. See stim guide macos.`
2989
+ },
2990
+ STIM_OFFLOAD_REFUSED: {
2991
+ summary: "the selected build machine cannot build this app",
2992
+ body: () => `STIM_OFFLOAD_REFUSED
2993
+
2994
+ A named --build-machine selection is strict. The message names the machine
2995
+ and why it cannot take or finish the build: not configured or paired, approval
2996
+ pending or denied, unreachable or changed pinned identity, incompatible
2997
+ toolchain/runtime/CPU, low disk, busy, sync/build failure, artifact failure,
2998
+ or a checkout that changed while it built. Unsupported build kinds also refuse.
2999
+ No local xcodebuild, Gradle or SwiftPM compile or alternate machine follows a
3000
+ refusal. Prebuild and pod install still run on this Mac before a named offload.
3001
+ Unlisted or unpaired names refuse at setup before consulting the cache, without
3002
+ a build record or failed-run stats. A listed paired name with a cache hit needs
3003
+ no build and does not contact the selected machine.
3004
+
3005
+ Run stim doctor --fix to ask for build access if not paired. Check
3006
+ stim settings get offload.machines. A person on the worker finds the id with
3007
+ stim-server devices and approves this Mac with stim-server devices grant <id> --build.
3008
+ Rerun with --build-machine auto for normal placement and local fallback, or
3009
+ --build-machine local to keep the build here.
3010
+ `
2246
3011
  },
2247
3012
  STIM_CLAIM_UNAVAILABLE: {
2248
3013
  summary: "a process identity or warm claim store is unavailable, so the protected operation refuses",
@@ -2574,13 +3339,14 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
2574
3339
  "this project's sim is X, but --device-type asked for Y"
2575
3340
  The project already owns a simulator of a different model, and Stim will
2576
3341
  not silently boot a different one. Reap it (\`worktree remove\`, or
2577
- \`gc --delete\`) and run \`stim ios\` again to create the requested model.
2578
- That loses the old sim's app state.
3342
+ \`gc --delete\`) and run \`stim ios\` again to create the requested model,
3343
+ which loses the old sim's app state, or pass \`--slot <name>\` to create it
3344
+ beside the current one. The \`remedy\` field in \`--json\` names the same
3345
+ choices.
2579
3346
 
2580
3347
  "this project's sim runs iOS X, but --runtime asked for Y"
2581
3348
  The same refusal for an explicit \`--runtime\` that names another installed
2582
- iOS version than the project's sim runs. Reap the sim the same way, or pass
2583
- \`--slot <name>\` to create one on the requested runtime beside it. The
3349
+ iOS version than the project's sim runs, with the same remedy. The
2584
3350
  ios.runtime setting alone never refuses; it applies at creation.
2585
3351
 
2586
3352
  "this project's emulator uses device profile X, but Y was requested"
@@ -2719,6 +3485,18 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
2719
3485
  holds one remote session, and Stim will not end the recorded one to make
2720
3486
  room -- it may be mid-run for whoever started it. Run \`stim stop\` for this
2721
3487
  workspace, then re-run with the platform you want. Nothing was created here.`
3488
+ },
3489
+ STIM_REMOTE_DEVICE_MISMATCH: {
3490
+ summary: "the recorded EAS session runs another model than --device-type asks for; stop, then rerun",
3491
+ body: () => `STIM_REMOTE_DEVICE_MISMATCH
3492
+ \`stim ios --remote eas --device-type <name>\` found this workspace's
3493
+ recorded EAS Simulator session still running another model, or the model
3494
+ EAS chose when no --device-type was given. EAS cannot change a running
3495
+ session's model, and Stim will not end the recorded session to make room --
3496
+ it may be mid-run for whoever started it. Run \`stim stop\` for this
3497
+ workspace, then rerun with the model you want. A recorded session that has
3498
+ already ended is replaced on the requested model instead, and without
3499
+ --device-type a live one is reused as it is. Nothing was created here.`
2722
3500
  },
2723
3501
  STIM_REMOTE_SESSION_STATE: {
2724
3502
  summary: "the EAS session was created but its state could not be recorded, so Stim stopped it",
@@ -2748,6 +3526,7 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
2748
3526
  The gate that proves a tunnel still reaches THIS workspace's Metro failed --
2749
3527
  before a session or a build, whether the tunnel is Expo's own, one Stim
2750
3528
  started (metro.tunnel: cloudflared/ngrok/auto), or a named metro.publicUrl.
3529
+ A recorded tailscale tunnel skips this public probe (\`guide metro\`).
2751
3530
  The usual cause: the tunnel was built for a port this workspace no longer
2752
3531
  holds (a stale one survived a \`stop\`/\`start\` that reserved a different
2753
3532
  port), and it now serves ANOTHER workspace's dev server -- healthy, and
@@ -2812,6 +3591,11 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
2812
3591
  relaunches that app on that device with this workspace's Metro port and loses
2813
3592
  in-memory state. Keep the existing --session flag and verify afterward.
2814
3593
 
3594
+ ADB COULD NOT LIST OR RESTORE THE REVERSE. Before an Android reload Stim
3595
+ checks the emulator's adb reverse for the Metro port. When adb cannot list
3596
+ or re-apply it, nothing was reloaded; the remedy names the adb reverse
3597
+ command to run before reloading again.
3598
+
2815
3599
  MORE THAN ONE MATCHING PEER IS NOT A FAILURE. A workspace Metro serves one
2816
3600
  app, so several matching peers are that app on several devices. Stim reloads
2817
3601
  every one of them and reports the count in the facts as targets.`
@@ -2935,10 +3719,22 @@ captured" (in metro.ndjson, bare RN)
2935
3719
  The command refused before doing anything: an unusable --wait value, a known
2936
3720
  setting with the wrong type ("Invalid <key> setting <value>. Expected <shape>."
2937
3721
  -- \`guide settings\` names the type each key takes), an invalid
2938
- Metro tunnel setting, an invalid android.dataPartitionSizeGb value, an unsafe
3722
+ Metro tunnel setting, \`stim macos\` with macos.product or macos.infoPlist
3723
+ unset (set both explicitly in .stim.json; see \`stim guide macos\`),
3724
+ \`stim macos --remote eas|proxy|auto\` (name a hosting Mac instead),
3725
+ an invalid android.dataPartitionSizeGb value, an unsafe
2939
3726
  android.avdConfig key or fragment, a malformed ios.signingIdentity,
2940
- ios.signingIdentitySha1 or ios.lanHost value, \`--device\` with an empty
2941
- serial or UDID, \`--device\` together with \`--remote\`, a working directory
3727
+ ios.signingIdentitySha1 or ios.lanHost value, a metro.port or
3728
+ STIM_METRO_PORT that another workspace reserves or another process holds,
3729
+ a changed pin while this workspace's dev server runs or its supervisor
3730
+ cannot be verified,
3731
+ \`--device\` with an empty
3732
+ serial or UDID, \`--device\` together with \`--remote\`, \`ios --runtime\`
3733
+ on a remote run (\`--remote\` or ios.remote; the remote backend picks the
3734
+ iOS version), \`ios --device-type\` on the proxy backend or with an
3735
+ eas-cli older than 22.2.0 on the eas backend, \`android --system-image\` or
3736
+ \`--device-profile\` on a remote run (\`--remote\` or android.remote), a
3737
+ working directory
2942
3738
  with no package.json above it, or one whose nearest package.json does not
2943
3739
  parse or depends on neither react-native nor expo, so the directory is not
2944
3740
  an app (the refusal names that package.json and says which of the two it
@@ -2958,7 +3754,10 @@ captured" (in metro.ndjson, bare RN)
2958
3754
  ios.deviceType, ios.runtime, android.systemImage or android.deviceProfile
2959
3755
  setting is checked the same way, and the check applies
2960
3756
  even when this workspace ALREADY owns a device, so a name that could never
2961
- create anything is caught rather than left to a later run. The
3757
+ create anything is caught rather than left to a later run. When the bad
3758
+ value came from a settings layer rather than a flag, the message also names
3759
+ the layer (workspace, repo, committed, or machine) it is set at, since all
3760
+ four settings are readable from the machine layer too. The
2962
3761
  \`pixel_fold\` and \`resizable\` profiles on a system image without
2963
3762
  foldable support (SupportPixelFold in its advancedFeatures.ini) refuse the
2964
3763
  same way, since the emulator quits on boot; the remedy names an installed
@@ -2972,6 +3771,12 @@ captured" (in metro.ndjson, bare RN)
2972
3771
  is on stdout, except \`logs --json\`, whose stdout stays empty NDJSON; the
2973
3772
  refusal is on stderr. \`stop\` outside a project refuses rather than
2974
3773
  reporting that nothing was running.
3774
+ For a changed Metro pin, run stim stop before retrying, or unset or restore
3775
+ the pin; for an unverified supervisor, stop it with the tool that started it. A foreign holder is refused before anything starts; this project's
3776
+ own Metro on the pin is attached to. stim web follows the same rules when
3777
+ its page uses Metro. ios and android with --no-metro-check use the pin,
3778
+ then the recorded port, then 8081, without probing or reserving; an invalid
3779
+ pin still refuses.
2975
3780
  These errors are caught before the port is reserved and before any build or
2976
3781
  device work, so nothing was started. The one listing they need
2977
3782
  (\`simctl list runtimes\`, the SDK's system-images directory, \`avdmanager
@@ -3085,6 +3890,16 @@ captured" (in metro.ndjson, bare RN)
3085
3890
  Matching this workspace's port or directory does not authorize cleanup,
3086
3891
  and stop has no override for process ownership.
3087
3892
 
3893
+ "port <n> is in use by <holder>; stim start will choose a free port" (status)
3894
+ A note, not a problem: no supervisor of this workspace runs, and
3895
+ \`stim start\` reserves a free port when its own is taken. The holder's pid
3896
+ and directory are in \`status --json\` under metro.heldBy.
3897
+
3898
+ "port <n> is in use by <holder>, so this workspace's Metro cannot serve on
3899
+ it" (status)
3900
+ This workspace's supervisor runs but another process answers its port.
3901
+ Run \`stim stop\`, then \`stim start\`, which reserves a free port.
3902
+
3088
3903
  "stop refusing to signal supervisor pid <n>: ..." (stop)
3089
3904
  The records disagree, the saved OS identity is unavailable, or it records a
3090
3905
  port this project did not reserve. If process inspection is denied, retry
@@ -3138,6 +3953,10 @@ not on any remote" (worktree remove)
3138
3953
  so per file -- those files die with the worktree either way, and a lockfile
3139
3954
  change anyone meant would have been committed. ONE other dirty path and the
3140
3955
  whole set is refused, churn included, so this never eats real work.
3956
+ Untracked \`.watchman-cookie-*\` files, which watchman writes into the roots
3957
+ it watches, are not work either: \`worktree remove\` deletes them itself and
3958
+ ignores them when it counts dirty paths. Any other untracked file still
3959
+ refuses.
3141
3960
  When it does refuse, the refusal PRINTS THE DIRTY PATHS, and the restore
3142
3961
  command under it carries those same paths: run it as printed rather than
3143
3962
  reaching for --force.
@@ -3268,9 +4087,9 @@ not on any remote" (worktree remove)
3268
4087
  A line such as \`npm warn exec ... will be installed\` is normal when using
3269
4088
  the no-install form.
3270
4089
 
3271
- "Unsupported engine" or a syntax error before Stim starts
3272
- Stim requires Node 22.12.0 or later.
3273
- Switch Node versions, then run the command again.
4090
+ "Unsupported engine" from npm, or a syntax error before Stim starts
4091
+ Stim requires Node 22.12.0 or later. Stim itself refuses an older Node
4092
+ with STIM_NODE_UNSUPPORTED; see that section.
3274
4093
 
3275
4094
 
3276
4095
  "Found no free Metro port between ..."
@@ -3353,6 +4172,30 @@ not on any remote" (worktree remove)
3353
4172
  Expo build-cache provider would each use a different store. Set the named
3354
4173
  variable to an absolute path, or unset it to use the default. Metro and the
3355
4174
  cache provider, which cannot refuse, ignore a relative value with a warning.`
4175
+ },
4176
+ STIM_NODE_UNSUPPORTED: {
4177
+ summary: "stim or stim-server started on a Node older than 22.12.0, often a project pin",
4178
+ body: () => `STIM_NODE_UNSUPPORTED ("Stim needs Node <floor> or later; this is Node <version> at <path>")
4179
+ stim and stim-server refuse before loading anything else when the Node that
4180
+ runs them is older than their engines floor. Both start through
4181
+ \`#!/usr/bin/env node\`, so the working directory can choose that Node: asdf,
4182
+ mise and Volta's node shim follow the project's .nvmrc, .node-version,
4183
+ .tool-versions or package.json, and nvm and fnm do when a shell hook
4184
+ switches versions on cd. A Stim installed with Volta keeps the Node it was
4185
+ installed with. Run Stim with a supported Node for that one command:
4186
+
4187
+ ASDF_NODEJS_VERSION=<version> stim <command>
4188
+ mise exec node@<version> -- stim <command>
4189
+ volta run --node <version> stim <command>
4190
+ fnm exec --using=<version> stim <command>
4191
+ nvm exec --silent <version> stim <command>
4192
+
4193
+ <version> is 22.12.0 or later. asdf, mise, fnm and nvm keep global packages
4194
+ per Node version, so when Stim was installed with npm under one of them, use
4195
+ the version it was installed with; asdf names it when it prints "No version
4196
+ is set for command stim" instead of running Stim. When that version is
4197
+ older than 22.12.0, first install Stim with npm under a supported version.
4198
+ The tools Stim starts inherit the override.`
3356
4199
  }
3357
4200
  }
3358
4201
  },
@@ -3463,6 +4306,17 @@ ambiguous no-platform request. Every reload goes over this workspace's Metro
3463
4306
  websocket, on both platforms. It never reopens a development-client URL,
3464
4307
  because that restarts the app rather than reloading its JavaScript.
3465
4308
 
4309
+ An Android reload first lists \`adb reverse\` on each live owned emulator
4310
+ launched against this Metro and re-applies the Metro port's reverse where it is
4311
+ missing. adb ties a reverse to its transport, and a transport that drops and
4312
+ reconnects comes back without it and without any error, which leaves the app
4313
+ cut off from Metro and Fast Refresh. After restoring one, Stim waits up to 8
4314
+ seconds, sending nothing, until the app on every emulator it checked is
4315
+ connected again, then sends the reload once. A dev server that cannot name
4316
+ its clients (bare React Native) gets a fixed 2.5-second wait instead. \`reverseRestored\` in the
4317
+ facts names the serials it restored. \`stim status\` reports the same loss as
4318
+ the android-reverse-missing issue.
4319
+
3466
4320
  How the message is addressed depends on the dev server, and \`strategy\` in
3467
4321
  the facts reports which you got. Where Metro can name its clients, Stim
3468
4322
  addresses every peer matching the platform and reports \`metro-websocket\`. A
@@ -3508,9 +4362,10 @@ If the app is stuck before loading its first bundle, restart its process with
3508
4362
  the printed force-stop and launcher commands. Foregrounding the same process
3509
4363
  does not restart initialization. Confirm the bundle request and expected UI.
3510
4364
 
3511
- Debug Android launches on an owned emulator created in the same run get up to
3512
- 60 seconds for bundle loading; the verify phase names that budget. Existing
3513
- emulators, remote targets, and physical devices keep the 20-second budget.
4365
+ Debug Android launches on an owned emulator that the run creates, or whose
4366
+ adoption from the pool it finishes, get up to 60 seconds for bundle loading;
4367
+ the verify phase names that budget. Existing emulators, remote targets, and physical devices keep the
4368
+ 20-second budget.
3514
4369
  Bundle delivery still requires the usual stability or app readiness check;
3515
4370
  an observed fatal error ends verification without waiting for the deadline.
3516
4371
 
@@ -3590,7 +4445,11 @@ WAITING FOR A CHANGE
3590
4445
  state, and recomputes every 30 seconds as a fallback. A log append updates
3591
4446
  only the log error count and device activity, no sooner than 15 seconds
3592
4447
  after the previous refresh. Ctrl+C, SIGTERM or closing its stdout ends it
3593
- with exit 0. Without --json it reprints the human view on change.`,
4448
+ with exit 0. On Linux, so does the exit of the process that started it
4449
+ when its stdout is a pipe. It notices within a few seconds even when
4450
+ nothing changes, except a closed stdout pipe on Linux whose starter is
4451
+ still running, which it notices at the next change.
4452
+ Without --json it reprints the human view on change.`,
3594
4453
  sections: {
3595
4454
  eas: {
3596
4455
  summary: "download a matching EAS development build; explicit profile, costs, cache and miss remedies",
@@ -3781,7 +4640,7 @@ result as proof instead of requiring an unrelated screenshot.`
3781
4640
  column:
3782
4641
 
3783
4642
  branch budget build cache caches carry
3784
- checkout deps device
4643
+ checkout daemons deps device
3785
4644
  devices error failed findings fingerprint gems
3786
4645
  install installs ip.txt lan launch lease
3787
4646
  lock log
@@ -3790,7 +4649,7 @@ result as proof instead of requiring an unrelated screenshot.`
3790
4649
  result
3791
4650
  services
3792
4651
  setting settings setup state stats stop
3793
- storage swap verify version workspace
4652
+ storage swap verify version workspace worktrees
3794
4653
 
3795
4654
  \`app\` and \`compilation cache\` join them in the stdout block a successful
3796
4655
  run ends with. When a native build runs, its compilation-cache result is
@@ -3901,6 +4760,16 @@ result as proof instead of requiring an unrelated screenshot.`
3901
4760
  is never touched: \`Reclaimed the environment; the working tree stays (it
3902
4761
  is the source checkout).\`
3903
4762
 
4763
+ \`gc\` names each slow step on stderr as it starts, in every mode; its
4764
+ report stays on stdout, and with \`--json\` stdout carries only the payload:
4765
+
4766
+ caches measuring 18 shared caches
4767
+ devices listing simulators and emulators
4768
+ worktrees checking 26 projects for merged or idle worktrees
4769
+ daemons watchman pid 49040: checking 12 roots
4770
+ daemons removing stale watchman root /w/app-409
4771
+ daemons waiting up to 10s for 1 Gradle daemon to exit: pid 51234
4772
+
3904
4773
  A GAP BETWEEN HEARTBEATS IS NOT A HANG. Stim runs device tools
3905
4774
  synchronously, so a long \`simctl\`, \`adb\` or copy call holds the timer
3906
4775
  until it returns; the next heartbeat then lands on the grid, which is why an
@@ -3908,26 +4777,40 @@ result as proof instead of requiring an unrelated screenshot.`
3908
4777
  killing a run.`
3909
4778
  },
3910
4779
  pool: {
3911
- summary: "parked and adopted simulators: what park and adoption clear or keep, the model and runtime match",
4780
+ summary: "parked and adopted simulators and emulators: what park and adoption clear or keep, the model and runtime match, emulator storage, gc erase",
3912
4781
  body: () => ` THE SIMULATOR POOL
3913
4782
  \`worktree remove\` PARKS this workspace's owned simulator instead of
3914
4783
  deleting it, and the next workspace that wants the same model and runtime
3915
- ADOPTS it. Adoption reuses the simulator but not its contents:
4784
+ ADOPTS it. Adoption reuses the simulator and the app installed on it, but
4785
+ not the previous workspace's state:
3916
4786
 
3917
- at park shut down, \`simctl erase\` (apps, app data, keychain,
3918
- privacy grants, pasteboard, photos, settings), renamed
3919
- \`stim-parked (<model> <runtime>) <4 hex>\`
4787
+ at park shut down and renamed
4788
+ \`stim-parked (<model> <runtime>) <4 hex>\`; nothing is
4789
+ erased, and the pool record names the installed app's
4790
+ bundle id and build cache key
3920
4791
  at adoption renamed for the adopting workspace, then, inside the boot
3921
4792
  the run pays anyway, \`simctl privacy reset all\` and
3922
- \`simctl keychain reset\`; at install, any other app is
3923
- uninstalled
3924
-
3925
- An erased simulator takes about 18 MB on disk instead of the gigabytes a used
3926
- one holds. Its first boot after the erase takes about 15s instead of a warm
3927
- boot's few seconds, and the app is always installed again. A SimSlim
3928
- profile survives the erase.
3929
-
3930
- Park erases only a simulator that reports Shutdown. It waits up to 15s for
4793
+ \`simctl keychain reset\`; at install, every other user app
4794
+ is uninstalled, and this workspace's app, when it is
4795
+ installed, has its preferences domain deleted (an
4796
+ expo-dev-client app's is replaced by the two dev-menu-off
4797
+ keys, see \`guide facts devmenu\`) and its data
4798
+ container's Documents, Library, tmp and SystemData emptied
4799
+
4800
+ Adoption always clears the app's data, whichever workspace parked the
4801
+ simulator: a new workspace starts from a fresh app state. The app itself
4802
+ stays installed, so the usual byte comparison decides the install: the same
4803
+ build prints \`install unchanged\` and skips the copy. A parked record's
4804
+ cache key that differs from this run's skips that comparison. App-group
4805
+ containers, the pasteboard, photos, Safari data and Simulator settings are
4806
+ not cleared; \`gc --cache parked --delete\` erases a parked simulator when a
4807
+ clean system image is needed.
4808
+
4809
+ A parked simulator keeps its apps and data, so it can hold gigabytes. It
4810
+ uses the Mac's disk, not a fixed partition, so there is no free-space check
4811
+ on iOS; \`stim gc\` lists each parked simulator's app and size.
4812
+
4813
+ Park takes only a simulator that reports Shutdown. It waits up to 15s for
3931
4814
  the shutdown to settle, retries the shutdown once, and waits up to 15s more.
3932
4815
  A simulator that is still booted after that is deleted instead, and
3933
4816
  \`worktree remove\` prints
@@ -3954,6 +4837,11 @@ result as proof instead of requiring an unrelated screenshot.`
3954
4837
  the next attempt, unless it may have left native work running. Deletion also
3955
4838
  puts an opaque marker on the pool record so older Stim versions keep it
3956
4839
  protected. New versions resolve that marker through the same identity claim.
4840
+ When a marker outlives its owner, Stim cannot tell whether the erase or
4841
+ deletion ran, so adoption treats the device as erased and drops the recorded
4842
+ app, cache key and scheme approvals. A \`gc --cache parked --delete\` erase
4843
+ that fails after the erase command started keeps the device parked without
4844
+ them for the same reason.
3957
4845
  A crash during a synchronous device-tool call leaves that work unverifiable; inspect
3958
4846
  the old process and its native children before following the claim's removal
3959
4847
  remedy. See \`stim guide errors STIM_CLAIM_REFUSED\`.
@@ -3968,8 +4856,7 @@ result as proof instead of requiring an unrelated screenshot.`
3968
4856
 
3969
4857
  device stim-app-412 (iPhone 17 26.5) (9C1F..) adopted (17s)
3970
4858
 
3971
- That time includes the first boot after the erase and the two resets, so it
3972
- runs longer than a plain boot.
4859
+ That time includes the two resets, so it runs longer than a plain boot.
3973
4860
  \`stim status\` prints one line while the pool is not empty:
3974
4861
 
3975
4862
  pool: 2 parked iOS simulators (max 3)
@@ -3977,12 +4864,21 @@ result as proof instead of requiring an unrelated screenshot.`
3977
4864
  \`stim gc\` reports the pool, and \`stim gc --delete\` empties every entry
3978
4865
  it can re-verify:
3979
4866
 
3980
- Parked simulators (2, 36 MB):
3981
- ios stim-parked (iPhone 17 26.5) 9c1f (9C1F..) iPhone 17 26.5 parked 3d ago 18 MB
3982
- --delete attempts every parked simulator and keeps failures.
4867
+ Parked simulators (2, 4.1G):
4868
+ ios stim-parked (iPhone 17 26.5) 9c1f (9C1F..) iPhone 17 26.5 parked 3d ago com.example.app 2.3G
4869
+ --delete attempts verified deletions and keeps failures.
4870
+
4871
+ \`stim gc --cache parked\` reports only the pool, and with --delete ERASES
4872
+ each parked simulator (\`simctl erase\`) and emulator (user data and
4873
+ snapshots) it can re-verify instead of deleting it. The device stays parked
4874
+ for adoption, with no app recorded; the next adopter installs its app again.
4875
+ \`--cache all\` leaves them alone. Erase takes only a simulator
4876
+ that reports Shutdown and an emulator that is not running and no workspace
4877
+ references, under the same per-device claim as adoption.
3983
4878
 
3984
- \`gc --older-than <days>\` reports and deletes only the devices parked at
3985
- least that long; one parked at an unknown time stays.
4879
+ \`gc --older-than <days>\` reports and deletes, or with \`--cache parked\`
4880
+ erases, only the devices parked at least that long; one parked at an unknown
4881
+ time stays.
3986
4882
 
3987
4883
  If simulator listing or deletion fails, \`gc --delete\` reports the failure
3988
4884
  and keeps that entry. It never turns an unverified absence into a dropped
@@ -3999,15 +4895,15 @@ result as proof instead of requiring an unrelated screenshot.`
3999
4895
  \`pool.androidParkedMax\` (default 3) or STIM_POOL_ANDROID_PARKED_MAX.
4000
4896
  A redirected STIM_HOME disables parking unless that environment override
4001
4897
  is set. Zero disables parking and adoption. \`stop\` keeps the assignment;
4002
- \`worktree remove\` parks eligible AVDs, and \`gc --delete\` empties the pool
4003
- (with --older-than, only the AVDs parked that long).
4898
+ \`worktree remove\` parks eligible AVDs, \`gc --delete\` empties the pool
4899
+ and \`gc --cache parked --delete\` wipes their user data (with
4900
+ --older-than, only the AVDs parked that long).
4004
4901
 
4005
4902
  Adoption matches the system image, data partition size, and the creation
4006
4903
  settings from android.avdConfig / android.avdConfigFile. The AVD keeps its
4007
- original stim-<label> name. Parking wipes its user data, cache and
4008
- encryption-key images and its snapshots, the files the emulator recreates
4009
- on the next boot, so a parked AVD takes a few MB and adoption pays a cold
4010
- boot (about 20s) and a fresh install.
4904
+ original stim-<label> name. Parking keeps its user data and Quick Boot
4905
+ snapshot, so the installed app stays and the pool record names its package
4906
+ and build cache key.
4011
4907
  Incompatible AVDs stay parked until eviction or GC. AVDs created by older
4012
4908
  versions without a recorded creation configuration are deleted at removal.
4013
4909
 
@@ -4023,10 +4919,9 @@ result as proof instead of requiring an unrelated screenshot.`
4023
4919
  manually clearing a claim, and keep the AVD data while its process state
4024
4920
  cannot be verified.
4025
4921
 
4026
- Android cleanup happens AFTER boot, before install or launch, for AVDs
4027
- parked with their data by older versions: \`adb shell pm clear\` clears
4028
- the adopting app's data while retaining its APK, and other third-party apps
4029
- are uninstalled. If ADB goes offline or closes the
4922
+ Android cleanup happens AFTER boot, before install or launch:
4923
+ \`adb shell pm clear\` clears the adopting app's data while retaining its
4924
+ APK, and other third-party apps are uninstalled. If ADB goes offline or closes the
4030
4925
  connection, Stim waits for boot readiness and retries cleanup for up to 30
4031
4926
  seconds, verifying the same owned AVD before each destructive command.
4032
4927
  Failed cleanup blocks launch and remains pending for a retry. The installed APK's SHA-256 must match the
@@ -4035,14 +4930,29 @@ result as proof instead of requiring an unrelated screenshot.`
4035
4930
  If the retained APK has a conflicting signer or version, adoption uninstalls
4036
4931
  it and retries installation. Adoption stays pending until installation succeeds.
4037
4932
 
4933
+ An emulator's /data partition is fixed, so an adopted AVD can run out of
4934
+ room. Before installing, adoption reads the free space on /data
4935
+ (\`adb shell df -k /data\`) and needs twice the APK size plus 512 MB. When
4936
+ there is less, it runs \`pm trim-caches\`; when that is not enough, it
4937
+ shuts the emulator down, wipes its user data through centralized teardown,
4938
+ boots it again and installs:
4939
+
4940
+ device stim-app has 97M free on /data after removing other apps and trimming caches; wiping its user data
4941
+ device stim-app (emulator-5556) booted with wiped user data
4942
+
4943
+ An install that fails with INSTALL_FAILED_INSUFFICIENT_STORAGE gets the
4944
+ same steps: trim caches and retry, then wipe and retry. Only an adopting
4945
+ run does this; a workspace's own emulator never loses data to it.
4946
+
4038
4947
  The wipe resets apps, accounts, device settings and the shared storage
4039
4948
  inside the data partition; the AVD's creation settings and any separate
4040
4949
  SD card image remain. Status lists parked Android emulators; GC
4041
- reports their system image, hardware profile, age and disk size.
4950
+ reports their system image, hardware profile, age, recorded app and disk
4951
+ size.
4042
4952
  `
4043
4953
  },
4044
4954
  builds: {
4045
- summary: "optional cache warm-up, build optimizations, fingerprints, .fingerprintignore, install unchanged, runtime state",
4955
+ summary: "optional cache warm-up, build optimizations, fingerprints, .fingerprintignore, install unchanged, running app restart, runtime state",
4046
4956
  body: () => `OPTIONAL CACHE WARM-UP FOR REPEATED NATIVE WORK
4047
4957
  When several native worktrees are coming, build the source checkout once to
4048
4958
  seed the shared caches before warming the linked worktrees. Skip this extra
@@ -4092,8 +5002,9 @@ PREDICTING THE NEXT BUILD (--plan)
4092
5002
  An Android plan reads the ABI from the emulator the slot records, or from
4093
5003
  the system image a new one would use. A plan refuses --device, --remote,
4094
5004
  --wait, --no-wait, --no-metro-check and --simulator-app with STIM_BAD_ARG.
4095
- Without --eas-profile it also refuses the android.remote setting and the
4096
- experimental compiler CAS with STIM_BAD_ARG, and refuses with
5005
+ Without --eas-profile it also refuses the ios.remote and android.remote
5006
+ settings with STIM_BAD_ARG. Without --eas-profile an Android plan also
5007
+ refuses the experimental compiler CAS with STIM_BAD_ARG, and refuses with
4097
5008
  STIM_NO_DEVICE when no system image is installed.
4098
5009
 
4099
5010
  IOS SCHEME SELECTION
@@ -4130,8 +5041,10 @@ AN ARTIFACT THE DEVICE ALREADY HOLDS IS NOT INSTALLED AGAIN
4130
5041
  install unchanged (emulator-5584 already has this build) (0.4s)
4131
5042
 
4132
5043
  On iOS the install line names the identity proof separately from the Expo
4133
- dev-client preference writes, so a slow simulator command is never charged
4134
- to an install that did not run:
5044
+ dev-client simulator writes, so a slow simulator command is never charged
5045
+ to an install that did not run. Those writes run only after a real install
5046
+ or for a scheme approval the device record lacks (\`stim guide facts
5047
+ devmenu\`), so a skipped rerun usually prints no second line:
4135
5048
 
4136
5049
  install unchanged (stim-app already has this build) (0.4s)
4137
5050
  install dev client prepared (0.9s)
@@ -4145,13 +5058,31 @@ AN ARTIFACT THE DEVICE ALREADY HOLDS IS NOT INSTALLED AGAIN
4145
5058
  \`--json\` carries installSkipped so a caller can tell a skipped run from an
4146
5059
  installed one.
4147
5060
 
5061
+ A RUNNING APP IS RESTARTED, LIKE XCODE'S RUN
5062
+ Before it launches, \`ios\` and \`android\` read the device's process list.
5063
+ When the app is already running, from an earlier run or after an install
5064
+ that was skipped, Stim stops it and launches it fresh: \`simctl terminate\`
5065
+ on a simulator, \`am force-stop\` on an emulator or an \`android --device\`
5066
+ phone. The new process requests its bundle, so launch verification has real
5067
+ evidence instead of an old process that fetched nothing. In-app state from
5068
+ the previous session does not survive. The launch line says so:
5069
+
5070
+ launch com.example.app restarted running app (was pid 4242) (0.9s)
5071
+
5072
+ A stop that fails refuses the launch rather than reusing the old process.
5073
+ When the process list cannot be read, Stim launches without stopping
5074
+ anything and verification reports what it observes. An iPhone run
5075
+ (\`ios --device\`) needs no extra step: its collector launches with
5076
+ devicectl's \`--terminate-existing\`. Remote targets launch through
5077
+ \`agent-device open --relaunch\`.
5078
+
4148
5079
  RUNTIME STATE AND BUILD INPUTS
4149
5080
  Runtime state is stored outside the project tree under
4150
5081
  $STIM_HOME/workspaces/<project>--<digest>/ (default ~/.stim/workspaces/).
4151
5082
  The aggregate run counters \`stats\` prints live beside it in
4152
5083
  $STIM_HOME/stats.json, one bucket per project and platform plus a machine-wide
4153
5084
  one, and the last 10 successful runs per project, platform and cache outcome
4154
- that size \`status\`'s build estimate. The running build itself is the
5085
+ (plus the last 10 that set up their device) that size \`status\`'s build estimate. The running build itself is the
4155
5086
  activeBuild key of the workspace's state.json, cleared when the run exits.
4156
5087
  No .gitignore entry is created or required.
4157
5088
  Native preparation can change project files: expo prebuild generates native
@@ -4554,6 +5485,27 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
4554
5485
  4. trim shared cache entries nothing has used for 14 days
4555
5486
  (\`gc --delete --older-than 14\` for the caches)
4556
5487
 
5488
+ IDLE SHUTDOWN (OFF BY DEFAULT): with devices.idleShutdownMinutes set, a
5489
+ workspace's supervisor shuts down that workspace's owned simulators and
5490
+ emulators once they have been idle that long, without waiting for a run
5491
+ to go over budget. Idle is step 1 with that many minutes in place of 10:
5492
+ booted, no driver, no Stim or agent-device lock, no build in progress, and
5493
+ no activity. A stim-server client viewing the device (the phone app's
5494
+ device viewer) counts as activity, here and in step 1. The supervisor
5495
+ checks once a minute after the workspace's last Stim command, in-app log
5496
+ and device log are all that old, rechecks under the workspace's native-run lock, and shuts devices down
5497
+ through the same teardown as \`stop\`: never deleted, and physical
5498
+ devices are never touched. It writes a device_idle_shutdown line to
5499
+ metro.ndjson and records deviceIdleShutdowns in state.json, so \`stim
5500
+ status\` shows "shut down after 30m idle" on the device until the next
5501
+ \`ios\` or \`android\` run boots it again. When metro.idleStopMinutes is
5502
+ shorter, the dev server's idle stop first shuts down the devices idle that
5503
+ long, because no supervisor is left to check afterwards. Nothing checks
5504
+ without a supervisor: release runs, and after \`stim stop\`. The setting
5505
+ is read when the supervisor starts. Stim Desktop's simulator view is not a
5506
+ stim-server client and does not count as a viewer. Turn it on with
5507
+ \`stim settings set devices.idleShutdownMinutes 30 --scope machine\`.
5508
+
4557
5509
  Steps 3 and 4 run only for disk. Memory and workspace limits never refuse;
4558
5510
  a run still over them prints one \`budget\` warning and continues. The
4559
5511
  current workspace is never reclaimed.
@@ -4572,8 +5524,8 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
4572
5524
  summary: "every flag per command, Android variants and flavors, the per-run simulator model, runtime and system image",
4573
5525
  body: () => `THE OPTION SURFACE, IN FULL
4574
5526
  start --json --wait <seconds> --remote --reset-cache
4575
- ios --slot <name> --json --plan --no-metro-check --no-build-cache --scheme <name> --configuration <name> --device-type <name> --runtime <version> --simulator-app <xcode|siniulator|stim-desktop> --device [udid] --wait <seconds> --no-wait --remote <proxy|eas>
4576
- android --slot <name> --json --plan --no-metro-check --no-build-cache --variant <name> --system-image <id> --device-profile <id> --device [serial] --wait <seconds> --no-wait --remote <proxy|eas>
5527
+ ios --build-machine <auto|local|name> --slot <name> --json --plan --no-metro-check --no-build-cache --scheme <name> --configuration <name> --device-type <name> --runtime <version> --simulator-app <xcode|siniulator|stim-desktop> --device [udid] --wait <seconds> --no-wait --remote <proxy|eas>
5528
+ android --build-machine <auto|local|name> --slot <name> --json --plan --no-metro-check --no-build-cache --variant <name> --system-image <id> --device-profile <id> --device [serial] --wait <seconds> --no-wait --remote <proxy|eas>
4577
5529
  reload [ios|android] --json
4578
5530
  device lock <ios|android> [id] --slot <name> --for <duration> --wait <seconds> --json;
4579
5531
  unlock [ios|android] --slot <name> --json
@@ -4585,7 +5537,7 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
4585
5537
  set <key> <value> --scope <layer> --json; unset <key> --scope <layer> --json
4586
5538
  doctor --json --fix --platform <ios|android>
4587
5539
  (--platform keeps shared checks and filters native findings)
4588
- gc --delete --older-than <days> --cache <name|all|workspaces> --worktrees --json
5540
+ gc --delete --older-than <days> --cache <name|all|workspaces|recordings|parked> --worktrees --json
4589
5541
  worktree warm --refresh; remove [path] --force
4590
5542
 
4591
5543
  DEVICE SLOTS
@@ -4609,23 +5561,32 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
4609
5561
  A BUILD).
4610
5562
  Named slots support local devices; remote sessions use the default slot.
4611
5563
  Names use 1-64 letters, digits, underscores or hyphens, starting with a letter
4612
- or digit. Reserved object-property names are refused.
5564
+ or digit. Reserved object-property names and web are refused.
4613
5565
 
4614
5566
  stop --slot shuts down that slot's owned devices and releases its leases,
4615
5567
  retaining Metro and sibling slots. stop --slot default targets only the
4616
5568
  workspace's default device, the same one ios/android address with no
4617
5569
  --slot and status reports with no [slot] label; it never touches a remote
4618
- session even though remote sessions use the default slot. Plain stop
4619
- handles the whole workspace.
5570
+ session even though remote sessions use the default slot. stop --slot web
5571
+ closes only the owned Chrome from stim web. A slot the workspace has not
5572
+ recorded is refused with STIM_BAD_ARG and the list of its slots; nothing is
5573
+ stopped. A slot whose first build is still running counts as recorded, and
5574
+ that build is interrupted. Plain stop handles the whole workspace.
4620
5575
  status reports named devices under slots and counts their memory. Device
4621
5576
  caps count every slot. Recycling uses the same model/runtime-matched pool
4622
5577
  for every slot, with one shared cap per platform and oldest-first eviction.
4623
5578
 
4624
- Metro cannot reliably identify a particular simulator's bundle request.
4625
- When slots coexist, a shared bundle event is not proof that a particular
4626
- slot launched: Debug launch can report unverified. Check the reported device
4627
- directly. Release verification still checks its process. reload ios/android
4628
- addresses matching Metro peers across slots; it is not a single-slot reload.
5579
+ When slots coexist, a Debug launch counts a Metro bundle delivery as proof
5580
+ only when it is attributable to that launch's device. On macOS, Metro records
5581
+ which simulator app process requested an iOS bundle (through lsof), so each
5582
+ simulator slot proves its own launch. Any other delivery (Android, a physical
5583
+ iPhone, or a simulator request lsof could not resolve) proves the launch only
5584
+ while no other slot of the same platform is running: a booted owned device,
5585
+ a log collector or a device lease. stop --slot <name> ends all three.
5586
+ Otherwise the launch reports unverified with "not provably to this device":
5587
+ check the reported device directly. Release verification still checks its
5588
+ process. reload ios/android addresses matching Metro peers across slots; it
5589
+ is not a single-slot reload.
4629
5590
 
4630
5591
  That is the whole surface today, and it is deliberately small. It can grow
4631
5592
  when a flag is genuinely the best answer -- but project-specific knowledge
@@ -4638,6 +5599,12 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
4638
5599
  worktrees of the same Git repository; the source checkout must be available.
4639
5600
  Running it in the source checkout refuses.
4640
5601
 
5602
+ Warm registers the app it prepares, so \`stim status\` lists the workspace
5603
+ with phase "warming" while it runs and "ready" once it succeeds, until the
5604
+ first start, ios, android, web or reload there or for 2 hours
5605
+ (\`guide facts status\`). In a monorepo worktree root, that app is the one
5606
+ the source checkout has registered.
5607
+
4641
5608
  \`stim doctor\` reports the source checkout's fitness as a seed -- how far
4642
5609
  behind its upstream it is, uncommitted tracked changes, an interrupted rebase
4643
5610
  or merge, a detached HEAD, a diverged branch, a branch that is not the
@@ -4718,7 +5685,7 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
4718
5685
  paths eligible under the source checkout's Git ignore rules, including .env
4719
5686
  and local configuration. The source's nonempty
4720
5687
  .worktreeexclude replaces its resolved worktree.exclude setting. Nested
4721
- registered worktrees, .DS_Store, .DerivedData, .idea, and
5688
+ registered worktrees, .DS_Store, .DerivedData, .idea, .jj, and
4722
5689
  android/build/generated/autolinking caches are excluded, including inside
4723
5690
  newly copied directories. Gradle regenerates autolinking
4724
5691
  for the destination checkout on its next build. Warm also skips paths
@@ -4797,7 +5764,21 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
4797
5764
  runtime when \`--runtime\` also resolved, which is what catches a pair like
4798
5765
  \`--device-type "iPhone 8" --runtime 26.5\` that each half would pass alone.
4799
5766
  \`--runtime\` takes a version (\`26.5\`) or a runtime's full name
4800
- (\`iOS 26.5\`), exactly; no prefix or suffix matches.
5767
+ (\`iOS 26.5\`), exactly; no prefix or suffix matches. On a remote run
5768
+ (\`--remote\` or the ios.remote setting) \`--runtime\` refuses with
5769
+ STIM_BAD_ARG, because the remote backend picks the iOS version. So does
5770
+ \`--device-type\` on the proxy backend. The eas backend honors
5771
+ \`--device-type\`: Stim passes it to \`eas simulator:start --device\`,
5772
+ unchecked against local runtimes, and refuses with STIM_BAD_ARG when
5773
+ eas-cli is older than 22.2.0, the first release with that flag. A
5774
+ recorded EAS session still running another model refuses with
5775
+ STIM_REMOTE_DEVICE_MISMATCH instead of being reused; one that already
5776
+ ended is replaced on the requested model. The ios.deviceType and
5777
+ ios.runtime settings do not refuse on a remote run; it ignores them.
5778
+ \`android --system-image\` and \`--device-profile\` refuse with
5779
+ STIM_BAD_ARG on every remote run (\`--remote\` or the android.remote
5780
+ setting), and a remote run ignores android.systemImage and
5781
+ android.deviceProfile.
4801
5782
 
4802
5783
  These flags describe a device that does not exist yet. When this workspace
4803
5784
  ALREADY owns a simulator and \`--device-type\` names a different model, or
@@ -4865,11 +5846,25 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
4865
5846
  and the remedy is the cable. Metro is unaffected: a Debug app reaches it
4866
5847
  over the LAN either way.
4867
5848
 
5849
+ WATCHING A LEASED ANDROID PHONE: while this workspace holds the phone's
5850
+ lease, stim-server can stream its screen to paired clients that ask for the
5851
+ physical device, and let one with control tap and type on it. stim-server
5852
+ pushes the scrcpy server to /data/local/tmp for the stream and deletes it
5853
+ when the stream stops; it installs nothing and changes no setting. Control
5854
+ ends when the lease is released or expires, and stim-server never takes a
5855
+ phone's lease itself.
5856
+
4868
5857
  \`stop\` releases this workspace's leases and stops its log collectors.
4869
5858
  On a physical iPhone that also closes the app, because its collector owns
4870
5859
  the devicectl launch session. \`gc --delete\` removes expired lease files;
4871
5860
  neither command shuts down the phone or uninstalls the app.
4872
5861
 
5862
+ WATCHING THE PHONE: while this workspace holds the lease, stim-server
5863
+ streams a USB-cabled iPhone's screen to the phone app, view only; Stim
5864
+ sends a physical iPhone no input. The lease ends with the run, so
5865
+ \`device lock ios <udid>\` keeps it watchable between runs. A Wi-Fi phone
5866
+ has no stream, and a locked phone shows its last frame until unlocked.
5867
+
4873
5868
  A device build is LOCAL-TIER ONLY. Its cache key is
4874
5869
  \`<fingerprint>-<configuration>-device\`, so a device app can never collide
4875
5870
  with the simulator one, and neither the build-cache provider nor the Expo
@@ -4895,8 +5890,9 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
4895
5890
  this workspace's Metro, and then wires the app to it: an expo-dev-client app
4896
5891
  through the deep link, passed to devicectl as \`--payload-url\` and followed
4897
5892
  by \`-- -EXDevMenuShowsAtLaunch 0 -EXDevMenuShowFloatingActionButton 0\`,
4898
- which is how a phone gets what a simulator gets from a defaults write, and a
4899
- bare app by writing \`<addr>:<port>\` into the app bundle's ip.txt --
5893
+ the same launch arguments Stim's own \`simctl launch --initialUrl\` carries on
5894
+ a simulator, and a bare app by writing \`<addr>:<port>\` into the app bundle's
5895
+ ip.txt --
4900
5896
  RCTBundleURLProvider's own mechanism, which honours a colon-bearing value
4901
5897
  verbatim and never consults the compiled RCT_METRO_PORT. Stim never sets
4902
5898
  that define: it would put the reserved port into a compiled input and fork
@@ -5076,6 +6072,17 @@ THE POOL: WHICH DEVICE AN ID-LESS \`--device\` PICKS
5076
6072
  android.keystorePassword override it (see \`guide settings\`). The cache
5077
6073
  entry itself is never modified.
5078
6074
 
6075
+ AN EXPO-UPDATES APP GETS A FRESH EMBEDDED UPDATE. expo-updates launches the
6076
+ update with the newest commitTime for the binary's runtime version, and the
6077
+ embedded bundle is one of those updates. The cached artifact's embedded
6078
+ manifest (app.manifest) carries the CACHED build's id and time, so an
6079
+ update the app downloaded after that build would launch instead of the
6080
+ swapped JS. The swap gives the copy's manifest a new id and this run's time,
6081
+ exactly as a full build does, on both platforms. An update published after
6082
+ the swap can still download and launch on a later start, as it would on a
6083
+ fresh build. A manifest the swap cannot rewrite, or one inside a dynamic
6084
+ EXUpdates.framework on iOS, falls back to a full build.
6085
+
5079
6086
  Before re-packing, THE ASSET GATE compares CONTENT HASHES of the assets
5080
6087
  React Native emits: what this workspace just emitted under --assets-dest
5081
6088
  against a manifest of what the cached build emitted, recorded as
@@ -5120,7 +6127,8 @@ THE POOL: WHICH DEVICE AN ID-LESS \`--device\` PICKS
5120
6127
  its builder's baked-in JS, a cache hit regenerates THIS workspace's bundle
5121
6128
  (the project's own \`expo export:embed\` / \`react-native bundle\`, plus
5122
6129
  its own hermesc when Hermes is enabled) into a copy of the artifact,
5123
- re-signs it and installs that; any swap failure falls back to a full build
6130
+ refreshes an expo-updates manifest as described above, re-signs it and
6131
+ installs that; any swap failure falls back to a full build
5124
6132
  rather than ever installing stale JS. Device logs are still collected, so
5125
6133
  \`logs --errors\` answers "does it repro in release/Hermes bytecode".
5126
6134
  A run with no \`--device\` installs on the simulator only. \`ios --device\`
@@ -5240,6 +6248,10 @@ WHAT RECLAIMS AN OWNED DEVICE
5240
6248
  command has used in that long, even though the
5241
6249
  project is still on disk, and clears only the
5242
6250
  devices parked at least that long
6251
+ stim gc --cache parked --delete
6252
+ ERASES (never deletes) verified parked simulators
6253
+ and wipes parked emulators' user data; they stay
6254
+ parked for adoption (\`guide lifecycle pool\`)
5243
6255
  stim gc --delete --worktrees
5244
6256
  also runs \`stim worktree remove\` on every clean,
5245
6257
  idle, Stim-managed linked worktree
@@ -5248,25 +6260,36 @@ WHAT RECLAIMS AN OWNED DEVICE
5248
6260
  shuts DOWN (never deletes) owned devices with no
5249
6261
  driver, claim or activity for that long
5250
6262
 
6263
+ \`gc --delete --cache watchman\` and \`gc --delete --cache gradle-daemons\`
6264
+ stop shared helper processes, not devices (\`guide cleanup memory\`).
6265
+
5251
6266
  \`worktree remove\` and \`gc --delete\` are the only two commands that delete;
5252
6267
  \`gc --delete\` deletes worktrees only through \`worktree remove\`. \`gc
5253
6268
  --delete\` also clears workspace build outputs, trims oversized workspace logs
5254
6269
  (\`guide cleanup disk\`) and removes orphaned workspace directories, never a
5255
- checkout. \`stim stop\` shuts a device
6270
+ checkout. Device recordings can also go without either: stim-server prunes
6271
+ them, and turning recording.enabled off deletes them (\`guide cleanup disk\`). \`stim stop\` shuts a device
5256
6272
  DOWN and leaves it assigned, which is what makes returning to a branch cost a
5257
6273
  boot rather than a create, a provision and a reinstall.
5258
6274
 
5259
6275
  Before shutting down, parking or deleting an owned device, Stim best-effort
5260
6276
  closes local agent-device sessions bound to its exact iOS UDID or live Android
5261
6277
  serial. An Android session must also name the owned AVD, because the next
5262
- emulator on a console port reuses its serial. \`stop\` closes a session only
6278
+ emulator on a console port reuses its serial. For an emulator that is already
6279
+ shut down, Stim closes the sessions that name its AVD on any serial that no
6280
+ connected device now holds. \`stop\` closes a session only
5263
6281
  when agent-device's claim on the device names it and was taken inside this
5264
- workspace; sessions from another workspace or claim stay open. Stim rechecks
6282
+ workspace. It also closes a session claimed from the workspace's git root
6283
+ itself, such as a worktree root above the app, but only after it has shut that
6284
+ device down and rechecked that the device is still stopped and that no other
6285
+ device holds its UDID or serial. Sessions from another workspace or claim,
6286
+ including sibling directories under the git root, stay open. Stim rechecks
5265
6287
  device ownership and uses agent-device's rejecting session target guard;
5266
6288
  sessions on other devices stay open. Physical devices are outside
5267
6289
  this cleanup. agent-device is optional: a missing binary skips this step, and a
5268
6290
  failed or timed-out list/close prints a device line on stderr while teardown
5269
- continues. Cleanup allows at most 15 seconds of agent-device calls per device.
6291
+ continues. Each cleanup pass allows at most 15 seconds of agent-device calls
6292
+ per device; the pass \`stop\` makes after shutdown has its own budget.
5270
6293
  A local daemon that cannot use socket transport or a CLI without the target guard
5271
6294
  skips cleanup with a warning; no remote daemon is used.
5272
6295
 
@@ -5315,7 +6338,8 @@ SWEEPING FINISHED WORKTREES
5315
6338
  one that is idle: no recorded use for --older-than days, 7 without it; a
5316
6339
  worktree whose last use is unknown is kept. Both need the same clean state;
5317
6340
  a worktree is kept when it is the source checkout, bare, locked, in use,
5318
- dirty (untracked files count; pod install churn alone does not), unpushed
6341
+ dirty (untracked files count; pod install churn and
6342
+ watchman's untracked \`.watchman-cookie-*\` files alone do not), unpushed
5319
6343
  (commits no remote-tracking ref or other local branch reaches), or has
5320
6344
  initialized submodules. Without --worktrees, the report leaves out the
5321
6345
  source checkout and roots outside git.
@@ -5370,11 +6394,14 @@ SWEEPING FINISHED WORKTREES
5370
6394
  takes the pull request whose head is HEAD, else one whose head contains
5371
6395
  HEAD. A pull request whose head HEAD is past ("PR #12 merged, and HEAD has
5372
6396
  commits it does not"), one unrelated to HEAD (an older use of the branch
5373
- name), and one from a fork do not count. A merged or closed one makes the worktree finished
5374
- under the same clean state. Merged, its commits are kept on GitHub, so local
5375
- commits whose remote branch was deleted do not block removal. Closed, they
5376
- do: a closed pull request whose remote branch is gone keeps the worktree as
5377
- unpushed. An open pull request never makes a worktree finished. When gh is
6397
+ name), and one from a fork that is not one of the repository's own remotes
6398
+ (a stranger's fork reusing the branch name) do not count; a pull request
6399
+ opened from the user's own fork counts like any other. A merged or closed
6400
+ one makes the worktree finished under the same clean state. Merged, its
6401
+ commits are kept on GitHub, so local commits whose remote branch was
6402
+ deleted do not block removal. Closed, they do: a closed pull request whose
6403
+ remote branch is gone keeps the worktree as unpushed. An open pull request
6404
+ never makes a worktree finished. When gh is
5378
6405
  not installed, not signed in, or fails, the JSON pullRequestUnknown field
5379
6406
  and the report say why, and gc judges from git alone. A detached HEAD is
5380
6407
  not looked up. \`stim worktree remove\` makes the same check when
@@ -5419,6 +6446,20 @@ ORPHANED WORKSPACE DIRECTORIES
5419
6446
  it. A directory with a missing or unparseable workspace.json, or a root on
5420
6447
  an unmounted volume, is reported under Skipped and never deleted.
5421
6448
 
6449
+ STALE STATUS CACHE ENTRIES
6450
+ \`stim status\` caches each folder's size under $STIM_HOME/disk-usage and
6451
+ each worktree's pull request under $STIM_HOME/pull-requests. \`worktree
6452
+ remove\` drops the entries of the worktree it removed (its checkout,
6453
+ node_modules and workspace folder, and its pull request), not the size
6454
+ entries of its owned devices' data folders. \`gc\` reports the
6455
+ entries of a folder or worktree that was removed some other way under "Stale
6456
+ status cache entries", and \`gc --delete\` removes them. An entry counts as
6457
+ stale only when the path it records is gone and its volume is mounted (a
6458
+ measured node_modules folder follows its checkout). An entry that records no
6459
+ readable path, one whose path cannot be checked, and one on an unmounted
6460
+ volume are kept and counted under Skipped. \`status\` measures a folder again
6461
+ if it comes back.
6462
+
5422
6463
  NAMED SERVER PORTS
5423
6464
  worktree remove stops TCP listeners on each named allocation and releases
5424
6465
  the ports. gc reports named allocations for missing workspaces; gc --delete
@@ -5610,9 +6651,65 @@ THE ONE CASE GC WILL NOT REAP
5610
6651
  is gone, and the unrelated process is never signalled. A missing, malformed,
5611
6652
  or unreadable identity leaves the record unverified and kept for a retry.
5612
6653
  Wall-clock timestamps and command names are not ownership proof.`
6654
+ },
6655
+ memory: {
6656
+ summary: "watchman and Gradle and Kotlin daemon memory in gc, and when gc --delete --cache watchman or gradle-daemons stops them",
6657
+ body: () => `MEMORY
6658
+ Every \`gc\` without --cache, and \`gc --cache watchman\` or
6659
+ \`gc --cache gradle-daemons\`, reports the long-lived helpers that grow while
6660
+ they run: the shared watchman daemon, Gradle daemons and Kotlin compile
6661
+ daemons. Each line gives the pid, physical footprint (the measure status
6662
+ uses; resident size when the footprint cannot be read), uptime and whether
6663
+ it is idle, and a kept one says why. The reclaimable line totals what
6664
+ the two commands below would free now.
6665
+
6666
+ Only these two commands stop anything, and each acts only on its own kind.
6667
+ Neither a plain \`gc --delete\` nor \`--cache all\` touches them:
6668
+ stim gc --delete --cache watchman
6669
+ runs \`watchman watch-del\` on each stale root
6670
+ with no subscription or trigger, then
6671
+ \`watchman shutdown-server\` only when
6672
+ \`debug-status\` lists no client but gc's own
6673
+ call and no root has a trigger; otherwise
6674
+ the daemon is kept and the report names
6675
+ each client, as the Stim workspace whose
6676
+ dev server it belongs to where it can
6677
+ stim gc --delete --cache gradle-daemons
6678
+ stops each Gradle daemon its own
6679
+ \`gradle --status\` reports IDLE, then each
6680
+ Kotlin compile daemon with no open client
6681
+ connection once every Gradle daemon is idle
6682
+
6683
+ A stale watch root is one whose directory is gone from a mounted volume, or
6684
+ that sits in a linked worktree git pruned while its directory stayed.
6685
+ Watchman drops a root itself when its directory is deleted, so stale roots
6686
+ are rare. Removing one stops its recrawls but does not shrink the daemon;
6687
+ only a restart returns its memory. After a shutdown the next client that
6688
+ needs watchman starts it again and re-watches the roots in its state file.
6689
+ Every watchman call uses --no-spawn, so gc never starts the daemon.
6690
+ Shutting it down under a Metro that uses watchman would break that Metro's
6691
+ file watching, which is why any connected client keeps it.
6692
+
6693
+ gc finds a Gradle daemon's Gradle user home from the daemon log it holds
6694
+ open (lsof), and asks the daemon's own distribution for its status with
6695
+ that home and the daemon's Java. When every daemon that status lists for a
6696
+ home and version is idle, gc runs that distribution's \`gradle --stop\`;
6697
+ otherwise it sends SIGTERM to each idle daemon, after checking the pid still
6698
+ has the same start time. A build that picks a daemon between gc's last
6699
+ check and the stop fails and must be run again, so gc re-checks the build
6700
+ locks and the daemon's status right before each stop. Nothing is stopped while an Android build lock, or a build slot no
6701
+ iOS build holds, is live or unresolved. stim-server stops the daemons of
6702
+ offloaded builds (offload.gradleDaemonIdleMinutes, \`guide settings\`), so
6703
+ while it runs gc keeps those. A daemon whose home, distribution or status cannot be read is
6704
+ kept. Gradle stops an idle daemon itself after 3 hours and Kotlin after 2
6705
+ hours by default.
6706
+
6707
+ --older-than does not apply to these kinds and is refused with
6708
+ STIM_BAD_ARG. With STIM_HOME set, gc skips them: they are machine-global.
6709
+ \`stim doctor\` notes a watchman footprint over 2 GiB.`
5613
6710
  },
5614
6711
  disk: {
5615
- summary: "disk usage, workspace build outputs and logs, AVD and build-log sizes, the data partition, trimming the shared caches",
6712
+ summary: "disk usage, workspace build outputs, logs and device recordings, AVD and build-log sizes, the data partition, trimming the shared caches",
5616
6713
  body: () => `DISK
5617
6714
  Logs, state, pidfiles and Xcode DerivedData are under the global workspace
5618
6715
  directory, and \`worktree remove\` reclaims them. \`gc --delete\` clears the
@@ -5622,7 +6719,8 @@ THE ONE CASE GC WILL NOT REAP
5622
6719
 
5623
6720
  Android AVDs normally live under ~/.android/avd, and a booted owned AVD can
5624
6721
  use several GB. \`worktree remove\` parks the workspace's owned AVD with its
5625
- user data and snapshots wiped, or deletes it when the pool is off or full;
6722
+ user data and snapshots kept, or deletes it when the pool is off or full;
6723
+ \`gc --cache parked --delete\` wipes a parked AVD's user data and snapshots;
5626
6724
  plain \`stop\` only shuts it down for reuse. Stim uses Android's default Quick Boot
5627
6725
  unless displayless Linux requires software rendering, where snapshots are
5628
6726
  disabled. The first boot and a boot after the emulator, system image, or AVD
@@ -5634,7 +6732,7 @@ THE ONE CASE GC WILL NOT REAP
5634
6732
  errors teardown\`).
5635
6733
  New owned AVDs default to an 8 GiB data partition, though project settings can
5636
6734
  change it. When enabled, Quick Boot keeps one automatic snapshot until the AVD is
5637
- parked or deleted.
6735
+ wiped or deleted.
5638
6736
  \`gc\` prints the on-disk size beside an orphaned or stale owned Android AVD
5639
6737
  when its content directory can be read, and beside an orphaned or stale
5640
6738
  owned simulator the data size simctl reports for it.
@@ -5665,15 +6763,20 @@ setting applies only to a newly created AVD; recreate the environment to adopt
5665
6763
  a changed value.
5666
6764
 
5667
6765
  WORKSPACE BUILD OUTPUTS
5668
- Each workspace directory holds derived-data/, gradle-build/, android-cas/
5669
- and cache-provider/. \`gc\` reports them as one detected cache, "Workspace
5670
- build outputs", with a per-workspace size, last use and verdict. Plain
6766
+ Each workspace directory holds derived-data/, gradle-build/, android-cas/,
6767
+ cache-provider/, macos/build, the staged macos/<Product>.app and macos/staging-*
6768
+ leftovers. \`gc\` reports them as one detected cache, "Workspace build
6769
+ outputs", with a per-workspace size, last use and verdict. Plain
5671
6770
  \`gc --delete\` clears them for every workspace not in use (see \`guide
5672
6771
  cleanup gc\`), before anything else. \`--older-than <days>\` limits that to
5673
6772
  workspaces idle at least that long, and keeps one whose last use is
5674
- unknown. \`--cache workspaces\` acts on them alone; \`--cache all\` includes
5675
- them. Only those four directories go: workspace.json, state.json, logs/,
5676
- locks and device records stay, so the workspace keeps its devices and ports.
6773
+ unknown. \`--cache workspaces\` acts on them alone, except in the workspace
6774
+ directories that plain \`gc --delete\` removes whole (dead projects and
6775
+ orphaned directories); \`--cache all\` includes them. macos/ itself,
6776
+ macos/runtime.lock and its claim set, workspace.json, state.json, logs/,
6777
+ other macOS files, locks and device records stay, as does the project's
6778
+ own .build. The workspace keeps its devices and ports. A running, building,
6779
+ unverified or hosted macOS app keeps its workspace untouched.
5677
6780
  stim gc --delete --cache workspaces --older-than 7
5678
6781
  Last use is the newest of the lastUsedAt that start, ios, android, reload
5679
6782
  and worktree warm record in state.json, lastBuild.startedAt,
@@ -5684,7 +6787,8 @@ WORKSPACE BUILD OUTPUTS
5684
6787
  The next build of an unchanged app installs from the shared build cache.
5685
6788
  After a native change the Xcode compilation cache speeds the rebuild, but on
5686
6789
  React Native 0.86 Swift does not use it (explicit modules are off), so that
5687
- build recompiles Swift.
6790
+ build recompiles Swift. The next \`stim macos\` rebuilds its scratch from
6791
+ nothing (a full Swift build) and restages the app.
5688
6792
 
5689
6793
  WORKSPACE LOGS
5690
6794
  \`gc\` reports the size of each workspace's logs/ (the workspaceLogs
@@ -5699,6 +6803,33 @@ WORKSPACE LOGS
5699
6803
  stops the collector. Build transcripts keep the whole run and are never
5700
6804
  trimmed, and nothing else under logs/ is touched.
5701
6805
 
6806
+ DEVICE RECORDINGS
6807
+ stim-server records owned simulators, emulators and the Stim-owned Chrome
6808
+ page while an automation tool drives them or a paired client watches them,
6809
+ into recordings/ in the workspace directory (\`guide settings\`). It keeps
6810
+ the last 15 minutes of recorded footage per device; idle time and time after
6811
+ \`stim stop\` age nothing out, so \`stop\` ends recording and the footage
6812
+ stays replayable. Across every workspace of this Stim home it keeps at most
6813
+ 1 GiB, deleting the oldest footage first. Recordings are deleted by:
6814
+
6815
+ stim worktree remove that workspace's recordings, with its
6816
+ workspace directory
6817
+ stim gc --delete the recordings of dead or orphaned
6818
+ workspaces, with their workspace directory;
6819
+ a workspace whose project root cannot be
6820
+ proven gone keeps them
6821
+ stim gc --delete --older-than <days>
6822
+ also footage recorded before that many days
6823
+ ago, in every workspace
6824
+ stim gc --delete --cache recordings
6825
+ every workspace's recordings, and nothing
6826
+ else; --cache all includes them
6827
+ recording.enabled false the recordings of each workspace it turns off
6828
+
6829
+ \`gc\` lists them in the recordings section of \`gc --json\` with each
6830
+ workspace's bytes, what a delete would remove (deleteBytes), and why the rest
6831
+ stays.
6832
+
5702
6833
  SHARED BUILD CACHES
5703
6834
  The caches that make a second workspace fast are alive by design and never
5704
6835
  included in a plain \`gc --delete\`. Every \`gc\` run reports them anyway,
@@ -5746,7 +6877,7 @@ read from, or an unknown key refuses with STIM_BAD_ARG naming the expected
5746
6877
  shape, before anything is written. Writes to the machine file take its lock
5747
6878
  and replace it atomically; a committed write keeps the file's other keys and
5748
6879
  indentation. Run \`stim settings\` from the app directory: workspace and
5749
- committed resolve from the nearest package.json, repo from its Git
6880
+ committed resolve from the nearest package.json or Package.swift, repo from its Git
5750
6881
  repository. From a monorepo web package that resolves to its worktree's app
5751
6882
  (see stim guide ports), workspace and committed are that app's entry and
5752
6883
  .stim.json. worktree.exclude and worktree.defaultBranch are read only from
@@ -5765,9 +6896,11 @@ carries its dotted key, layers, and environment override under "x-stim".
5765
6896
  Resolution order, first match wins:
5766
6897
  1. workspace ~/.stim/config.json, under this project's entry
5767
6898
  2. repo ~/.stim/config.json, under this repo's git common dir
5768
- 3. committed .stim.json beside the app's package.json
5769
- 4. machine ~/.stim/config.json, top-level optimizations and
5770
- android.deviceProfile only
6899
+ 3. committed .stim.json beside the app's package.json or Package.swift
6900
+ 4. machine ~/.stim/config.json, top-level optimizations,
6901
+ ios.deviceType, ios.runtime, android.systemImage,
6902
+ android.deviceProfile and devices.idleShutdownMinutes
6903
+ only
5771
6904
  5. Stim default
5772
6905
  An environment override, where a setting has one, wins over every layer.
5773
6906
 
@@ -5797,15 +6930,21 @@ KEYS STIM READS
5797
6930
  ios.deviceType e.g. "iPhone 17 Pro" -- the simulator model this
5798
6931
  workspace's owned sim is created as, spelled exactly as
5799
6932
  \`xcrun simctl list devicetypes\` names it, and one an
5800
- installed runtime can create. The \`--device-type\`
5801
- flag overrides this per invocation. A name no installed
5802
- runtime offers is STIM_BAD_ARG and the creatable names
5803
- are printed
6933
+ installed runtime can create. Readable from the
6934
+ machine layer too, so one machine can default every
6935
+ workspace to a model; a project layer still wins. The
6936
+ \`--device-type\` flag overrides this per invocation. A
6937
+ name no installed runtime offers is STIM_BAD_ARG and
6938
+ the creatable names are printed, naming the layer the
6939
+ value came from
5804
6940
  ios.runtime e.g. "26.2" -- the iOS runtime that sim is created on,
5805
6941
  as a version ("26.2") or a runtime's full name
5806
- ("iOS 26.2"); nothing else matches. The \`--runtime\`
5807
- flag overrides this per invocation, and an uninstalled
5808
- version refuses the same way
6942
+ ("iOS 26.2"); nothing else matches. Readable from the
6943
+ machine layer too, so one machine can default every
6944
+ workspace to a runtime; a project layer still wins.
6945
+ The \`--runtime\` flag overrides this per invocation,
6946
+ and an uninstalled version refuses the same way,
6947
+ naming the layer the value came from
5809
6948
  ios.configuration e.g. "Release" -- the Xcode configuration to build
5810
6949
  (simulator only). Committing
5811
6950
  { "ios": { "configuration": "Release" } } makes every
@@ -5857,9 +6996,12 @@ KEYS STIM READS
5857
6996
  interface.
5858
6997
  android.systemImage e.g. "system-images;android-36;google_apis;arm64-v8a"
5859
6998
  -- the sdkmanager package id the owned AVD is created
5860
- from. The \`--system-image\` flag overrides this per
5861
- invocation, and an id this SDK has not installed is
5862
- STIM_BAD_ARG with the installed ids printed.
6999
+ from. Readable from the machine layer too, so one
7000
+ machine can default every workspace to an image; a
7001
+ project layer still wins. The \`--system-image\` flag
7002
+ overrides this per invocation, and an id this SDK has
7003
+ not installed is STIM_BAD_ARG with the installed ids
7004
+ printed, naming the layer the value came from.
5863
7005
  android.deviceProfile e.g. "pixel_fold" -- the avdmanager hardware profile a
5864
7006
  new owned AVD is created with, spelled exactly as
5865
7007
  \`avdmanager list device -c\` prints it. Defaults to
@@ -5963,15 +7105,35 @@ ${ANDROID_AVD_CONFIG_HELP.map((line) => ` ${line}`).joi
5963
7105
  shares this machine and is the only mode that needs no
5964
7106
  tunnel. "expo" lets the Expo dev server tunnel itself.
5965
7107
  "cloudflared" and "ngrok" name a managed provider
5966
- explicitly. Any other value is refused as invalid.
7108
+ explicitly. "tailscale" explicitly selects a tailnet-only
7109
+ foreground serve process, never Funnel; "auto" never
7110
+ selects it. The device must share the tailnet. See
7111
+ \`guide metro\`. Any other value is refused as invalid.
5967
7112
  metro.ngrokUrl the stable managed ngrok URL. It requires metro.tunnel
5968
7113
  "ngrok" and passes --url to ngrok http. Stim owns
5969
7114
  this process.
5970
7115
  metro.publicUrl an existing tunnel's URL. Takes precedence over
5971
7116
  starting one, whatever metro.tunnel says -- Stim
5972
7117
  did not create it, so a Metro request through it is
5973
- still gated the same way a managed tunnel's is. Set it
7118
+ still gated through a public bundle probe. Set it
5974
7119
  before Expo start so the manifest advertises it.
7120
+ metro.port this workspace's Metro port, 1024-65535;
7121
+ STIM_METRO_PORT overrides it. Stim reserves this
7122
+ number instead of picking one from 8082 up, for a
7123
+ workspace whose tools already expect a port. A
7124
+ committed value suits a single checkout; give each
7125
+ worktree its own in the workspace layer or the
7126
+ environment. start refuses with STIM_BAD_ARG when
7127
+ another workspace reserves it or another process
7128
+ holds it. start, ios, android and web refuse a
7129
+ changed pin while this workspace's dev server runs
7130
+ (run stim stop before retrying) or its supervisor
7131
+ cannot be verified (stop it with the tool that
7132
+ started it). A foreign holder is refused before
7133
+ anything starts; this project's Metro on the pin is
7134
+ attached to. --no-metro-check uses the pin, then the
7135
+ recorded port, then 8081, without probing or reserving;
7136
+ an invalid pin still refuses.
5975
7137
  metro.warmupUrl optional object with per-platform bundle URLs:
5976
7138
  metro.warmupUrl.ios
5977
7139
  metro.warmupUrl.android
@@ -5999,6 +7161,23 @@ ${ANDROID_AVD_CONFIG_HELP.map((line) => ` ${line}`).joi
5999
7161
  driven. Read
6000
7162
  when \`start\` spawns the supervisor; see
6001
7163
  \`guide metro\`.
7164
+ devices.idleShutdownMinutes
7165
+ minutes an owned simulator or emulator stays idle
7166
+ (\`gc --idle\` conditions; an open stim-server viewer
7167
+ counts as activity) before its workspace's supervisor
7168
+ shuts it down, never deletes it. Default 0, never.
7169
+ Machine or project layers. Read when the supervisor
7170
+ starts; see \`guide lifecycle budget\`.
7171
+ macos.product the explicit Swift Package executable product built in
7172
+ Debug by stim macos
7173
+ macos.infoPlist development Info.plist relative to Package.swift,
7174
+ without shared URL schemes or an update feed
7175
+ macos.arguments string array passed directly to the executable;
7176
+ see stim guide macos for the prototype's limits
7177
+ macos.assetCatalog optional .xcassets directory relative to Package.swift,
7178
+ compiled into Contents/Resources before signing
7179
+ macos.resources map of Contents/Resources destinations to source paths
7180
+ relative to Package.swift; see stim guide macos
6002
7181
  web.url the page \`stim web\` opens in the owned Chrome, an
6003
7182
  http:// or https:// URL. {port:<label>} becomes the
6004
7183
  workspace's named port (allocated like \`stim ports
@@ -6178,7 +7357,9 @@ to its app path for every command it starts.
6178
7357
  THE DEVICE POOL BOUNDS ARE MACHINE-LEVEL TOO
6179
7358
  \`pool.iosParkedMax\` caps how many parked simulators \`worktree remove\` may
6180
7359
  leave behind for a later workspace to adopt. It is machine-level for the same
6181
- reason: the disk they sit on is the whole machine's, about 18 MB each once erased.
7360
+ reason: the disk they sit on is the whole machine's. A parked simulator keeps
7361
+ its apps and data, so it can hold gigabytes until \`stim gc --cache parked
7362
+ --delete\` erases it.
6182
7363
 
6183
7364
  {
6184
7365
  "pool": { "iosParkedMax": 3 }
@@ -6205,6 +7386,211 @@ test suites and the end-to-end harness use one -- and a scoped config must not
6205
7386
  leave simulators on the machine it cannot account for. A redirected home that
6206
7387
  wants a pool says so with the variable.
6207
7388
 
7389
+ HOSTING MACHINES ARE MACHINE-LEVEL
7390
+ \`hosting.machines\` names Macs that may host owned simulator sessions,
7391
+ by MagicDNS name with an optional serve port (default 7443). Name each node
7392
+ and port once. This is separate from build offloading:
7393
+
7394
+ stim settings set hosting.machines '["janics-mac-mini"]'
7395
+ stim doctor --fix
7396
+
7397
+ Only \`doctor --fix\` asks for device-host access. A person on that Mac
7398
+ approves the printed id with \`stim-server devices grant <id> --device-host\`.
7399
+ Hosting grants include no read, control or build capability.
7400
+ $STIM_HOME/device-host-machines.json stores a private token and pinned tailnet
7401
+ node. Doctor never prints the token; it reports each machine under deviceHosts
7402
+ in JSON. Stim sends tokens only to the pinned node's own tailnet address,
7403
+ with its MagicDNS name for TLS and Host routing. A changed node refuses
7404
+ access, and uncertain replies or unreadable credentials preserve the pin.
7405
+ Invalid hosting settings report an error and preserve every saved credential.
7406
+ \`doctor --fix\` forgets entries removed from hosting.machines; remove a
7407
+ replaced machine, run it, then re-add the name to request a new approval.
7408
+ A definite revoked or lapsed request can be requested again with --fix.
7409
+ An in-progress approval inspection reports busy rather than replacing its
7410
+ pending token. \`stim macos --remote <machine>\` runs a macOS app on an
7411
+ approved machine (stim guide macos); \`ios\` and \`android\` do not yet
7412
+ place sessions on these machines. To view or control a hosted macOS app, a person
7413
+ on that Mac approves Screen & System Audio Recording and Device Control and Data
7414
+ Access (Accessibility on macOS 26 and earlier) for Stim Host, the app
7415
+ \`stim-server service install\` runs the server under.
7416
+
7417
+ On a hosting Mac, \`hosting.agentDriver\` names the tool it starts so a
7418
+ client's coding agent can drive the macOS apps it hosts for that client.
7419
+ The default, \`none\`, starts nothing. \`agent-device\` starts its
7420
+ daemon only when that agent-device can lease a single macOS app (its
7421
+ \`macos-app\` lease backend); otherwise agent control reports \`none\` with a
7422
+ notice, and no client is handed the Mac's desktop. STIM_AGENT_DEVICE_BIN in
7423
+ stim-server's environment names an agent-device binary to use instead of
7424
+ ~/.local/bin/agent-device. \`doctor\` on that Mac notes a hosted macOS app that runs while
7425
+ the setting is \`none\`.
7426
+
7427
+ BUILD MACHINES ARE MACHINE-LEVEL
7428
+ \`offload.machines\` lists the Macs on the tailnet that may build for this one,
7429
+ by MagicDNS name, each optionally with the port of its \`tailscale serve\`
7430
+ route (default 7443):
7431
+
7432
+ {
7433
+ "offload": { "machines": ["janics-mac-mini"] }
7434
+ }
7435
+
7436
+ \`doctor --fix\`, run in any app directory, asks each named Mac for build access and pins its tailnet
7437
+ node in $STIM_HOME/build-machines.json. A person approves the request on that
7438
+ Mac with \`stim-server devices grant <id> --build\`; doctor prints the id.
7439
+ Stim connects to a named Mac only while its name still belongs to the pinned
7440
+ node and never sends the token to another node. \`doctor\` reports each
7441
+ machine's pairing state and, for an approved machine, asks it for one build
7442
+ offer and lists every reason it would not take this app's iOS build: no
7443
+ answer, another Stim build, CPU, Xcode, simulator SDK or CocoaPods, no
7444
+ Bundler for an app whose Gemfile.lock pins CocoaPods, no iPhone simulator on
7445
+ the runtime \`stim ios\` builds for here, low disk, or busy. With Package.swift
7446
+ and no platform selection on a Mac, it also checks the macOS Xcode and SDK.
7447
+
7448
+ \`offload.machine\` (machine scope, default auto) selects placement for
7449
+ \`ios\`, \`android\` and \`macos\`. \`--build-machine <auto|local|name>\`
7450
+ overrides STIM_OFFLOAD_MACHINE, which overrides the setting. A blank environment
7451
+ value is unset. Trimmed auto/local are case-insensitive. Machine names match
7452
+ configured names case-insensitively, with port 7443 when omitted; reports use
7453
+ the configured entry.
7454
+
7455
+ auto follows offload.mode and keeps its local fallback behavior
7456
+ local builds only on this Mac for this invocation
7457
+ name requires a matching entry in offload.machines, already paired and
7458
+ approved; ignores offload.mode and this Mac's load/slot gating
7459
+
7460
+ A name never prompts for pairing, tries another machine or falls back here.
7461
+ Any refusal or failure is STIM_OFFLOAD_REFUSED with the machine and reason.
7462
+ Device, Release/non-Debug, --remote, Android CAS compiler builds, builds with
7463
+ the build cache off, and an unknown simulator runtime cannot build there and
7464
+ refuse on a cache miss. Invalid values, unlisted names and unpaired names refuse
7465
+ at setup before consulting the cache; they create no build record or failed-run
7466
+ stats. A listed paired name with a cache hit compiles nothing, contacts no worker
7467
+ and records buildMachine with builtOn absent. Build failures retain the selected
7468
+ value; builtOn is here or the configured machine entry when compilation ran.
7469
+ Prebuild and pod install still run on this Mac before a named offload. A refusal
7470
+ starts no local xcodebuild, Gradle or SwiftPM compile.
7471
+ Run stim doctor --fix to ask for build access if not paired. A person on the
7472
+ worker finds the id with stim-server devices and approves it with
7473
+ stim-server devices grant <id> --build. Check stim settings get offload.machines,
7474
+ or rerun with --build-machine auto or --build-machine local. Ctrl-C cancels
7475
+ the remote build without a local xcodebuild, Gradle or SwiftPM compile.
7476
+
7477
+ Copyable agent prompt:
7478
+ Run stim ios --build-machine janics-mac-mini. If it refuses, report the
7479
+ STIM_OFFLOAD_REFUSED reason and remedy; do not retry with another placement.
7480
+
7481
+ With offload.machine auto, \`offload.mode\` decides where an iOS simulator Debug build or an Android
7482
+ emulator debug build or a stim macos SwiftPM Debug build compiles:
7483
+
7484
+ auto (default) here while this Mac has capacity: a free
7485
+ \`concurrency.maxBuilds\` slot (always, with no build limit) and a
7486
+ load per core under \`offload.maxLoadPerCore\`. Otherwise on a
7487
+ build machine that accepts the build and is expected to be faster:
7488
+ while every slot here is busy, any machine that accepts; while only
7489
+ the load is high, a machine whose load per core is lower than this
7490
+ Mac's. A machine too old to report its load counts only while every
7491
+ slot here is busy.
7492
+ force on a build machine whenever one accepts it
7493
+ off always here
7494
+
7495
+ Load per core is the 5-minute load average divided by the CPU count; a Mac's
7496
+ native builds are its Stim runs in prebuild, pods or compile on that Mac, not
7497
+ the ones it offloaded.
7498
+ \`offload.maxLoadPerCore\` (default 2) is the load per core at which a Mac
7499
+ counts as saturated, both here and on a build machine.
7500
+
7501
+ STIM_OFFLOAD_MODE overrides it for one command. Device, Release and
7502
+ \`--remote\` builds, Android builds with the Apple Clang CAS compiler cache,
7503
+ and iOS/Android runs with the build cache off, always build here.
7504
+ macOS uses matching Xcode and macOS SDK, with a per-client SwiftPM dependency
7505
+ cache on the worker. It skips JavaScript dependencies, prebuild and pods; see
7506
+ stim guide macos. macOS artifacts are not cached.
7507
+ An offloaded iOS/Android build first runs prebuild (and \`pod install\` for iOS) here,
7508
+ then asks every paired machine what it can build. It takes one whose Stim
7509
+ build and CPU match this Mac exactly, with enough disk, that does not decline,
7510
+ preferring the one that already holds this repository, then the least loaded,
7511
+ and moves to the next one in that order when a machine that offered fails the
7512
+ sync or refuses to start the build.
7513
+ For iOS the machine's Xcode and simulator SDK must match, and it needs an
7514
+ iPhone simulator on the target runtime. Its CocoaPods must match too, unless
7515
+ the app's Gemfile.lock pins CocoaPods: both Macs then run that version through
7516
+ bundler, so the machine needs only Bundler on its stim-server PATH and
7517
+ installs the pinned gems itself on the first build. For Android its JDK major
7518
+ version must match, and its Android SDK must hold the NDK, build-tools and
7519
+ compile platform that the project's React Native version names in
7520
+ gradle/libs.versions.toml; Gradle and AGP come from the synced project. When
7521
+ no machine takes the build in auto placement, the run prints one line with each machine's
7522
+ reason, such as \`janics-mac-mini: busy (load at or above 2/core; load
7523
+ 8.2/core, 2 builds) -> building here\`, and compiles here. It sends the files
7524
+ \`git ls-files -co --exclude-standard\` lists (the machine keeps only files it
7525
+ lacks, so an unignored secret such as \`.env\` is sent too), builds with
7526
+ Stim's own code there, refuses unless the fingerprint there equals this one,
7527
+ and brings the \`.app\` or APK back. An Android build there uses this Mac's
7528
+ variant, target ABI, Gradle build cache, PCH and compiler cache choices, so
7529
+ the APK matches the ABI-narrowed cache key. Stim checks the archive's sha256
7530
+ and fingerprints this checkout again before it stores the app under the
7531
+ post-mutation key and installs it the usual way, so an APK is still compared
7532
+ with the installed one before an install is skipped.
7533
+ While a machine builds, Stim pings it every 15 seconds and treats a minute of
7534
+ silence as a dropped connection while it waits for the build. When the
7535
+ connection drops without a close, Stim reconnects to the same pinned machine
7536
+ for up to 3 minutes and takes the running build back; the machine keeps it
7537
+ running for 5 minutes without a connection, then cancels it. With auto, a drop while the
7538
+ app is fetched still builds here. Interrupting the run with Ctrl-C closes the
7539
+ connection, which cancels the build there; a run killed another way, or
7540
+ interrupted while it reconnects, leaves it until those 5 minutes pass. With auto, a failure after a machine took the build prints \`offload failed: <reason>
7541
+ -> building here\` and compiles here. The run's lastBuilds entry and
7542
+ \`--json\` facts record the reason as offloadFallback. Offloading holds no
7543
+ local build slot; that fallback takes one. \`stim stats\` keeps where each
7544
+ compiling build ran and why (\`guide facts stats\`).
7545
+ An offloaded app lands only in this Mac's build cache, not in a remote cache
7546
+ provider. A project whose xcodebuild changes its own fingerprinted inputs
7547
+ fails the offload fingerprint check: auto builds here; a named placement refuses.
7548
+
7549
+ On the build machine, stim-server keeps each client's checkouts, dependencies,
7550
+ DerivedData, compilation cache, ccache and Gradle home under
7551
+ \`offload.workerRoot\` (absolute; default $STIM_HOME/build-worker), one area
7552
+ and Stim home per client and repository, and one Gradle home per client. It
7553
+ runs one offloaded build at a time. It declines a build while that volume has
7554
+ less than 10 GB free, while its own native builds and the offloaded one fill
7555
+ its \`concurrency.maxBuilds\`, or while its load per core is at or above its
7556
+ \`offload.maxLoadPerCore\`, and reports the reason in its offer. It boots and
7557
+ installs nothing. While an offloaded build runs there, it holds one of that
7558
+ Mac's \`concurrency.maxBuilds\` slots, so a local Stim build there waits for it
7559
+ and counts it as busy; the slot is freed when the build ends, is cancelled or
7560
+ its process is gone. Android builds there run on the JDK in stim-server's
7561
+ JAVA_HOME (else the macOS default JDK) with the SDK in its ANDROID_HOME (else
7562
+ ~/Library/Android/sdk). The client's Gradle daemon stays warm there for
7563
+ \`offload.gradleDaemonIdleMinutes\` (default 30; 0 stops it when each build
7564
+ ends; a new value applies from the next daemon) and never holds a build
7565
+ slot. While none of the client's builds runs, stim-server stops it sooner
7566
+ when a build is cancelled, the client is revoked, or the machine has less
7567
+ than 2 GB of memory available, and deleting the client's directory under the
7568
+ worker root stops it too.
7569
+ \`stim-server service install [--serve]\` on the build machine keeps stim-server
7570
+ running as a login LaunchAgent; \`--path-prepend <dir>\` and \`--env KEY=VALUE\`
7571
+ pin a PATH entry or variable such as a private CocoaPods that stim-server's
7572
+ login-shell environment would otherwise replace.
7573
+ \`stim-server service update --release <version>\` there moves the service to
7574
+ that exact stim-server release from the public npm registry once npm verifies
7575
+ its integrity and registry signatures; \`--from <dir>\` installs the packed
7576
+ packages of a checkout instead. It waits for offloaded builds and hosted
7577
+ sessions to finish, restarts the job, and switches back when the new server
7578
+ does not answer within 90 seconds; \`stim-server service rollback\` returns
7579
+ to the previous server. A client Mac approved for builds or device hosting
7580
+ there can ask for the same update over its tailnet connection: an npm release
7581
+ always, and its own packed build only while \`server.acceptClientBuilds\` is
7582
+ true on that machine (default false).
7583
+ Installation alone does not prove readiness; check \`stim-server service status\`
7584
+ and its reported log. The server listens before it touches the Stim home; a
7585
+ read-only child process reads the Stim home, server and recording directories,
7586
+ and until it returns \`/health\` answers 503 with \`startup.state\` pending.
7587
+ A read that does not return within 10 seconds, plus up to one second to stop
7588
+ the child, leaves the server listening and degraded, with the reason in
7589
+ \`/health\` and \`stim-server service status\`, and no status followers,
7590
+ watchers, native helpers or recorder; it reads again every 30 seconds and
7591
+ serves once a read returns. It preserves claims and recordings; it does not
7592
+ restore an inaccessible volume or grant filesystem access.
7593
+
6208
7594
  THE GC WORKTREE GRACE PERIOD IS MACHINE-LEVEL
6209
7595
  \`gc.worktreeGraceMinutes\` is how long \`gc --delete\` waits before it removes
6210
7596
  a merged or idle linked worktree. The clock starts at the worktree's latest
@@ -6221,6 +7607,25 @@ which overrides the file. Absent means 120. \`0\` removes a finished worktree at
6221
7607
  once. A value that is not a whole number 0 or more is warned about by \`gc\`,
6222
7608
  which then uses 120. See \`guide cleanup\` for what else keeps a worktree.
6223
7609
 
7610
+ DEVICE RECORDINGS CAN BE TURNED OFF
7611
+ stim-server records the screens of owned simulators, emulators and the
7612
+ Stim-owned Chrome page while an automation tool drives them or a paired client
7613
+ watches them, so the phone app can scrub back through them. It keeps the last
7614
+ 15 minutes of footage per device under $STIM_HOME/workspaces/<id>/recordings/,
7615
+ on this Mac only, and serves it only to paired clients. \`recording.enabled\`
7616
+ turns it off:
7617
+
7618
+ stim settings set recording.enabled false --scope machine
7619
+
7620
+ It is a boolean, true by default, set at machine, repo or workspace scope; the
7621
+ workspace layer wins, then repo, then machine. STIM_RECORDING (true, false, 1
7622
+ or 0) overrides every layer; stim-server reads it from its own environment when
7623
+ it starts. Turning it off takes effect at once: \`stim settings set\` deletes
7624
+ the recordings of every workspace it turns off, and stim-server stops recording
7625
+ them within seconds, as soon as \`stim status\` shows recording.enabled false
7626
+ for them. Turning it back on records from then on. See \`guide cleanup\` for
7627
+ how recordings are removed otherwise.
7628
+
6224
7629
  STIM NEEDS NO PROJECT CHANGES TO RUN
6225
7630
  Nothing above is required to use Stim. The performance caches that used to
6226
7631
  be setup steps are supplied by Stim on the command lines it composes itself:
@@ -6516,7 +7921,7 @@ An https:// URL on a plain HTTP server fails with ERR_SSL_PROTOCOL_ERROR, and
6516
7921
  an http:// URL on an HTTPS server with ERR_EMPTY_RESPONSE; the remedy line
6517
7922
  prints the settings command that switches the scheme. web.viewport phone
6518
7923
  gives the page a 390x844 touch screen at 3x instead of the 1280x800 desktop
6519
- window. See stim guide settings.
7924
+ page. See stim guide settings.
6520
7925
 
6521
7926
  MONOREPO RECIPE: A VITE PACKAGE WITH A BASE PATH AND HTTPS
6522
7927
 
@@ -6576,11 +7981,28 @@ platform "web":
6576
7981
  page document is error whatever its
6577
7982
  status; another request's network failure or HTTP 5xx is
6578
7983
  error, a 4xx warn, a canceled request debug; browser messages such
6579
- as CSP violations; the browser's own lifecycle
7984
+ as CSP violations; the browser's own lifecycle; in-app
7985
+ route changes (history API or fragment) as web_route with
7986
+ url, at most four a second
7987
+ src agent input a DevTools client other than Stim's sent to the page
7988
+ while connected, as agent_action with driver (the tool),
7989
+ command click, type, press or scroll, msg (e.g. 'Clicked
7990
+ button "Save"', "Typed 12 characters into input#email"),
7991
+ deviceId (web.targetId) and details. Typed text is never
7992
+ recorded, only its length. Clicks, key presses, text input
7993
+ and wheel scrolls in the top frame are observed; pointer
7994
+ moves and iframes are not, and a typing, key or scroll burst
7995
+ is one record. Input from Stim Desktop's or the phone's Take
7996
+ over is not recorded, nor is other input within 3 seconds of
7997
+ it, nor input while no other client is connected. A person
7998
+ clicking a headed window while a tool is connected counts as
7999
+ that tool, and a script that disconnects within a few hundred
8000
+ milliseconds of its first input goes unrecorded
6580
8001
  Expo also prints web console calls on Metro ("Web LOG"), so they can appear
6581
8002
  twice: once from the page (client), once from Metro (metro, level info).
6582
8003
  Each load of the page's top-level document is a page-load marker: stim web,
6583
- stim reload, and a reload or navigation the page makes itself. logs --errors
8004
+ stim reload, and a reload or navigation the page makes itself. A reload's
8005
+ marker carries reload: true. logs --errors
6584
8006
  and the status error count report the page's records (platform web) from the
6585
8007
  latest load only, the way Chrome DevTools clears its console when the page
6586
8008
  navigates. A page load does not hide the native app's errors, and an ios or
@@ -6596,7 +8018,20 @@ them start their own browser:
6596
8018
  The port reaches only the Stim profile: Chrome refuses remote debugging on
6597
8019
  your default profile, and Stim never attaches to a browser it did not start.
6598
8020
  The port is managed like metro: stim ports lists it as web-cdp (managed),
6599
- and ports get, stop and release refuse it.
8021
+ and ports get, stop and release refuse it. While a tool is connected,
8022
+ status reports it as the browser's driver (web.activity, "driven by
8023
+ playwright"), the way it names agent-device on a simulator, and its clicks,
8024
+ typing and scrolls are agent actions: stim logs --source agent lists them,
8025
+ web.activity.recent["agent-action"] dates the newest, and Stim Desktop and
8026
+ the phone show them in the Web device's agent feed. web.targetId is the
8027
+ owned page; a tab another tool opens is not captured.
8028
+
8029
+ stim status also reports the document the page loaded last and how that
8030
+ load went: web.page { url, state: loading | loaded | failed, error?, route? }.
8031
+ route is the URL the page shows after an in-app route change since that
8032
+ load, and is absent when none moved it. A failed
8033
+ load (the dev server is down, a certificate error, a crash) shows on status's
8034
+ web line.
6600
8035
 
6601
8036
  OWNERSHIP AND CLEANUP
6602
8037
 
@@ -6605,6 +8040,8 @@ claim for its lifetime; Chrome is the claim's child. Stim signals the
6605
8040
  supervisor or Chrome only after verifying the process identity it recorded.
6606
8041
  stim stop closes Chrome and keeps the profile, so cookies and
6607
8042
  storage survive the next stim web
8043
+ stim stop --slot web closes only Chrome, keeping the profile, Metro and
8044
+ every device
6608
8045
  stim worktree remove closes Chrome and deletes the profile
6609
8046
  stim gc --delete does the same for a workspace whose path is gone,
6610
8047
  and forgets ledger entries of deleted profiles
@@ -6614,10 +8051,385 @@ which would otherwise block the next launch. status reports
6614
8051
  browser-unverified when an identity cannot be proven; follow stim guide
6615
8052
  errors teardown.
6616
8053
 
8054
+ STIM DESKTOP
8055
+
8056
+ Stim Desktop shows the owned Chrome as a Web tile next to the simulators,
8057
+ with live frames of the page read from its DevTools endpoint, the page's URL,
8058
+ a "Page failed to load" pill and "Driven by" when a tool is attached. Take
8059
+ over sends clicks, scrolls and keys to the page. The tile's buttons open the
8060
+ URL in your own browser (never the Stim profile), run stim reload web, and
8061
+ close the browser with stim stop --slot web.
8062
+
8063
+ THE PHONE APP
8064
+
8065
+ The phone app shows the page as a Web tile in the devices grid and the
8066
+ workspace screen, and opens it in the device viewer as H.264 video through
8067
+ stim-server. With Control on, taps click, drags scroll and the keyboard types;
8068
+ Back goes back in the page's history. Reload in the workspace menu runs stim
8069
+ reload web. While stim-server recorded the page (\`guide settings\`, device
8070
+ recordings), the viewer can scrub back through it, with the agent's actions
8071
+ as markers.
8072
+
6617
8073
  LIMITS
6618
8074
 
6619
- Chrome and Chromium only. No Stim Desktop or phone viewer yet. The owned page
6620
- is one tab: a page the app opens in a new window is not captured.`
8075
+ Chrome and Chromium only. The owned page is one tab: a page the app opens in
8076
+ a new window is not captured.`
8077
+ },
8078
+ macos: {
8079
+ summary: "Swift Package macOS Debug apps: owned bundle, logs, local window viewing, and --remote on another Mac",
8080
+ body: () => `MACOS: SWIFT PACKAGE DEVELOPMENT PROTOTYPE
8081
+
8082
+ If Stim is not installed globally, replace stim with npx stim.
8083
+
8084
+ Run from the directory containing Package.swift. This prototype builds one
8085
+ explicit executable product in Debug, creates an isolated development app,
8086
+ and launches it. It does not use package scripts, Metro, simulators, Xcode
8087
+ projects, release builds, provisioning, or store distribution.
8088
+
8089
+ Set macos.product and macos.infoPlist explicitly in .stim.json. Optional
8090
+ macos.arguments is an array of arguments passed directly to the executable:
8091
+
8092
+ { "macos": { "product": "MyApp", "infoPlist": "Support/Info-Development.plist" } }
8093
+
8094
+ The plist must contain CFBundleIdentifier and CFBundleExecutable matching the
8095
+ product. Use a development plist without shared URL schemes or an update feed.
8096
+ Stim gives the copied bundle a workspace-specific identifier. SwiftPM resource
8097
+ bundles and frameworks in the reported build directory are copied into it;
8098
+ macos.resources maps destinations under Contents/Resources to file or directory
8099
+ sources relative to the Swift Package directory, for example:
8100
+
8101
+ { "macos": { "resources": { "AppIcon.icns": "Support/AppIcon-Dev.icns" },
8102
+ "assetCatalog": "Support/Assets.xcassets" } }
8103
+
8104
+ macos.assetCatalog selects an optional .xcassets directory compiled by fixed
8105
+ xcrun actool arguments. Copies and the compiled Assets.car are added before
8106
+ ad hoc signing. If actool does not emit Assets.car, staging refuses; set
8107
+ LSMinimumSystemVersion in the plist when Xcode requires a deployment target.
8108
+ Destinations are non-empty relative paths without empty, . or
8109
+ .. segments, at most 1024 characters, and cannot overlap another resource,
8110
+ Assets.car when a catalog is set, or a SwiftPM resource bundle. At most 256
8111
+ resources are allowed. Sources must exist as regular files or directories;
8112
+ their realpaths must stay inside the git repository root, or the Swift Package
8113
+ directory when there is no git root, and a source directory cannot contain
8114
+ symbolic links. Sources can use ../ to reach other files
8115
+ inside that repository. Custom packaging scripts are not run and extra
8116
+ executables, including Stim Desktop's sim-fold helper, are not built.
8117
+
8118
+ stim macos # fixed SwiftPM Debug build, then launch
8119
+ stim macos --json # one launch record; progress goes to stderr
8120
+ stim status --json # environments[].macos and build state
8121
+ stim logs --source build
8122
+ stim logs --errors
8123
+ stim stop # stop this workspace's owned app and supervisor
8124
+
8125
+ Each invocation stops its previous owned app, rebuilds and launches. SwiftPM
8126
+ keeps incremental outputs in the workspace's runtime directory under STIM_HOME.
8127
+ Local stim macos starts the app in the background without activating it or
8128
+ changing focus: it sets STIM_BACKGROUND_LAUNCH=1 in the app's environment, which
8129
+ Stim Desktop honors. An app that activates itself at launch still takes focus.
8130
+ Hosted launches (macos --remote) do not set it.
8131
+ macOS artifacts are not cached. This prototype has no --plan, --slot, or reload command.
8132
+ A failed build records its error and compiler output without launching an app.
8133
+ Runtime stdout and stderr become client records; build output becomes build
8134
+ records, all with platform "macos". Unexpected app exits are error records.
8135
+ Stim runs the app with NSUnbufferedIO=YES, so Swift print output arrives per
8136
+ line instead of when the app exits.
8137
+
8138
+ BUILD OFFLOAD
8139
+
8140
+ --build-machine <auto|local|name> overrides STIM_OFFLOAD_MACHINE and the
8141
+ machine setting offload.machine (default auto). local builds here; a name
8142
+ requires the matching configured and paired build machine, ignoring offload.mode
8143
+ and this Mac's capacity. Any failure is STIM_OFFLOAD_REFUSED without a local xcodebuild, Gradle or
8144
+ SwiftPM compile or another machine. Invalid, unlisted or unpaired selections
8145
+ refuse before stopping the app or changing its build record. See stim guide errors STIM_OFFLOAD_REFUSED.
8146
+
8147
+ With offload.machine auto, offload.mode places these SwiftPM Debug builds:
8148
+ auto builds here while this Mac has capacity, force uses an approved build
8149
+ machine when one accepts, and off always builds here. Configure offload.machines
8150
+ and approve build access as described in stim guide settings. The worker needs
8151
+ matching Stim, CPU architecture, Xcode and macOS SDK, plus network access to
8152
+ fetch package dependencies the first time. It keeps SwiftPM dependencies in a
8153
+ per-client cache and incremental outputs per repository. It runs fixed swift
8154
+ build commands without JavaScript installs, prebuild or pods. It receives only
8155
+ the files git lists (tracked and untracked, not ignored). Resource and catalog
8156
+ sources must be among those files. The client sends resolved sources relative
8157
+ to the repository root; the worker resolves them inside its checkout.
8158
+
8159
+ The client validates the development plist and resource entries before asking a machine. It verifies
8160
+ the returned archive's sha256, bundle identifier, executable, declared resources
8161
+ and ad hoc signature
8162
+ before replacing the owned bundle, then launches locally as usual. With auto, every offload
8163
+ failure falls back to the local build, including force. Failed staging preserves
8164
+ the previous bundle. No failed artifact is promoted or cached. The build record
8165
+ is unchanged by invalid, unlisted or unpaired setup refusals. It
8166
+ carries buildMachine (selected) and builtOn (here or the machine, absent until
8167
+ a build runs), and errorCode on a typed failure. It carries offloadedTo only for a remote build, and offloadFallback when an offload
8168
+ attempt falls back here. Placement and failure reasons are in build logs.
8169
+
8170
+ OWNERSHIP AND LOCAL VIEWING
8171
+
8172
+ The supervisor owns a process-identity claim with the app as its child. Stop
8173
+ signals only verified recorded identities. An unverifiable or malformed owner
8174
+ refuses with STIM_MACOS_OWNER_UNVERIFIED; other apps remain untouched.
8175
+
8176
+ Stim Desktop shows this app in its workspace, with Build and run, Open app (local
8177
+ apps only) and Stop, and a live preview that updates itself while the app runs. Capture and Open app verify the recorded executable, bundle
8178
+ identifier, PID and process start time. Open app rechecks that the captured window
8179
+ is still the app's front window, or raises the pinned one, then activates that
8180
+ owned app for normal native input.
8181
+ The preview follows the app's front standard window, its main window with any
8182
+ attached sheet, as the app opens, switches, closes or resizes windows. It never
8183
+ captures another process's windows, menus or the desktop. Without Device Control
8184
+ and Data Access permission (Accessibility on macOS 26 and earlier) Stim cannot
8185
+ tell which window is in front, so the preview shows only an app whose one window
8186
+ contains the others. The viewer never captures its own process recursively.
8187
+ The captured view is read-only; background mouse/keyboard relay is not included.
8188
+
8189
+ Capture requires existing Screen & System Audio Recording permission (Screen
8190
+ Recording on macOS 14); Open app also requires Device Control and Data Access
8191
+ permission (Accessibility on macOS 26 and earlier). The first native viewer opening
8192
+ shows one Desktop setup screen for both permissions, named for this Mac's macOS
8193
+ version, with statuses, Request permissions, Settings and Check again. Settings
8194
+ opens the matching System Settings > Privacy & Security pane. You approve normal macOS requests; Stim never resets or grants access
8195
+ automatically. Permissions on the app card reopens setup. Builds never prompt.
8196
+ If unavailable, use the normal app window and
8197
+ read the workspace logs.
8198
+
8199
+ PHONE VIEWING AND CONTROL
8200
+
8201
+ A paired phone shows native app and build state in the workspace and home list.
8202
+ Tap the app tile to view the app's front window through stim-server. The server
8203
+ requires read access and its macos-window feature; it rechecks the recorded PID,
8204
+ process start time, executable and bundle before capture and on every frame.
8205
+ It never captures the desktop or another application, and offers no replay.
8206
+ The view follows the app's front standard window like the Desktop preview. A
8207
+ server advertising macos-windows also sends a macos-windows event on the frames
8208
+ subscription with the captured window and the app's windows (id, title and frame
8209
+ in points), after subscribing and whenever they change. Viewing starts only with
8210
+ an open window; after the app closes its last one the view reports a delay until
8211
+ another opens.
8212
+
8213
+ WINDOWS
8214
+
8215
+ The view follows the app's front window by default. A controller can pin it to
8216
+ one of the app's windows instead: with a server advertising macos-window-select,
8217
+ input.window { session, window: <id from macos-windows> } brings that window to
8218
+ the front of the app, and capture and input stay on it while another window
8219
+ comes forward on the Mac. input.window { session, window: null } follows the
8220
+ front window again. The pin also ends when its window closes or minimizes, or
8221
+ when the Control session that set it ends for any reason, including release,
8222
+ takeover, disconnect and the five-minute idle timeout, since viewers without
8223
+ Control cannot unpin; macos-windows reports pinned true while it holds. Choosing
8224
+ a window while the app shows a modal dialog, or one that just closed, is dropped
8225
+ without ending Control. The phone's Control toolbar and Stim Desktop's app card and
8226
+ hosted viewer offer the same choice as a Window menu: Follow front window, or a
8227
+ window by title. Stim Desktop's local preview pins without Control because it
8228
+ only raises the window among the app's own windows. An agent that drives the app
8229
+ through the stim-server protocol sends the same input.window.
8230
+
8231
+ A server advertising macos-window-control also supports the phone's Control
8232
+ mode on a control pairing. Tap/click and drag act on the displayed window;
8233
+ Scroll mode turns a drag into pixel scrolling. The main toolbar offers Keyboard
8234
+ and Scroll. Keyboard types printable ASCII and attaches an extra-key strip with
8235
+ Tab, Escape, Backspace, arrows and labeled Select all, Undo, Save, Copy, Paste,
8236
+ Cut and Find shortcuts. Shift, Control, Option and Command apply to the next
8237
+ supported key, then clear; dismissing the keyboard clears them too.
8238
+
8239
+ A newly built phone client with Keyboard Controller is required; this native
8240
+ change cannot be delivered to an older client by a JavaScript update. Servers
8241
+ advertising macos-keyboard-extended accept modified a-z and 0-9 one key at a
8242
+ time. Older servers retain fixed shortcuts and navigation but cannot receive
8243
+ other modified letters or digits. Multi-character modified input and symbols
8244
+ are not supported; ordinary typing keeps using input.text.
8245
+
8246
+ Shortcuts for a-z and 0-9 use the key that types the character in the Mac's
8247
+ current keyboard layout, using the Command layer when Command is held. Dvorak
8248
+ and Dvorak-QWERTY Command are supported. On Russian and similar layouts
8249
+ (Cyrillic, Greek, Hebrew, Arabic), Latin-letter shortcuts work with Command;
8250
+ Control-only or Option-only letter shortcuts are refused. With Control, the
8251
+ layout's Control table must also yield the requested character or, for letters,
8252
+ its C0 control character; otherwise the shortcut is refused. The Mac's selected
8253
+ input source is read on each key, so switching layouts takes effect on
8254
+ the next key. Letters and digits available only with Shift or Option, or through
8255
+ a dead key (for example digits on AZERTY), are refused with a reason naming the
8256
+ layout, and Control ends. Stim does not add modifiers to reach those characters.
8257
+ Ordinary typing and navigation do not depend on the layout. Symbols such as
8258
+ comma remain unsupported key names.
8259
+ Control posts input to the owned process without activating it or raising its
8260
+ window; only choosing a window to pin raises it among the app's windows. Only when the captured window is not the app's key window (or its
8261
+ attached sheet) does Stim activate the app to deliver input, waiting up to one
8262
+ second for focus. The helper then sends a controlActivated notice, which stim-server
8263
+ logs.
8264
+ Clicks on views that reject the first mouse, such as custom views and SwiftUI
8265
+ onTapGesture regions, do not land while the app is in the background. Use
8266
+ Desktop's Open app to bring the app to the front for those views.
8267
+ Control holds one exclusive server session per owned app, ends on disconnect,
8268
+ revocation, takeover or five minutes without input, and does not take a CLI
8269
+ simulator/device lock. Each action rechecks the exact owned process and that the
8270
+ captured window is still the app's front standard window, or the pinned one. The
8271
+ app's other windows are allowed; input goes to the captured window, and a sheet attached to it takes
8272
+ focus and pointer input. Input that arrives while the view moves to another
8273
+ window is dropped and Control continues. A modal dialog window refuses input. A
8274
+ sheet larger than the captured window is not supported. Existing Device
8275
+ Control and Data Access permission (Accessibility on macOS 26 and earlier) is
8276
+ required. The phone and server never request or reset it. A refusal ends Control
8277
+ with its reason while viewing and logs remain usable. Older servers remain
8278
+ view-only.
8279
+ Native Control uses dynamically resolved private CoreGraphics input SPI in the
8280
+ server helper, outside the phone and Mac App Store app binaries. A missing symbol
8281
+ or incompatible macOS version refuses Control while viewing and logs remain usable.
8282
+
8283
+ The existing capture host needs Screen & System Audio Recording permission (Screen
8284
+ Recording on macOS 14). If capture is denied,
8285
+ open Permissions in Stim Desktop on the host Mac, approve its normal OS requests
8286
+ and reconnect the phone viewer. The Desktop host presents setup on the first
8287
+ native viewer opening, including one initiated by the phone. A server started
8288
+ outside Desktop uses that launching host's permissions; granting this copy of Stim
8289
+ may not apply to it. The server never requests or resets permissions. Status and logs still work. Tap the build card for
8290
+ SwiftPM output or the logs card for native runtime stdout and stderr. Metro is not
8291
+ used.
8292
+
8293
+ ON ANOTHER MAC
8294
+
8295
+ stim macos --remote <machine> builds the Debug app on this Mac and runs it on an
8296
+ approved hosting Mac over the tailnet, without SSH. The machine must be listed
8297
+ in hosting.machines and approved: stim doctor --fix asks it, a person on that
8298
+ Mac runs stim-server devices grant <id> --device-host, and stim doctor then
8299
+ records the approval. Stim connects only to the machine's pinned tailnet node.
8300
+ A refusal or an unreachable host fails the command; it never launches here
8301
+ instead.
8302
+
8303
+ The --remote value is a hosting Mac name from hosting.machines. macOS refuses
8304
+ eas and proxy because it has neither backend. It also refuses auto until
8305
+ automatic placement ships. These reserved names are case-insensitive and
8306
+ trimmed; they refuse with STIM_BAD_ARG before state access or a connection.
8307
+
8308
+ Logs: the host records the hosted app's stdout, stderr and exit like a local run.
8309
+ stim logs, including --errors, --json and --follow, asks the host over the same
8310
+ approved connection, copies the records it has not copied yet into the
8311
+ workspace's logs/macos-host.ndjson and prints them with the local records.
8312
+ stim stop copies the last ones, including the exit record, before it forgets the
8313
+ placement, so the logs stay readable after stop. A host that cannot answer, because
8314
+ it is unreachable or runs a stim-server that predates this, costs one stderr
8315
+ warning (after up to 10 seconds of connecting); stdout still carries the records already copied, so logs --json stays
8316
+ valid NDJSON. The host's unified log is not collected: os.Logger output that is
8317
+ not written to stderr does not appear.
8318
+
8319
+ Phones and Stim Desktop view and control the hosted app through this Mac's
8320
+ stim-server, which relays to the host. The phone sends clicks, scrolls,
8321
+ text and keys; Stim Desktop's Control sends clicks, drags and typed text.
8322
+
8323
+ stim macos --remote mini # build here, deliver, launch on mini
8324
+ stim macos --remote mini --json # { platform, product, launchId, build, host }
8325
+ stim status --json # environments[].macos.state and macos.host
8326
+ stim stop # stop the session on mini and confirm it
8327
+ stim worktree remove <path> # also stops it
8328
+
8329
+ The copy keeps the plist's own CFBundleIdentifier. The host runs it as
8330
+ <id>.hosted<slot> from a fixed slot pool, so the bundle id stays the same
8331
+ across rebuilds; whether macOS keeps the permissions the hosted app asks for
8332
+ itself also depends on how the host signs it. Running the
8333
+ command again reuses the session and delivers a new copy. macos.arguments are
8334
+ passed to the hosted launch as plain arguments, without environment injection:
8335
+ at most 32 arguments, 1024 characters each and 8192 characters total. Empty
8336
+ strings are allowed; NUL, CR and LF are refused. Values are stored in the host's
8337
+ app receipt and visible in process listings, so do not put secrets there. Older
8338
+ hosts ignore them and Stim warns to update stim-server on the host.
8339
+ status --json reports the host's applied arguments under
8340
+ environments[].macos.arguments.
8341
+ A workspace has one macOS app: a local stim macos
8342
+ refuses while it runs on a host, and --remote with a different machine refuses
8343
+ until stim stop. When the host cannot be reached or does not confirm the stop,
8344
+ the placement stays recorded; restore the connection and run stim stop again.
8345
+
8346
+ Viewing and controlling the hosted app from a phone needs Screen & System Audio
8347
+ Recording and Device Control and Data Access (Accessibility on macOS 26 and
8348
+ earlier), granted once on the host to the app that runs stim-server, not to the
8349
+ hosted app. stim-server service install runs the server under the signed Stim
8350
+ Host app (dev.stim.host, installed in ~/Applications) and shows
8351
+ macOS's own requests on that Mac's screen; a person there approves them.
8352
+ stim-server service status shows the grants, and stim doctor here reports an
8353
+ approved host that lacks them. A server started by Stim Desktop uses Desktop's
8354
+ grants.
8355
+
8356
+ Hosted delivery carries the staged bundle with declared resources and compiled assets.
8357
+ When offload built it on the host's own tailnet node, the host copies the files
8358
+ from the build it kept for this Mac instead of receiving them again, admitting
8359
+ only bytes that match the bundle's digests here. That needs both the build and
8360
+ the device-host approval for this Mac. Files the host cannot take, an older
8361
+ host, or a build fetched more than 10 minutes earlier are uploaded as before.
8362
+
8363
+ status --json reports macos.host { machine, session, appSlot, appAttempt,
8364
+ bundleId, agent }. For hosted placements only, status asks the host for the
8365
+ session state with about a 3 s timeout per connection and request and a 10 s
8366
+ cache. It reports stopped when the host says the session stopped, for example
8367
+ after a stim-server restart there, and unverified when the host is unreachable
8368
+ or cannot confirm. Run stim macos --remote <machine> to launch a stopped app
8369
+ again, or stim stop to clear or reconcile the placement. The host field stays
8370
+ in status --json for cleanup.
8371
+ While a placement is recorded, gc treats the workspace as in use. agent says
8372
+ how a coding agent drives the app: { driver: "none", setting:
8373
+ "hosting.agentDriver" } until the hosting Mac's owner sets that setting there,
8374
+ or { driver: "agent-device", remoteConfig, command }. remoteConfig is a mode
8375
+ 0600 file in the workspace directory that holds the credential; run the command
8376
+ it names and never print the file. stop, worktree remove and gc first run
8377
+ agent-device close and disconnect for the connection that agent-device reports
8378
+ as connected to that remote config (the default or active session), then delete
8379
+ the file, so the next hosted workspace needs no manual disconnect. Any other
8380
+ connection, including one under another session name, stays untouched, and a
8381
+ failure or a missing agent-device is reported without blocking the stop. When
8382
+ the hosted session ends, stim-server removes its agent-device session
8383
+ directories under its own state directory. Start with agent-device open
8384
+ <bundleId> --remote-config <path>, using macos.host.bundleId; the lease allows
8385
+ open, close, snapshot, wait, find, get, is, click, fill, press, type, focus,
8386
+ scroll, screenshot and batch on that app only. The agent-device on this Mac must
8387
+ know the macos-app lease backend. A request outside that command list, or a
8388
+ method the relay does not forward, is refused with an agent-device UNAUTHORIZED
8389
+ error whose details.reason is STIM_AGENT_REQUEST_REFUSED and whose message names
8390
+ the refused command or method; do not retry it.
8391
+
8392
+ Agents that built, launched and drove test copies on another Mac with an SSH
8393
+ script such as mini-desktop.sh use these instead:
8394
+
8395
+ script command stim
8396
+ build stim macos --remote <mac>
8397
+ launch the same command; the host launches it under the
8398
+ session's own home
8399
+ shot <out.png> agent-device screenshot --remote-config <path>
8400
+ click, rclick, key, type agent-device click, press or type --remote-config <path>
8401
+ quit stim stop (verified stop; the host frees the slot
8402
+ and removes the app's defaults)
8403
+ clean stim worktree remove
8404
+ 3 concurrent slots concurrency.maxDevices on the host; one bundle id
8405
+ slot per session
8406
+
8407
+ The agent-device rows work once the host's agent field names agent-device.
8408
+
8409
+ DESKTOP DOGFOOD
8410
+
8411
+ This repository's apps/desktop/.stim.json selects the full StimDesktop app,
8412
+ its development plist, asset catalog, fonts, branding and licences. Its sim-fold
8413
+ helper is not built. It monitors the regular Stim home alongside the installed
8414
+ app, with a workspace-specific bundle identifier and separate preferences.
8415
+ Its launch arguments disable automatic cleanup and notification alerts in this development copy.
8416
+ Use Window > SwiftUI Playground for in-memory production screen fixtures.
8417
+
8418
+ cd apps/desktop
8419
+ stim macos
8420
+ stim logs --source build
8421
+ stim stop
8422
+
8423
+ When using an unreleased CLI from this repository, build the packages first and
8424
+ set STIM_BIN to the absolute packages/stim-cli/dist/cli.mjs path before running
8425
+ that executable's macos command. The development app inherits this CLI override;
8426
+ the installed app's CLI preference and running server remain unchanged.
8427
+
8428
+ Ask an agent: "In my Swift Package app, configure the executable product and a
8429
+ development Info.plist for stim macos. Build and show its owned window in Stim
8430
+ Desktop, verify a source edit and readable failed-build logs, then stop only
8431
+ this workspace's app. Do not change permissions or use custom build scripts."
8432
+ `
6621
8433
  }
6622
8434
  };
6623
8435
  //#endregion
@@ -6699,7 +8511,7 @@ function renderIndex(version) {
6699
8511
  return lines.join("\n");
6700
8512
  }
6701
8513
  function guideCommand(program, version, status = (running) => guideStatus({
6702
- projectRoot: findProjectRoot(process.cwd()),
8514
+ projectRoot: findProjectRoot$1(process.cwd()),
6703
8515
  running
6704
8516
  })) {
6705
8517
  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) => {