stim 1.14.0 → 1.16.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 (152) hide show
  1. package/README.md +2 -0
  2. package/dist/activity-BxwdD81S.mjs +609 -0
  3. package/dist/agent-device-usage-output-Dpih1Tfa.mjs +411 -0
  4. package/dist/{android-d6rGq1pA.mjs → android-B_NumVyQ.mjs} +169 -81
  5. package/dist/{android-LbPFRWVL.mjs → android-Ift4kVvt.mjs} +517 -413
  6. package/dist/android-cas-8U4DPqeR.mjs +167 -0
  7. package/dist/android-cas-compiler.mjs +2 -2
  8. package/dist/{android-XpvCrV7q.mjs → android-r2rBL8kV.mjs} +2 -2
  9. package/dist/{app-install-BpgSgkJj.mjs → app-install-CKWdbXIX.mjs} +128 -151
  10. package/dist/attempt-Bk2ouQVL.mjs +39 -0
  11. package/dist/budget-DjrURfQn.mjs +2700 -0
  12. package/dist/{build-plan-Dcr-3ie0.mjs → build-plan-DjeSb2Xl.mjs} +192 -653
  13. package/dist/build-slots-rUk77xqh.mjs +132 -0
  14. package/dist/{cache-manifest-C68QQ0ua.mjs → cache-manifest-DnUHBJaU.mjs} +2 -2
  15. package/dist/cache-manifest.mjs +1 -1
  16. package/dist/{chrome-BPCiC3ZX.mjs → chrome-BrcVWQyD.mjs} +1 -1
  17. package/dist/cli-BkYZ_6hy.mjs +51 -0
  18. package/dist/cli.mjs +6 -42
  19. package/dist/client-BTY686R6.mjs +1368 -0
  20. package/dist/collector-run.mjs +9 -195
  21. package/dist/{command-output-DVcpw6eE.mjs → command-output-DnJMdEY-.mjs} +1 -1
  22. package/dist/{config-lvc7d3C2.mjs → config-D0DbpZtM.mjs} +28 -29
  23. package/dist/{created-devices-CllH_8eb.mjs → created-devices-D5owjN4C.mjs} +2 -2
  24. package/dist/{dependency-state-DsT7aKeo.mjs → dependency-state-B6cMXZab.mjs} +2 -2
  25. package/dist/{deps-D9IcKqXS.mjs → deps-urbmfXig.mjs} +3 -3
  26. package/dist/{detached-entry-DJHoca49.mjs → detached-entry-CiTEgMR1.mjs} +1 -1
  27. package/dist/dev-client-CroAIjjj.mjs +43 -0
  28. package/dist/{device-CjuuAJjQ.mjs → device-DZQjF7H5.mjs} +8 -7
  29. package/dist/device-capacity-BHwxNBlu.mjs +136 -0
  30. package/dist/device-host-worker.d.mts +1 -0
  31. package/dist/device-host-worker.mjs +964 -0
  32. package/dist/device-ios-BadmeL2V.mjs +381 -0
  33. package/dist/{device-lease-5c3WrVRm.mjs → device-lease-D-hLqx1b.mjs} +43 -36
  34. package/dist/{device-pool-BiyDyPP-.mjs → device-lease-run-CqjShqWW.mjs} +6 -126
  35. package/dist/device-pool-CUaEWMMm.mjs +125 -0
  36. package/dist/{device-remote-D87zEzaq.mjs → device-remote-D5WVWqOq.mjs} +69 -163
  37. package/dist/{doctor-x9pnU9fq.mjs → doctor-Cimf-gGB.mjs} +241 -103
  38. package/dist/{error-diagnostics-C5HzcwCW.mjs → error-diagnostics-B9L4Ae0x.mjs} +15 -18
  39. package/dist/{exec-DK8KaVHK.mjs → exec-CaErVv7t.mjs} +5 -1
  40. package/dist/{gc-ko6mUAYD.mjs → gc-Bqy4bjEu.mjs} +599 -108
  41. package/dist/{android-cas-iZK3DOCr.mjs → gradle-BsO1MdsH.mjs} +8 -294
  42. package/dist/{guide-6R0VNFqI.mjs → guide-B_uyKTxW.mjs} +2511 -239
  43. package/dist/{guide-status-BZrFPIA_.mjs → guide-status-CjEZMAD4.mjs} +3 -3
  44. package/dist/hosted-client-a-WmqE2L.mjs +197 -0
  45. package/dist/hosted-ios-CIWelIqf.mjs +220 -0
  46. package/dist/hosted-logs-D5g041Q3.mjs +187 -0
  47. package/dist/hosted-macos-BSZyIb3y.mjs +135 -0
  48. package/dist/idle-DYVymG92.mjs +1099 -0
  49. package/dist/idle-shutdown-CTsDnAde.mjs +122 -0
  50. package/dist/in-use-DvBw0Kxo.mjs +130 -0
  51. package/dist/{ios-BwDPrnYQ.mjs → ios-BaInn3sW.mjs} +201 -10
  52. package/dist/ios-Cr1h5gnO.mjs +189 -0
  53. package/dist/{ios-P1f5nm2n.mjs → ios-aq3QoGI3.mjs} +1159 -633
  54. package/dist/{ios-device-BiOsSVzd.mjs → ios-device-Ee7sDXfp.mjs} +1 -1
  55. package/dist/{ios-device-DGglU3E_.mjs → ios-device-FfCN_C8p.mjs} +12 -7
  56. package/dist/ios-state-DS5UpYTH.mjs +48 -0
  57. package/dist/{launch-verify-Deg4tB3Z.mjs → launch-verify-2DBRizjo.mjs} +5 -5
  58. package/dist/logs-CHEVK8zD.mjs +371 -0
  59. package/dist/machines-BIBUdhpj.mjs +265 -0
  60. package/dist/macos-CSYEAFmF.mjs +473 -0
  61. package/dist/macos-run.d.mts +5 -0
  62. package/dist/macos-run.mjs +2 -0
  63. package/dist/maintenance-run.d.mts +4 -0
  64. package/dist/maintenance-run.mjs +254 -0
  65. package/dist/{metro-CcEo8Fl8.mjs → metro-BeK1ZPz1.mjs} +20 -120
  66. package/dist/metro-gateway-BJ-rxQAB.mjs +84 -0
  67. package/dist/{named-ports-Blg8IxOh.mjs → named-ports-Bk_YXEmE.mjs} +6 -59
  68. package/dist/native-run-NaBrRFtQ.mjs +147 -0
  69. package/dist/{native-runtime-DjBHRg5m.mjs → native-runtime-DBzRnEKS.mjs} +9 -5
  70. package/dist/{ndjson-CDY9IkVC.mjs → ndjson-PclOIwVY.mjs} +2 -2
  71. package/dist/offload-worker.d.mts +122 -0
  72. package/dist/offload-worker.mjs +606 -0
  73. package/dist/{ownership-DyYDF5ww.mjs → ownership-DiA16AsN.mjs} +3 -2
  74. package/dist/ownership-hCkPzGmr.mjs +571 -0
  75. package/dist/{page-BHUzTDFt.mjs → page-QF_Ke10s.mjs} +7 -2
  76. package/dist/{ports-BguS83ex.mjs → ports-R2k4Knz3.mjs} +3 -3
  77. package/dist/prebuild-D2fsca_Z.mjs +213 -0
  78. package/dist/preview-DhVbvPAZ.mjs +318 -0
  79. package/dist/process-identity-BmRm1qV0.mjs +12 -0
  80. package/dist/{project-CWlvEaeN.mjs → project-Du91Ilvc.mjs} +74 -19
  81. package/dist/pull-request-pxnDCQzn.mjs +322 -0
  82. package/dist/pull-requests.d.mts +120 -0
  83. package/dist/pull-requests.mjs +2 -0
  84. package/dist/recordings-BIXOzVnr.mjs +37 -0
  85. package/dist/{reload-BiniwsSZ.mjs → reload-rM9jV1fz.mjs} +109 -24
  86. package/dist/{remote-cache-CcR6o6lI.mjs → remote-cache-C8-V13XN.mjs} +5 -5
  87. package/dist/run-f4vI6EnK.mjs +119 -0
  88. package/dist/{server-bare-DGfgyJ5J.mjs → server-bare-CpPGmnmO.mjs} +33 -15
  89. package/dist/server-expo-CVqO2c35.mjs +2 -0
  90. package/dist/{server-expo-BVuwQ65v.mjs → server-expo-MC0rHlOC.mjs} +8 -12
  91. package/dist/settings-BBpMcDbD.mjs +25 -0
  92. package/dist/{settings-B-fsC-Om.mjs → settings-ZkDzkzvB.mjs} +22 -10
  93. package/dist/settings-oTSkkNrM.mjs +1439 -0
  94. package/dist/settings.schema.json +845 -97
  95. package/dist/{simslim-DrDN4WuV.mjs → simslim-B8-7AoTI.mjs} +8 -132
  96. package/dist/{slot-launch-D5t8EiMf.mjs → slot-launch-B8VrHxn1.mjs} +11 -8
  97. package/dist/{spawn-claims-Drw_o-Fb.mjs → spawn-claims-D0XfSe8u.mjs} +1 -1
  98. package/dist/spawn-entry-jw19m8Nl.mjs +17 -0
  99. package/dist/stage-CzaiwFsI.mjs +143 -0
  100. package/dist/start-CX8Ig-U2.mjs +2 -0
  101. package/dist/{start-s4RXp1wy.mjs → start-ClDfwXa4.mjs} +116 -65
  102. package/dist/state-3DwSQIBK.mjs +41 -0
  103. package/dist/state-B4PQFXMW.d.mts +12 -0
  104. package/dist/{state-Dk9RVXI7.mjs → state-C1jB7Q8q.mjs} +70 -8
  105. package/dist/{state-DHqvsbqC.mjs → state-DcW6vOd_.mjs} +7 -4
  106. package/dist/stats-CEZ_i5je.mjs +77 -0
  107. package/dist/{status-BgAuxESg.mjs → status-BuvBFeII.mjs} +118 -20
  108. package/dist/{status-BsPAnfOI.mjs → status-Cs7VLg9D.mjs} +542 -283
  109. package/dist/status-DlKvVJBE.mjs +49 -0
  110. package/dist/stdout-reader-C8yjre1x.mjs +23 -0
  111. package/dist/stim-desktop-Cnz8CPnl.mjs +63 -0
  112. package/dist/stim-footprint.swift +32 -0
  113. package/dist/{stim-installations-CINXy_su.mjs → stim-installations-QMEI32v1.mjs} +1 -1
  114. package/dist/stop-B0Yxtmiy.mjs +48 -0
  115. package/dist/{stop-BzreOWJp.mjs → stop-Bffozsac.mjs} +83 -16
  116. package/dist/{stop-BWIF_6BK.mjs → stop-Dt_dM16A.mjs} +2 -2
  117. package/dist/stop-cause-CidxIqup.mjs +97 -0
  118. package/dist/supervisor-run.d.mts +43 -11
  119. package/dist/supervisor-run.mjs +363 -24
  120. package/dist/support-CAP8qDUF.mjs +316 -0
  121. package/dist/support-D4aYJniO.mjs +201 -0
  122. package/dist/teardown-CL6vDs2Y.mjs +727 -0
  123. package/dist/{xcode-Cp5KvK9x.mjs → toolchain-DkUflduf.mjs} +364 -14
  124. package/dist/trigger-Bemu8ytw.mjs +65 -0
  125. package/dist/trigger-CXts6qt1.mjs +2 -0
  126. package/dist/warm-progress-CpGv3Wb6.mjs +732 -0
  127. package/dist/{web-BsQs1WlO.mjs → web-DWUaMp8P.mjs} +37 -21
  128. package/dist/web-run.mjs +310 -13
  129. package/dist/workspace-process-lock-BL7Q0HCO.mjs +57 -0
  130. package/dist/workspace-state-B8-aJ1_5.mjs +371 -0
  131. package/dist/workspaces-BV4pdHFz.mjs +306 -0
  132. package/dist/{worktree-UrQWXSeb.mjs → worktree-K-xj9Db0.mjs} +625 -256
  133. package/dist/worktree-jnG8Iph5.mjs +2 -0
  134. package/package.json +10 -5
  135. package/shim/bundle-response.cjs +28 -12
  136. package/dist/activity-CYVDcrt3.mjs +0 -356
  137. package/dist/budget-CGDkhvv_.mjs +0 -1745
  138. package/dist/build-progress-DxKpwnaS.mjs +0 -247
  139. package/dist/idle-DXtx3RbX.mjs +0 -191
  140. package/dist/in-use-DlqBDKqY.mjs +0 -249
  141. package/dist/logs-Civ4gbBH.mjs +0 -486
  142. package/dist/merge-state-ytFQCeh4.mjs +0 -262
  143. package/dist/server-expo-DIHDhvGg.mjs +0 -2
  144. package/dist/settings-D61iNOsB.mjs +0 -629
  145. package/dist/start-Cetfvy8l.mjs +0 -2
  146. package/dist/stats-CFpFGlJV.mjs +0 -62
  147. package/dist/stats-CX1Z8Tca.mjs +0 -368
  148. package/dist/stim-desktop-CXt8-Z3J.mjs +0 -40
  149. package/dist/stop-cause-hjGBOZTG.mjs +0 -1231
  150. package/dist/worktree-BVrHsYZG.mjs +0 -752
  151. package/dist/worktree-CHWWvm5v.mjs +0 -2
  152. package/dist/{metro-store-DzeWkfqW.mjs → metro-store-CDDZHtPl.mjs} +1 -1
@@ -1,8 +1,8 @@
1
- import { a as findProjectRoot } from "./project-CWlvEaeN.mjs";
2
- import { t as ANDROID_AVD_CONFIG_HELP } from "./settings-D61iNOsB.mjs";
3
- import { t as RECENT_LAUNCH_MS } from "./status-BgAuxESg.mjs";
4
- import { t as guideStatus } from "./guide-status-BZrFPIA_.mjs";
5
- import { SETTINGS_SCHEMA_URL } from "@stim-cli/core/state";
1
+ import { t as ANDROID_AVD_CONFIG_HELP } from "./settings-oTSkkNrM.mjs";
2
+ import { o as findProjectRoot$1 } from "./project-Du91Ilvc.mjs";
3
+ import { t as RECENT_LAUNCH_MS } from "./status-BuvBFeII.mjs";
4
+ import { t as guideStatus } from "./guide-status-CjEZMAD4.mjs";
5
+ import { SETTINGS, SETTINGS_SCHEMA_URL } from "@stim-cli/core/state";
6
6
  import chalk from "chalk";
7
7
  //#endregion
8
8
  //#region src/guide/index.ts
@@ -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
@@ -39,14 +43,28 @@ reach multiple devices. A launch counts a bundle delivery only when it can
39
43
  attribute it to its own device; read guide lifecycle options for the rule.
40
44
  Use stop --slot <name> for one slot, or plain stop for the whole workspace.
41
45
 
46
+ HOSTED IOS
47
+
48
+ For ios --remote <machine>, read guide lifecycle hosted-ios. Stop before switching
49
+ between a local simulator and a hosting Mac. Hosted Debug requires the local
50
+ Metro supervisor; restart a missing or older one with stim stop; stim start.
51
+ It never falls back here; stop reconciles the recorded host before changing placement.
52
+ For hosted iOS, use agent-device <command> --remote-config <file> from ios.host.agent
53
+ in stim status --json. Open the installed app first; never use the host UDID locally.
54
+ See guide lifecycle hosted-ios for the agent policy and setup.
55
+
56
+ When driving a device yourself with agent-device, set AGENT_DEVICE_STATE_DIR
57
+ from agentDevice.stateDir in stim ios|android|macos --json or status --json.
58
+ Its sessions and logs are then cleaned with the workspace. Do not set
59
+ AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH or AGENT_DEVICE_CLAIMS_DIR; the runner
60
+ build cache and device claims stay shared. Read stim guide logs for examples.
61
+
42
62
  NORMAL WORKFLOW
43
63
 
44
- Work in the current checkout by default. When the task needs another branch or
45
- an isolated environment, take the worktree workflow: create a linked worktree
46
- with Git and warm its ignored state. If a harness already created this linked
47
- worktree, run stim worktree warm here instead of creating another one. It
48
- copies missing ignored paths from the source checkout, including eligible .env
49
- and local configuration files. It preserves the branch, tracked files, and
64
+ Create a linked worktree with Git and warm its ignored state. If a harness
65
+ already created this linked worktree, run stim worktree warm here instead of
66
+ creating another one. It copies missing ignored paths from the source checkout,
67
+ including eligible .env and local configuration files. It preserves the branch, tracked files, and
50
68
  every existing destination entry; existing ignored directories are skipped
51
69
  whole, not filled in. Add --refresh to fast-forward the source checkout and
52
70
  install what moved there before the copy; it refuses a source checkout with local
@@ -76,7 +94,8 @@ other apps or other workspaces' devices.
76
94
 
77
95
  stim doctor --platform ios # or: --platform android
78
96
 
79
- # Skip Git creation if the harness already created this linked worktree.
97
+ # Worktree workflow. Skip Git creation if the harness already created this
98
+ # linked worktree.
80
99
  git worktree add -b <branch> <worktree-path> HEAD
81
100
  cd <worktree-path>
82
101
  stim worktree warm
@@ -84,6 +103,12 @@ other apps or other workspaces' devices.
84
103
  stim start
85
104
  stim ios # or: stim android, or: stim web
86
105
 
106
+ When Stim Desktop is installed, worktree warm, start, ios, android and web
107
+ print "Open in Stim Desktop: <link>" on stderr (links.desktop in JSON). The
108
+ first time one prints it in a workspace, tell the user once: "You can watch
109
+ this workspace in Stim Desktop: <link>". Do not repeat it after later
110
+ commands. When no command prints it, do not mention Stim Desktop.
111
+
87
112
  For the web target, stim web opens the page in a Stim-owned headless Chrome
88
113
  and captures its console and errors in stim logs. It never starts a web
89
114
  server: start the dev server first, then run stim web. For Expo web the dev
@@ -105,6 +130,12 @@ To learn whether the next build is a cache hit and how long it should take,
105
130
  without building, run stim ios --plan (or stim android --plan); see stim guide
106
131
  lifecycle builds.
107
132
 
133
+ Agents never set STIM_ACCESS_TICKET, run stim-server setup, edit offload.*
134
+ or hosting.* for the user, or approve requests. Read stim guide settings for
135
+ Desktop access tickets and person-owned approval.
136
+ Read stim guide settings to select a build machine. Keep a requested named
137
+ placement when recovering STIM_OFFLOAD_REFUSED: report its reason and remedy.
138
+
108
139
  Read stim guide lifecycle concurrency when a build waits on another workspace
109
140
  or a build call times out. A native build can outlive a shell timeout; if the
110
141
  tool call timed out, retry the same command and follow its printed remedy if
@@ -128,6 +159,8 @@ waiting times out.
128
159
 
129
160
  RULES DURING THE LOOP
130
161
 
162
+ - Agents never run stim-server setup and never approve access requests.
163
+ A person on the worker approves builds and device hosting (guide settings).
131
164
  - Run Stim from the app directory: the one whose package.json depends on
132
165
  react-native or expo. Anywhere else -- a monorepo root, a tools package --
133
166
  start, ios and android refuse with STIM_NO_PROJECT naming that package.json,
@@ -197,10 +230,11 @@ the stim-<label> (<model> <runtime>) name. Never point Stim at a user-created
197
230
  emulator or simulator.
198
231
 
199
232
  worktree remove parks the workspace's simulator or emulator for later adoption.
200
- Before owned-device teardown, Stim best-effort closes local agent-device sessions
233
+ During owned-device teardown, Stim best-effort closes local agent-device sessions
201
234
  on that exact device; failures warn and teardown continues (guide cleanup).
202
- A parked device is Stim-owned: never delete one by hand. gc --delete clears verified
203
- entries and keeps failures; see guide lifecycle pool. First launch on a
235
+ A parked device is Stim-owned: never delete or erase one by hand. gc --delete clears
236
+ verified entries and keeps failures; gc --cache parked --delete erases them and keeps
237
+ them parked; see guide lifecycle pool. First launch on a
204
238
  physical iPhone can need the one-time taps named by the remedy.
205
239
 
206
240
  stim android --device [serial] and stim ios --device [udid] install on a
@@ -298,8 +332,11 @@ Read the matching guide before acting in these situations:
298
332
  | Parallel iOS, simulator stall, or memory pressure | stim guide lifecycle simslim |
299
333
  | Android boot timeout | stim guide errors STIM_NO_DEVICE |
300
334
  | gc or orphaned resources | stim guide cleanup gc |
335
+ | watchman or Gradle daemon memory | stim guide cleanup memory |
301
336
  | worktree remove refusal or --force | stim guide errors remove |
302
337
  | Cleanup failure or unverified cleanup ownership | stim guide errors teardown |
338
+ | Swift Package macOS development | stim guide macos |
339
+ | macOS app on another Mac (stim macos --remote) | stim guide macos |
303
340
  | Unfamiliar state or JSON field | stim guide facts payloads |
304
341
  | Refusal without a code | stim guide errors |
305
342
 
@@ -328,12 +365,14 @@ FULL TOPIC LIST
328
365
  stim guide lifecycle release # Release configurations and ...Release variants
329
366
  stim guide facts # the --json payloads
330
367
  stim guide facts devmenu # the Expo dev menu or Tools button over the app
368
+ stim guide macos # Swift Package Debug apps, logs, local preview, --remote on another Mac
331
369
  stim guide web # stim web: owned Chrome, page logs, launched, teardown
332
370
  stim guide ports # named ports for web and API servers
333
371
  stim guide metro # supervisor, custom Metro, tunnels, and remote devices
334
372
  stim guide logs # filters, record shape, and capture limits
335
373
  stim guide cleanup # what reclaims a device, and what deletes
336
374
  stim guide cleanup collector # an unproven collector pid; why the app on a phone closed
375
+ stim guide cleanup memory # watchman and Gradle daemon memory; gc --cache watchman
337
376
  stim guide settings # configuration files and supported keys`
338
377
  },
339
378
  facts: {
@@ -352,6 +391,15 @@ stdout for \`--json\`. Every other line goes to stderr, so it is always safe
352
391
  to pipe. \`logs --json\` is the one exception: it is NDJSON, one record per
353
392
  line by design (see \`guide logs\`), not this single-payload contract.
354
393
 
394
+ status adds archived (newest first, bounded by archive.maxCount) and
395
+ archivedUsage { count, bytes, byKind: { logs, recordings, agentActions, record } }.
396
+ Each archive carries id, projectRoot, project, workspace, worktree facts,
397
+ removedAt, removedBy, lastUsedAt, builds, agents, bytes, expires and version.
398
+ replacedBy is the live environment path when its canonical root matches.
399
+ Older producers may omit both fields.
400
+ Plain status prints Archived: <count> workspace(s), <size> when count is positive;
401
+ status --watch reuses archive reads until archive changes and emits no duplicates.
402
+
355
403
  status's unprovisionedWorktrees lists the linked worktrees with no Stim
356
404
  environment in every repository with a registered environment, plus the
357
405
  repository status runs from. A worktree counts as having an
@@ -360,9 +408,13 @@ environment when one is registered at it or inside it. Each entry is
360
408
  directory of a bare repository. status lists worktrees from git's own worktree
361
409
  records, and on macOS runs no git in a worktree with no environment under a
362
410
  protected folder such as ~/Documents, so listing it raises no privacy prompt.
363
- \`worktree warm\` does not create an environment; \`start\`, \`ios\`,
364
- \`android\` and \`doctor\` register it. An environment's worktree is the
365
- linked worktree it is registered at or inside.
411
+ \`worktree warm\` registers the app it prepares, before it copies: the
412
+ nearest React Native or Expo app above where it runs when that is below the
413
+ worktree root; else the one app the source checkout has registered, at the
414
+ same path in this worktree; else the worktree root when it is the app. A warm
415
+ that finds none registers nothing and says so on stderr, and \`start\`, \`ios\`,
416
+ \`android\` and \`doctor\` register the app later. An environment's worktree is the linked worktree it is
417
+ registered at or inside.
366
418
 
367
419
  Each worktree entry, in unprovisionedWorktrees and in an environment's
368
420
  worktree, carries git:
@@ -392,6 +444,46 @@ change, and for at most 60 s, so a file edit, creation or deletion that is
392
444
  not staged can take up to a minute to show. Plain status prints "git: 2 changed, 1 untracked, ahead 3" under
393
445
  each environment, and the same after each worktree with no environment.
394
446
 
447
+ The paired phone's Work sheet can open changed and untracked files when the
448
+ server advertises workspace-diff. File lists and selected patches load on
449
+ demand, not through status. The read-only preview is capped at 200 files per
450
+ list and 256 KiB per patch response; binary and unavailable files are labeled. Active Git clean/process filters
451
+ (including Git LFS) refuse without running those commands.
452
+
453
+ A worktree entry also carries pullRequest, the GitHub pull request of its
454
+ branch and HEAD, once \`status --watch\` has looked it up:
455
+
456
+ pullRequest { number, url, title, state, checks, reviewDecision,
457
+ checkedAt }, null when GitHub has none for the branch and
458
+ HEAD, absent when never looked up: gh missing or signed
459
+ out, no GitHub remote, or status could not read the
460
+ worktree's git. A later gh failure keeps the last answer.
461
+ state "open", "draft", "merged" or "closed"
462
+ checks { passing, failing, pending } counts of the head commit's
463
+ check runs and commit statuses, or null when it has none
464
+ reviewDecision "approved", "changes-requested", "review-required" or null
465
+ checkedAt when Stim last asked GitHub
466
+
467
+ A worktree entry with git also carries gitChip, the git chip Stim Desktop and
468
+ the phone app show for it:
469
+
470
+ gitChip { parts, ci }
471
+ parts in order: { kind: "arrows", ahead, behind } when either is above
472
+ 0; { kind: "changed", count } of changed plus untracked entries;
473
+ { kind: "merged", into } when mergedInto is set and the pull
474
+ request is not merged; { kind: "no-upstream" } when the branch is
475
+ not merged and upstream is null
476
+ ci "failing" when a check fails, else "pending" when one is pending,
477
+ else "passing" when one passes; null without a pull request or
478
+ checks
479
+
480
+ \`status --watch\` asks GitHub off its refresh path, through the same gh api
481
+ graphql lookup gc uses, one call per repository, for a worktree whose lookup
482
+ is over 5 minutes old or whose branch or HEAD moved. It runs git in the
483
+ repository, never in the worktree. It caches each answer
484
+ under $STIM_HOME/pull-requests, which one-shot status only reads, and never
485
+ warns when gh is unavailable.
486
+
395
487
  status's remoteDevices lists each environment's recorded EAS Simulator
396
488
  session. The session is billable while it runs, and it makes the environment
397
489
  live. status reads only local records; it does not ask EAS whether the session
@@ -409,10 +501,37 @@ is still running.
409
501
  Stim created the session, or null
410
502
 
411
503
  Plain status prints one "remote <platform>: EAS session <id> billable" line
412
- per session, with the preview URL.`,
504
+ per session, with the preview URL.
505
+
506
+ status's physicalDevices lists each physical phone or tablet the environment
507
+ holds an unexpired lease on, from \`ios --device\`, \`android --device\` or
508
+ \`device lock\`, in every slot. A run lease ends with its run, so a phone
509
+ stays listed after the run only while \`device lock\` holds it. A
510
+ simulator or emulator lease is not listed. The field is absent when the
511
+ workspace leases no physical device.
512
+
513
+ physicalDevices [{ platform, slot, id, name, model, owned, physical,
514
+ connection, lease }]
515
+ id the UDID or adb serial
516
+ name the device's own name, else the name the lease recorded
517
+ model devicectl's marketing name ("iPhone 12 Pro"), null when
518
+ unread, or the Android model the lease recorded
519
+ owned always false: Stim uses a physical device, never owns it
520
+ physical always true
521
+ connection "connected" devicectl can reach the phone, or adb lists
522
+ the serial as device
523
+ "disconnected" the tool answered without it
524
+ "unknown" the tool could not be read in time
525
+ status reuses one devicectl or adb listing for 30 s
526
+ under \`status --watch\`
527
+ lease { holder, kind, grantedAt, expiresAt }: holder is the
528
+ workspace path, kind "declared" (device lock) or "run"
529
+
530
+ Plain status prints "ios: Old iPhone (physical, iPhone 12 Pro) connected --
531
+ leased until <time>" for each one.`,
413
532
  sections: {
414
533
  payloads: {
415
- summary: "every field of the start, ios, android, web and reload payloads, the error contract, the device rules",
534
+ summary: "every field of the start, ios, android, macos, web and reload payloads, the error contract, the device rules",
416
535
  body: () => ` stim start --json
417
536
 
418
537
  port the Metro port RESERVED for this workspace
@@ -420,7 +539,21 @@ per session, with the preview URL.`,
420
539
  already answering that Stim did not start
421
540
  mode "bare-inproc" | "expo-child" | null (see \`guide metro\`)
422
541
  logsDir where the NDJSON timeline is written
542
+ agentDevice { stateDir }: absolute workspace agent-device state path;
543
+ set AGENT_DEVICE_STATE_DIR to it (see guide logs)
423
544
  alreadyRunning true when nothing needed starting
545
+ links { desktop }: a stim-desktop://workspace?path=<workspace>
546
+ link that shows this workspace in Stim Desktop. Absent when
547
+ no app on this Mac opens stim-desktop links. Plain output
548
+ prints it on stderr as "Open in Stim Desktop: <link>"; so
549
+ does worktree warm, which has no JSON payload
550
+
551
+ stim macos --json
552
+
553
+ platform "macos"
554
+ product, launchId, build, host? the launch record (see guide macos)
555
+ agentDevice { stateDir }: absolute workspace agent-device state path
556
+ for local or hosted launches with a local workspace
424
557
 
425
558
  stim ios --json
426
559
 
@@ -471,7 +604,12 @@ per session, with the preview URL.`,
471
604
  scheme the explicit shared Xcode scheme selected by --scheme;
472
605
  absent for automatic selection; not the app URL scheme
473
606
  cacheKey the shared-build-cache key derived from it (the
474
- configuration is part of it: -release-sim vs -debug-sim)
607
+ configuration is part of it: -release-sim vs
608
+ -debug-sim-arm64). A single-architecture simulator build
609
+ ends in its architecture (-arm64 or -x86-64): a local Debug
610
+ build compiles this Mac's, a --remote build the remote
611
+ host's. A local non-Debug simulator build compiles every
612
+ architecture the project lists and has no suffix
475
613
  With --eas-profile, fingerprint is computed by EAS CLI using the selected
476
614
  profile/environment, and cacheKey identifies the EAS project and build ID
477
615
  separately from local native builds. cacheHit is "remote" for the EAS
@@ -490,6 +628,15 @@ per session, with the preview URL.`,
490
628
  the device; it is a page, not a deep link.
491
629
  cacheSkipped true only when --no-build-cache was passed: "nothing was
492
630
  looked up", which is a different fact from "nothing was found"
631
+ buildMachine selected auto, local or machine name, including cache hits
632
+ builtOn here or machine name when a build ran; absent otherwise
633
+ offloadedTo only on an app a build machine compiled: its
634
+ offload.machines entry (see \`guide settings\`). cacheHit is
635
+ false for it. The Android payload carries it too
636
+ offloadFallback only on an app compiled here after the run considered
637
+ offloading it: why it built here -- one reason per
638
+ machine when none took the build, or why the offload
639
+ stopped. The Android payload carries it too
493
640
  compilationCache
494
641
  Xcode compilation-cache activity for a compiled iOS app:
495
642
  { status: "reported", hits, cacheableTasks, hitRatePercent }
@@ -561,6 +708,7 @@ per session, with the preview URL.`,
561
708
  launch with no process id is "unverified", and
562
709
  \`stim logs --errors\` has the device log that says why
563
710
  logs { dir }
711
+ agentDevice { stateDir }: absolute workspace agent-device state path
564
712
  durationMs wall time for the whole run
565
713
 
566
714
  stim android --json
@@ -588,7 +736,8 @@ per session, with the preview URL.`,
588
736
  hw.device.name. Null where systemImage is null, and on an
589
737
  AVD of the old generic profile, which has no hw.device.name
590
738
  fingerprint / cacheKey / cacheHit / cacheSkipped / waitedForBuild /
591
- appPath / installSkipped / launched
739
+ appPath / installSkipped / launched / buildMachine / builtOn /
740
+ offloadedTo / offloadFallback
592
741
  as above -- cacheKey keys on the VARIANT here
593
742
  (<fingerprint>-productionrelease-sim). A Debug artifact for
594
743
  a proven target ABI also ends in that ABI
@@ -630,8 +779,10 @@ per session, with the preview URL.`,
630
779
  through ccache -- ccache absent from PATH, a project that
631
780
  sets its own CMake compiler launcher, or a Gradle run whose
632
781
  native work was all up to date. None of the three is an
633
- error, and this field is separate from cacheHit
782
+ error, and this field is separate from cacheHit. On an
783
+ offloaded APK it is the build machine's ccache
634
784
  logs the workspace log directory
785
+ agentDevice { stateDir }: absolute workspace agent-device state path
635
786
  durationMs wall time for the whole run
636
787
  reclaimed present only when the run was over its disk or memory
637
788
  budget and reclaimed first (\`start\`, \`ios\` and
@@ -647,6 +798,8 @@ per session, with the preview URL.`,
647
798
  { started: true, reason }
648
799
  reason is "not running" or "stopped (idle)" when the
649
800
  supervisor had stopped it after metro.idleStopMinutes
801
+ links as in start, with &platform= and, for a named slot, &slot=,
802
+ so Stim Desktop can focus that device
650
803
 
651
804
  stim reload [ios|android|web] --json
652
805
 
@@ -659,7 +812,8 @@ per session, with the preview URL.`,
659
812
  deviceId the exact owned simulator UDID or emulator serial targeted;
660
813
  for web, the owned Chrome's DevTools endpoint
661
814
  deviceName the owned simulator or AVD name; for web, the Chrome version
662
- appId the live bundle id or Android package; for web, the page URL
815
+ appId the live bundle id or Android package; for web, the URL the
816
+ owned page was on when Stim sent the reload
663
817
  metroPort the workspace's verified Metro port; for web, the reserved
664
818
  Metro port or null
665
819
  strategy how the reload was addressed.
@@ -677,6 +831,9 @@ per session, with the preview URL.`,
677
831
  broadcast. Greater than 1 means several devices are running
678
832
  this app on that Metro and the request addressed all of
679
833
  them, not only deviceId. Completion is not observed
834
+ reverseRestored the Android serials whose adb reverse for metroPort was
835
+ missing and that Stim re-applied before reloading; [] when
836
+ none was, and always [] for iOS and web
680
837
 
681
838
  stim web --json
682
839
 
@@ -692,6 +849,8 @@ per session, with the preview URL.`,
692
849
  profile the Stim-owned Chrome user data directory
693
850
  cdpEndpoint http://127.0.0.1:<port>, the reserved DevTools endpoint, or
694
851
  null when not running
852
+ targetId the DevTools target id of the owned page, or null when not
853
+ running
695
854
  reused true when the running Chrome navigated again instead of
696
855
  starting: same --headed, viewport and certificate options
697
856
  launched true | "bundling" | "unverified" (see \`guide web\`)
@@ -699,6 +858,7 @@ per session, with the preview URL.`,
699
858
  that does not use {port:metro}
700
859
  logs { dir }: web.ndjson holds the page records
701
860
  durationMs wall time of the run
861
+ links as in start, with &platform=web
702
862
 
703
863
  stim doctor --json
704
864
 
@@ -719,8 +879,42 @@ per session, with the preview URL.`,
719
879
  null when off; plan lists what the next start, ios or
720
880
  android would reclaim, in the \`reclaimed\` shape without
721
881
  freedMb, and is empty while under budget
882
+ deviceHosts one { machine, state, dnsName?, deviceId?, requestedAt?, host? }
883
+ per hosting.machines entry. state is "approved", "pending",
884
+ "not-asked", "revoked", "node-changed", "not-on-tailnet",
885
+ "tailscale-off", "unreachable", "invalid",
886
+ "credentials-unavailable" or "busy". Tokens stay private.
887
+ Only --fix asks for access or forgets removed names.
888
+ An approved hosting machine does not imply build approval
889
+ or change ios/android placement; see \`guide settings\`.
890
+ host is { name, screenRecording, accessibility }, present
891
+ only for approved machines whose worker reports it.
892
+ buildMachines one { machine, state, dnsName?, deviceId?, requestedAt?,
893
+ offloadable?, reasons?, problems?, capacity?, host? } per
894
+ offload.machines entry; state is "approved", "pending", "not-asked",
895
+ "revoked" (revoked, or the request lapsed), "node-changed",
896
+ "not-on-tailnet", "tailscale-off", "unreachable" or
897
+ "invalid". host has the same shape and presence rule as in
898
+ deviceHosts. An approved machine also carries offloadable,
899
+ true when it would take this app's builds now (iOS
900
+ simulator unless --platform android, Android emulator when
901
+ --platform android or the app has android/ or uses Expo,
902
+ plus macOS SwiftPM when Package.swift exists and no platform
903
+ is selected on a Mac), and reasons, each reason it would not,
904
+ prefixed "iOS: ", "Android: " or "macOS: " when only one platform
905
+ has it, with a finding
906
+ per reason (code build-machine-<reason>: unreachable,
907
+ checkout, stim-build, arch, xcode, simulator-sdk, macos-sdk,
908
+ cocoapods, bundler, runtime, jdk, android-sdk, ndk,
909
+ build-tools, compile-sdk, disk or busy). problems lists the same
910
+ reasons as { code, reason } with that code. capacity
911
+ is the machine's offer: { running, max, diskFreeBytes,
912
+ minDiskFreeBytes, cpus?, loadPerCore?, builds?, maxBuilds?,
913
+ maxLoadPerCore?, declined? }; an older stim-server omits the
914
+ optional fields
722
915
  findings the diagnostic findings; a lower resolved Stim is a
723
916
  costs-time finding with a PATH or installation remedy
917
+ offload-candidate is a note after 3+ successful local cold builds in 7 days average over 3 min, with no offload.machines and an online tailnet Mac; open Stim Desktop Settings > Build machines > Add
724
918
 
725
919
  ON FAILURE
726
920
  \`start\`, \`ios\`, \`android\` and \`web\` all print the error contract instead,
@@ -741,7 +935,8 @@ ON FAILURE
741
935
  lease, Ctrl-C with no build tool running) exits 130 with no payload.
742
936
 
743
937
  \`stop --json\` prints { root, ok, supervisor, collectors, metro, device,
744
- port, metroTunnel, releasedLeases }. When it cannot end the \`ios\` or
938
+ port, metroTunnel, releasedLeases, macos? }. macos is { status: stopped | failed,
939
+ reason? } when a macOS app is recorded. When it cannot end the \`ios\` or
745
940
  \`android\` run holding the workspace, it prints the error contract with
746
941
  root and ok: false instead, plus device.remote when it ended a recorded EAS
747
942
  session first, and exits 1:
@@ -787,16 +982,50 @@ RULES
787
982
  that is the URL the launcher hands to the check; on the
788
983
  outer deep link it does nothing there. Android reads it on
789
984
  either.
790
- ON A SIMULATOR, before a local dev-client openurl, Stim
791
- preapproves CoreSimulatorBridge for exactly the installed
792
- bundle id and discovered scheme on its owned simulator. That
793
- suppresses iOS's first-launch confirmation;
794
- unrelated schemes remain unapproved. It also writes
985
+ ON AN OWNED SIMULATOR, Stim preapproves CoreSimulatorBridge
986
+ for the installed bundle id and every URL scheme its Info.plist
987
+ declares, except system and third-party SDK schemes, also for
988
+ apps without expo-dev-client. The "Open in <App>?" alert can
989
+ still appear for an unapproved scheme, on a physical device or
990
+ user-created simulator, or from another opener such as Safari.
991
+ After \`simctl openurl\` or \`agent-device open <url>\`, expect
992
+ it for such schemes; take a snapshot and tap Open.
993
+ Approvals persist until erasure; Stim records them and writes
994
+ only missing ones. Parking drops the record, so adoption
995
+ rewrites them once.
996
+ THE DEV MENU AND ITS BUTTON stay off a simulator two ways,
997
+ which the onboarding flag does NOT cover. Stim's own
998
+ \`simctl launch --initialUrl\` ends in
999
+ \`-EXDevMenuShowsAtLaunch 0 -EXDevMenuShowFloatingActionButton 0\`
1000
+ as trailing argv, the same mechanism the phone launch uses:
1001
+ NSUserDefaults reads the argument domain ahead of the
1002
+ persisted one, so Stim's own launch comes up on the app, not
1003
+ the menu, without writing to the simulator. Stim also writes
795
1004
  EXDevMenuShowsAtLaunch=false and
796
- EXDevMenuShowFloatingActionButton=false, which the flag does
797
- NOT cover, and those together are what keep the menu and its
798
- button off a simulator entirely, so device automation opens
799
- on the app. The
1005
+ EXDevMenuShowFloatingActionButton=false to the app's
1006
+ persisted defaults, but only on a real install -- one that
1007
+ is not skipped as byte-identical -- or when its own launch
1008
+ falls back to \`simctl openurl\` (no console capture in flight),
1009
+ which carries no launch arguments at all and so writes them
1010
+ itself before opening the URL. Adoption clears the app's
1011
+ data by replacing its defaults with those two keys (one
1012
+ \`defaults import\`), so an install that adoption then skips
1013
+ as byte-identical keeps them too. Those persisted values
1014
+ cover launches Stim did not make: a home-screen tap, an
1015
+ agent relaunch, or a crash restart. The \`__expo_\` deep-link
1016
+ params below last one process and do not persist.
1017
+ When the project's installed expo-dev-launcher (found
1018
+ through expo-dev-client, no network) is 58.0.0 or later
1019
+ (expo/expo#49651), Stim's own deep link also carries
1020
+ \`disableFab=1&disableAutoLaunch=1\` on the outer link and
1021
+ on the project url, plus
1022
+ \`__expo_disable_fab=1&__expo_disable_auto_launch=1\` on the
1023
+ project url, which is the only url \`simctl launch
1024
+ --initialUrl\` hands the launcher and the one 58.0.7 reads
1025
+ the \`__expo_\` spellings from, for that launch only. So
1026
+ device automation opens on the app.
1027
+ The \`install dev client prepared (...)\` line appears
1028
+ only when one of those simulator writes ran. The
800
1029
  unverified warning therefore leads with the picker, then
801
1030
  prints the openurl
802
1031
  retry. ON LOCAL ANDROID the same deep link also carries the
@@ -808,8 +1037,8 @@ RULES
808
1037
  Remote Android opens only the URL, so that intent-extra
809
1038
  suppression does not apply there.
810
1039
  Every Stim deep link also carries an outer \`disableFab=1\`
811
- query parameter. Versions with expo/expo#49651 use that as a
812
- session-only override; earlier versions ignore it. Stim does
1040
+ query parameter. Versions with expo/expo#49651 read it and
1041
+ hide the button; earlier versions ignore it. Stim does
813
1042
  not rewrite expo-dev-menu's private SharedPreferences XML:
814
1043
  that internal file is not a supported API, and changing it
815
1044
  would persist over the user's own Tools-button setting. The
@@ -856,7 +1085,7 @@ RULES
856
1085
  writes and the Local Network grant both survive an
857
1086
  UPGRADE install. Android's intent extra prevents the menu's
858
1087
  automatic launch; versions with expo/expo#49651 also honor
859
- the session-only FAB flag in Stim's deep link.
1088
+ the outer FAB flag in Stim's deep link.
860
1089
  The phone's unverified remedy is also ROUTED, not a fixed
861
1090
  list. When this launch's device records carry the Local
862
1091
  Network path reason, the remedy leads with that evidence and
@@ -871,7 +1100,7 @@ RULES
871
1100
  },
872
1101
  gc: {
873
1102
  summary: "the gc report payload: mode, sections, reasons, failures, results, inventory, and the gc refusals",
874
- body: () => ` stim gc [--delete] [--older-than <days>] [--cache <name|all|workspaces>]
1103
+ body: () => ` stim gc [--delete] [--older-than <days>] [--cache <name|all|workspaces|recordings|parked>]
875
1104
  [--worktrees] [--idle <duration>] --json
876
1105
 
877
1106
  The report the text prints, as one payload. Show the user its sections
@@ -884,7 +1113,8 @@ RULES
884
1113
  idle the --idle duration in milliseconds, or null
885
1114
  cacheScope the --cache name, or null. When set, devices, project
886
1115
  entries and locks were not inspected and their sections
887
- are empty
1116
+ are empty, except that --cache parked fills
1117
+ parkedSimulators and parkedEmulators
888
1118
  olderThan the --older-than days, or null
889
1119
  worktreeSweep null without --worktrees; otherwise { olderThan, defaulted }:
890
1120
  the idle days a linked worktree needs, and whether that is
@@ -898,10 +1128,13 @@ RULES
898
1128
  run. status is "done", "kept" (left alone, detail says
899
1129
  why) or "failed" (detail says why and what to retry).
900
1130
  kind: device, parkedDevice, idleDevice, deviceRecord,
901
- workspaceOutputs, workspaceDirectory, project, buildLock,
902
- buildSlot, deviceLease, easSession, worktree, cache.
1131
+ workspaceOutputs, recording, workspaceDirectory, project,
1132
+ buildLock, buildSlot, deviceLease, easSession, worktree,
1133
+ cache.
903
1134
  label is a device, path or cache name; id is the UDID,
904
- AVD name or path behind it, or null
1135
+ AVD name or path behind it, or null. Under --cache
1136
+ parked, a "done" parkedDevice was erased and stays
1137
+ parked; otherwise it was deleted
905
1138
  inventory null except on a dry run without --cache or --idle. Report
906
1139
  only: gc never acts on it, even under --delete.
907
1140
  { devices, runtimes, systemImages, notices }
@@ -933,6 +1166,10 @@ RULES
933
1166
  timing out or an AVD or system image folder Stim
934
1167
  cannot read. A macOS privacy denial (EPERM) names the
935
1168
  Privacy & Security setting to grant
1169
+ maintenance top-level { mode, pressure, actions, blocked,
1170
+ skips, note, invalid? }, the next report-only maintenance plan
1171
+ from live pressure and cached sizes. note says when no
1172
+ pass has run yet. No du runs for this preview.
936
1173
  sections one array per report section, in the text order. Every key
937
1174
  is present, empty when there is nothing to report:
938
1175
  deadProjects { path }
@@ -955,10 +1192,12 @@ RULES
955
1192
  recent-activity worktree becomes removable,
956
1193
  else null. Without --worktrees, the source
957
1194
  checkout and roots outside git are left out
958
- parkedSimulators { udid, name, model, runtime, parkedAt, bytes,
959
- listed } with --older-than, only those parked
960
- at least that long
961
- parkedEmulators { name, systemImage, deviceProfile, parkedAt, bytes, listed }
1195
+ parkedSimulators { udid, name, model, runtime, parkedAt, app,
1196
+ bytes, listed } app is the bundle id recorded
1197
+ at park, else null; with --older-than, only
1198
+ those parked at least that long
1199
+ parkedEmulators { name, systemImage, deviceProfile, parkedAt, app,
1200
+ bytes, listed } app is the package name;
962
1201
  likewise
963
1202
  orphanedDevices { kind, id, name, bytes, directory }
964
1203
  unverifiedDevices { kind, id, name, command } stim-* devices this
@@ -976,6 +1215,10 @@ RULES
976
1215
  root, and browser profile paths whose
977
1216
  directory is gone; --delete forgets the
978
1217
  ledger entry only
1218
+ staleStatusCaches { kind: "disk-usage" | "pull-request", path,
1219
+ file, bytes } entries of $STIM_HOME/disk-usage
1220
+ and pull-requests whose path is gone from a
1221
+ mounted volume; --delete removes the file
979
1222
  idleDevices { kind, id, name, project, slot, lastActivityAt,
980
1223
  idleForMs, buildInProgress } booted owned
981
1224
  devices whose status activity is "idle";
@@ -999,14 +1242,52 @@ RULES
999
1242
  Metro, client and device logs over twice the
1000
1243
  8 MiB cap; willTrim marks the ones it would
1001
1244
  trim
1245
+ recordings { dir, projectRoot, bytes, deleteBytes,
1246
+ willDelete, withWorkspace, reason, detail }
1247
+ recordings/ of each workspace; deleteBytes is
1248
+ what --delete would remove; withWorkspace
1249
+ marks those of a gone workspace, removed with
1250
+ its workspace directory
1002
1251
  workspaceBuildOutputs { dir, projectRoot, bytes, idleDays, willClear,
1003
1252
  reason, detail } derived-data, gradle-build,
1004
- android-cas and cache-provider of each
1253
+ android-cas, cache-provider, macos/build,
1254
+ macos/<Product>.app and macos/staging-* of each
1005
1255
  workspace; willClear marks the ones --delete
1006
- would clear
1256
+ would clear; --cache workspaces omits the
1257
+ directories that --delete removes whole (dead
1258
+ projects and orphanedWorkspaces), and --cache
1259
+ all does not
1260
+ agentDevice <AgentDeviceUsage|null> report-only; workspace state
1261
+ goes only with its workspace;
1262
+ null with any --cache scope
1263
+ swiftpmCache <SwiftpmCacheUsage|null> report-only, never deleted;
1264
+ null with any --cache scope
1007
1265
  caches { name, dir, source, bytes, note, willEmpty,
1008
- emptySkipped } alive, not garbage; willEmpty
1009
- marks the ones --delete would empty
1266
+ emptySkipped, scopedEmpty } alive, not
1267
+ garbage; willEmpty marks the ones --delete
1268
+ would empty. scopedEmpty is { willEmpty,
1269
+ emptySkipped } and says what \`--delete
1270
+ --cache <selector>\` without --older-than
1271
+ would do to the cache; it is present without
1272
+ --cache and null with it, where willEmpty
1273
+ already says so. Caches that share a name
1274
+ carry their directory's last component, as
1275
+ "Metro transform cache: <store>"
1276
+ memory { kind: "watchman" | "gradleDaemon" |
1277
+ "kotlinDaemon", cacheKind: "watchman" |
1278
+ "gradle-daemons", pid, startedAt, bytes,
1279
+ measure: "footprint" | "rss", version,
1280
+ gradleHome, offloadClient, state: "idle" |
1281
+ "busy" | "unknown", reclaimable, reason,
1282
+ detail } without --cache, or with --cache
1283
+ watchman or gradle-daemons; reclaimable marks
1284
+ the ones gc --delete --cache <cacheKind>
1285
+ would stop (\`guide cleanup memory\`)
1286
+ watchmanRoots { path, stale: "missing" | "pruned-worktree" |
1287
+ null, subscriptions, triggers, removable,
1288
+ detail } removable marks the stale roots
1289
+ --cache watchman would watch-del
1290
+ memoryNotices { message } why the memory sweep was skipped
1010
1291
  bytes is null when the size is unknown: not measured, or the measurement
1011
1292
  failed. idleDays is null when the last use is unknown. listed is false for
1012
1293
  a parked device that is no longer on this machine, and null when its
@@ -1018,6 +1299,9 @@ RULES
1018
1299
  workspaceBuildOutputs unresolved | in-use | last-use-unknown |
1019
1300
  recently-used
1020
1301
  workspaceLogs unresolved | in-use | collector
1302
+ recordings unresolved | retained | recently-recorded
1303
+ memory in-use | busy | unknown | build-running |
1304
+ stim-server
1021
1305
  linkedWorktrees not-a-worktree | bare-repository |
1022
1306
  source-checkout-unknown | source-checkout |
1023
1307
  locked | in-use | status-unreadable | dirty |
@@ -1032,26 +1316,110 @@ RULES
1032
1316
 
1033
1317
  - a --cache name that no shared cache carries; the remedy names the
1034
1318
  caches on this machine
1035
- - --cache together with --worktrees or --idle`
1319
+ - --cache together with --worktrees or --idle
1320
+ - --cache watchman or gradle-daemons together with --older-than`
1036
1321
  },
1037
1322
  status: {
1038
- 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",
1323
+ 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",
1039
1324
  body: () => ` stim status --json
1040
1325
 
1326
+ logs { dir, errorsSinceMarker }, or null without a log directory
1327
+ agentDevice { stateDir }: absolute workspace agent-device state path on
1328
+ every environment, shared by its slots. Reporting it creates
1329
+ no directory; agent-device creates it when used.
1330
+
1331
+ Each environment carries phase, where the workspace is in its lifecycle:
1332
+
1333
+ phase "warming" stim worktree warm runs in it now
1334
+ "ready" its last warm succeeded and nothing has run there
1335
+ since: no start, ios, android, web or reload, for
1336
+ at most 2 hours
1337
+ "live" live is true: Metro, a device, Chrome or a remote
1338
+ session of it runs
1339
+ "idle" none of these
1340
+ phaseSince when the warm started ("warming") or finished ("ready"); null
1341
+ for "live" and "idle"
1342
+ warmStep "refresh" or "copy", the step a warming workspace is in;
1343
+ absent in other phases
1344
+ platforms ordered supported app platforms: ios, android, macos, web;
1345
+ always present, [] when none are detected. Expo uses its
1346
+ explicit platforms list, otherwise ios and android plus web
1347
+ with react-native-web. Bare apps use ios/*.xcodeproj or
1348
+ *.xcworkspace and android/ Gradle project files. web.url adds
1349
+ web to any app and overrides an Expo list; a bare app has no
1350
+ other route to web. macOS needs Package.swift, macos.product
1351
+ and macos.infoPlist. Detection reads config literals and
1352
+ files, never runs project scripts.
1353
+ recording { enabled }: whether stim-server may record the workspace's
1354
+ device screens for replay, from recording.enabled
1355
+
1356
+ Each environment also carries stage, where the workspace is as Stim
1357
+ Desktop and the phone app show it: { kind, since, platform, closedApps }.
1358
+ The first kind that applies wins:
1359
+
1360
+ kind "building" a build runs; platform names it
1361
+ "warming" phase is "warming" and nothing is live
1362
+ "ready" phase is "ready" and nothing is live
1363
+ "build-failed" the newest run of either platform failed;
1364
+ platform names it
1365
+ "running" live is true or a remote session runs
1366
+ "stopped" none of these
1367
+ since when it began: the build's start, phaseSince, the failed
1368
+ run's finishedAt (else startedAt), the supervisor's start or
1369
+ metro.lastStop.at; null when unknown
1370
+ platform "ios" or "android" for "building" and "build-failed", else null
1371
+ closedApps for "running", [{ platform, slot }] of the devices whose
1372
+ appPresence is "closed", the default slot first, then each
1373
+ slot in order, iOS before Android; empty otherwise
1374
+
1375
+ A warm records warming under its own ownership claim, so a warm that was
1376
+ killed or failed reads as idle, never as warming. Plain \`stim status\`
1377
+ marks the workspace [warming: <step>], [ready] or [idle].
1378
+
1041
1379
  Each environment carries issues, the things in that workspace that need
1042
- the user, and warnings, the same issues as text ("<slot>: " when not the
1043
- default slot, then "<message>; run \`<remedy>\`"):
1380
+ the user, and warnings, its error and warning issues as text ("<slot>: "
1381
+ when not the default slot, then "<message>; run \`<remedy>\`"):
1044
1382
 
1045
1383
  issues [{ code, severity, message, remedy, workspace, slot? }]
1046
1384
 
1047
- code port-not-ours something other than this workspace's
1048
- Metro answers its reserved port
1049
- sim-missing the recorded simulator no longer exists
1385
+ severity "error" Stim cannot verify or safely act on something; a
1386
+ person resolves it (stim guide errors teardown)
1387
+ "warning" something is broken now and needs someone to act:
1388
+ an app cannot reach Metro, a port is held, an
1389
+ expected device vanished
1390
+ "info" a fact the next normal stim command handles by
1391
+ itself, so it needs no action
1392
+ The apps show error and warning and hide info; plain stim
1393
+ status prints info as a dim note with no remedy; warnings
1394
+ excludes info. The info codes: sim-missing, sim-without-metro,
1395
+ avd-missing, and port-not-ours while no supervisor runs.
1396
+
1397
+ code port-not-ours another process answers Metro on this
1398
+ workspace's reserved port. "info" with
1399
+ remedy stim start when no supervisor
1400
+ runs: start reserves a free port.
1401
+ "warning" when this workspace's
1402
+ supervisor runs and cannot serve there
1403
+ (remedy stim stop), or its supervisor is
1404
+ unverified. The message names the holder:
1405
+ "Metro for workspace <name>" when it runs
1406
+ inside another workspace, else "a dev
1407
+ server in <last dirs>"; metro.heldBy
1408
+ carries its pid and cwd
1409
+ sim-missing the recorded simulator no longer exists;
1410
+ always "info", stim ios creates a new one
1050
1411
  sim-without-metro the simulator is booted and no Metro
1051
- serves the workspace
1412
+ serves the workspace; always "info"
1052
1413
  avd-serial-changed the owned emulator came back on another
1053
1414
  serial, so Metro forwarding is lost
1054
- avd-missing the recorded AVD no longer exists
1415
+ avd-missing the recorded AVD no longer exists; always
1416
+ "info", stim android creates a new one
1417
+ android-reverse-missing the owned emulator runs this
1418
+ workspace's debug app against its live
1419
+ Metro, but the adb reverse for that Metro
1420
+ port is gone, so the app cannot reach
1421
+ Metro; the remedy stim reload android
1422
+ restores it
1055
1423
  avd-not-detected adb does not see the owned emulator while
1056
1424
  the workspace expects it: it holds an
1057
1425
  unexpired lease on its serial, or it
@@ -1071,8 +1439,8 @@ RULES
1071
1439
  browser-orphaned the owned Chrome runs but its supervisor
1072
1440
  exited, so page logs are not captured;
1073
1441
  stim stop closes it
1074
- severity "error" when stop or start refuses until it is resolved, else
1075
- "warning"
1442
+ severity the rule above; "error" is also what makes stop or start
1443
+ refuse until it is resolved
1076
1444
  remedy a command to run from workspace, such as "stim android --slot
1077
1445
  fold"
1078
1446
  slot absent for the default slot
@@ -1085,20 +1453,45 @@ RULES
1085
1453
  An environment where \`stim web\` ran carries web, its Stim-owned Chrome:
1086
1454
 
1087
1455
  web { browser, version, running, pid, supervisorPid, url, headless,
1088
- viewport, profile, cdpEndpoint }
1456
+ viewport, profile, cdpEndpoint, targetId, page?, activity? }
1089
1457
 
1090
1458
  running the browser supervisor and Chrome are both verified live;
1091
- pid, supervisorPid and cdpEndpoint are null when false
1459
+ pid, supervisorPid, cdpEndpoint and targetId are null when
1460
+ false, and page and activity are absent
1461
+ url the page stim web opened; page.url is the document the page
1462
+ loaded last, and page.route the URL an in-app route change
1463
+ (history API or fragment) moved it to since
1092
1464
  cdpEndpoint http://127.0.0.1:<port>, the reserved loopback DevTools
1093
1465
  endpoint of that Chrome. Attach Playwright MCP
1094
1466
  (--cdp-endpoint) or agent-browser (--cdp <port>) to it; it
1095
1467
  reaches only the Stim profile, never your own browser
1468
+ targetId the DevTools target id of the owned page, the one Stim
1469
+ Desktop and the phone stream; other tabs are not the page
1096
1470
  profile the Stim-owned user data directory under STIM_HOME
1471
+ page { url, state, error?, route? }: the page's latest load, from
1472
+ the newest page-load marker in web.ndjson; null before the
1473
+ first. route is absent until an in-app route change after
1474
+ that load
1475
+ "loading" the document was requested; no load event yet
1476
+ "loaded" the load event fired
1477
+ "failed" the document failed (connection refused, HTTP
1478
+ error, certificate) or the page crashed; error
1479
+ is the log message
1480
+ activity like a device's activity below. driver.tool names a
1481
+ DevTools client connected to cdpEndpoint (agent-browser,
1482
+ agent-device, argent, playwright, puppeteer,
1483
+ chrome-devtools-mcp, or the executable's name); Stim's own
1484
+ connections (the browser supervisor, stim-server, Stim
1485
+ Desktop, the stim CLI) are not drivers. basis: cdp-client
1486
+ (one lsof of the DevTools port); agent-action (the newest
1487
+ agent input the browser supervisor recorded in web.ndjson)
1488
+ and page-log (the newest other
1489
+ web.ndjson record) for recency
1097
1490
 
1098
1491
  Each booted simulator and detected emulator in environments (and in
1099
1492
  slots) carries activity; a shut-down or physical device has none:
1100
1493
 
1101
- activity { state, driver?, lastActivityAt?, basis }
1494
+ activity { state, driver?, lastActivityAt?, recent?, basis }
1102
1495
 
1103
1496
  state "driven" a live claim or driver holds the device now
1104
1497
  "active" no driver, but activity in the last 10 minutes
@@ -1106,25 +1499,46 @@ RULES
1106
1499
  "unknown" a claim or driver check could not be read;
1107
1500
  never treated as idle
1108
1501
  driver { tool, pid, since } for "driven": agent-device, stim device
1109
- lock, xcodebuild, idb, maestro, appium, simctl,
1502
+ lock, argent, xcodebuild, idb, maestro, appium, simctl,
1110
1503
  uiautomator or instrumentation; pid and since are null when
1111
1504
  the claim does not record them
1112
1505
  lastActivityAt the newest of this device's app log records, this
1113
- platform's Metro bundle requests and the workspace's last
1114
- Stim run, rounded down to the minute; absent when none is
1506
+ platform's Metro bundle requests, the workspace's last
1507
+ Stim run, while agent-device drives it the agent's last
1508
+ recorded action, and now while a stim-server client views
1509
+ it, rounded down to the minute; absent when none is
1115
1510
  recorded
1511
+ recent the newest time of each kind of evidence behind
1512
+ lastActivityAt: agent-action, device-log, metro-bundle,
1513
+ workspace-use, supervisor-start, page-log or viewer, each
1514
+ rounded down to the minute. viewer is now while a
1515
+ stim-server client streams the device's frames
1116
1516
  basis the evidence behind state, strongest first:
1117
1517
  agent-device-claim, agent-device-lease agent-device state,
1118
1518
  read only; live only when every recorded process is alive
1119
- with its recorded start time, so a reused pid is dead
1519
+ with its recorded start time, so a reused pid is dead.
1520
+ A runner lease that declares agent-device's device
1521
+ claims is never a driver: without a claim, the runner
1522
+ is one agent-device kept warm after close or handed
1523
+ off when its daemon exited
1120
1524
  device-lock an unexpired \`stim device lock\` lease
1121
- driver-process a host process naming the UDID or serial
1122
- instrumentation an on-device uiautomator or androidx.test
1123
- process (one adb shell ps per emulator)
1124
- device-log, metro-bundle, workspace-use recency
1525
+ driver-process a host process naming the UDID or
1526
+ serial, other than such a runner
1527
+ instrumentation an on-device uiautomator, androidx.test or
1528
+ argent helper process (one adb shell ps
1529
+ per emulator)
1530
+ device-log, metro-bundle, workspace-use, agent-action,
1531
+ viewer recency
1125
1532
 
1126
1533
  Plain \`stim status\` appends it to each device line: "driven by
1127
1534
  agent-device for 12m", "active", "idle 3h", or "activity unknown (...)".
1535
+
1536
+ An owned device that is not booted after its supervisor shut it down for
1537
+ devices.idleShutdownMinutes carries idleShutdown, and plain \`status\`
1538
+ appends "shut down after 30m idle at <at>" to its line; the next \`ios\` or
1539
+ \`android\` run for that slot clears it (\`guide lifecycle budget\`):
1540
+
1541
+ idleShutdown { at, idleMinutes }
1128
1542
  \`gc --idle <duration>\` shuts down owned devices idle that long
1129
1543
  (\`guide cleanup gc\`).
1130
1544
 
@@ -1143,6 +1557,16 @@ RULES
1143
1557
  app is absent when the device is not owned or no app id is known: the
1144
1558
  process was not checked.
1145
1559
 
1560
+ Each ios and android device record, in every slot, also carries
1561
+ appPresence, whether the device lacks the app:
1562
+
1563
+ appPresence "none" the device runs, its platform's latest run failed
1564
+ and builds carries the platform with no
1565
+ succeeded run
1566
+ "closed" the device runs and app.state is "stopped"
1567
+ null otherwise, including a device that is not running
1568
+ and a physical device
1569
+
1146
1570
  This is current process state, read from one host ps (simulator apps are
1147
1571
  host processes) and the same adb shell ps as activity, so a status --watch
1148
1572
  refresh notices an exit within its 30-second fallback. It is not launch
@@ -1151,6 +1575,11 @@ RULES
1151
1575
  status appends "<id> running", "<id> not running" or "<id> process
1152
1576
  unknown" to the device line.
1153
1577
 
1578
+ An environment's metro carries tunnel { provider, url } for a recorded
1579
+ managed tunnel on its reserved port: ngrok, cloudflared or tailscale.
1580
+ Plain status prints the provider and URL, with "tailnet-only" for tailscale.
1581
+ This describes the record, not a reachability check (\`guide metro\`).
1582
+
1154
1583
  An environment's metro carries idleStop { reason: "idle", at, idleMinutes }
1155
1584
  when its supervisor stopped the dev server for idleness and nothing serves
1156
1585
  the port since; plain \`status\` prints "stopped (idle)" (\`guide metro\`).
@@ -1163,12 +1592,32 @@ RULES
1163
1592
  { reason: "vanished", pid, startedAt } the recorded supervisor is gone
1164
1593
  and recorded no cause
1165
1594
  (\`guide metro\`, WHY IT STOPPED).
1595
+ When another process answers Metro on the port, metro carries
1596
+ heldBy { pid, cwd }, cwd null when its directory could not be read.
1597
+
1598
+ metro also carries bundle, from the bundle requests Stim's middleware
1599
+ records in the workspace's metro log: an app's, and Stim's own prefetch
1600
+ before a launch. It is absent when the log's tail holds none.
1601
+
1602
+ bundle { bundling, platform?, startedAt?, percent?, last? }
1603
+ bundling true while a request is in flight: Metro runs, and the request
1604
+ started under the current dev server less than 10 minutes ago
1605
+ platform with bundling, the in-flight request's "ios" or "android";
1606
+ startedAt when it started
1607
+ percent 0-100, only when Metro reported progress for that request,
1608
+ which it does for clients that accept multipart responses
1609
+ last { platform, status, durationMs, finishedAt }: the newest
1610
+ finished request, status "ok" or "failed", durationMs from the
1611
+ request to the end of the response
1166
1612
 
1167
1613
  Each entry of environments also carries build: null, or the ios or android
1168
1614
  run that holds this workspace's native-run.lock:
1169
1615
 
1170
1616
  build { platform, slot, state, phase, startedAt, phaseStartedAt,
1171
- outcome, expectedMs, expectedPhaseMs, basis }
1617
+ outcome, outcomeKnown, cacheLookupOutcome?, expectedMs, expectedPhaseMs,
1618
+ completedPhaseMs?, basis,
1619
+ plannedPhases, missReason?, missProvisional?, detail?, placement,
1620
+ waitingOn? }
1172
1621
 
1173
1622
  state "running" while the run's own native-run claim is live;
1174
1623
  "stale" when that claim was released or its process is
@@ -1176,30 +1625,102 @@ RULES
1176
1625
  record; "unknown" when the claim set cannot be resolved.
1177
1626
  Never treat a stale record as a build in progress.
1178
1627
  phase prepare | cache-lookup | wait | prebuild | pods |
1179
- compile | install | launch. prepare covers settings,
1180
- Metro and the owned device; wait is waiting on another
1181
- workspace's build of the same fingerprint; install
1182
- includes waiting for the device to boot.
1628
+ compile | device | install | launch. prepare covers
1629
+ settings and Metro; wait is waiting on another
1630
+ workspace's build of the same fingerprint. Creating,
1631
+ adopting or booting the owned simulator or emulator
1632
+ before the cache lookup is prepare time. device starts
1633
+ once the app is ready and covers waiting for the
1634
+ device: its boot, adoption cleanup, or a physical
1635
+ device's lease and connection check. A boot that
1636
+ finishes during the build adds no device time. An
1637
+ --eas-profile run has no cache lookup, so its outcome
1638
+ stays the project's most recent one until install.
1183
1639
  startedAt when the run started; phaseStartedAt when its phase did
1184
- outcome "cold" once the run reached prebuild, pods or compile,
1185
- "hit" once it reached install without them. Before
1186
- that, the outcome of this project's most recent run.
1640
+ outcome "cold" after the local/provider lookups resolve a miss;
1641
+ "hit" after a cached artifact is ready to reuse, including
1642
+ a shared-build hit or a recheck after prebuild or pods.
1643
+ A later recheck can replace the first lookup's outcome.
1644
+ Before resolution, the outcome of this project's most
1645
+ recent run.
1646
+ outcomeKnown true once outcome is this run's own, false while it is
1647
+ the project's most recent one
1648
+ cacheLookupOutcome "hit" or "miss" once an actual cache lookup resolves;
1649
+ absent before resolution and on runs that skip lookup,
1650
+ such as --eas-profile
1187
1651
  expectedMs the median duration of this project's last successful
1188
1652
  runs with that outcome on that platform, or null with
1189
- no history
1653
+ no history. The run estimates twice: when it starts,
1654
+ from every run with the project's most recent outcome,
1655
+ and once when outcomeKnown turns true. From then on only
1656
+ runs that created, adopted or cold-booted their device
1657
+ the way this run did count; a run that reuses a booted
1658
+ device also counts runs recorded before Stim tagged them
1659
+ until 3 tagged ones exist. Runs that finish meanwhile do
1660
+ not change the estimate.
1190
1661
  expectedPhaseMs the median duration of this phase in those runs, or null
1191
- basis how many runs the medians come from (at most 10)
1662
+ when none of them entered it
1663
+ completedPhaseMs milliseconds spent in each phase the run already left,
1664
+ summing repeated visits. The current visit is excluded;
1665
+ its phase appears only if visited earlier. Absent before
1666
+ any phase completes and on older Stim versions
1667
+ basis how many runs the expectedMs median comes from (at most
1668
+ 10 per kind: device-setup runs keep their own 10)
1669
+ plannedPhases [{ phase, expectedMs }] in phase order: the phases at
1670
+ least half of those runs entered, each with its median
1671
+ in them; null with no such run. The current phase can be
1672
+ one it does not list, such as wait. Draw a progress bar
1673
+ from it rather than from builds, which holds only this
1674
+ workspace's runs and includes offloaded ones
1675
+ missReason once the run knows its cache lookup missed, why, in the
1676
+ shape of lastBuilds missReason below
1677
+ missProvisional true while missReason is the first lookup's miss and the
1678
+ run will prebuild or install pods and look the key up
1679
+ again, because those can change the fingerprint. A hit on
1680
+ that second lookup removes missReason and sets outcome to
1681
+ hit; otherwise missReason is replaced with the final one,
1682
+ whose rekeyedBy lists what changed the key, and
1683
+ missProvisional goes away before compile
1684
+ detail during compile, once the build tool printed a line Stim
1685
+ reads: { step, unit, done, total, line, updatedAt }
1686
+ step the tool's step: configure, compile, link, resources,
1687
+ script, dex, package or sign; null before one is known
1688
+ unit "targets" for xcodebuild, "tasks" for Gradle
1689
+ done xcodebuild: targets it finished (a target counts once
1690
+ xcodebuild touches or signs its product, so an
1691
+ incremental build can end below total); Gradle: tasks
1692
+ it reported so far
1693
+ total xcodebuild: targets in its dependency graph; Gradle: null,
1694
+ since its plain output gives no total
1695
+ line the latest compile, link or task line, paths shortened
1696
+ to file names, at most 160 characters
1697
+ updatedAt when the build last wrote it; the run writes it at most
1698
+ every 2 seconds
1699
+ placement where the build runs: "local", or while it is offloaded
1700
+ { host, phase, startedAt, phaseStartedAt }. host is the
1701
+ offload.machines entry; phase is the step there: sync,
1702
+ deps, prebuild, pods, build (xcodebuild or Gradle) or fetch;
1703
+ startedAt is when the offload started and phaseStartedAt
1704
+ when that step did. Meanwhile phase above follows it as
1705
+ prebuild, pods or compile.
1706
+ waitingOn while phase is "wait" and the holder is known: { path },
1707
+ the workspace whose build of the same artifact this run
1708
+ waits for; absent otherwise. The lock is machine-wide, so
1709
+ path is usually an entry of environments but can name a
1710
+ workspace that is not listed.
1192
1711
 
1193
1712
  Plain \`stim status\` prints the same as one line per workspace:
1194
1713
 
1195
1714
  build: ios compile, 1m10s elapsed -- about 3 min left (median of 4 cold runs)
1715
+ build: ios compile on janics-mac-mini (build, 2m10s), 3m05s elapsed
1196
1716
 
1197
1717
  An environment with a recorded run also carries lastBuilds, each
1198
1718
  platform's most recent ios or android run, finished or failed:
1199
1719
 
1200
1720
  lastBuilds { ios?, android? }, each { platform, status, cacheHit,
1201
1721
  cacheSkipped, durationMs, fingerprint, startedAt, finishedAt,
1202
- errorCode?, missReason?, diagnostics? }
1722
+ errorCode?, missReason?, buildMachine?, builtOn?, offloadedTo?, offloadFallback?,
1723
+ diagnostics? }
1203
1724
 
1204
1725
  status "ok" or "failed"
1205
1726
  cacheHit "local" or "remote" for an app from that cache tier; false
@@ -1220,10 +1741,43 @@ RULES
1220
1741
  diagnostics only on a failed run whose compiler reported errors: up to
1221
1742
  5 { file, line, column, message }, null where the compiler
1222
1743
  gave no position.
1744
+ cause only on a failed run: { key, file, line }, the first
1745
+ diagnostic with a file and a line, keyed "<file>:<line>",
1746
+ else the errorCode (or "failed") with null file and line.
1747
+ Failed runs in a row with the same key failed the same way.
1748
+ Invalid, unlisted or unpaired selections refuse during setup, before cache
1749
+ lookup, and create no lastBuild record or failed-run stats.
1750
+ buildMachine selected auto, local or configured machine entry; always on new build records
1751
+ builtOn here or machine name; absent when no build ran (cache/refusal)
1752
+ offloadedTo only on a run a build machine compiled: its name
1753
+ offloadFallback
1754
+ only on a run that considered offloading and built here: one
1755
+ reason per machine when none took the build, or why the
1756
+ offload stopped
1223
1757
 
1224
1758
  Plain status prints "last build: ios local cache in 12s, android compiled
1225
1759
  in 7m02s". To predict the next run instead, see \`guide facts plan\`.
1226
1760
 
1761
+ Swift Package development carries macos, separate from simulator devices:
1762
+ product, bundle, bundleId, executable, launchId, arguments
1763
+ app?, supervisor? { pid, processToken, startedAtMicros }
1764
+ state running | orphaned | stopped | unverified
1765
+ build { state: running | ok | failed, startedAt,
1766
+ finishedAt?, durationMs?, error?,
1767
+ buildMachine?, builtOn?, errorCode?, offloadedTo?, offloadFallback? }
1768
+ host? { machine, session, appSlot, appAttempt, bundleId,
1769
+ agent } when stim macos --remote placed the app on
1770
+ another Mac; it then has no local app or supervisor
1771
+ hostLaunched? true when that host reported a live app, false when
1772
+ stopped, "unverified" when it could not confirm one
1773
+ A live app without its supervisor is orphaned. An identity the system cannot
1774
+ verify is unverified. For hosted placements only, status asks the host for
1775
+ the session state with about a 3 s timeout per connection and request and
1776
+ a 10 s cache. It reports stopped when the host says the session stopped,
1777
+ for example after a stim-server restart there, and unverified when the
1778
+ host is unreachable or cannot confirm. The placement stays recorded.
1779
+ See stim guide macos for local capture, hosting and cleanup.
1780
+
1227
1781
  An environment with a recorded run also carries builds, each platform's
1228
1782
  last 10 runs, newest first. Its newest entry that is not "interrupted" is
1229
1783
  the run lastBuilds reports, once a run has recorded builds.
@@ -1243,23 +1797,121 @@ RULES
1243
1797
  cacheKey the cache key the run looked up or stored under
1244
1798
  phases milliseconds spent in each phase the run entered, among
1245
1799
  prepare, cache-lookup, wait, prebuild, pods, compile,
1246
- install and launch
1800
+ device, install and launch; runs recorded before device
1801
+ existed count their device time in prepare and install
1247
1802
 
1248
1803
  Only runs that record a last build are listed: a run that stops before
1249
1804
  looking up a build, such as a bad flag, is not. Estimates (expectedMs) come from run statistics, not builds.
1250
1805
 
1251
- There is no completion fraction: a compile's log volume depends on what
1252
- is already built, so it does not measure progress.
1253
-
1254
- An environment's memoryMb is an estimate, not a measurement: a fixed amount
1255
- per booted simulator, detected emulator, running Metro and running Chrome.
1256
- capacity.committedMb sums it, and the memory budget uses the same estimate.
1257
- What is using CPU and memory now is the top-level machine section:
1258
-
1259
- machine null, or { owners: [{ kind, name, workspace, slot?, id, owned,
1260
- cpuPercent, residentMb, processes }] }
1261
-
1262
- kind simulator a booted simulator's launchd_sim tree
1806
+ done and total are counts, not a completion fraction: a target or task
1807
+ can take a second or ten minutes, and cached work finishes instantly.
1808
+
1809
+ Each environment also carries disk, and each owned simulator and owned
1810
+ emulator in ios, android and slots carries disk, once \`status --watch\` has
1811
+ measured them:
1812
+
1813
+ disk (environment) { worktreeBytes, nodeModulesBytes, buildBytes,
1814
+ measuredAt }
1815
+ worktreeBytes the linked worktree, or else the checkout holding the
1816
+ workspace, node_modules included
1817
+ nodeModulesBytes node_modules at that root and at the workspace path;
1818
+ part of worktreeBytes
1819
+ buildBytes Stim's folder for the workspace: Xcode derived data,
1820
+ Gradle outputs and logs
1821
+ measuredAt the oldest of the three measurements; a figure not
1822
+ measured yet is null
1823
+ disk (device) { bytes, measuredAt }: the simulator's data folder
1824
+ under CoreSimulator/Devices, or the AVD's .avd folder
1825
+
1826
+ \`status --watch\` runs one du at a time off its refresh path, and measures
1827
+ a folder at most every 5 minutes while its environment is live and every
1828
+ hour otherwise. It caches each size under $STIM_HOME/disk-usage, which
1829
+ one-shot status only reads, so the fields appear once a watcher, such as
1830
+ stim-server or Stim Desktop, has measured.
1831
+
1832
+ An environment carries agents when coding-agent sessions work in it, most
1833
+ recently active first:
1834
+
1835
+ agents [{ tool, sessionId, title?, cwd, startedAt?, lastActiveAt?,
1836
+ pid?, openUrl?, webUrl? }]
1837
+ tool claude-code | codex
1838
+ title the short name the tool keeps for the session; never a
1839
+ prompt or any conversation content
1840
+ cwd the session's working directory, canonical
1841
+ lastActiveAt the tool's last update of the session, or the last Stim
1842
+ command the session ran here when that is newer
1843
+ openUrl opens the session in the Claude desktop app
1844
+ (claude://code/continue) or the Codex app
1845
+ (codex://threads/<id>) when that app is installed on the
1846
+ Mac; absent for a Claude Code session started in a terminal
1847
+ webUrl https://claude.ai/code/<id> for a Claude Code session with
1848
+ Remote Control connected; it opens the session in a browser
1849
+ or the Claude mobile app
1850
+
1851
+ A session works in an environment when its cwd is the environment's
1852
+ path or inside it (the deepest environment wins), or is the environment's
1853
+ git worktree root. \`status --watch\` looks for sessions at most every 15
1854
+ seconds: Claude Code sessions in ~/.claude/sessions whose process runs,
1855
+ and Codex threads in $CODEX_HOME (~/.codex) updated in the last 30
1856
+ minutes. It caches them in $STIM_HOME/agent-sessions.json, which one-shot
1857
+ status reads and ignores once it is 2 minutes old. Both sources are other
1858
+ tools' internal files; a missing or changed one only drops its sessions.
1859
+ \`start\`, \`ios\`, \`android\`, \`web\`, \`reload\` and \`worktree warm\`
1860
+ also record the CLAUDE_CODE_SESSION_ID or CODEX_THREAD_ID of the shell
1861
+ that ran them, so that session is listed for 30 minutes after the command
1862
+ even when status cannot see it, and for as long as it runs.
1863
+
1864
+ When a watcher stops finding a session it found on its previous look, it
1865
+ records the session in each environment it worked in, under
1866
+ $STIM_HOME/workspaces/<id>/ended-agents.json, for 3 days:
1867
+
1868
+ endedAgents [{ tool, sessionId, title?, cwd, startedAt?, lastActiveAt?,
1869
+ openUrl?, webUrl?, endedAt }], most recently ended first
1870
+ endedAt the last time a watcher found the session running; a Codex
1871
+ thread ends 30 minutes after its last update
1872
+
1873
+ The other fields are as the watcher last found them. A session that runs
1874
+ again is listed in agents only.
1875
+
1876
+ memorySource says how a memory figure was obtained:
1877
+
1878
+ footprint physical footprint, the figure Activity Monitor's Memory
1879
+ column shows, read by a small helper Stim compiles with the
1880
+ Xcode command line tools into $STIM_HOME/helpers on first use
1881
+ rss summed resident set size, for machine owners when the helper
1882
+ cannot be built (no Xcode command line tools, Linux). It
1883
+ overstates a simulator many times over.
1884
+ estimate a fixed amount per booted simulator, detected emulator,
1885
+ running Metro and running Chrome
1886
+
1887
+ An environment's memoryMb is what its own processes use now, the sum of
1888
+ memoryMb over the machine owners whose workspace is that environment,
1889
+ builds included, when machine.memorySource is footprint. Otherwise it is
1890
+ the estimate, with memorySource estimate.
1891
+
1892
+ capacity.committedMb sums memoryMb. The memory budget plans before a boot
1893
+ and always uses the estimate. What is using CPU and memory now is the
1894
+ top-level machine section:
1895
+
1896
+ maintenance { mode, lastChecks: { pressure, size }, pressure, sizes,
1897
+ lastPass, running, recent, plan, invalid?, claim? }
1898
+ Report-only observations; every plan action's kind starts with would-.
1899
+ lastChecks are epoch milliseconds or null. running comes only from a
1900
+ live maintenance/run.claims owner, never from a check stamp.
1901
+ lastPass carries startedAt, durationMs, trigger, mode, freedBytes (0),
1902
+ actions, stopped (0), blocked. recent holds the last 20 action, failure
1903
+ and blocked NDJSON records from maintenance/maintenance.ndjson.
1904
+ invalid names invalid settings; invalid cache caps use their defaults,
1905
+ while invalid maintenance settings disable passes. claim contains an
1906
+ unresolved claim's path and reason, plus removeCommand from the claim
1907
+ protocol; inspect the holder before running that command.
1908
+
1909
+ machine null, or { memorySource, owners: [{ kind, name, workspace,
1910
+ slot?, id, owned, cpuPercent, residentMb, memoryMb,
1911
+ processes }] }
1912
+
1913
+ kind macos the owned Swift Package app and supervisor
1914
+ simulator a booted simulator's launchd_sim tree
1263
1915
  emulator an emulator's launcher and qemu tree, by its -avd
1264
1916
  metro a workspace's supervisor and Metro trees
1265
1917
  build a running ios or android run's process tree,
@@ -1281,14 +1933,19 @@ RULES
1281
1933
  residentMb summed resident set size. Pages shared between processes
1282
1934
  count once per process, so a simulator reports well above
1283
1935
  its physical footprint.
1936
+ memoryMb summed physical footprint when machine.memorySource is
1937
+ footprint; a process the helper cannot read, another user's
1938
+ or one that just exited, counts its resident size. Equals
1939
+ residentMb when memorySource is rss.
1284
1940
 
1285
1941
  Each process counts in exactly one owner, the one whose root process is
1286
1942
  its nearest ancestor, so the supervisor a build started counts as Metro,
1287
1943
  not as the build, and a Gradle or Kotlin daemon counts in the build that
1288
1944
  started it until that build exits, then as shared. Processes with no owner
1289
1945
  are left out. machine comes from one host ps, the one status reads for
1290
- device activity, and is null when no simulator is booted, no workspace is
1291
- live and no build runs: status then reads no process table. \`status --watch --json\` rereads it every 15 seconds while
1946
+ device activity, and one run of the footprint helper. It is null when no
1947
+ simulator is booted, no workspace is live and no build runs: status then
1948
+ runs neither. \`status --watch --json\` rereads both every 15 seconds while
1292
1949
  machine is not null, with no other subprocess.`
1293
1950
  },
1294
1951
  plan: {
@@ -1311,7 +1968,9 @@ RULES
1311
1968
  outcome "hit" or "cold"; null when the run would refuse
1312
1969
  expectedMs the median wall time of this project's last successful runs
1313
1970
  with that outcome on that platform, or null with none
1314
- basis how many runs expectedMs comes from (at most 10)
1971
+ basis how many runs expectedMs comes from (at most 20: runs that
1972
+ set up their device and runs that did not are not told apart
1973
+ before the run)
1315
1974
  missReason on a predicted cold build with cache reads on, why the
1316
1975
  cache has no app, in the shape of lastBuilds.<platform>
1317
1976
  .missReason (\`guide facts status\`), compared with the same
@@ -1338,14 +1997,61 @@ RULES
1338
1997
  { "version": 1,
1339
1998
  "project": { "key": "<path>", "ios": <bucket|null>,
1340
1999
  "android": <bucket|null> } | null,
1341
- "machine": { "ios": <bucket|null>, "android": <bucket|null> } }
2000
+ "machine": { "ios": <bucket|null>, "android": <bucket|null> },
2001
+ "offload": { "today": { "here", "offloaded", "fellBack" },
2002
+ "machines": { "<machine>": { "today": <day>,
2003
+ "total": <totals> } },
2004
+ "placements": [<placement>, ...] },
2005
+ "agentDevice": <AgentDeviceUsage|null>,
2006
+ "swiftpmCache": <SwiftpmCacheUsage|null> }
1342
2007
 
1343
2008
  \`project\` is null outside a project; a platform with no run yet is null.
1344
2009
  A bucket carries runs, failed, hits, misses, coldRuns, coldRunMs, hitRuns,
1345
2010
  hitRunMs, timeSavedMs, firstRunAt and lastRunAt, plus lastColdBuildMs and
1346
- lastPodsMs once the project has compiled or installed pods. Milliseconds are
2011
+ lastPodsMs once the project has compiled or installed pods, and
2012
+ offloadedRuns, offloadedRunMs and lastOffloadHost once a build machine
2013
+ compiled one. An offloaded run is a miss but not a cold run, so the cold
2014
+ average, time saved and build estimates stay local. Milliseconds are
1347
2015
  integers.
1348
2016
 
2017
+ AGENT-DEVICE DISK USAGE
2018
+ agentDevice: { version: 1, measuredAt, bytes, complete, stateDir,
2019
+ runnerBuilds, workspaces, hosted }
2020
+ bytes sums known state, separate runner builds, workspace and hosted bytes;
2021
+ complete is false when a measurement fails. Individual unknown byte fields
2022
+ are null. Absent directories have zero bytes and present: false.
2023
+ stateDir defaults to ~/.agent-device. AGENT_DEVICE_STATE_DIR overrides it
2024
+ only when it does not overlap a workspace agent-device dir or the hosted dir.
2025
+ stateDir: { dir, present, bytes, sessions: { dir, bytes, count },
2026
+ logs: { dir, bytes }, other: { bytes, largest: [{ name, bytes }] } }
2027
+ runnerBuilds: { dir, present, bytes, sharedBytes, platforms: [
2028
+ { platform, dir, bytes, entries: [{ name, dir, bytes, lastUsedAt,
2029
+ packageVersion, xcodeBuildVersion, inUse, inUseReason }] }] }
2030
+ inUseReason is lease, lock, unreadable or null. Lease means a live owner or
2031
+ runner matched by start time; lock means a live or unknown lock owner.
2032
+ Unreadable lock owners and unreadable or unknown leases use unreadable.
2033
+ Dead lock owners and leases do not mark entries in use.
2034
+ workspaces: [{ dir, projectRoot, bytes }]; hosted: { dir, bytes, sessions } | null.
2035
+ $STIM_HOME/agent-device-usage.json caches the measurement and resolved roots
2036
+ for 10 minutes. stats and unscoped gc measure; no other command measures this
2037
+ state. Server stats.get reads only the cached value.
2038
+ Stim never trims or deletes the shared runner builds, sessions, logs and other
2039
+ state or the hosted driver dir; a workspace's own agent-device dir goes only with its workspace.
2040
+
2041
+ SWIFTPM CACHE DISK USAGE
2042
+ swiftpmCache: { version: 1, measuredAt, dir, present, bytes, complete } | null.
2043
+ The user-level cache is ~/Library/Caches/org.swift.swiftpm on macOS and
2044
+ org.swift.swiftpm under XDG_CACHE_HOME (default ~/.cache) elsewhere.
2045
+ It is shared by every SwiftPM build on the machine; Stim never deletes it.
2046
+ bytes is null and complete is false if du fails; absent directories have
2047
+ present: false, bytes: 0 and complete: true. Plain output omits absent caches.
2048
+ $STIM_HOME/swiftpm-cache-usage.json caches the measurement for 10 minutes,
2049
+ keyed on the resolved dir. stats and unscoped gc alone run du -sk with a
2050
+ 20-second timeout. Server stats.get reads only the cached value. status,
2051
+ start, ios, android and the budget gate never measure it. gc reports it as
2052
+ sections.swiftpmCache, never in caches or actionable; --delete and
2053
+ --older-than leave it untouched, and any --cache scope reports null.
2054
+
1349
2055
  HOW A RUN IS COUNTED (\`stats\`)
1350
2056
  Every \`ios\` or \`android\` invocation that got as far as computing a
1351
2057
  cache key is one run, in this project's bucket and in the machine-wide one.
@@ -1365,7 +2071,8 @@ HOW A RUN IS COUNTED (\`stats\`)
1365
2071
  is therefore an ESTIMATE and is printed as one. The file is
1366
2072
  $STIM_HOME/stats.json (see \`guide lifecycle builds\`). Beside the
1367
2073
  aggregates it keeps the last 10 successful runs per project, platform and
1368
- outcome (hit or cold) that did not wait for another workspace's build: each
2074
+ outcome (hit or cold), plus the last 10 that created, adopted or cold-booted
2075
+ their device, that did not wait for another workspace's build: each
1369
2076
  one's duration and per-phase durations. \`stim status\` estimates a running
1370
2077
  build from them (see \`guide facts status\`); \`stats --json\` does not
1371
2078
  print them.
@@ -1386,7 +2093,30 @@ HOW A RUN IS COUNTED (\`stats\`)
1386
2093
  a hang. A project with no record yet gets \`(1m00s)\`, the elapsed alone,
1387
2094
  and a warm run has no long phase to size. That read takes no lock and
1388
2095
  ignores what it cannot read, so nothing about statistics can change a
1389
- run's outcome.`
2096
+ run's outcome.
2097
+
2098
+ BUILD PLACEMENT (\`offload\`)
2099
+ Every run that compiles records where it built and why, in a placement: { at, project, platform,
2100
+ decision, reason, machine?, buildMs?, slotWaitMs?, localEstimateMs?, failed? }.
2101
+ slotWaitMs is whole milliseconds waiting for a build slot, present only when positive.
2102
+ decision is "here", "offloaded", or "fell-back" (it tried a build machine
2103
+ and built here). reason is why: the run's \`placement:\` reason, such as
2104
+ "load 0.6/core, 1 of 3 build slots busy here" (auto keeps the build here
2105
+ while this Mac has room) or "offload.mode is off" (which prints no line),
2106
+ or for a fallback the cause, prefixed by the machine it concerns, such as
2107
+ "mini: no less loaded (load 1.2/core there, 0.6/core here)". buildMs is the compile here, or the offloaded build's
2108
+ total (offer, sync, build and fetch); localEstimateMs is this project's
2109
+ last cold build here before the run. A run that hits the cache or waits
2110
+ for another workspace's build compiles nothing and records none.
2111
+ placements lists them newest first: the last 100 from the last 7 days.
2112
+ Per machine, \`total\` keeps { offloaded, offloadedMs, savedMs, fallbacks,
2113
+ lastOffloadAt?, lastFallbackAt? } for good and \`today\` counts { offloaded,
2114
+ offloadedMs, savedMs, fallbacks } from the placements on this Mac's local
2115
+ calendar day. savedMs sums localEstimateMs minus buildMs over offloaded
2116
+ builds that had an estimate, so it is an ESTIMATE and is negative when the
2117
+ machine was slower. \`offload.today\` counts the day's placements by
2118
+ decision. A fallback counts against a machine only when the run knows
2119
+ which one: the machine that failed, or the only one paired.`
1390
2120
  }
1391
2121
  }
1392
2122
  },
@@ -1410,10 +2140,49 @@ another worktree's bundler.
1410
2140
  --wait, it does not wait for a device lease.
1411
2141
  --remote expose Metro for a remote device
1412
2142
 
1413
- Plain \`stim start\` is local and does not create a public tunnel. Remote intent
1414
- comes from \`start --remote\`, \`ios.remote\`, or \`android.remote\`. The
2143
+ Plain \`stim start\` is local and does not create a tunnel. Remote intent
2144
+ comes from \`start --remote\`, eas/proxy in \`ios.remote\`, or \`android.remote\`. The
1415
2145
  \`metro.tunnel\` setting selects the provider after remote intent exists.
1416
2146
 
2147
+ HOSTED IOS METRO
2148
+ ios --remote <machine> keeps Metro local and needs no start --remote.
2149
+ The supervisor watches private gateway requests in workspace state, binds
2150
+ only this Mac's Tailscale address and accepts only the pinned hosting peer
2151
+ with a per-session secret. The host's bridge is loopback-only. Port and secret
2152
+ stay in 0600 workspace state and never appear in status, JSON or logs.
2153
+ metro.tunnel and metro.publicUrl are ignored. Supervisor restart recreates
2154
+ the recorded gateway; stop or a stopped hosted session closes it. A recorded
2155
+ hosted session prevents Metro idle stop. Release runs skip the bridge.
2156
+ Hosted Debug runs require the supervisor's private gateway support before
2157
+ reservation. --no-metro-check refuses; restart a missing or older supervisor
2158
+ with stim stop; stim start.
2159
+ Native iOS logs are pulled from the host by stim logs, including --errors;
2160
+ JavaScript logs still arrive through Metro. Stop pulls native logs before
2161
+ deleting the simulator. Read guide lifecycle hosted-ios for placement and cleanup.
2162
+
2163
+ TAILNET-ONLY METRO
2164
+ Install Tailscale and sign in on this machine and the remote device or
2165
+ agent-device daemon's host, then select the mode explicitly:
2166
+
2167
+ stim settings set metro.tunnel tailscale --scope workspace
2168
+ stim start --remote
2169
+
2170
+ The device must be on the same tailnet, so the eas backend refuses it. "auto"
2171
+ never selects tailscale.
2172
+ Stim runs a foreground \`tailscale serve --https=<metro port>
2173
+ http://127.0.0.1:<metro port>\` process, never Funnel. It prints
2174
+ https://<tailnet-name>:<metro port> and \`status\` reports the recorded
2175
+ provider and URL. Stim reads the URL from the child's pipes, without a log
2176
+ file, and skips the public bundle probe for this recorded tunnel: this
2177
+ machine may not reach its own tailnet name.
2178
+
2179
+ \`stop\` and \`worktree remove\` stop only the recorded process after
2180
+ verifying its identity; ending that foreground session removes its route.
2181
+ A route Stim did not create is never touched. If the port already has a
2182
+ serve route, Tailscale refuses and Stim prints Tailscale's error message.
2183
+ \`tailscale serve status --json\` shows foreground sessions under the
2184
+ top-level Foreground key; plain \`tailscale serve status\` does not.
2185
+
1417
2186
  BUNDLE WARMUP
1418
2187
  After verifying Metro, \`stim ios\` and \`stim android\` prefetch the
1419
2188
  platform's development bundle while native work continues. Expo supplies the
@@ -1441,7 +2210,7 @@ BUNDLE WARMUP
1441
2210
  REMOTE DEVICE BACKENDS
1442
2211
  Metro exposure and device selection are separate:
1443
2212
 
1444
- stim start --remote prepare public Metro
2213
+ stim start --remote prepare remote Metro
1445
2214
  stim ios --remote proxy use an agent-device daemon
1446
2215
  stim android --remote eas create an EAS Simulator session
1447
2216
 
@@ -1452,6 +2221,12 @@ REMOTE DEVICE BACKENDS
1452
2221
  requires AGENT_DEVICE_DAEMON_BASE_URL and
1453
2222
  AGENT_DEVICE_DAEMON_AUTH_TOKEN. Stim creates no remote session for it.
1454
2223
 
2224
+ An iOS remote build compiles one simulator architecture, since xcodebuild
2225
+ cannot target the remote simulator itself: the proxy daemon's host
2226
+ architecture from its /health hostArch, and arm64 when the daemon does not
2227
+ report one, cannot be reached within 3 seconds, or runs on EAS, whose hosts
2228
+ are Apple silicon. The cache key ends in that architecture.
2229
+
1455
2230
  The EAS backend needs eas-cli 21.6.0 or later and an account with EAS
1456
2231
  Simulator access. An EAS session is billable. Stim prepares the app before
1457
2232
  it creates or reconnects the session, so a failed build starts no session
@@ -1466,7 +2241,17 @@ IDEMPOTENT
1466
2241
  A healthy dev server on the reserved port is a no-op: \`start\` prints the
1467
2242
  facts with alreadyRunning: true and starts nothing. A foreign process holding
1468
2243
  the reserved port moves the RESERVATION instead, so the project is never
1469
- stranded on a port it can never use.
2244
+ stranded on a port it can never use. A port pinned with metro.port or
2245
+ STIM_METRO_PORT never moves: \`start\` refuses with STIM_BAD_ARG and names
2246
+ the holder before anything starts or the pin is reserved. This project's
2247
+ own Metro already on the pin is attached to. Changing the pin while this
2248
+ workspace's dev server runs refuses until stim stop; a supervisor that cannot
2249
+ be verified also refuses, until you stop it with the tool that started it.
2250
+ You can also unset or restore the pin. stim web follows
2251
+ the same rules when its page uses Metro.
2252
+ ios and android with --no-metro-check use the pin, then the recorded port,
2253
+ then 8081, without probing, reserving or writing the registry. An invalid
2254
+ pin still refuses with STIM_BAD_ARG. Release builds still skip Metro.
1470
2255
 
1471
2256
  WHAT THE SUPERVISOR IS
1472
2257
  One detached process per workspace. There is no machine-wide daemon, nothing
@@ -1517,7 +2302,10 @@ WHAT THE SUPERVISOR IS
1517
2302
  connected app alone does not keep the server. It records a
1518
2303
  supervisor_idle_stopped line in metro.ndjson and devServerStop in
1519
2304
  state.json, so \`status\` shows "stopped (idle)" rather than a crash.
1520
- Devices stay booted. The next \`stim start\` starts it again and clears the
2305
+ Devices stay booted, unless devices.idleShutdownMinutes is set: then the
2306
+ idle stop first shuts down the workspace's owned devices idle for the
2307
+ shorter of the two (\`guide lifecycle budget\`). The next \`stim start\`
2308
+ starts it again and clears the
1521
2309
  record, also when it reuses a dev server that another process started. A
1522
2310
  Debug \`ios\` or \`android\` run starts it the same way and reports
1523
2311
  devServer.reason "stopped (idle)". The setting
@@ -1547,6 +2335,23 @@ WHAT THE SUPERVISOR IS
1547
2335
  supervisor logs supervisor_vanished (level warn) for it, and until then
1548
2336
  \`status\` reports metro.lastStop.reason "vanished".
1549
2337
 
2338
+ WATCHMAN ROOTS: a Metro that uses watchman registers a watch root in the
2339
+ shared watchman daemon and never removes it. Bare React Native Metro uses
2340
+ watchman whenever it is installed; Expo SDK 56 and later use it only when
2341
+ metro.config.js sets resolver.useWatchman to true. When the supervisor stops
2342
+ its dev server, it removes with \`watchman watch-del\` a root that appeared
2343
+ after it started, contains the workspace and carried this Metro's
2344
+ subscription, once no other client subscribes to it or has a trigger on it.
2345
+ It logs watchman_root_removed, or watchman_root_kept with the reason, in
2346
+ metro.ndjson. It never starts the daemon and leaves every other root alone,
2347
+ including those jest registers.
2348
+ A supervisor that dies without stopping leaves its root: remove it with
2349
+ \`watchman watch-del <root>\`. Removing a root stops its recrawls; the
2350
+ daemon's memory footprint shrinks only when the daemon restarts, which drops
2351
+ the watches of every other client. \`stim gc --cache watchman\` reports the
2352
+ daemon's footprint, roots and clients, and with --delete shuts it down only
2353
+ when no client is connected (\`guide cleanup memory\`).
2354
+
1550
2355
  ENVIRONMENT: the supervisor -- and through it the dev server, including a
1551
2356
  metro.config.js evaluated inside the expo child -- inherits the environment
1552
2357
  of the \`start\` call that SPAWNED it. A later \`start\` that finds a healthy
@@ -1607,7 +2412,14 @@ If Stim is not installed globally, replace stim with npx stim.
1607
2412
  get <label> allocates on first use and prints only the number to stdout.
1608
2413
  Repeated calls reuse it, even while a server is listening. Labels start with
1609
2414
  a letter and contain up to 64 letters, digits, underscores, or hyphens.
1610
- metro is reserved; use stim start and stim stop for managed Metro.
2415
+ metro is reserved; use stim start and stim stop for managed Metro, and
2416
+ metro.port or STIM_METRO_PORT to choose its number (guide settings).
2417
+ Changing the pin while this workspace's dev server runs refuses until
2418
+ stim stop; an unverified supervisor also blocks the change until you stop it with the tool that started it. A foreign holder
2419
+ of the pin is refused before anything starts; this project's own Metro on
2420
+ it is attached to. stim web follows these rules when its page uses Metro.
2421
+ ios and android with --no-metro-check use the pin, then the recorded port,
2422
+ then 8081, without probing or reserving; an invalid pin still refuses.
1611
2423
 
1612
2424
  New allocations scan TCP ports 8900-8999. They skip registry reservations
1613
2425
  and existing listeners, announcing occupied ports and upward retries on
@@ -1659,6 +2471,34 @@ instead of allocating a separate label in every worktree.`
1659
2471
  summary: "Querying the merged NDJSON timeline, and what --errors means",
1660
2472
  body: () => `LOGS
1661
2473
 
2474
+ If Stim is not installed globally, replace stim with npx stim.
2475
+
2476
+ AGENT-DEVICE WORKSPACE STATE
2477
+
2478
+ When you drive a device yourself with agent-device, opt in to the workspace's
2479
+ state directory reported as agentDevice.stateDir in the stim ios, android and
2480
+ macos output and in status. Read it once from status, run from the app root:
2481
+
2482
+ STATE_DIR="$(stim status --json | jq -r '.environments[] | select(.path=="'"$(pwd -P)"'") | .agentDevice.stateDir')"
2483
+
2484
+ Then prefix each agent-device command, so the choice survives shells that do
2485
+ not keep exports:
2486
+
2487
+ AGENT_DEVICE_STATE_DIR="$STATE_DIR" agent-device open <bundle id> --platform ios --session <name>
2488
+ AGENT_DEVICE_STATE_DIR="$STATE_DIR" agent-device snapshot --session <name>
2489
+
2490
+ The path is $STIM_HOME/workspaces/<name>/agent-device. agent-device creates it
2491
+ itself. Sessions (including events, runner logs and request text), logs,
2492
+ metro-sessions, allocations and daemon files move there. The apple-runner build
2493
+ cache and device claims stay shared; do not set
2494
+ AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH or AGENT_DEVICE_CLAIMS_DIR.
2495
+ Each active workspace adds one agent-device daemon, which exits after five
2496
+ idle minutes with no open session. The variable is opt-in per command: forgetting it falls back to
2497
+ ~/.agent-device.
2498
+ stim stop closes owned-device sessions there, stim logs --source agent reads
2499
+ them, and stim worktree remove stops the daemon and removes the directory.
2500
+ Hosted/stim-server sessions use their own state directory and need none of this.
2501
+
1662
2502
  stim logs [filters]
1663
2503
 
1664
2504
  Reads every *.ndjson file in the global workspace logs directory, merges them into one timeline
@@ -1688,11 +2528,16 @@ The only exit-1 paths are a malformed query and no project.
1688
2528
  Device collectors and build records carry their named slot. Use --slot phone
1689
2529
  for that slot's timeline; its launch marker cannot hide a sibling's errors.
1690
2530
  Untagged legacy records belong to default. Metro/client records are shared and
1691
- usually untagged: use an unfiltered workspace query to inspect those errors.
2531
+ usually untagged, so every --slot query includes them, windowed by that slot's
2532
+ own launch and by bundles of the platforms it launched. When another slot's
2533
+ relaunch hides a Metro error from the unfiltered query, --slot <name> for the
2534
+ slot that printed it still shows it.
1692
2535
 
1693
2536
  FLAGS
1694
- --slot <name> only this slot's records (default includes untagged records)
1695
- --source <s...> metro, client, device, build, agent (one or more), or all.
2537
+ --slot <name> only this slot's records plus, once it has launched, the
2538
+ shared untagged Metro and app client records (not the web
2539
+ page's); default also includes untagged legacy records
2540
+ --source <s...> metro, client, device, build, agent, maintenance (one or more), or all.
1696
2541
  An unknown value is REJECTED rather than quietly matching
1697
2542
  nothing.
1698
2543
  --level <l> minimum level: debug, info, warn, error, fatal
@@ -1705,7 +2550,12 @@ FLAGS
1705
2550
  owned Chrome's device errors (platform web: failed
1706
2551
  requests, browser errors).
1707
2552
  Capped at 20 printed records.
1708
- --follow keep streaming until interrupted (Ctrl+C is exit 0)
2553
+ --follow keep streaming until interrupted (Ctrl+C is exit 0). Closing
2554
+ its stdout also ends it with exit 0; on Linux, so does the
2555
+ exit of the process that started it when stdout is a pipe.
2556
+ It notices within a few seconds even when nothing new
2557
+ arrives, except a closed stdout pipe on Linux whose starter
2558
+ is still running, which it notices at the next record.
1709
2559
  --json the raw records, one per line, so stdout is valid NDJSON.
1710
2560
  ZERO matches is ZERO bytes on stdout (an empty NDJSON
1711
2561
  stream), exit 0 -- parse stdout line by line, never as one
@@ -1755,9 +2605,19 @@ FLAGS
1755
2605
  errors are reported -- and nothing else. A failed attempt's own summary
1756
2606
  and details land at or after its marker, so they stay reported.
1757
2607
  a LAUNCH marker (src build, written before \`ios\` / \`android\` attempts
1758
- launch) resets EVERYTHING except the web page's records. It precedes the
1759
- tool call so an immediate native crash is not hidden by a marker written
1760
- after the process died.
2608
+ launch) resets EVERYTHING in its slot except the web page's records. It
2609
+ precedes the tool call so an immediate native crash is not hidden by a
2610
+ marker written after the process died. Metro and client output names no
2611
+ device, so in the unfiltered query the newest launch of any slot resets
2612
+ it, and the newest bundle of any platform resets Metro errors; with
2613
+ --slot <name>, that slot's newest launch and the newest bundle of a
2614
+ platform it launched do. Metro records that name their platform (bare
2615
+ React Native bundle records; the "iOS Bundled" / "Android Bundled" /
2616
+ "RSC(iOS) Bundled" tag Expo prints for bundle markers, so a "DOM
2617
+ Bundled" line does not close a native slot's window) follow only that
2618
+ platform, and a slot query leaves out iOS or Android ones when it never
2619
+ launched that platform. A single slot that runs both iOS and Android still shares one
2620
+ window for untagged Metro errors.
1761
2621
  a PAGE-LOAD marker (the web_navigation record \`stim web\` writes for each
1762
2622
  load of the page's top-level document) resets only the page's records,
1763
2623
  those with platform web. It is written when the load starts, so a
@@ -1800,9 +2660,12 @@ FLAGS
1800
2660
  In --follow mode the marker window is dropped -- every error arriving from
1801
2661
  then on is by definition after the last marker seen.
1802
2662
 
1803
- \`stim status\` reports the same count per workspace, as
1804
- logs.errorsSinceMarker: the same query, the same scope, so the two can never
1805
- disagree about whether this workspace is failing.
2663
+ \`stim status\` reports the errors of the same query and scope per
2664
+ workspace, as logs.errorsSinceMarker, so the two never disagree about
2665
+ whether this workspace is failing. It counts failures as the Stim Desktop and
2666
+ phone logs list them: a Metro bundle failure is one error, although it is a
2667
+ marker line, the error line and a failed bundle response in the log, so the
2668
+ count can be lower than the records this command prints.
1806
2669
 
1807
2670
  THE RECORD
1808
2671
  { ts, src, level, msg } always. ts is epoch milliseconds; src is one of
@@ -1822,7 +2685,14 @@ THE RECORD
1822
2685
  there are none. Stim adds it at query time; it is not in the
1823
2686
  log files.
1824
2687
 
2688
+ Maintenance records use src: maintenance and appear in the plain timeline.
2689
+ stim logs --source maintenance
2690
+ Add --errors to --source maintenance to show only maintenance_failure events;
2691
+ failures before a later launch marker are hidden. Machine passes are reported in
2692
+ status and maintenance/maintenance.ndjson; this command reads workspace logs.
2693
+
1825
2694
  WHAT WRITES WHAT
2695
+ maintenance.ndjson workspace maintenance actions, failures and explaining skips
1826
2696
  metro.ndjson the bundler, in both supervisor modes
1827
2697
  client.ndjson in-app console logs and redboxes -- BARE PROJECTS ONLY.
1828
2698
  In expo-child mode everything Expo prints lands in
@@ -1960,13 +2830,18 @@ WHAT WRITES WHAT
1960
2830
  archive rather than a stream. Streaming with full fidelity needs
1961
2831
  libimobiledevice or pymobiledevice3, which are third-party installs Stim
1962
2832
  does not require. See appandflow/stim#179.
1963
- agent no file. Each query reads agent-device's session
1964
- records (~/.agent-device/sessions, or
1965
- AGENT_DEVICE_STATE_DIR) for this workspace's owned
2833
+ agent agent input on the owned Chrome page is in
2834
+ web.ndjson (\`guide web\`). For simulators and
2835
+ emulators, no file: each query reads agent-device's
2836
+ session records (~/.agent-device/sessions, or
2837
+ AGENT_DEVICE_STATE_DIR, plus existing workspace
2838
+ agent-device directories) for this workspace's owned
1966
2839
  simulators and emulators, read-only. An action is
1967
2840
  info (msg is agent-device's summary, e.g. "Tapped
1968
2841
  (201, 731)"; event agent_action with command, session,
1969
- deviceId and details); a failed command is error
2842
+ deviceId and details, and startedAt when agent-device
2843
+ logged the command's start: ts is when it finished,
2844
+ after any --settle wait); a failed command is error
1970
2845
  (event agent_failed). A plain query includes them;
1971
2846
  --errors only with --source agent or all. iOS sessions
1972
2847
  match a simulator through their runner.log, Android
@@ -1997,6 +2872,23 @@ WHAT WRITES WHAT
1997
2872
  Every refusal listed here carries a stable CODE, whichever command prints it.
1998
2873
  Branch on the code, never on the message.`,
1999
2874
  sections: {
2875
+ STIM_HOSTING_REFUSED: {
2876
+ summary: "a named hosting Mac refused or could not confirm its iOS session; no local fallback",
2877
+ body: () => `STIM_HOSTING_REFUSED
2878
+ The message names the hosting Mac and its reason: unreachable or changed
2879
+ tailnet node, no installed simulator choice, no capacity, elevated or unknown
2880
+ memory pressure, or an unresolved reservation or delivery. Stim boots nothing
2881
+ locally and never tries another Mac. Check stim-server and Tailscale there,
2882
+ then run stim doctor. Correct --device-type / --runtime when the offer names
2883
+ an unavailable choice. A session that exists stays recorded even if delivery
2884
+ fails: retry stim ios --remote <machine>, or run stim stop to reconcile it.
2885
+ An unreachable stop keeps the placement; rerun stim stop when the host answers.
2886
+ A failed build handoff uses upload instead. If native log queries are unavailable,
2887
+ logs prints a note on stderr and shows copied records. Update an older stim-server
2888
+ on the host to enable iOS handoff and native logs (hello feature hosted-ios-data).
2889
+ A name outside hosting.machines is STIM_BAD_ARG. Missing or pending hosting
2890
+ approval keeps the doctor --fix and stim-server devices grant remedies.`
2891
+ },
2000
2892
  STIM_EAS_BUILD_MISSING: {
2001
2893
  summary: "no completed EAS development build matches; build only with session authorization",
2002
2894
  separator: "--- EAS BUILD CODES (`ios --eas-profile` / `android --eas-profile`) ---",
@@ -2163,8 +3055,12 @@ Branch on the code, never on the message.`,
2163
3055
  On a release cache hit Stim regenerates this workspace's JS bundle into a
2164
3056
  COPY of the cached artifact before installing it -- \`ios --configuration
2165
3057
  Release\` into a copy of the .app, \`android --variant ...Release\` into a
2166
- copy of the APK. When any step of that swap fails (the bundle command,
2167
- hermesc, the re-sign, zipalign, apksigner), the run does NOT install the
3058
+ copy of the APK. An iOS app that sets React Native's RCTUseAssetCatalog
3059
+ Info.plist key also gets its RNAssets.bundle image catalog recompiled with
3060
+ actool from the new bundle. When any step of that swap fails (reading the
3061
+ app's Info.plist, the bundle command, hermesc, actool, the expo-updates
3062
+ manifest refresh, the re-sign, zipalign, apksigner), the run does NOT
3063
+ install the
2168
3064
  cached artifact -- its baked-in JS is the builder's, not yours -- and does
2169
3065
  NOT fail: it prints a \`swap failed at <step>: ... --
2170
3066
  building fresh instead\` note on stderr and falls back to a full build. If
@@ -2278,6 +3174,39 @@ Branch on the code, never on the message.`,
2278
3174
  Legacy inline \`deletionClaim\` fields in config.json have no process
2279
3175
  identity and require separate manual inspection; \`stim guide lifecycle pool\`
2280
3176
  describes their field-only recovery.`
3177
+ },
3178
+ STIM_MACOS_OWNER_UNVERIFIED: {
3179
+ summary: "the macOS app owner cannot be verified; no signal is sent",
3180
+ body: () => `STIM_MACOS_OWNER_UNVERIFIED
3181
+ The recorded macOS process identity is unavailable or its workspace record
3182
+ is malformed. Stim sends no signal to an owner it cannot verify. Inspect the
3183
+ named process and workspace record before repairing it; do not replace a PID
3184
+ or token with another running app. Retry stim stop once identity inspection
3185
+ works. See stim guide macos.`
3186
+ },
3187
+ STIM_OFFLOAD_REFUSED: {
3188
+ summary: "the selected build machine cannot build this app",
3189
+ body: () => `STIM_OFFLOAD_REFUSED
3190
+
3191
+ A named --build-machine selection is strict. The message names the machine
3192
+ and why it cannot take or finish the build: not configured or paired, approval
3193
+ pending or denied, unreachable or changed pinned identity, incompatible
3194
+ toolchain/runtime/CPU, low disk, busy, sync/build failure, artifact failure,
3195
+ or a checkout that changed while it built. Unsupported build kinds also refuse.
3196
+ No local xcodebuild, Gradle or SwiftPM compile or alternate machine follows a
3197
+ refusal. Prebuild and pod install still run on this Mac before a named offload.
3198
+ Unlisted or unpaired names refuse at setup before consulting the cache, without
3199
+ a build record or failed-run stats. A listed paired name with a cache hit needs
3200
+ no build and does not contact the selected machine.
3201
+
3202
+ Run stim doctor --fix to ask for build access if not paired. Check
3203
+ stim settings get offload.machines. A person on the worker finds the id with
3204
+ stim-server devices and approves this Mac with stim-server devices grant <id> --build,
3205
+ or runs stim-server setup on the worker with its node, ticket and expiry
3206
+ (guide settings). Agents never run setup or approve requests.
3207
+ Rerun with --build-machine auto for normal placement and local fallback, or
3208
+ --build-machine local to keep the build here.
3209
+ `
2281
3210
  },
2282
3211
  STIM_CLAIM_UNAVAILABLE: {
2283
3212
  summary: "a process identity or warm claim store is unavailable, so the protected operation refuses",
@@ -2755,6 +3684,18 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
2755
3684
  holds one remote session, and Stim will not end the recorded one to make
2756
3685
  room -- it may be mid-run for whoever started it. Run \`stim stop\` for this
2757
3686
  workspace, then re-run with the platform you want. Nothing was created here.`
3687
+ },
3688
+ STIM_REMOTE_DEVICE_MISMATCH: {
3689
+ summary: "the recorded EAS session runs another model than --device-type asks for; stop, then rerun",
3690
+ body: () => `STIM_REMOTE_DEVICE_MISMATCH
3691
+ \`stim ios --remote eas --device-type <name>\` found this workspace's
3692
+ recorded EAS Simulator session still running another model, or the model
3693
+ EAS chose when no --device-type was given. EAS cannot change a running
3694
+ session's model, and Stim will not end the recorded session to make room --
3695
+ it may be mid-run for whoever started it. Run \`stim stop\` for this
3696
+ workspace, then rerun with the model you want. A recorded session that has
3697
+ already ended is replaced on the requested model instead, and without
3698
+ --device-type a live one is reused as it is. Nothing was created here.`
2758
3699
  },
2759
3700
  STIM_REMOTE_SESSION_STATE: {
2760
3701
  summary: "the EAS session was created but its state could not be recorded, so Stim stopped it",
@@ -2784,6 +3725,7 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
2784
3725
  The gate that proves a tunnel still reaches THIS workspace's Metro failed --
2785
3726
  before a session or a build, whether the tunnel is Expo's own, one Stim
2786
3727
  started (metro.tunnel: cloudflared/ngrok/auto), or a named metro.publicUrl.
3728
+ A recorded tailscale tunnel skips this public probe (\`guide metro\`).
2787
3729
  The usual cause: the tunnel was built for a port this workspace no longer
2788
3730
  holds (a stale one survived a \`stop\`/\`start\` that reserved a different
2789
3731
  port), and it now serves ANOTHER workspace's dev server -- healthy, and
@@ -2848,6 +3790,11 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
2848
3790
  relaunches that app on that device with this workspace's Metro port and loses
2849
3791
  in-memory state. Keep the existing --session flag and verify afterward.
2850
3792
 
3793
+ ADB COULD NOT LIST OR RESTORE THE REVERSE. Before an Android reload Stim
3794
+ checks the emulator's adb reverse for the Metro port. When adb cannot list
3795
+ or re-apply it, nothing was reloaded; the remedy names the adb reverse
3796
+ command to run before reloading again.
3797
+
2851
3798
  MORE THAN ONE MATCHING PEER IS NOT A FAILURE. A workspace Metro serves one
2852
3799
  app, so several matching peers are that app on several devices. Stim reloads
2853
3800
  every one of them and reports the count in the facts as targets.`
@@ -2971,12 +3918,22 @@ captured" (in metro.ndjson, bare RN)
2971
3918
  The command refused before doing anything: an unusable --wait value, a known
2972
3919
  setting with the wrong type ("Invalid <key> setting <value>. Expected <shape>."
2973
3920
  -- \`guide settings\` names the type each key takes), an invalid
2974
- Metro tunnel setting, an invalid android.dataPartitionSizeGb value, an unsafe
3921
+ Metro tunnel setting, \`stim macos\` with macos.product or macos.infoPlist
3922
+ unset (set both explicitly in .stim.json; see \`stim guide macos\`),
3923
+ \`stim macos --remote eas|proxy|auto\` (name a hosting Mac instead),
3924
+ an invalid android.dataPartitionSizeGb value, an unsafe
2975
3925
  android.avdConfig key or fragment, a malformed ios.signingIdentity,
2976
- ios.signingIdentitySha1 or ios.lanHost value, \`--device\` with an empty
2977
- serial or UDID, \`--device\` together with \`--remote\`, \`ios --device-type\`
2978
- or \`--runtime\` on a remote run (\`--remote\` or ios.remote; the remote
2979
- backend picks its own device), a working directory
3926
+ ios.signingIdentitySha1 or ios.lanHost value, a metro.port or
3927
+ STIM_METRO_PORT that another workspace reserves or another process holds,
3928
+ a changed pin while this workspace's dev server runs or its supervisor
3929
+ cannot be verified,
3930
+ \`--device\` with an empty
3931
+ serial or UDID, \`--device\` together with \`--remote\`, \`ios --runtime\`
3932
+ on an eas/proxy run (\`--remote\` or ios.remote; that backend picks the
3933
+ iOS version), \`ios --device-type\` on the proxy backend or with an
3934
+ eas-cli older than 22.2.0 on the eas backend, \`android --system-image\` or
3935
+ \`--device-profile\` on a remote run (\`--remote\` or android.remote), a
3936
+ working directory
2980
3937
  with no package.json above it, or one whose nearest package.json does not
2981
3938
  parse or depends on neither react-native nor expo, so the directory is not
2982
3939
  an app (the refusal names that package.json and says which of the two it
@@ -2996,7 +3953,10 @@ captured" (in metro.ndjson, bare RN)
2996
3953
  ios.deviceType, ios.runtime, android.systemImage or android.deviceProfile
2997
3954
  setting is checked the same way, and the check applies
2998
3955
  even when this workspace ALREADY owns a device, so a name that could never
2999
- create anything is caught rather than left to a later run. The
3956
+ create anything is caught rather than left to a later run. When the bad
3957
+ value came from a settings layer rather than a flag, the message also names
3958
+ the layer (workspace, repo, committed, or machine) it is set at, since all
3959
+ four settings are readable from the machine layer too. The
3000
3960
  \`pixel_fold\` and \`resizable\` profiles on a system image without
3001
3961
  foldable support (SupportPixelFold in its advancedFeatures.ini) refuse the
3002
3962
  same way, since the emulator quits on boot; the remedy names an installed
@@ -3010,6 +3970,12 @@ captured" (in metro.ndjson, bare RN)
3010
3970
  is on stdout, except \`logs --json\`, whose stdout stays empty NDJSON; the
3011
3971
  refusal is on stderr. \`stop\` outside a project refuses rather than
3012
3972
  reporting that nothing was running.
3973
+ For a changed Metro pin, run stim stop before retrying, or unset or restore
3974
+ 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
3975
+ own Metro on the pin is attached to. stim web follows the same rules when
3976
+ its page uses Metro. ios and android with --no-metro-check use the pin,
3977
+ then the recorded port, then 8081, without probing or reserving; an invalid
3978
+ pin still refuses.
3013
3979
  These errors are caught before the port is reserved and before any build or
3014
3980
  device work, so nothing was started. The one listing they need
3015
3981
  (\`simctl list runtimes\`, the SDK's system-images directory, \`avdmanager
@@ -3123,6 +4089,16 @@ captured" (in metro.ndjson, bare RN)
3123
4089
  Matching this workspace's port or directory does not authorize cleanup,
3124
4090
  and stop has no override for process ownership.
3125
4091
 
4092
+ "port <n> is in use by <holder>; stim start will choose a free port" (status)
4093
+ A note, not a problem: no supervisor of this workspace runs, and
4094
+ \`stim start\` reserves a free port when its own is taken. The holder's pid
4095
+ and directory are in \`status --json\` under metro.heldBy.
4096
+
4097
+ "port <n> is in use by <holder>, so this workspace's Metro cannot serve on
4098
+ it" (status)
4099
+ This workspace's supervisor runs but another process answers its port.
4100
+ Run \`stim stop\`, then \`stim start\`, which reserves a free port.
4101
+
3126
4102
  "stop refusing to signal supervisor pid <n>: ..." (stop)
3127
4103
  The records disagree, the saved OS identity is unavailable, or it records a
3128
4104
  port this project did not reserve. If process inspection is denied, retry
@@ -3176,6 +4152,10 @@ not on any remote" (worktree remove)
3176
4152
  so per file -- those files die with the worktree either way, and a lockfile
3177
4153
  change anyone meant would have been committed. ONE other dirty path and the
3178
4154
  whole set is refused, churn included, so this never eats real work.
4155
+ Untracked \`.watchman-cookie-*\` files, which watchman writes into the roots
4156
+ it watches, are not work either: \`worktree remove\` deletes them itself and
4157
+ ignores them when it counts dirty paths. Any other untracked file still
4158
+ refuses.
3179
4159
  When it does refuse, the refusal PRINTS THE DIRTY PATHS, and the restore
3180
4160
  command under it carries those same paths: run it as printed rather than
3181
4161
  reaching for --force.
@@ -3306,9 +4286,9 @@ not on any remote" (worktree remove)
3306
4286
  A line such as \`npm warn exec ... will be installed\` is normal when using
3307
4287
  the no-install form.
3308
4288
 
3309
- "Unsupported engine" or a syntax error before Stim starts
3310
- Stim requires Node 22.12.0 or later.
3311
- Switch Node versions, then run the command again.
4289
+ "Unsupported engine" from npm, or a syntax error before Stim starts
4290
+ Stim requires Node 22.12.0 or later. Stim itself refuses an older Node
4291
+ with STIM_NODE_UNSUPPORTED; see that section.
3312
4292
 
3313
4293
 
3314
4294
  "Found no free Metro port between ..."
@@ -3391,6 +4371,30 @@ not on any remote" (worktree remove)
3391
4371
  Expo build-cache provider would each use a different store. Set the named
3392
4372
  variable to an absolute path, or unset it to use the default. Metro and the
3393
4373
  cache provider, which cannot refuse, ignore a relative value with a warning.`
4374
+ },
4375
+ STIM_NODE_UNSUPPORTED: {
4376
+ summary: "stim or stim-server started on a Node older than 22.12.0, often a project pin",
4377
+ body: () => `STIM_NODE_UNSUPPORTED ("Stim needs Node <floor> or later; this is Node <version> at <path>")
4378
+ stim and stim-server refuse before loading anything else when the Node that
4379
+ runs them is older than their engines floor. Both start through
4380
+ \`#!/usr/bin/env node\`, so the working directory can choose that Node: asdf,
4381
+ mise and Volta's node shim follow the project's .nvmrc, .node-version,
4382
+ .tool-versions or package.json, and nvm and fnm do when a shell hook
4383
+ switches versions on cd. A Stim installed with Volta keeps the Node it was
4384
+ installed with. Run Stim with a supported Node for that one command:
4385
+
4386
+ ASDF_NODEJS_VERSION=<version> stim <command>
4387
+ mise exec node@<version> -- stim <command>
4388
+ volta run --node <version> stim <command>
4389
+ fnm exec --using=<version> stim <command>
4390
+ nvm exec --silent <version> stim <command>
4391
+
4392
+ <version> is 22.12.0 or later. asdf, mise, fnm and nvm keep global packages
4393
+ per Node version, so when Stim was installed with npm under one of them, use
4394
+ the version it was installed with; asdf names it when it prints "No version
4395
+ is set for command stim" instead of running Stim. When that version is
4396
+ older than 22.12.0, first install Stim with npm under a supported version.
4397
+ The tools Stim starts inherit the override.`
3394
4398
  }
3395
4399
  }
3396
4400
  },
@@ -3481,8 +4485,10 @@ apply. The run then reports \`devServer: { started: true, reason }\` in its
3481
4485
  JSON, with reason "not running" or "stopped (idle)". When that start fails,
3482
4486
  the run refuses with the start's own code (STIM_METRO_TIMEOUT,
3483
4487
  STIM_SUPERVISOR_EXITED, ...) before any build work. Release builds and
3484
- \`--no-metro-check\` neither check nor start the dev server. \`--remote\`
3485
- starts it the way \`stim start --remote\` does. Step 2 remains useful to warm
4488
+ \`--no-metro-check\` neither check nor start the dev server; hosted Debug iOS
4489
+ refuses --no-metro-check with STIM_BAD_ARG. \`--remote\`
4490
+ with eas or proxy starts it the way \`stim start --remote\` does. A named
4491
+ hosting Mac keeps Metro local through a private bridge; see hosted-ios. Step 2 remains useful to warm
3486
4492
  Metro while other work happens.
3487
4493
 
3488
4494
  Repeat step 3 whenever a NATIVE input changes. A JS-only edit needs nothing --
@@ -3501,6 +4507,17 @@ ambiguous no-platform request. Every reload goes over this workspace's Metro
3501
4507
  websocket, on both platforms. It never reopens a development-client URL,
3502
4508
  because that restarts the app rather than reloading its JavaScript.
3503
4509
 
4510
+ An Android reload first lists \`adb reverse\` on each live owned emulator
4511
+ launched against this Metro and re-applies the Metro port's reverse where it is
4512
+ missing. adb ties a reverse to its transport, and a transport that drops and
4513
+ reconnects comes back without it and without any error, which leaves the app
4514
+ cut off from Metro and Fast Refresh. After restoring one, Stim waits up to 8
4515
+ seconds, sending nothing, until the app on every emulator it checked is
4516
+ connected again, then sends the reload once. A dev server that cannot name
4517
+ its clients (bare React Native) gets a fixed 2.5-second wait instead. \`reverseRestored\` in the
4518
+ facts names the serials it restored. \`stim status\` reports the same loss as
4519
+ the android-reverse-missing issue.
4520
+
3504
4521
  How the message is addressed depends on the dev server, and \`strategy\` in
3505
4522
  the facts reports which you got. Where Metro can name its clients, Stim
3506
4523
  addresses every peer matching the platform and reports \`metro-websocket\`. A
@@ -3546,9 +4563,10 @@ If the app is stuck before loading its first bundle, restart its process with
3546
4563
  the printed force-stop and launcher commands. Foregrounding the same process
3547
4564
  does not restart initialization. Confirm the bundle request and expected UI.
3548
4565
 
3549
- Debug Android launches on an owned emulator created in the same run get up to
3550
- 60 seconds for bundle loading; the verify phase names that budget. Existing
3551
- emulators, remote targets, and physical devices keep the 20-second budget.
4566
+ Debug Android launches on an owned emulator that the run creates, or whose
4567
+ adoption from the pool it finishes, get up to 60 seconds for bundle loading;
4568
+ the verify phase names that budget. Existing emulators, remote targets, and physical devices keep the
4569
+ 20-second budget.
3552
4570
  Bundle delivery still requires the usual stability or app readiness check;
3553
4571
  an observed fatal error ends verification without waiting for the deadline.
3554
4572
 
@@ -3634,6 +4652,166 @@ WAITING FOR A CHANGE
3634
4652
  still running, which it notices at the next change.
3635
4653
  Without --json it reprints the human view on change.`,
3636
4654
  sections: {
4655
+ "hosted-ios": {
4656
+ summary: "iOS on a named approved Mac: strict placement, private Metro, status and stop",
4657
+ body: () => `IOS ON A HOSTING MAC
4658
+
4659
+ If Stim is not installed globally, replace stim with npx stim.
4660
+
4661
+ stim settings set hosting.machines '["janics-mac-mini"]'
4662
+ stim doctor --fix
4663
+ # A person there approves: stim-server devices grant <id> --device-host
4664
+ # Or runs stim-server setup on the worker with its client node, ticket and expiry.
4665
+ stim doctor
4666
+ stim ios --remote janics-mac-mini --device-type "iPhone 17 Pro" --runtime "iOS 27.0"
4667
+ stim status --json
4668
+ stim reload ios
4669
+ stim stop
4670
+
4671
+ The same setup serves macos --remote. Approval is separate from build, read
4672
+ and phone control. Set ios.remote to the machine name for a workspace default;
4673
+ no flag or setting runs here. eas and proxy keep their remote backend meanings.
4674
+ auto is accepted but refuses with STIM_BAD_ARG until automatic placement ships.
4675
+ Android --remote <machine> refuses with STIM_BAD_ARG: Android on a paired Mac
4676
+ is not available yet.
4677
+
4678
+ A named Mac is strict: STIM_HOSTING_REFUSED names its reason; nothing boots
4679
+ here or elsewhere. --device cannot use a hosting Mac, and --simulator-app
4680
+ refuses. The host boots headless and ignores this Mac's iosSimulatorApp setting.
4681
+ --slot, --device-type and --runtime select the hosted simulator; --scheme,
4682
+ --configuration and --eas-profile select the app as usual. The build targets
4683
+ the offered simulator architecture, not this Mac's. --build-machine remains
4684
+ independent and selects a compatible build worker for a Debug cache miss.
4685
+
4686
+ Stim builds or fetches before reserving, records the session immediately, then
4687
+ uploads the app on every run. Debug keeps Metro here; its supervisor owns a
4688
+ private gateway bound to this Mac's Tailscale address, pinned to the host peer
4689
+ and protected by a per-session secret. The host exposes only a loopback bridge.
4690
+ No start --remote is needed; metro.tunnel and metro.publicUrl are ignored.
4691
+ Debug requires a running supervisor with private gateway support before reserving
4692
+ a simulator. --no-metro-check refuses with STIM_BAD_ARG for hosted Debug runs.
4693
+ If its supervisor is missing or older, run stim stop; stim start, then retry.
4694
+ Non-Debug runs skip Metro. Launch is unverified until this workspace's Metro
4695
+ provides bundle evidence, or the host proves a live release process.
4696
+
4697
+ A rerun on the same named Mac reattaches and delivers a new app attempt. A
4698
+ stopped or missing session is replaced; an unreachable or unknown owner refuses
4699
+ replacement. Switching between this Mac and a hosting Mac, or to a different
4700
+ hosting Mac, refuses for that slot: run stim stop first.
4701
+ Status adds ios.host { machine, session, selected, device: { name, runtime },
4702
+ agent, state } per slot. A shutdown local simulator is replaced in status; a
4703
+ booted or unknown local simulator stays visible alongside ios.host with a warning.
4704
+ The default slot appears only in ios, never in slots[]. No host UDID or gateway secret enters local device state or
4705
+ status. Ready is normal; stopped has a rerun hint; unknown or unreachable is
4706
+ unverified. A host server restart stops its sessions.
4707
+
4708
+ stop and worktree remove wait for the host to stop, delete exactly its owned
4709
+ simulator without parking, close the gateway and clear placement. stop --json
4710
+ reports each hosted slot under outcomes.device["ios:host:<slot>"], retaining
4711
+ local device outcomes and successful siblings when another slot fails. Revoked or
4712
+ missing sessions clear placement too. An unreachable host retains placement
4713
+ and fails cleanup: rerun stim stop when it answers. Missing or unapproved local
4714
+ credentials require stim doctor to restore access before retrying stop. An
4715
+ unreadable machine or session remains recorded; status warns and stop names the
4716
+ workspace-state key to inspect. Only that slot's ios run refuses; restore its
4717
+ recorded machine and session from the host, then run stim stop for that slot.
4718
+ Reload returns a JSON error with that remedy when no other app can reload;
4719
+ logs remain readable. Unknown selection or agent metadata is normalized
4720
+ without discarding the owner. devices.idleShutdownMinutes
4721
+ does not stop a hosted simulator in this phase; its recorded session keeps Metro
4722
+ from idle stopping. Reruns upload only missing manifest and file digests from a
4723
+ session-scoped store; the host verifies bytes before reuse. When the iOS build
4724
+ ran on the hosting Mac's pinned node, it takes matching files from that build.
4725
+ A refused or timed-out handoff falls back to upload, retrying a still-busy host
4726
+ for at most one minute.
4727
+
4728
+ stim logs and stim logs --errors pull native device records from bounded host
4729
+ queries. Concurrent followers share a collection, throttled per session, without
4730
+ blocking app delivery, viewing or control. Stop limits each log drain to 30 seconds with
4731
+ progress on stderr and a no-progress guard. The host collects a bounded final
4732
+ tail before deletion, including on revocation or server close; stop copies it
4733
+ back afterwards. If the final collection drops a backlog interval and eventually
4734
+ succeeds, a device warning record names that interval. A damaged collection checkpoint is ignored and
4735
+ rebuilt; it never blocks reading collected records. The client waits up to
4736
+ 180 seconds for stop. The final collection and worker termination paths fit within
4737
+ that wait; an in-flight handoff copy and closing Metro or view transports are
4738
+ outside those worker bounds.
4739
+ Collected records remain in the session home after deletion;
4740
+ app blobs and materialized bundles are removed. Native queries read persisted
4741
+ entries, overlap by five seconds and de-duplicate; info-level or later-persisted
4742
+ entries may be unavailable. JavaScript logs arrive through Metro.
4743
+ The host advertises hosted-ios-data for iOS handoff and native logs. An older
4744
+ host gets an update note, uses uploads and shows logs already copied here.
4745
+
4746
+ AGENT CONTROL
4747
+
4748
+ On the hosting Mac, set hosting.agentDriver to agent-device. Both Macs need
4749
+ agent-device 0.21.20 or later (the policy and remote-config contract). The host
4750
+ starts one loopback daemon per installed iOS session under a child-aware
4751
+ ownership claim. Its daemon policy pins exactly that simulator UDID and denies
4752
+ shutdown. Stop, close, revocation, reinstall and graceful server close end its
4753
+ daemon and grant. A server crash can leave detached daemon processes and claims;
4754
+ restart alone does not prove they stopped. A daemon exit restarts only that
4755
+ session and rotates its grant; rerun stim ios to reattach and refresh the config.
4756
+ An unresolved proxy or daemon retains its claim and blocks reinstall and native
4757
+ teardown; clear a named claim only after proving its owner and child are gone.
4758
+
4759
+ The client writes a separate 0600 remote-config file for each workspace slot.
4760
+ Status prints agent: <command>; ios.host.agent in --json carries driver,
4761
+ remoteConfig and command, never the token. Use the returned file:
4762
+
4763
+ agent-device open <bundleId> --remote-config <file>
4764
+ agent-device snapshot --remote-config <file>
4765
+ agent-device click <ref> --remote-config <file>
4766
+ agent-device screenshot --remote-config <file>
4767
+
4768
+ The command shape is agent-device <command> --remote-config <file>. Open leases
4769
+ the simulator automatically with ios-instance and the proxy provider, scoped
4770
+ to this session's tenant. Close releases it; Stim still owns the simulator.
4771
+ The config names daemonBaseUrl, daemonAuthToken, tenant, sessionIsolation,
4772
+ runId, clientId, deviceKey, leaseBackend, leaseProvider and platform; it carries
4773
+ no preallocated leaseId. stim stop closes the matching agent-device connection
4774
+ and removes that slot's config after the host confirms stop or revocation.
4775
+ The host stops the session daemon and identity-checked agent-device runners
4776
+ targeting its exact simulator UDID before deleting the simulator, even without close.
4777
+
4778
+ Allowed: devices, open, close, snapshot, diff, wait, find, get, is, click, fill,
4779
+ press, type, focus, scroll, screenshot, longpress, swipe, back, home, orientation,
4780
+ appstate, alert and batch. These inspect or interact with the simulator and
4781
+ installed apps. devices lists only the session's simulator, filtered by the
4782
+ daemon policy. Its device claims are session-local and record / as the workspace;
4783
+ a host-side agent-device using the same simulator is not visible to the hosted daemon.
4784
+ Client device selectors are stripped and requests are pinned to iOS and that
4785
+ UDID; an explicitly named foreign UDID is refused. Selecting another device,
4786
+ boot/shutdown/erase, installs, uninstall, uploads, push, record, logs, network,
4787
+ perf, trace, clipboard and settings are refused. Stim installs; the agent cannot
4788
+ enumerate other host devices or write host files. Ambient client fields (cwd,
4789
+ stateDir, config and remoteConfig) are stripped, including inside batch steps.
4790
+ Host paths and launch inputs (--out, baseline, launchConsole, developerDir,
4791
+ installSource and similar fields) are refused, including inside batch steps.
4792
+ Only agent-device's generated screenshot temp artifact is accepted and returned
4793
+ to the client; arbitrary host screenshot destinations are refused. Runtime
4794
+ hints are dropped and only reporting/artifact metadata passes through.
4795
+
4796
+ If hosting.agentDriver is none, the driver cannot enforce its policy, or the
4797
+ host does not advertise hosted-ios-agent, agent reports
4798
+ { driver: 'none', setting: 'hosting.agentDriver' }. Update the host and check
4799
+ its setting and agent-device installation. No other simulator becomes reachable.
4800
+ Client hello carries only name and version, not features, so the host cannot gate
4801
+ daemon startup or grant issuance on client support. The client checks the host's
4802
+ hosted-ios-agent feature before using a grant.
4803
+
4804
+ Stim Desktop and the phone app view and control the simulator through this
4805
+ Mac's stim-server relay, with an "on <machine>" label. Turn on Serve to phones
4806
+ in Desktop; pair the phone with this Mac, not the hosting Mac. frames.subscribe
4807
+ and control.begin address workspace, platform ios and slot, including named
4808
+ slots. The relay shares one authenticated host connection with hosted macOS
4809
+ viewers. Stopped sessions show a rerun remedy; unavailable hosts report why
4810
+ frames cannot arrive. Replay, duoFrame and physical targets are refused for
4811
+ hosted subscriptions. Desktop hides local Simulator.app, rotation, hardware
4812
+ buttons and simulator options; touch and text use the relay. The phone can also
4813
+ send the host's supported button, rotation and posture inputs.`
4814
+ },
3637
4815
  eas: {
3638
4816
  summary: "download a matching EAS development build; explicit profile, costs, cache and miss remedies",
3639
4817
  body: () => `EAS DEVELOPMENT BUILDS
@@ -3822,8 +5000,9 @@ result as proof instead of requiring an unrelated screenshot.`
3822
5000
  The labels are a closed set, and nothing else is ever printed in that
3823
5001
  column:
3824
5002
 
5003
+ agent-device
3825
5004
  branch budget build cache caches carry
3826
- checkout deps device
5005
+ checkout daemons deps device
3827
5006
  devices error failed findings fingerprint gems
3828
5007
  install installs ip.txt lan launch lease
3829
5008
  lock log
@@ -3832,7 +5011,7 @@ result as proof instead of requiring an unrelated screenshot.`
3832
5011
  result
3833
5012
  services
3834
5013
  setting settings setup state stats stop
3835
- storage swap verify version workspace
5014
+ storage swap verify version workspace worktrees
3836
5015
 
3837
5016
  \`app\` and \`compilation cache\` join them in the stdout block a successful
3838
5017
  run ends with. When a native build runs, its compilation-cache result is
@@ -3943,6 +5122,16 @@ result as proof instead of requiring an unrelated screenshot.`
3943
5122
  is never touched: \`Reclaimed the environment; the working tree stays (it
3944
5123
  is the source checkout).\`
3945
5124
 
5125
+ \`gc\` names each slow step on stderr as it starts, in every mode; its
5126
+ report stays on stdout, and with \`--json\` stdout carries only the payload:
5127
+
5128
+ caches measuring 18 shared caches
5129
+ devices listing simulators and emulators
5130
+ worktrees checking 26 projects for merged or idle worktrees
5131
+ daemons watchman pid 49040: checking 12 roots
5132
+ daemons removing stale watchman root /w/app-409
5133
+ daemons waiting up to 10s for 1 Gradle daemon to exit: pid 51234
5134
+
3946
5135
  A GAP BETWEEN HEARTBEATS IS NOT A HANG. Stim runs device tools
3947
5136
  synchronously, so a long \`simctl\`, \`adb\` or copy call holds the timer
3948
5137
  until it returns; the next heartbeat then lands on the grid, which is why an
@@ -3950,26 +5139,40 @@ result as proof instead of requiring an unrelated screenshot.`
3950
5139
  killing a run.`
3951
5140
  },
3952
5141
  pool: {
3953
- summary: "parked and adopted simulators: what park and adoption clear or keep, the model and runtime match",
5142
+ summary: "parked and adopted simulators and emulators: what park and adoption clear or keep, the model and runtime match, emulator storage, gc erase",
3954
5143
  body: () => ` THE SIMULATOR POOL
3955
5144
  \`worktree remove\` PARKS this workspace's owned simulator instead of
3956
5145
  deleting it, and the next workspace that wants the same model and runtime
3957
- ADOPTS it. Adoption reuses the simulator but not its contents:
5146
+ ADOPTS it. Adoption reuses the simulator and the app installed on it, but
5147
+ not the previous workspace's state:
3958
5148
 
3959
- at park shut down, \`simctl erase\` (apps, app data, keychain,
3960
- privacy grants, pasteboard, photos, settings), renamed
3961
- \`stim-parked (<model> <runtime>) <4 hex>\`
5149
+ at park shut down and renamed
5150
+ \`stim-parked (<model> <runtime>) <4 hex>\`; nothing is
5151
+ erased, and the pool record names the installed app's
5152
+ bundle id and build cache key
3962
5153
  at adoption renamed for the adopting workspace, then, inside the boot
3963
5154
  the run pays anyway, \`simctl privacy reset all\` and
3964
- \`simctl keychain reset\`; at install, any other app is
3965
- uninstalled
3966
-
3967
- An erased simulator takes about 18 MB on disk instead of the gigabytes a used
3968
- one holds. Its first boot after the erase takes about 15s instead of a warm
3969
- boot's few seconds, and the app is always installed again. A SimSlim
3970
- profile survives the erase.
3971
-
3972
- Park erases only a simulator that reports Shutdown. It waits up to 15s for
5155
+ \`simctl keychain reset\`; at install, every other user app
5156
+ is uninstalled, and this workspace's app, when it is
5157
+ installed, has its preferences domain deleted (an
5158
+ expo-dev-client app's is replaced by the two dev-menu-off
5159
+ keys, see \`guide facts devmenu\`) and its data
5160
+ container's Documents, Library, tmp and SystemData emptied
5161
+
5162
+ Adoption always clears the app's data, whichever workspace parked the
5163
+ simulator: a new workspace starts from a fresh app state. The app itself
5164
+ stays installed, so the usual byte comparison decides the install: the same
5165
+ build prints \`install unchanged\` and skips the copy. A parked record's
5166
+ cache key that differs from this run's skips that comparison. App-group
5167
+ containers, the pasteboard, photos, Safari data and Simulator settings are
5168
+ not cleared; \`gc --cache parked --delete\` erases a parked simulator when a
5169
+ clean system image is needed.
5170
+
5171
+ A parked simulator keeps its apps and data, so it can hold gigabytes. It
5172
+ uses the Mac's disk, not a fixed partition, so there is no free-space check
5173
+ on iOS; \`stim gc\` lists each parked simulator's app and size.
5174
+
5175
+ Park takes only a simulator that reports Shutdown. It waits up to 15s for
3973
5176
  the shutdown to settle, retries the shutdown once, and waits up to 15s more.
3974
5177
  A simulator that is still booted after that is deleted instead, and
3975
5178
  \`worktree remove\` prints
@@ -3996,6 +5199,11 @@ result as proof instead of requiring an unrelated screenshot.`
3996
5199
  the next attempt, unless it may have left native work running. Deletion also
3997
5200
  puts an opaque marker on the pool record so older Stim versions keep it
3998
5201
  protected. New versions resolve that marker through the same identity claim.
5202
+ When a marker outlives its owner, Stim cannot tell whether the erase or
5203
+ deletion ran, so adoption treats the device as erased and drops the recorded
5204
+ app, cache key and scheme approvals. A \`gc --cache parked --delete\` erase
5205
+ that fails after the erase command started keeps the device parked without
5206
+ them for the same reason.
3999
5207
  A crash during a synchronous device-tool call leaves that work unverifiable; inspect
4000
5208
  the old process and its native children before following the claim's removal
4001
5209
  remedy. See \`stim guide errors STIM_CLAIM_REFUSED\`.
@@ -4010,8 +5218,7 @@ result as proof instead of requiring an unrelated screenshot.`
4010
5218
 
4011
5219
  device stim-app-412 (iPhone 17 26.5) (9C1F..) adopted (17s)
4012
5220
 
4013
- That time includes the first boot after the erase and the two resets, so it
4014
- runs longer than a plain boot.
5221
+ That time includes the two resets, so it runs longer than a plain boot.
4015
5222
  \`stim status\` prints one line while the pool is not empty:
4016
5223
 
4017
5224
  pool: 2 parked iOS simulators (max 3)
@@ -4019,12 +5226,21 @@ result as proof instead of requiring an unrelated screenshot.`
4019
5226
  \`stim gc\` reports the pool, and \`stim gc --delete\` empties every entry
4020
5227
  it can re-verify:
4021
5228
 
4022
- Parked simulators (2, 36 MB):
4023
- ios stim-parked (iPhone 17 26.5) 9c1f (9C1F..) iPhone 17 26.5 parked 3d ago 18 MB
4024
- --delete attempts every parked simulator and keeps failures.
5229
+ Parked simulators (2, 4.1G):
5230
+ ios stim-parked (iPhone 17 26.5) 9c1f (9C1F..) iPhone 17 26.5 parked 3d ago com.example.app 2.3G
5231
+ --delete attempts verified deletions and keeps failures.
5232
+
5233
+ \`stim gc --cache parked\` reports only the pool, and with --delete ERASES
5234
+ each parked simulator (\`simctl erase\`) and emulator (user data and
5235
+ snapshots) it can re-verify instead of deleting it. The device stays parked
5236
+ for adoption, with no app recorded; the next adopter installs its app again.
5237
+ \`--cache all\` leaves them alone. Erase takes only a simulator
5238
+ that reports Shutdown and an emulator that is not running and no workspace
5239
+ references, under the same per-device claim as adoption.
4025
5240
 
4026
- \`gc --older-than <days>\` reports and deletes only the devices parked at
4027
- least that long; one parked at an unknown time stays.
5241
+ \`gc --older-than <days>\` reports and deletes, or with \`--cache parked\`
5242
+ erases, only the devices parked at least that long; one parked at an unknown
5243
+ time stays.
4028
5244
 
4029
5245
  If simulator listing or deletion fails, \`gc --delete\` reports the failure
4030
5246
  and keeps that entry. It never turns an unverified absence into a dropped
@@ -4041,15 +5257,15 @@ result as proof instead of requiring an unrelated screenshot.`
4041
5257
  \`pool.androidParkedMax\` (default 3) or STIM_POOL_ANDROID_PARKED_MAX.
4042
5258
  A redirected STIM_HOME disables parking unless that environment override
4043
5259
  is set. Zero disables parking and adoption. \`stop\` keeps the assignment;
4044
- \`worktree remove\` parks eligible AVDs, and \`gc --delete\` empties the pool
4045
- (with --older-than, only the AVDs parked that long).
5260
+ \`worktree remove\` parks eligible AVDs, \`gc --delete\` empties the pool
5261
+ and \`gc --cache parked --delete\` wipes their user data (with
5262
+ --older-than, only the AVDs parked that long).
4046
5263
 
4047
5264
  Adoption matches the system image, data partition size, and the creation
4048
5265
  settings from android.avdConfig / android.avdConfigFile. The AVD keeps its
4049
- original stim-<label> name. Parking wipes its user data, cache and
4050
- encryption-key images and its snapshots, the files the emulator recreates
4051
- on the next boot, so a parked AVD takes a few MB and adoption pays a cold
4052
- boot (about 20s) and a fresh install.
5266
+ original stim-<label> name. Parking keeps its user data and Quick Boot
5267
+ snapshot, so the installed app stays and the pool record names its package
5268
+ and build cache key.
4053
5269
  Incompatible AVDs stay parked until eviction or GC. AVDs created by older
4054
5270
  versions without a recorded creation configuration are deleted at removal.
4055
5271
 
@@ -4065,10 +5281,9 @@ result as proof instead of requiring an unrelated screenshot.`
4065
5281
  manually clearing a claim, and keep the AVD data while its process state
4066
5282
  cannot be verified.
4067
5283
 
4068
- Android cleanup happens AFTER boot, before install or launch, for AVDs
4069
- parked with their data by older versions: \`adb shell pm clear\` clears
4070
- the adopting app's data while retaining its APK, and other third-party apps
4071
- are uninstalled. If ADB goes offline or closes the
5284
+ Android cleanup happens AFTER boot, before install or launch:
5285
+ \`adb shell pm clear\` clears the adopting app's data while retaining its
5286
+ APK, and other third-party apps are uninstalled. If ADB goes offline or closes the
4072
5287
  connection, Stim waits for boot readiness and retries cleanup for up to 30
4073
5288
  seconds, verifying the same owned AVD before each destructive command.
4074
5289
  Failed cleanup blocks launch and remains pending for a retry. The installed APK's SHA-256 must match the
@@ -4077,10 +5292,25 @@ result as proof instead of requiring an unrelated screenshot.`
4077
5292
  If the retained APK has a conflicting signer or version, adoption uninstalls
4078
5293
  it and retries installation. Adoption stays pending until installation succeeds.
4079
5294
 
5295
+ An emulator's /data partition is fixed, so an adopted AVD can run out of
5296
+ room. Before installing, adoption reads the free space on /data
5297
+ (\`adb shell df -k /data\`) and needs twice the APK size plus 512 MB. When
5298
+ there is less, it runs \`pm trim-caches\`; when that is not enough, it
5299
+ shuts the emulator down, wipes its user data through centralized teardown,
5300
+ boots it again and installs:
5301
+
5302
+ device stim-app has 97M free on /data after removing other apps and trimming caches; wiping its user data
5303
+ device stim-app (emulator-5556) booted with wiped user data
5304
+
5305
+ An install that fails with INSTALL_FAILED_INSUFFICIENT_STORAGE gets the
5306
+ same steps: trim caches and retry, then wipe and retry. Only an adopting
5307
+ run does this; a workspace's own emulator never loses data to it.
5308
+
4080
5309
  The wipe resets apps, accounts, device settings and the shared storage
4081
5310
  inside the data partition; the AVD's creation settings and any separate
4082
5311
  SD card image remain. Status lists parked Android emulators; GC
4083
- reports their system image, hardware profile, age and disk size.
5312
+ reports their system image, hardware profile, age, recorded app and disk
5313
+ size.
4084
5314
  `
4085
5315
  },
4086
5316
  builds: {
@@ -4134,8 +5364,9 @@ PREDICTING THE NEXT BUILD (--plan)
4134
5364
  An Android plan reads the ABI from the emulator the slot records, or from
4135
5365
  the system image a new one would use. A plan refuses --device, --remote,
4136
5366
  --wait, --no-wait, --no-metro-check and --simulator-app with STIM_BAD_ARG.
4137
- Without --eas-profile it also refuses the android.remote setting and the
4138
- experimental compiler CAS with STIM_BAD_ARG, and refuses with
5367
+ Without --eas-profile it also refuses the ios.remote and android.remote
5368
+ settings with STIM_BAD_ARG. Without --eas-profile an Android plan also
5369
+ refuses the experimental compiler CAS with STIM_BAD_ARG, and refuses with
4139
5370
  STIM_NO_DEVICE when no system image is installed.
4140
5371
 
4141
5372
  IOS SCHEME SELECTION
@@ -4172,8 +5403,10 @@ AN ARTIFACT THE DEVICE ALREADY HOLDS IS NOT INSTALLED AGAIN
4172
5403
  install unchanged (emulator-5584 already has this build) (0.4s)
4173
5404
 
4174
5405
  On iOS the install line names the identity proof separately from the Expo
4175
- dev-client preference writes, so a slow simulator command is never charged
4176
- to an install that did not run:
5406
+ dev-client simulator writes, so a slow simulator command is never charged
5407
+ to an install that did not run. Those writes run only after a real install
5408
+ or for a scheme approval the device record lacks (\`stim guide facts
5409
+ devmenu\`), so a skipped rerun usually prints no second line:
4177
5410
 
4178
5411
  install unchanged (stim-app already has this build) (0.4s)
4179
5412
  install dev client prepared (0.9s)
@@ -4211,7 +5444,7 @@ $STIM_HOME/workspaces/<project>--<digest>/ (default ~/.stim/workspaces/).
4211
5444
  The aggregate run counters \`stats\` prints live beside it in
4212
5445
  $STIM_HOME/stats.json, one bucket per project and platform plus a machine-wide
4213
5446
  one, and the last 10 successful runs per project, platform and cache outcome
4214
- that size \`status\`'s build estimate. The running build itself is the
5447
+ (plus the last 10 that set up their device) that size \`status\`'s build estimate. The running build itself is the
4215
5448
  activeBuild key of the workspace's state.json, cleared when the run exits.
4216
5449
  No .gitignore entry is created or required.
4217
5450
  Native preparation can change project files: expo prebuild generates native
@@ -4614,6 +5847,27 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
4614
5847
  4. trim shared cache entries nothing has used for 14 days
4615
5848
  (\`gc --delete --older-than 14\` for the caches)
4616
5849
 
5850
+ IDLE SHUTDOWN (OFF BY DEFAULT): with devices.idleShutdownMinutes set, a
5851
+ workspace's supervisor shuts down that workspace's owned simulators and
5852
+ emulators once they have been idle that long, without waiting for a run
5853
+ to go over budget. Idle is step 1 with that many minutes in place of 10:
5854
+ booted, no driver, no Stim or agent-device lock, no build in progress, and
5855
+ no activity. A stim-server client viewing the device (the phone app's
5856
+ device viewer) counts as activity, here and in step 1. The supervisor
5857
+ checks once a minute after the workspace's last Stim command, in-app log
5858
+ and device log are all that old, rechecks under the workspace's native-run lock, and shuts devices down
5859
+ through the same teardown as \`stop\`: never deleted, and physical
5860
+ devices are never touched. It writes a device_idle_shutdown line to
5861
+ metro.ndjson and records deviceIdleShutdowns in state.json, so \`stim
5862
+ status\` shows "shut down after 30m idle" on the device until the next
5863
+ \`ios\` or \`android\` run boots it again. When metro.idleStopMinutes is
5864
+ shorter, the dev server's idle stop first shuts down the devices idle that
5865
+ long, because no supervisor is left to check afterwards. Nothing checks
5866
+ without a supervisor: release runs, and after \`stim stop\`. The setting
5867
+ is read when the supervisor starts. Stim Desktop's simulator view is not a
5868
+ stim-server client and does not count as a viewer. Turn it on with
5869
+ \`stim settings set devices.idleShutdownMinutes 30 --scope machine\`.
5870
+
4617
5871
  Steps 3 and 4 run only for disk. Memory and workspace limits never refuse;
4618
5872
  a run still over them prints one \`budget\` warning and continues. The
4619
5873
  current workspace is never reclaimed.
@@ -4632,8 +5886,8 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
4632
5886
  summary: "every flag per command, Android variants and flavors, the per-run simulator model, runtime and system image",
4633
5887
  body: () => `THE OPTION SURFACE, IN FULL
4634
5888
  start --json --wait <seconds> --remote --reset-cache
4635
- 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>
4636
- 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>
5889
+ 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 <eas|proxy|auto|machine>
5890
+ 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>
4637
5891
  reload [ios|android] --json
4638
5892
  device lock <ios|android> [id] --slot <name> --for <duration> --wait <seconds> --json;
4639
5893
  unlock [ios|android] --slot <name> --json
@@ -4645,7 +5899,7 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
4645
5899
  set <key> <value> --scope <layer> --json; unset <key> --scope <layer> --json
4646
5900
  doctor --json --fix --platform <ios|android>
4647
5901
  (--platform keeps shared checks and filters native findings)
4648
- gc --delete --older-than <days> --cache <name|all|workspaces> --worktrees --json
5902
+ gc --delete --older-than <days> --cache <name|all|workspaces|recordings|parked> --worktrees --json
4649
5903
  worktree warm --refresh; remove [path] --force
4650
5904
 
4651
5905
  DEVICE SLOTS
@@ -4667,7 +5921,7 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
4667
5921
  and prints what it waits for. \`stop --slot\` interrupts a build only for
4668
5922
  its own slot or when it stops the workspace's only device (see STOP DURING
4669
5923
  A BUILD).
4670
- Named slots support local devices; remote sessions use the default slot.
5924
+ Named slots support local devices and hosted iOS simulators; eas/proxy sessions use the default slot.
4671
5925
  Names use 1-64 letters, digits, underscores or hyphens, starting with a letter
4672
5926
  or digit. Reserved object-property names and web are refused.
4673
5927
 
@@ -4707,6 +5961,12 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
4707
5961
  worktrees of the same Git repository; the source checkout must be available.
4708
5962
  Running it in the source checkout refuses.
4709
5963
 
5964
+ Warm registers the app it prepares, so \`stim status\` lists the workspace
5965
+ with phase "warming" while it runs and "ready" once it succeeds, until the
5966
+ first start, ios, android, web or reload there or for 2 hours
5967
+ (\`guide facts status\`). In a monorepo worktree root, that app is the one
5968
+ the source checkout has registered.
5969
+
4710
5970
  \`stim doctor\` reports the source checkout's fitness as a seed -- how far
4711
5971
  behind its upstream it is, uncommitted tracked changes, an interrupted rebase
4712
5972
  or merge, a detached HEAD, a diverged branch, a branch that is not the
@@ -4787,7 +6047,7 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
4787
6047
  paths eligible under the source checkout's Git ignore rules, including .env
4788
6048
  and local configuration. The source's nonempty
4789
6049
  .worktreeexclude replaces its resolved worktree.exclude setting. Nested
4790
- registered worktrees, .DS_Store, .DerivedData, .idea, and
6050
+ registered worktrees, .DS_Store, .DerivedData, .idea, .jj, and
4791
6051
  android/build/generated/autolinking caches are excluded, including inside
4792
6052
  newly copied directories. Gradle regenerates autolinking
4793
6053
  for the destination checkout on its next build. Warm also skips paths
@@ -4866,11 +6126,23 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
4866
6126
  runtime when \`--runtime\` also resolved, which is what catches a pair like
4867
6127
  \`--device-type "iPhone 8" --runtime 26.5\` that each half would pass alone.
4868
6128
  \`--runtime\` takes a version (\`26.5\`) or a runtime's full name
4869
- (\`iOS 26.5\`), exactly; no prefix or suffix matches. Both flags choose
4870
- the local owned simulator only: with \`--remote\` or the ios.remote setting
4871
- they refuse with STIM_BAD_ARG, because the remote backend picks its own
4872
- device. The ios.deviceType and ios.runtime settings do not refuse there;
4873
- a remote run ignores them.
6129
+ (\`iOS 26.5\`), exactly; no prefix or suffix matches. On a remote run
6130
+ (eas/proxy via \`--remote\` or ios.remote) \`--runtime\` refuses with
6131
+ STIM_BAD_ARG, because the remote backend picks the iOS version. So does
6132
+ \`--device-type\` on the proxy backend. The eas backend honors
6133
+ \`--device-type\`: Stim passes it to \`eas simulator:start --device\`,
6134
+ unchecked against local runtimes, and refuses with STIM_BAD_ARG when
6135
+ eas-cli is older than 22.2.0, the first release with that flag. A
6136
+ recorded EAS session still running another model refuses with
6137
+ STIM_REMOTE_DEVICE_MISMATCH instead of being reused; one that already
6138
+ ended is replaced on the requested model. The ios.deviceType and
6139
+ ios.runtime settings do not refuse on an eas/proxy run; it ignores them.
6140
+ A named hosting Mac uses both selectors and its installed choices instead;
6141
+ an unavailable choice is STIM_HOSTING_REFUSED. See hosted-ios.
6142
+ \`android --system-image\` and \`--device-profile\` refuse with
6143
+ STIM_BAD_ARG on every remote run (\`--remote\` or the android.remote
6144
+ setting), and a remote run ignores android.systemImage and
6145
+ android.deviceProfile.
4874
6146
 
4875
6147
  These flags describe a device that does not exist yet. When this workspace
4876
6148
  ALREADY owns a simulator and \`--device-type\` names a different model, or
@@ -4938,11 +6210,25 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
4938
6210
  and the remedy is the cable. Metro is unaffected: a Debug app reaches it
4939
6211
  over the LAN either way.
4940
6212
 
6213
+ WATCHING A LEASED ANDROID PHONE: while this workspace holds the phone's
6214
+ lease, stim-server can stream its screen to paired clients that ask for the
6215
+ physical device, and let one with control tap and type on it. stim-server
6216
+ pushes the scrcpy server to /data/local/tmp for the stream and deletes it
6217
+ when the stream stops; it installs nothing and changes no setting. Control
6218
+ ends when the lease is released or expires, and stim-server never takes a
6219
+ phone's lease itself.
6220
+
4941
6221
  \`stop\` releases this workspace's leases and stops its log collectors.
4942
6222
  On a physical iPhone that also closes the app, because its collector owns
4943
6223
  the devicectl launch session. \`gc --delete\` removes expired lease files;
4944
6224
  neither command shuts down the phone or uninstalls the app.
4945
6225
 
6226
+ WATCHING THE PHONE: while this workspace holds the lease, stim-server
6227
+ streams a USB-cabled iPhone's screen to the phone app, view only; Stim
6228
+ sends a physical iPhone no input. The lease ends with the run, so
6229
+ \`device lock ios <udid>\` keeps it watchable between runs. A Wi-Fi phone
6230
+ has no stream, and a locked phone shows its last frame until unlocked.
6231
+
4946
6232
  A device build is LOCAL-TIER ONLY. Its cache key is
4947
6233
  \`<fingerprint>-<configuration>-device\`, so a device app can never collide
4948
6234
  with the simulator one, and neither the build-cache provider nor the Expo
@@ -4968,8 +6254,9 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
4968
6254
  this workspace's Metro, and then wires the app to it: an expo-dev-client app
4969
6255
  through the deep link, passed to devicectl as \`--payload-url\` and followed
4970
6256
  by \`-- -EXDevMenuShowsAtLaunch 0 -EXDevMenuShowFloatingActionButton 0\`,
4971
- which is how a phone gets what a simulator gets from a defaults write, and a
4972
- bare app by writing \`<addr>:<port>\` into the app bundle's ip.txt --
6257
+ the same launch arguments Stim's own \`simctl launch --initialUrl\` carries on
6258
+ a simulator, and a bare app by writing \`<addr>:<port>\` into the app bundle's
6259
+ ip.txt --
4973
6260
  RCTBundleURLProvider's own mechanism, which honours a colon-bearing value
4974
6261
  verbatim and never consults the compiled RCT_METRO_PORT. Stim never sets
4975
6262
  that define: it would put the reserved port into a compiled input and fork
@@ -5149,6 +6436,17 @@ THE POOL: WHICH DEVICE AN ID-LESS \`--device\` PICKS
5149
6436
  android.keystorePassword override it (see \`guide settings\`). The cache
5150
6437
  entry itself is never modified.
5151
6438
 
6439
+ AN EXPO-UPDATES APP GETS A FRESH EMBEDDED UPDATE. expo-updates launches the
6440
+ update with the newest commitTime for the binary's runtime version, and the
6441
+ embedded bundle is one of those updates. The cached artifact's embedded
6442
+ manifest (app.manifest) carries the CACHED build's id and time, so an
6443
+ update the app downloaded after that build would launch instead of the
6444
+ swapped JS. The swap gives the copy's manifest a new id and this run's time,
6445
+ exactly as a full build does, on both platforms. An update published after
6446
+ the swap can still download and launch on a later start, as it would on a
6447
+ fresh build. A manifest the swap cannot rewrite, or one inside a dynamic
6448
+ EXUpdates.framework on iOS, falls back to a full build.
6449
+
5152
6450
  Before re-packing, THE ASSET GATE compares CONTENT HASHES of the assets
5153
6451
  React Native emits: what this workspace just emitted under --assets-dest
5154
6452
  against a manifest of what the cached build emitted, recorded as
@@ -5193,7 +6491,8 @@ THE POOL: WHICH DEVICE AN ID-LESS \`--device\` PICKS
5193
6491
  its builder's baked-in JS, a cache hit regenerates THIS workspace's bundle
5194
6492
  (the project's own \`expo export:embed\` / \`react-native bundle\`, plus
5195
6493
  its own hermesc when Hermes is enabled) into a copy of the artifact,
5196
- re-signs it and installs that; any swap failure falls back to a full build
6494
+ refreshes an expo-updates manifest as described above, re-signs it and
6495
+ installs that; any swap failure falls back to a full build
5197
6496
  rather than ever installing stale JS. Device logs are still collected, so
5198
6497
  \`logs --errors\` answers "does it repro in release/Hermes bytecode".
5199
6498
  A run with no \`--device\` installs on the simulator only. \`ios --device\`
@@ -5294,8 +6593,56 @@ wipe state to upgrade: it records ownership needed for safe teardown. Use the
5294
6593
  same slot-aware CLI for all commands while named assignments exist; older
5295
6594
  versions cannot reliably manage their assignments.
5296
6595
 
6596
+ ARCHIVE
6597
+
6598
+ Linked-worktree removal and gc dead-project pruning keep history under
6599
+ $STIM_HOME/archive by default. See stim guide cleanup archive.
6600
+
6601
+ MAINTENANCE
6602
+
6603
+ Automatic maintenance is report-only in this release: it measures, plans and
6604
+ logs, and never stops or deletes resources. The default mode is report; it is
6605
+ off in CI and scoped STIM_HOME homes unless STIM_MAINTENANCE is explicit.
6606
+ Commands trigger a detached pass when disk and memory checks (every minute)
6607
+ or directory sizes (hourly) are due. guide, settings and help do not trigger
6608
+ it; gc --delete also skips the hook. status --watch also triggers checks.
6609
+ Size checks defer under high load. Attempts back off for at least one minute.
6610
+ Measured directories are workspace build outputs and Stim's shared native,
6611
+ Metro, ccache, Swift compilation and registered caches. Pressure checks read
6612
+ free disk on the Stim home, projects and worker root volumes. On macOS the
6613
+ memory signal is the sysctl pressure level, with no signal if sysctl fails.
6614
+ Other platforms use os.freemem(); macOS never falls back to it.
6615
+ Memory pressure is recorded only; memory stops are deferred to a later phase.
6616
+
6617
+ stim status last checks, plan and running pass
6618
+ stim status --json maintenance observations and recent records
6619
+ stim logs --source maintenance this workspace's maintenance records
6620
+ stim gc live pressure and cached-size preview
6621
+ stim settings set maintenance.mode off
6622
+
6623
+ The machine log is $STIM_HOME/maintenance/maintenance.ndjson, rotated at
6624
+ maintenance.logMaxMb with the old generation retained for
6625
+ maintenance.logRetentionDays. Child crashes use maintenance/child.log.
6626
+ Actions and explaining skips are logged only when newly planned. A pass is
6627
+ logged after a size check or a change to actions, skips or blocked reasons.
6628
+ maintenance.logChecks enables debug observations; default false.
6629
+ status and doctor report invalid settings and unresolved claims with a removal
6630
+ command to run only after confirming the holder is gone. Invalid cache caps
6631
+ fall back to their own defaults; invalid maintenance settings disable passes.
6632
+ The run claim serializes passes with gc --delete; gc refuses a held claim
6633
+ with the holder and recovery guidance instead of waiting.
6634
+
5297
6635
  CLEANUP AND DISK
5298
6636
 
6637
+ worktree remove stops the agent-device daemon when the workspace's
6638
+ agent-device/daemon.json exists, then removes its state directory with the
6639
+ workspace. If agent-device is missing or the stop fails, it keeps the directory
6640
+ and reports the retry command: agent-device daemon stop --state-dir <dir> --clean.
6641
+ A daemon.json whose process is gone is stale and does not block removal.
6642
+ gc --delete of a finished worktree does the same.
6643
+ stop closes owned-device sessions in that directory without deleting it.
6644
+ See stim guide logs for AGENT_DEVICE_STATE_DIR examples.
6645
+
5299
6646
  WHAT RECLAIMS AN OWNED DEVICE
5300
6647
  stim worktree remove parks eligible owned simulators and emulators
5301
6648
  (\`guide lifecycle pool\`); deletes them when
@@ -5313,6 +6660,10 @@ WHAT RECLAIMS AN OWNED DEVICE
5313
6660
  command has used in that long, even though the
5314
6661
  project is still on disk, and clears only the
5315
6662
  devices parked at least that long
6663
+ stim gc --cache parked --delete
6664
+ ERASES (never deletes) verified parked simulators
6665
+ and wipes parked emulators' user data; they stay
6666
+ parked for adoption (\`guide lifecycle pool\`)
5316
6667
  stim gc --delete --worktrees
5317
6668
  also runs \`stim worktree remove\` on every clean,
5318
6669
  idle, Stim-managed linked worktree
@@ -5321,25 +6672,36 @@ WHAT RECLAIMS AN OWNED DEVICE
5321
6672
  shuts DOWN (never deletes) owned devices with no
5322
6673
  driver, claim or activity for that long
5323
6674
 
6675
+ \`gc --delete --cache watchman\` and \`gc --delete --cache gradle-daemons\`
6676
+ stop shared helper processes, not devices (\`guide cleanup memory\`).
6677
+
5324
6678
  \`worktree remove\` and \`gc --delete\` are the only two commands that delete;
5325
6679
  \`gc --delete\` deletes worktrees only through \`worktree remove\`. \`gc
5326
6680
  --delete\` also clears workspace build outputs, trims oversized workspace logs
5327
6681
  (\`guide cleanup disk\`) and removes orphaned workspace directories, never a
5328
- checkout. \`stim stop\` shuts a device
6682
+ checkout. Device recordings can also go without either: stim-server prunes
6683
+ them, and turning recording.enabled off deletes them (\`guide cleanup disk\`). \`stim stop\` shuts a device
5329
6684
  DOWN and leaves it assigned, which is what makes returning to a branch cost a
5330
6685
  boot rather than a create, a provision and a reinstall.
5331
6686
 
5332
6687
  Before shutting down, parking or deleting an owned device, Stim best-effort
5333
6688
  closes local agent-device sessions bound to its exact iOS UDID or live Android
5334
6689
  serial. An Android session must also name the owned AVD, because the next
5335
- emulator on a console port reuses its serial. \`stop\` closes a session only
6690
+ emulator on a console port reuses its serial. For an emulator that is already
6691
+ shut down, Stim closes the sessions that name its AVD on any serial that no
6692
+ connected device now holds. \`stop\` closes a session only
5336
6693
  when agent-device's claim on the device names it and was taken inside this
5337
- workspace; sessions from another workspace or claim stay open. Stim rechecks
6694
+ workspace. It also closes a session claimed from the workspace's git root
6695
+ itself, such as a worktree root above the app, but only after it has shut that
6696
+ device down and rechecked that the device is still stopped and that no other
6697
+ device holds its UDID or serial. Sessions from another workspace or claim,
6698
+ including sibling directories under the git root, stay open. Stim rechecks
5338
6699
  device ownership and uses agent-device's rejecting session target guard;
5339
6700
  sessions on other devices stay open. Physical devices are outside
5340
6701
  this cleanup. agent-device is optional: a missing binary skips this step, and a
5341
6702
  failed or timed-out list/close prints a device line on stderr while teardown
5342
- continues. Cleanup allows at most 15 seconds of agent-device calls per device.
6703
+ continues. Each cleanup pass allows at most 15 seconds of agent-device calls
6704
+ per device; the pass \`stop\` makes after shutdown has its own budget.
5343
6705
  A local daemon that cannot use socket transport or a CLI without the target guard
5344
6706
  skips cleanup with a warning; no remote daemon is used.
5345
6707
 
@@ -5350,9 +6712,77 @@ written by a newer Stim -- costs one dim line on stderr and is otherwise left
5350
6712
  alone; only the next \`ios\` or \`android\` run moves an unparseable one aside
5351
6713
  to stats.json.corrupt-<unix ms> and starts a new one.`,
5352
6714
  sections: {
6715
+ archive: {
6716
+ summary: "retained workspace history and explicit archive cleanup",
6717
+ body: () => `ARCHIVE
6718
+ Linked-worktree removal and gc pruning a registered root that is gone keep
6719
+ state, ended agents, the error index, logs, closed recording segments and
6720
+ workspace-local agent-device sessions under $STIM_HOME/archive.
6721
+ Build outputs and open recording segments are deleted with the workspace.
6722
+ Archives have no checkout, devices, ports or running processes. A recreated
6723
+ path starts fresh; status links its earlier archives with replacedBy.
6724
+ Paired clients with read can select archived[].id with archive instead of
6725
+ workspace for server log queries and replay. Archived log subscriptions send
6726
+ retained records, then logs-ended. Archived frame subscriptions need at and
6727
+ cannot be physical or go live. Replay ranges report recording disabled.
6728
+ Reads stay on this Mac.
6729
+ An updated server is required; older servers refuse with bad-request when
6730
+ workspace is omitted. There is no CLI archive log flag.
6731
+ archive.enabled defaults true and accepts every settings layer. STIM_HOME
6732
+ disables archives unless STIM_ARCHIVE_ENABLED is set. Empty stubs and orphaned
6733
+ workspace directories are not archived. A failure prints one stderr line
6734
+ beginning "could not archive:" and removal continues with empty stdout.
6735
+
6736
+ Retention runs on every worktree removal, archived or not, once an archive
6737
+ exists. It deletes records past their age or count limit first, then expires
6738
+ recordings, agent actions and logs by age from removedAt; trims per-workspace
6739
+ logs (oldest rotated generations, then build logs, then oldest other files);
6740
+ and removes the largest kind of the oldest archive while over total caps.
6741
+ Archives with expired artifacts keep their record until the record limit. recording.enabled false at machine scope deletes archived
6742
+ recordings on the next retention pass. Sizes are binary GB/MB; 0 keeps none.
6743
+ See stim guide settings for the nine archive.* keys and overrides.
6744
+
6745
+ stim gc --cache archived list ids, kinds, bytes and expiry
6746
+ stim gc --delete --cache archived delete every archive
6747
+ stim gc --delete --cache archived --older-than 14
6748
+ only removed at least 14 days ago
6749
+ stim gc --delete --cache archived:<id> delete one; ignores --older-than
6750
+ stim gc --delete --cache archived-logs delete logs, keep records
6751
+ stim gc --delete --cache archived-recordings delete recordings, keep records
6752
+ stim gc --delete --cache archived-agent delete agent actions, keep records
6753
+ Per-kind selectors also accept --older-than, measured from removedAt.
6754
+ Normal gc, --cache all, --worktrees and unscoped --older-than exclude archives.
6755
+ Archive scopes report abandoned staging directories and delete only those
6756
+ with no live or unresolved claim. An unresolved claim names its removal command.
6757
+
6758
+ Directories are 0700 and files 0600. No redaction is applied; logs and agent
6759
+ actions can contain secrets. Use archive.enabled false for a sensitive repo.
6760
+ Archived logs and replay are not exposed through CLI or app readers yet.`
6761
+ },
5353
6762
  gc: {
5354
6763
  summary: "what gc and worktree remove delete, keep and refuse: orphans, stale records, locks, leases, EAS sessions",
5355
- body: () => `LINKED WORKTREES
6764
+ body: () => `AGENT-DEVICE (REPORT ONLY)
6765
+ Unscoped gc reports agent-device runner builds by platform and entry, last use,
6766
+ agent-device and Xcode versions, sessions, logs, other state, workspace state
6767
+ and hosted driver state. Stim never selects this state as a cache.
6768
+ Stim never trims or deletes the shared runner builds, sessions, logs and other
6769
+ state or the hosted driver dir; a workspace's own agent-device dir goes only with its workspace.
6770
+ A live owner or runner lease marks an entry in use. A lock marks it in use only
6771
+ when its owner is live or unknown. Unreadable lock owners and unreadable or unknown
6772
+ leases conservatively mark entries in use. Dead lock owners and leases do not.
6773
+ Use agent-device's own tooling or remove directories yourself to clear unused
6774
+ state. --cache scopes omit this report, including --cache all.
6775
+
6776
+ SWIFTPM CACHE (REPORT ONLY)
6777
+ Unscoped gc also reports the user-level SwiftPM cache after agent-device:
6778
+ ~/Library/Caches/org.swift.swiftpm on macOS, or org.swift.swiftpm under
6779
+ XDG_CACHE_HOME (default ~/.cache) elsewhere. It is shared by every SwiftPM
6780
+ build on this machine. Stim reports it and never deletes it. It is never in
6781
+ caches, never changes actionable, and --delete and --older-than never select
6782
+ it. Any --cache scope, including all, omits measurement and reports
6783
+ sections.swiftpmCache: null. Only stats and unscoped gc measure it.
6784
+
6785
+ LINKED WORKTREES
5356
6786
  \`stim worktree remove\` works with any linked worktree, warmed or not.
5357
6787
  Git registration identifies the worktree; a Stim registry entry is not
5358
6788
  required. Before it reclaims anything, the command refuses a worktree git
@@ -5388,7 +6818,8 @@ SWEEPING FINISHED WORKTREES
5388
6818
  one that is idle: no recorded use for --older-than days, 7 without it; a
5389
6819
  worktree whose last use is unknown is kept. Both need the same clean state;
5390
6820
  a worktree is kept when it is the source checkout, bare, locked, in use,
5391
- dirty (untracked files count; pod install churn alone does not), unpushed
6821
+ dirty (untracked files count; pod install churn and
6822
+ watchman's untracked \`.watchman-cookie-*\` files alone do not), unpushed
5392
6823
  (commits no remote-tracking ref or other local branch reaches), or has
5393
6824
  initialized submodules. Without --worktrees, the report leaves out the
5394
6825
  source checkout and roots outside git.
@@ -5443,11 +6874,14 @@ SWEEPING FINISHED WORKTREES
5443
6874
  takes the pull request whose head is HEAD, else one whose head contains
5444
6875
  HEAD. A pull request whose head HEAD is past ("PR #12 merged, and HEAD has
5445
6876
  commits it does not"), one unrelated to HEAD (an older use of the branch
5446
- name), and one from a fork do not count. A merged or closed one makes the worktree finished
5447
- under the same clean state. Merged, its commits are kept on GitHub, so local
5448
- commits whose remote branch was deleted do not block removal. Closed, they
5449
- do: a closed pull request whose remote branch is gone keeps the worktree as
5450
- unpushed. An open pull request never makes a worktree finished. When gh is
6877
+ name), and one from a fork that is not one of the repository's own remotes
6878
+ (a stranger's fork reusing the branch name) do not count; a pull request
6879
+ opened from the user's own fork counts like any other. A merged or closed
6880
+ one makes the worktree finished under the same clean state. Merged, its
6881
+ commits are kept on GitHub, so local commits whose remote branch was
6882
+ deleted do not block removal. Closed, they do: a closed pull request whose
6883
+ remote branch is gone keeps the worktree as unpushed. An open pull request
6884
+ never makes a worktree finished. When gh is
5451
6885
  not installed, not signed in, or fails, the JSON pullRequestUnknown field
5452
6886
  and the report say why, and gc judges from git alone. A detached HEAD is
5453
6887
  not looked up. \`stim worktree remove\` makes the same check when
@@ -5492,6 +6926,20 @@ ORPHANED WORKSPACE DIRECTORIES
5492
6926
  it. A directory with a missing or unparseable workspace.json, or a root on
5493
6927
  an unmounted volume, is reported under Skipped and never deleted.
5494
6928
 
6929
+ STALE STATUS CACHE ENTRIES
6930
+ \`stim status\` caches each folder's size under $STIM_HOME/disk-usage and
6931
+ each worktree's pull request under $STIM_HOME/pull-requests. \`worktree
6932
+ remove\` drops the entries of the worktree it removed (its checkout,
6933
+ node_modules and workspace folder, and its pull request), not the size
6934
+ entries of its owned devices' data folders. \`gc\` reports the
6935
+ entries of a folder or worktree that was removed some other way under "Stale
6936
+ status cache entries", and \`gc --delete\` removes them. An entry counts as
6937
+ stale only when the path it records is gone and its volume is mounted (a
6938
+ measured node_modules folder follows its checkout). An entry that records no
6939
+ readable path, one whose path cannot be checked, and one on an unmounted
6940
+ volume are kept and counted under Skipped. \`status\` measures a folder again
6941
+ if it comes back.
6942
+
5495
6943
  NAMED SERVER PORTS
5496
6944
  worktree remove stops TCP listeners on each named allocation and releases
5497
6945
  the ports. gc reports named allocations for missing workspaces; gc --delete
@@ -5683,9 +7131,65 @@ THE ONE CASE GC WILL NOT REAP
5683
7131
  is gone, and the unrelated process is never signalled. A missing, malformed,
5684
7132
  or unreadable identity leaves the record unverified and kept for a retry.
5685
7133
  Wall-clock timestamps and command names are not ownership proof.`
7134
+ },
7135
+ memory: {
7136
+ summary: "watchman and Gradle and Kotlin daemon memory in gc, and when gc --delete --cache watchman or gradle-daemons stops them",
7137
+ body: () => `MEMORY
7138
+ Every \`gc\` without --cache, and \`gc --cache watchman\` or
7139
+ \`gc --cache gradle-daemons\`, reports the long-lived helpers that grow while
7140
+ they run: the shared watchman daemon, Gradle daemons and Kotlin compile
7141
+ daemons. Each line gives the pid, physical footprint (the measure status
7142
+ uses; resident size when the footprint cannot be read), uptime and whether
7143
+ it is idle, and a kept one says why. The reclaimable line totals what
7144
+ the two commands below would free now.
7145
+
7146
+ Only these two commands stop anything, and each acts only on its own kind.
7147
+ Neither a plain \`gc --delete\` nor \`--cache all\` touches them:
7148
+ stim gc --delete --cache watchman
7149
+ runs \`watchman watch-del\` on each stale root
7150
+ with no subscription or trigger, then
7151
+ \`watchman shutdown-server\` only when
7152
+ \`debug-status\` lists no client but gc's own
7153
+ call and no root has a trigger; otherwise
7154
+ the daemon is kept and the report names
7155
+ each client, as the Stim workspace whose
7156
+ dev server it belongs to where it can
7157
+ stim gc --delete --cache gradle-daemons
7158
+ stops each Gradle daemon its own
7159
+ \`gradle --status\` reports IDLE, then each
7160
+ Kotlin compile daemon with no open client
7161
+ connection once every Gradle daemon is idle
7162
+
7163
+ A stale watch root is one whose directory is gone from a mounted volume, or
7164
+ that sits in a linked worktree git pruned while its directory stayed.
7165
+ Watchman drops a root itself when its directory is deleted, so stale roots
7166
+ are rare. Removing one stops its recrawls but does not shrink the daemon;
7167
+ only a restart returns its memory. After a shutdown the next client that
7168
+ needs watchman starts it again and re-watches the roots in its state file.
7169
+ Every watchman call uses --no-spawn, so gc never starts the daemon.
7170
+ Shutting it down under a Metro that uses watchman would break that Metro's
7171
+ file watching, which is why any connected client keeps it.
7172
+
7173
+ gc finds a Gradle daemon's Gradle user home from the daemon log it holds
7174
+ open (lsof), and asks the daemon's own distribution for its status with
7175
+ that home and the daemon's Java. When every daemon that status lists for a
7176
+ home and version is idle, gc runs that distribution's \`gradle --stop\`;
7177
+ otherwise it sends SIGTERM to each idle daemon, after checking the pid still
7178
+ has the same start time. A build that picks a daemon between gc's last
7179
+ check and the stop fails and must be run again, so gc re-checks the build
7180
+ locks and the daemon's status right before each stop. Nothing is stopped while an Android build lock, or a build slot no
7181
+ iOS build holds, is live or unresolved. stim-server stops the daemons of
7182
+ offloaded builds (offload.gradleDaemonIdleMinutes, \`guide settings\`), so
7183
+ while it runs gc keeps those. A daemon whose home, distribution or status cannot be read is
7184
+ kept. Gradle stops an idle daemon itself after 3 hours and Kotlin after 2
7185
+ hours by default.
7186
+
7187
+ --older-than does not apply to these kinds and is refused with
7188
+ STIM_BAD_ARG. With STIM_HOME set, gc skips them: they are machine-global.
7189
+ \`stim doctor\` notes a watchman footprint over 2 GiB.`
5686
7190
  },
5687
7191
  disk: {
5688
- summary: "disk usage, workspace build outputs and logs, AVD and build-log sizes, the data partition, trimming the shared caches",
7192
+ summary: "disk usage, workspace build outputs, logs and device recordings, AVD and build-log sizes, the data partition, trimming the shared caches",
5689
7193
  body: () => `DISK
5690
7194
  Logs, state, pidfiles and Xcode DerivedData are under the global workspace
5691
7195
  directory, and \`worktree remove\` reclaims them. \`gc --delete\` clears the
@@ -5695,7 +7199,8 @@ THE ONE CASE GC WILL NOT REAP
5695
7199
 
5696
7200
  Android AVDs normally live under ~/.android/avd, and a booted owned AVD can
5697
7201
  use several GB. \`worktree remove\` parks the workspace's owned AVD with its
5698
- user data and snapshots wiped, or deletes it when the pool is off or full;
7202
+ user data and snapshots kept, or deletes it when the pool is off or full;
7203
+ \`gc --cache parked --delete\` wipes a parked AVD's user data and snapshots;
5699
7204
  plain \`stop\` only shuts it down for reuse. Stim uses Android's default Quick Boot
5700
7205
  unless displayless Linux requires software rendering, where snapshots are
5701
7206
  disabled. The first boot and a boot after the emulator, system image, or AVD
@@ -5707,7 +7212,7 @@ THE ONE CASE GC WILL NOT REAP
5707
7212
  errors teardown\`).
5708
7213
  New owned AVDs default to an 8 GiB data partition, though project settings can
5709
7214
  change it. When enabled, Quick Boot keeps one automatic snapshot until the AVD is
5710
- parked or deleted.
7215
+ wiped or deleted.
5711
7216
  \`gc\` prints the on-disk size beside an orphaned or stale owned Android AVD
5712
7217
  when its content directory can be read, and beside an orphaned or stale
5713
7218
  owned simulator the data size simctl reports for it.
@@ -5738,15 +7243,20 @@ setting applies only to a newly created AVD; recreate the environment to adopt
5738
7243
  a changed value.
5739
7244
 
5740
7245
  WORKSPACE BUILD OUTPUTS
5741
- Each workspace directory holds derived-data/, gradle-build/, android-cas/
5742
- and cache-provider/. \`gc\` reports them as one detected cache, "Workspace
5743
- build outputs", with a per-workspace size, last use and verdict. Plain
7246
+ Each workspace directory holds derived-data/, gradle-build/, android-cas/,
7247
+ cache-provider/, macos/build, the staged macos/<Product>.app and macos/staging-*
7248
+ leftovers. \`gc\` reports them as one detected cache, "Workspace build
7249
+ outputs", with a per-workspace size, last use and verdict. Plain
5744
7250
  \`gc --delete\` clears them for every workspace not in use (see \`guide
5745
7251
  cleanup gc\`), before anything else. \`--older-than <days>\` limits that to
5746
7252
  workspaces idle at least that long, and keeps one whose last use is
5747
- unknown. \`--cache workspaces\` acts on them alone; \`--cache all\` includes
5748
- them. Only those four directories go: workspace.json, state.json, logs/,
5749
- locks and device records stay, so the workspace keeps its devices and ports.
7253
+ unknown. \`--cache workspaces\` acts on them alone, except in the workspace
7254
+ directories that plain \`gc --delete\` removes whole (dead projects and
7255
+ orphaned directories); \`--cache all\` includes them. macos/ itself,
7256
+ macos/runtime.lock and its claim set, workspace.json, state.json, logs/,
7257
+ other macOS files, locks and device records stay, as does the project's
7258
+ own .build. The workspace keeps its devices and ports. A running, building,
7259
+ unverified or hosted macOS app keeps its workspace untouched.
5750
7260
  stim gc --delete --cache workspaces --older-than 7
5751
7261
  Last use is the newest of the lastUsedAt that start, ios, android, reload
5752
7262
  and worktree warm record in state.json, lastBuild.startedAt,
@@ -5757,7 +7267,8 @@ WORKSPACE BUILD OUTPUTS
5757
7267
  The next build of an unchanged app installs from the shared build cache.
5758
7268
  After a native change the Xcode compilation cache speeds the rebuild, but on
5759
7269
  React Native 0.86 Swift does not use it (explicit modules are off), so that
5760
- build recompiles Swift.
7270
+ build recompiles Swift. The next \`stim macos\` rebuilds its scratch from
7271
+ nothing (a full Swift build) and restages the app.
5761
7272
 
5762
7273
  WORKSPACE LOGS
5763
7274
  \`gc\` reports the size of each workspace's logs/ (the workspaceLogs
@@ -5772,6 +7283,33 @@ WORKSPACE LOGS
5772
7283
  stops the collector. Build transcripts keep the whole run and are never
5773
7284
  trimmed, and nothing else under logs/ is touched.
5774
7285
 
7286
+ DEVICE RECORDINGS
7287
+ stim-server records owned simulators, emulators and the Stim-owned Chrome
7288
+ page while an automation tool drives them or a paired client watches them,
7289
+ into recordings/ in the workspace directory (\`guide settings\`). It keeps
7290
+ the last 15 minutes of recorded footage per device; idle time and time after
7291
+ \`stim stop\` age nothing out, so \`stop\` ends recording and the footage
7292
+ stays replayable. Across every workspace of this Stim home it keeps at most
7293
+ 1 GiB, deleting the oldest footage first. Recordings are deleted by:
7294
+
7295
+ stim worktree remove that workspace's recordings, with its
7296
+ workspace directory
7297
+ stim gc --delete the recordings of dead or orphaned
7298
+ workspaces, with their workspace directory;
7299
+ a workspace whose project root cannot be
7300
+ proven gone keeps them
7301
+ stim gc --delete --older-than <days>
7302
+ also footage recorded before that many days
7303
+ ago, in every workspace
7304
+ stim gc --delete --cache recordings
7305
+ every workspace's recordings, and nothing
7306
+ else; --cache all includes them
7307
+ recording.enabled false the recordings of each workspace it turns off
7308
+
7309
+ \`gc\` lists them in the recordings section of \`gc --json\` with each
7310
+ workspace's bytes, what a delete would remove (deleteBytes), and why the rest
7311
+ stays.
7312
+
5775
7313
  SHARED BUILD CACHES
5776
7314
  The caches that make a second workspace fast are alive by design and never
5777
7315
  included in a plain \`gc --delete\`. Every \`gc\` run reports them anyway,
@@ -5819,7 +7357,7 @@ read from, or an unknown key refuses with STIM_BAD_ARG naming the expected
5819
7357
  shape, before anything is written. Writes to the machine file take its lock
5820
7358
  and replace it atomically; a committed write keeps the file's other keys and
5821
7359
  indentation. Run \`stim settings\` from the app directory: workspace and
5822
- committed resolve from the nearest package.json, repo from its Git
7360
+ committed resolve from the nearest package.json or Package.swift, repo from its Git
5823
7361
  repository. From a monorepo web package that resolves to its worktree's app
5824
7362
  (see stim guide ports), workspace and committed are that app's entry and
5825
7363
  .stim.json. worktree.exclude and worktree.defaultBranch are read only from
@@ -5838,9 +7376,11 @@ carries its dotted key, layers, and environment override under "x-stim".
5838
7376
  Resolution order, first match wins:
5839
7377
  1. workspace ~/.stim/config.json, under this project's entry
5840
7378
  2. repo ~/.stim/config.json, under this repo's git common dir
5841
- 3. committed .stim.json beside the app's package.json
5842
- 4. machine ~/.stim/config.json, top-level optimizations and
5843
- android.deviceProfile only
7379
+ 3. committed .stim.json beside the app's package.json or Package.swift
7380
+ 4. machine ~/.stim/config.json, top-level optimizations,
7381
+ ios.deviceType, ios.runtime, android.systemImage,
7382
+ android.deviceProfile and devices.idleShutdownMinutes
7383
+ and archive.enabled
5844
7384
  5. Stim default
5845
7385
  An environment override, where a setting has one, wins over every layer.
5846
7386
 
@@ -5866,19 +7406,31 @@ An app's .stim.json can contain:
5866
7406
  "android": { "variant": "productionDebug" }
5867
7407
  }
5868
7408
 
7409
+ ARCHIVE SETTINGS
7410
+ ${SETTINGS.filter((setting) => setting.key.startsWith("archive.")).map((setting) => ` ${setting.key}: ${setting.description}; default ${setting.default}; layers ${setting.scopes.join(", ")}; ${setting.env}`).join("\n")}
7411
+ Sizes use binary GB/MB. Archive ages and caps accept 0 to keep none.
7412
+ STIM_HOME makes archive.enabled false unless STIM_ARCHIVE_ENABLED is set.
7413
+ See stim guide cleanup archive for retention and explicit deletion.
7414
+
5869
7415
  KEYS STIM READS
5870
7416
  ios.deviceType e.g. "iPhone 17 Pro" -- the simulator model this
5871
7417
  workspace's owned sim is created as, spelled exactly as
5872
7418
  \`xcrun simctl list devicetypes\` names it, and one an
5873
- installed runtime can create. The \`--device-type\`
5874
- flag overrides this per invocation. A name no installed
5875
- runtime offers is STIM_BAD_ARG and the creatable names
5876
- are printed
7419
+ installed runtime can create. Readable from the
7420
+ machine layer too, so one machine can default every
7421
+ workspace to a model; a project layer still wins. The
7422
+ \`--device-type\` flag overrides this per invocation. A
7423
+ name no installed runtime offers is STIM_BAD_ARG and
7424
+ the creatable names are printed, naming the layer the
7425
+ value came from
5877
7426
  ios.runtime e.g. "26.2" -- the iOS runtime that sim is created on,
5878
7427
  as a version ("26.2") or a runtime's full name
5879
- ("iOS 26.2"); nothing else matches. The \`--runtime\`
5880
- flag overrides this per invocation, and an uninstalled
5881
- version refuses the same way
7428
+ ("iOS 26.2"); nothing else matches. Readable from the
7429
+ machine layer too, so one machine can default every
7430
+ workspace to a runtime; a project layer still wins.
7431
+ The \`--runtime\` flag overrides this per invocation,
7432
+ and an uninstalled version refuses the same way,
7433
+ naming the layer the value came from
5882
7434
  ios.configuration e.g. "Release" -- the Xcode configuration to build
5883
7435
  (simulator only). Committing
5884
7436
  { "ios": { "configuration": "Release" } } makes every
@@ -5886,9 +7438,10 @@ KEYS STIM READS
5886
7438
  embedded JS, no Metro, cache keyed -release-sim, and
5887
7439
  a JS-bundle swap on cache hits. The \`--configuration\`
5888
7440
  flag overrides this per invocation. Unset means Debug.
5889
- ios.remote "proxy" or "eas" to use that remote backend, the same
5890
- as passing \`--remote proxy\` or \`--remote eas\`. The
5891
- build still runs here; only the device is elsewhere.
7441
+ ios.remote "proxy", "eas", or a named approved Mac from
7442
+ hosting.machines, with the same meaning as --remote.
7443
+ "auto" is accepted but refuses until automatic placement
7444
+ ships. Unset runs here. See lifecycle hosted-ios.
5892
7445
  ios.simslimProfile a SimSlim JSON profile under the app directory,
5893
7446
  at most 64 KiB. Install the
5894
7447
  external tool once with
@@ -5930,9 +7483,12 @@ KEYS STIM READS
5930
7483
  interface.
5931
7484
  android.systemImage e.g. "system-images;android-36;google_apis;arm64-v8a"
5932
7485
  -- the sdkmanager package id the owned AVD is created
5933
- from. The \`--system-image\` flag overrides this per
5934
- invocation, and an id this SDK has not installed is
5935
- STIM_BAD_ARG with the installed ids printed.
7486
+ from. Readable from the machine layer too, so one
7487
+ machine can default every workspace to an image; a
7488
+ project layer still wins. The \`--system-image\` flag
7489
+ overrides this per invocation, and an id this SDK has
7490
+ not installed is STIM_BAD_ARG with the installed ids
7491
+ printed, naming the layer the value came from.
5936
7492
  android.deviceProfile e.g. "pixel_fold" -- the avdmanager hardware profile a
5937
7493
  new owned AVD is created with, spelled exactly as
5938
7494
  \`avdmanager list device -c\` prints it. Defaults to
@@ -6036,15 +7592,35 @@ ${ANDROID_AVD_CONFIG_HELP.map((line) => ` ${line}`).joi
6036
7592
  shares this machine and is the only mode that needs no
6037
7593
  tunnel. "expo" lets the Expo dev server tunnel itself.
6038
7594
  "cloudflared" and "ngrok" name a managed provider
6039
- explicitly. Any other value is refused as invalid.
7595
+ explicitly. "tailscale" explicitly selects a tailnet-only
7596
+ foreground serve process, never Funnel; "auto" never
7597
+ selects it. The device must share the tailnet. See
7598
+ \`guide metro\`. Any other value is refused as invalid.
6040
7599
  metro.ngrokUrl the stable managed ngrok URL. It requires metro.tunnel
6041
7600
  "ngrok" and passes --url to ngrok http. Stim owns
6042
7601
  this process.
6043
7602
  metro.publicUrl an existing tunnel's URL. Takes precedence over
6044
7603
  starting one, whatever metro.tunnel says -- Stim
6045
7604
  did not create it, so a Metro request through it is
6046
- still gated the same way a managed tunnel's is. Set it
7605
+ still gated through a public bundle probe. Set it
6047
7606
  before Expo start so the manifest advertises it.
7607
+ metro.port this workspace's Metro port, 1024-65535;
7608
+ STIM_METRO_PORT overrides it. Stim reserves this
7609
+ number instead of picking one from 8082 up, for a
7610
+ workspace whose tools already expect a port. A
7611
+ committed value suits a single checkout; give each
7612
+ worktree its own in the workspace layer or the
7613
+ environment. start refuses with STIM_BAD_ARG when
7614
+ another workspace reserves it or another process
7615
+ holds it. start, ios, android and web refuse a
7616
+ changed pin while this workspace's dev server runs
7617
+ (run stim stop before retrying) or its supervisor
7618
+ cannot be verified (stop it with the tool that
7619
+ started it). A foreign holder is refused before
7620
+ anything starts; this project's Metro on the pin is
7621
+ attached to. --no-metro-check uses the pin, then the
7622
+ recorded port, then 8081, without probing or reserving;
7623
+ an invalid pin still refuses.
6048
7624
  metro.warmupUrl optional object with per-platform bundle URLs:
6049
7625
  metro.warmupUrl.ios
6050
7626
  metro.warmupUrl.android
@@ -6072,6 +7648,23 @@ ${ANDROID_AVD_CONFIG_HELP.map((line) => ` ${line}`).joi
6072
7648
  driven. Read
6073
7649
  when \`start\` spawns the supervisor; see
6074
7650
  \`guide metro\`.
7651
+ devices.idleShutdownMinutes
7652
+ minutes an owned simulator or emulator stays idle
7653
+ (\`gc --idle\` conditions; an open stim-server viewer
7654
+ counts as activity) before its workspace's supervisor
7655
+ shuts it down, never deletes it. Default 0, never.
7656
+ Machine or project layers. Read when the supervisor
7657
+ starts; see \`guide lifecycle budget\`.
7658
+ macos.product the explicit Swift Package executable product built in
7659
+ Debug by stim macos
7660
+ macos.infoPlist development Info.plist relative to Package.swift,
7661
+ without shared URL schemes or an update feed
7662
+ macos.arguments string array passed directly to the executable;
7663
+ see stim guide macos for the prototype's limits
7664
+ macos.assetCatalog optional .xcassets directory relative to Package.swift,
7665
+ compiled into Contents/Resources before signing
7666
+ macos.resources map of Contents/Resources destinations to source paths
7667
+ relative to Package.swift; see stim guide macos
6075
7668
  web.url the page \`stim web\` opens in the owned Chrome, an
6076
7669
  http:// or https:// URL. {port:<label>} becomes the
6077
7670
  workspace's named port (allocated like \`stim ports
@@ -6129,7 +7722,7 @@ ${ANDROID_AVD_CONFIG_HELP.map((line) => ` ${line}`).joi
6129
7722
  them from the environment or the machine layers.
6130
7723
 
6131
7724
  Each setting takes its documented type: string, array of strings, number,
6132
- boolean, or object. ios.remote, android.remote, metro.tunnel, web.viewport,
7725
+ boolean, or object. android.remote, metro.tunnel, web.viewport,
6133
7726
  optimizations.android.compilerCache and optimizations.android.pch take only
6134
7727
  their listed choices. A value of the wrong
6135
7728
  type or outside those choices is refused by name on every command that resolves
@@ -6185,6 +7778,18 @@ overrides the file:
6185
7778
  budget is off. A value of the wrong shape refuses start, ios and android with
6186
7779
  STIM_BAD_ARG. See \`guide lifecycle budget\` for what each limit reclaims.
6187
7780
 
7781
+ AUTOMATIC MAINTENANCE IS MACHINE-LEVEL
7782
+ Report-only in this release: it measures and plans, and deletes or stops nothing.
7783
+ STIM_HOME and CI make the mode off unless STIM_MAINTENANCE is set.
7784
+ See \`guide cleanup\` for checks, plans and log paths.
7785
+
7786
+ ${SETTINGS.filter((setting) => setting.key.startsWith("maintenance.") || /^caches\..*MaxGb$/.test(setting.key)).map((setting) => ` ${setting.key} ${setting.env} ${setting.default === void 0 ? "unset" : `default ${setting.default}`}
7787
+ ${setting.description}`).join("\n")}
7788
+
7789
+ Maintenance uses the sysctl pressure level on macOS, with no memory signal
7790
+ when sysctl fails. os.freemem() and maintenance.minAvailableMemoryGb apply
7791
+ only on other platforms.
7792
+
6188
7793
  THE IOS SIMULATOR APP IS MACHINE-LEVEL
6189
7794
  Top-level \`iosSimulatorApp\` in ~/.stim/config.json selects the macOS app that
6190
7795
  displays Stim's owned local iOS simulator after boot. It is not a project
@@ -6251,7 +7856,9 @@ to its app path for every command it starts.
6251
7856
  THE DEVICE POOL BOUNDS ARE MACHINE-LEVEL TOO
6252
7857
  \`pool.iosParkedMax\` caps how many parked simulators \`worktree remove\` may
6253
7858
  leave behind for a later workspace to adopt. It is machine-level for the same
6254
- reason: the disk they sit on is the whole machine's, about 18 MB each once erased.
7859
+ reason: the disk they sit on is the whole machine's. A parked simulator keeps
7860
+ its apps and data, so it can hold gigabytes until \`stim gc --cache parked
7861
+ --delete\` erases it.
6255
7862
 
6256
7863
  {
6257
7864
  "pool": { "iosParkedMax": 3 }
@@ -6278,6 +7885,242 @@ test suites and the end-to-end harness use one -- and a scoped config must not
6278
7885
  leave simulators on the machine it cannot account for. A redirected home that
6279
7886
  wants a pool says so with the variable.
6280
7887
 
7888
+ DESKTOP ACCESS TICKETS
7889
+ Stim Desktop's Add wizard sets STIM_ACCESS_TICKET for one \`doctor --fix\`
7890
+ run. Agents never set it, never run \`stim-server setup\`, never edit
7891
+ \`offload.*\` or \`hosting.*\` for the user, and never approve requests.
7892
+ Stim sends the ticket (43 base64url characters) only in build and device-host access
7893
+ requests and stores only its SHA-256 ticketHash. With --fix, a still-pending
7894
+ request is replaced when its ticketHash is missing or differs from the current
7895
+ ticket; matching hashes and approved credentials are preserved.
7896
+ Approved doctor entries under buildMachines and deviceHosts include host
7897
+ { name, screenRecording, accessibility } when the worker reports it.
7898
+
7899
+ HOSTING MACHINES ARE MACHINE-LEVEL
7900
+ \`hosting.machines\` names Macs that may host owned simulator sessions,
7901
+ by MagicDNS name with an optional serve port (default 7443). Name each node
7902
+ and port once. This is separate from build offloading:
7903
+
7904
+ stim settings set hosting.machines '["janics-mac-mini"]'
7905
+ stim doctor --fix
7906
+
7907
+ Only \`doctor --fix\` asks for device-host access. A person on that Mac
7908
+ approves the printed id with \`stim-server devices grant <id> --device-host\`.
7909
+ A person can also run \`stim-server setup\` on the worker: one node, one ticket,
7910
+ one expiry, at most one request per capability, with per-grant y/N in a
7911
+ terminal or \`--yes\` otherwise. Agents never run \`stim-server setup\` or
7912
+ approve requests. Setup never changes TCC or enables Funnel; an SSH-driven
7913
+ run is not offered. Without a terminal or --yes, setup refuses before installing
7914
+ unless every chosen capability already has a matching approval. Desktop reuse
7915
+ requires an existing tailnet route. Ctrl-C or SIGTERM completes the journal,
7916
+ releases the setup claim and exits 1; a typed N also exits 1. Hosting grants
7917
+ include no read, control or build capability.
7918
+ $STIM_HOME/device-host-machines.json stores a private token and pinned tailnet
7919
+ node. Doctor never prints the token; it reports each machine under deviceHosts
7920
+ in JSON. Stim sends tokens only to the pinned node's own tailnet address,
7921
+ with its MagicDNS name for TLS and Host routing. A changed node refuses
7922
+ access, and uncertain replies or unreadable credentials preserve the pin.
7923
+ Invalid hosting settings report an error and preserve every saved credential.
7924
+ \`doctor --fix\` forgets entries removed from hosting.machines; remove a
7925
+ replaced machine, run it, then re-add the name to request a new approval.
7926
+ A definite revoked or lapsed request can be requested again with --fix.
7927
+ An in-progress approval inspection reports busy rather than replacing its
7928
+ pending token. \`stim macos --remote <machine>\` runs a macOS app on an
7929
+ approved machine (stim guide macos); \`ios\` and \`android\` do not yet
7930
+ place sessions on these machines. To view or control a hosted macOS app, a person
7931
+ on that Mac approves Screen & System Audio Recording and Device Control and Data
7932
+ Access (Accessibility on macOS 26 and earlier) for Stim Host, the app
7933
+ \`stim-server service install\` runs the server under.
7934
+
7935
+ On a hosting Mac, \`hosting.agentDriver\` names the tool it starts so a
7936
+ client's coding agent can drive the macOS apps and iOS simulators it hosts for that client.
7937
+ The default, \`none\`, starts nothing. For macOS, \`agent-device\` starts its
7938
+ shared daemon only when that agent-device can lease a single app (its
7939
+ \`macos-app\` lease backend); otherwise agent control reports \`none\` with a
7940
+ notice, and no client is handed the Mac's desktop. STIM_AGENT_DEVICE_BIN in
7941
+ stim-server's environment names an agent-device binary to use instead of
7942
+ ~/.local/bin/agent-device. Hosted iOS uses one daemon per simulator, pinned by
7943
+ its UDID policy; both Macs need agent-device 0.21.20 or later. See guide lifecycle
7944
+ hosted-ios. \`doctor\` on that Mac notes an installed hosted app that runs while
7945
+ the setting is \`none\`.
7946
+
7947
+ BUILD MACHINES ARE MACHINE-LEVEL
7948
+ \`offload.machines\` lists the Macs on the tailnet that may build for this one,
7949
+ by MagicDNS name, each optionally with the port of its \`tailscale serve\`
7950
+ route (default 7443):
7951
+
7952
+ {
7953
+ "offload": { "machines": ["janics-mac-mini"] }
7954
+ }
7955
+
7956
+ \`doctor --fix\`, run in any app directory, asks each named Mac for build access and pins its tailnet
7957
+ node in $STIM_HOME/build-machines.json. A person approves the request on that
7958
+ Mac with \`stim-server devices grant <id> --build\`; doctor prints the id.
7959
+ A person can instead run \`stim-server setup\` on the worker with a node,
7960
+ ticket and expiry, under the approval rule above. Build never includes read
7961
+ or control.
7962
+ Stim connects to a named Mac only while its name still belongs to the pinned
7963
+ node and never sends the token to another node. \`doctor\` reports each
7964
+ machine's pairing state and, for an approved machine, asks it for one build
7965
+ offer and lists every reason it would not take this app's iOS build: no
7966
+ answer, another Stim build, CPU, Xcode, simulator SDK or CocoaPods, no
7967
+ Bundler for an app whose Gemfile.lock pins CocoaPods, no iPhone simulator on
7968
+ the runtime \`stim ios\` builds for here, low disk, or busy. With Package.swift
7969
+ and no platform selection on a Mac, it also checks the macOS Xcode and SDK.
7970
+
7971
+ \`offload.machine\` (machine scope, default auto) selects placement for
7972
+ \`ios\`, \`android\` and \`macos\`. \`--build-machine <auto|local|name>\`
7973
+ overrides STIM_OFFLOAD_MACHINE, which overrides the setting. A blank environment
7974
+ value is unset. Trimmed auto/local are case-insensitive. Machine names match
7975
+ configured names case-insensitively, with port 7443 when omitted; reports use
7976
+ the configured entry.
7977
+
7978
+ auto follows offload.mode and keeps its local fallback behavior
7979
+ local builds only on this Mac for this invocation
7980
+ name requires a matching entry in offload.machines, already paired and
7981
+ approved; ignores offload.mode and this Mac's load/slot gating
7982
+
7983
+ A name never prompts for pairing, tries another machine or falls back here.
7984
+ Any refusal or failure is STIM_OFFLOAD_REFUSED with the machine and reason.
7985
+ Device, Release/non-Debug, --remote, Android CAS compiler builds, builds with
7986
+ the build cache off, and an unknown simulator runtime cannot build there and
7987
+ refuse on a cache miss. Invalid values, unlisted names and unpaired names refuse
7988
+ at setup before consulting the cache; they create no build record or failed-run
7989
+ stats. A listed paired name with a cache hit compiles nothing, contacts no worker
7990
+ and records buildMachine with builtOn absent. Build failures retain the selected
7991
+ value; builtOn is here or the configured machine entry when compilation ran.
7992
+ Prebuild and pod install still run on this Mac before a named offload. A refusal
7993
+ starts no local xcodebuild, Gradle or SwiftPM compile.
7994
+ Run stim doctor --fix to ask for build access if not paired. A person on the
7995
+ worker finds the id with stim-server devices and approves it with
7996
+ stim-server devices grant <id> --build, or runs stim-server setup there with
7997
+ the client node, ticket and expiry. Check stim settings get offload.machines,
7998
+ or rerun with --build-machine auto or --build-machine local. Ctrl-C cancels
7999
+ the remote build without a local xcodebuild, Gradle or SwiftPM compile.
8000
+
8001
+ Copyable agent prompt:
8002
+ Run stim ios --build-machine janics-mac-mini. If it refuses, report the
8003
+ STIM_OFFLOAD_REFUSED reason and remedy; do not retry with another placement.
8004
+
8005
+ With offload.machine auto, \`offload.mode\` decides where an iOS simulator Debug build or an Android
8006
+ emulator debug build or a stim macos SwiftPM Debug build compiles:
8007
+
8008
+ auto (default) here while this Mac has capacity: a free
8009
+ \`concurrency.maxBuilds\` slot (always, with no build limit) and a
8010
+ load per core under \`offload.maxLoadPerCore\`. Otherwise on a
8011
+ build machine that accepts the build and is expected to be faster:
8012
+ while every slot here is busy, any machine that accepts; while only
8013
+ the load is high, a machine whose load per core is lower than this
8014
+ Mac's. A machine too old to report its load counts only while every
8015
+ slot here is busy.
8016
+ force on a build machine whenever one accepts it
8017
+ off always here
8018
+
8019
+ Load per core is the 5-minute load average divided by the CPU count; a Mac's
8020
+ native builds are its Stim runs in prebuild, pods or compile on that Mac, not
8021
+ the ones it offloaded.
8022
+ \`offload.maxLoadPerCore\` (default 2) is the load per core at which a Mac
8023
+ counts as saturated, both here and on a build machine.
8024
+
8025
+ STIM_OFFLOAD_MODE overrides it for one command. Device, Release and
8026
+ \`--remote eas|proxy\` builds, Android builds with the Apple Clang CAS compiler cache,
8027
+ and iOS/Android runs with the build cache off, always build here. Hosted iOS
8028
+ Debug builds can use a separate --build-machine, targeting the hosting Mac's
8029
+ architecture and runtime.
8030
+ macOS uses matching Xcode and macOS SDK, with a per-client SwiftPM dependency
8031
+ cache on the worker. It skips JavaScript dependencies, prebuild and pods; see
8032
+ stim guide macos. macOS artifacts are not cached.
8033
+ An offloaded iOS/Android build first runs prebuild (and \`pod install\` for iOS) here,
8034
+ then asks every paired machine what it can build. It takes one whose Stim
8035
+ build matches this Mac and CPU matches the target simulator, with enough disk, that does not decline,
8036
+ preferring the one that already holds this repository, then the least loaded,
8037
+ and moves to the next one in that order when a machine that offered fails the
8038
+ sync or refuses to start the build.
8039
+ For iOS the machine's Xcode and simulator SDK must match, and it needs an
8040
+ iPhone simulator on the target runtime. Its CocoaPods must match too, unless
8041
+ the app's Gemfile.lock pins CocoaPods: both Macs then run that version through
8042
+ bundler, so the machine needs only Bundler on its stim-server PATH and
8043
+ installs the pinned gems itself on the first build. For Android its JDK major
8044
+ version must match, and its Android SDK must hold the NDK, build-tools and
8045
+ compile platform that the project's React Native version names in
8046
+ gradle/libs.versions.toml; Gradle and AGP come from the synced project. When
8047
+ no machine takes the build in auto placement, the run prints one line with each machine's
8048
+ reason, such as \`janics-mac-mini: busy (load at or above 2/core; load
8049
+ 8.2/core, 2 builds) -> building here\`, and compiles here. It sends the files
8050
+ \`git ls-files -co --exclude-standard\` lists (the machine keeps only files it
8051
+ lacks, so an unignored secret such as \`.env\` is sent too), builds with
8052
+ Stim's own code there, refuses unless the fingerprint there equals this one,
8053
+ and brings the \`.app\` or APK back. An Android build there uses this Mac's
8054
+ variant, target ABI, Gradle build cache, PCH and compiler cache choices, so
8055
+ the APK matches the ABI-narrowed cache key. Stim checks the archive's sha256
8056
+ and fingerprints this checkout again before it stores the app under the
8057
+ post-mutation key and installs it the usual way, so an APK is still compared
8058
+ with the installed one before an install is skipped.
8059
+ While a machine builds, Stim pings it every 15 seconds and treats a minute of
8060
+ silence as a dropped connection while it waits for the build. When the
8061
+ connection drops without a close, Stim reconnects to the same pinned machine
8062
+ for up to 3 minutes and takes the running build back; the machine keeps it
8063
+ running for 5 minutes without a connection, then cancels it. With auto, a drop while the
8064
+ app is fetched still builds here. Interrupting the run with Ctrl-C closes the
8065
+ connection, which cancels the build there; a run killed another way, or
8066
+ 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>
8067
+ -> building here\` and compiles here. The run's lastBuilds entry and
8068
+ \`--json\` facts record the reason as offloadFallback. Offloading holds no
8069
+ local build slot; that fallback takes one. \`stim stats\` keeps where each
8070
+ compiling build ran and why (\`guide facts stats\`).
8071
+ An offloaded app lands only in this Mac's build cache, not in a remote cache
8072
+ provider. A project whose xcodebuild changes its own fingerprinted inputs
8073
+ fails the offload fingerprint check: auto builds here; a named placement refuses.
8074
+
8075
+ On the build machine, stim-server keeps each client's checkouts, dependencies,
8076
+ DerivedData, compilation cache, ccache and Gradle home under
8077
+ \`offload.workerRoot\` (absolute; default $STIM_HOME/build-worker), one area
8078
+ and Stim home per client and repository, and one Gradle home per client. It
8079
+ runs one offloaded build at a time. It declines a build while that volume has
8080
+ less than 10 GB free, while its own native builds and the offloaded one fill
8081
+ its \`concurrency.maxBuilds\`, or while its load per core is at or above its
8082
+ \`offload.maxLoadPerCore\`, and reports the reason in its offer. It boots and
8083
+ installs nothing. While an offloaded build runs there, it holds one of that
8084
+ Mac's \`concurrency.maxBuilds\` slots, so a local Stim build there waits for it
8085
+ and counts it as busy; the slot is freed when the build ends, is cancelled or
8086
+ its process is gone. Android builds there run on the JDK in stim-server's
8087
+ JAVA_HOME (else the macOS default JDK) with the SDK in its ANDROID_HOME (else
8088
+ ~/Library/Android/sdk). The client's Gradle daemon stays warm there for
8089
+ \`offload.gradleDaemonIdleMinutes\` (default 30; 0 stops it when each build
8090
+ ends; a new value applies from the next daemon) and never holds a build
8091
+ slot. While none of the client's builds runs, stim-server stops it sooner
8092
+ when a build is cancelled, the client is revoked, or the machine has less
8093
+ than 2 GB of memory available, and deleting the client's directory under the
8094
+ worker root stops it too.
8095
+ \`stim-server service install [--serve]\` on the build machine keeps stim-server
8096
+ running as a login LaunchAgent; \`--path-prepend <dir>\` and \`--env KEY=VALUE\`
8097
+ pin a PATH entry or variable such as a private CocoaPods that stim-server's
8098
+ login-shell environment would otherwise replace.
8099
+ \`stim-server service update --release <version>\` there moves the service to
8100
+ that exact stim-server release from the public npm registry once npm verifies
8101
+ its integrity and registry signatures; \`--from <dir>\` installs the packed
8102
+ packages of a checkout instead. It waits for offloaded builds and hosted
8103
+ sessions to finish, restarts the job, and switches back when the new server
8104
+ exits or does not answer within 90 seconds; \`stim-server service rollback\` returns
8105
+ to the previous server. A client Mac approved for builds or device hosting
8106
+ there can ask for the same update over its tailnet connection: an npm release
8107
+ always, and its own packed build only while \`server.acceptClientBuilds\` is
8108
+ true on that machine (default false).
8109
+ Replacing a loaded service waits up to 45 seconds for its existing listeners to
8110
+ release the port. Uninstall does not wait for the port. If stopping the old job
8111
+ or starting the new one fails, install restores the previous plist and attempts
8112
+ to restart a previously loaded LaunchAgent; if it cannot, it prints the
8113
+ launchctl bootout/bootstrap remedy. A running LaunchAgent does not prove readiness; check \`stim-server service status\`
8114
+ and its reported log. The server listens before it touches the Stim home; a
8115
+ read-only child process reads the Stim home, server and recording directories,
8116
+ and until it returns \`/health\` answers 503 with \`startup.state\` pending.
8117
+ A read that does not return within 10 seconds, plus up to one second to stop
8118
+ the child, leaves the server listening and degraded, with the reason in
8119
+ \`/health\` and \`stim-server service status\`, and no status followers,
8120
+ watchers, native helpers or recorder; it reads again every 30 seconds and
8121
+ serves once a read returns. It preserves claims and recordings; it does not
8122
+ restore an inaccessible volume or grant filesystem access.
8123
+
6281
8124
  THE GC WORKTREE GRACE PERIOD IS MACHINE-LEVEL
6282
8125
  \`gc.worktreeGraceMinutes\` is how long \`gc --delete\` waits before it removes
6283
8126
  a merged or idle linked worktree. The clock starts at the worktree's latest
@@ -6294,6 +8137,25 @@ which overrides the file. Absent means 120. \`0\` removes a finished worktree at
6294
8137
  once. A value that is not a whole number 0 or more is warned about by \`gc\`,
6295
8138
  which then uses 120. See \`guide cleanup\` for what else keeps a worktree.
6296
8139
 
8140
+ DEVICE RECORDINGS CAN BE TURNED OFF
8141
+ stim-server records the screens of owned simulators, emulators and the
8142
+ Stim-owned Chrome page while an automation tool drives them or a paired client
8143
+ watches them, so the phone app can scrub back through them. It keeps the last
8144
+ 15 minutes of footage per device under $STIM_HOME/workspaces/<id>/recordings/,
8145
+ on this Mac only, and serves it only to paired clients. \`recording.enabled\`
8146
+ turns it off:
8147
+
8148
+ stim settings set recording.enabled false --scope machine
8149
+
8150
+ It is a boolean, true by default, set at machine, repo or workspace scope; the
8151
+ workspace layer wins, then repo, then machine. STIM_RECORDING (true, false, 1
8152
+ or 0) overrides every layer; stim-server reads it from its own environment when
8153
+ it starts. Turning it off takes effect at once: \`stim settings set\` deletes
8154
+ the recordings of every workspace it turns off, and stim-server stops recording
8155
+ them within seconds, as soon as \`stim status\` shows recording.enabled false
8156
+ for them. Turning it back on records from then on. See \`guide cleanup\` for
8157
+ how recordings are removed otherwise.
8158
+
6297
8159
  STIM NEEDS NO PROJECT CHANGES TO RUN
6298
8160
  Nothing above is required to use Stim. The performance caches that used to
6299
8161
  be setup steps are supplied by Stim on the command lines it composes itself:
@@ -6649,11 +8511,28 @@ platform "web":
6649
8511
  page document is error whatever its
6650
8512
  status; another request's network failure or HTTP 5xx is
6651
8513
  error, a 4xx warn, a canceled request debug; browser messages such
6652
- as CSP violations; the browser's own lifecycle
8514
+ as CSP violations; the browser's own lifecycle; in-app
8515
+ route changes (history API or fragment) as web_route with
8516
+ url, at most four a second
8517
+ src agent input a DevTools client other than Stim's sent to the page
8518
+ while connected, as agent_action with driver (the tool),
8519
+ command click, type, press or scroll, msg (e.g. 'Clicked
8520
+ button "Save"', "Typed 12 characters into input#email"),
8521
+ deviceId (web.targetId) and details. Typed text is never
8522
+ recorded, only its length. Clicks, key presses, text input
8523
+ and wheel scrolls in the top frame are observed; pointer
8524
+ moves and iframes are not, and a typing, key or scroll burst
8525
+ is one record. Input from Stim Desktop's or the phone's Take
8526
+ over is not recorded, nor is other input within 3 seconds of
8527
+ it, nor input while no other client is connected. A person
8528
+ clicking a headed window while a tool is connected counts as
8529
+ that tool, and a script that disconnects within a few hundred
8530
+ milliseconds of its first input goes unrecorded
6653
8531
  Expo also prints web console calls on Metro ("Web LOG"), so they can appear
6654
8532
  twice: once from the page (client), once from Metro (metro, level info).
6655
8533
  Each load of the page's top-level document is a page-load marker: stim web,
6656
- stim reload, and a reload or navigation the page makes itself. logs --errors
8534
+ stim reload, and a reload or navigation the page makes itself. A reload's
8535
+ marker carries reload: true. logs --errors
6657
8536
  and the status error count report the page's records (platform web) from the
6658
8537
  latest load only, the way Chrome DevTools clears its console when the page
6659
8538
  navigates. A page load does not hide the native app's errors, and an ios or
@@ -6669,7 +8548,20 @@ them start their own browser:
6669
8548
  The port reaches only the Stim profile: Chrome refuses remote debugging on
6670
8549
  your default profile, and Stim never attaches to a browser it did not start.
6671
8550
  The port is managed like metro: stim ports lists it as web-cdp (managed),
6672
- and ports get, stop and release refuse it.
8551
+ and ports get, stop and release refuse it. While a tool is connected,
8552
+ status reports it as the browser's driver (web.activity, "driven by
8553
+ playwright"), the way it names agent-device on a simulator, and its clicks,
8554
+ typing and scrolls are agent actions: stim logs --source agent lists them,
8555
+ web.activity.recent["agent-action"] dates the newest, and Stim Desktop and
8556
+ the phone show them in the Web device's agent feed. web.targetId is the
8557
+ owned page; a tab another tool opens is not captured.
8558
+
8559
+ stim status also reports the document the page loaded last and how that
8560
+ load went: web.page { url, state: loading | loaded | failed, error?, route? }.
8561
+ route is the URL the page shows after an in-app route change since that
8562
+ load, and is absent when none moved it. A failed
8563
+ load (the dev server is down, a certificate error, a crash) shows on status's
8564
+ web line.
6673
8565
 
6674
8566
  OWNERSHIP AND CLEANUP
6675
8567
 
@@ -6689,10 +8581,390 @@ which would otherwise block the next launch. status reports
6689
8581
  browser-unverified when an identity cannot be proven; follow stim guide
6690
8582
  errors teardown.
6691
8583
 
8584
+ STIM DESKTOP
8585
+
8586
+ Stim Desktop shows the owned Chrome as a Web tile next to the simulators,
8587
+ with live frames of the page read from its DevTools endpoint, the page's URL,
8588
+ a "Page failed to load" pill and "Driven by" when a tool is attached. Take
8589
+ over sends clicks, scrolls and keys to the page. The tile's buttons open the
8590
+ URL in your own browser (never the Stim profile), run stim reload web, and
8591
+ close the browser with stim stop --slot web.
8592
+
8593
+ THE PHONE APP
8594
+
8595
+ The phone app shows the page as a Web tile in the devices grid and the
8596
+ workspace screen, and opens it in the device viewer as H.264 video through
8597
+ stim-server. With Control on, taps click, drags scroll and the keyboard types;
8598
+ Back goes back in the page's history. Reload in the workspace menu runs stim
8599
+ reload web. While stim-server recorded the page (\`guide settings\`, device
8600
+ recordings), the viewer can scrub back through it, with the agent's actions
8601
+ as markers.
8602
+
6692
8603
  LIMITS
6693
8604
 
6694
- Chrome and Chromium only. No Stim Desktop or phone viewer yet. The owned page
6695
- is one tab: a page the app opens in a new window is not captured.`
8605
+ Chrome and Chromium only. The owned page is one tab: a page the app opens in
8606
+ a new window is not captured.`
8607
+ },
8608
+ macos: {
8609
+ summary: "Swift Package macOS Debug apps: owned bundle, logs, local window viewing, and --remote on another Mac",
8610
+ body: () => `MACOS: SWIFT PACKAGE DEVELOPMENT PROTOTYPE
8611
+
8612
+ If Stim is not installed globally, replace stim with npx stim.
8613
+
8614
+ ios --remote <machine> shares hosting.machines and its separate device-host
8615
+ approval. Read guide lifecycle hosted-ios for simulator placement and Metro.
8616
+
8617
+ Run from the directory containing Package.swift. This prototype builds one
8618
+ explicit executable product in Debug, creates an isolated development app,
8619
+ and launches it. It does not use package scripts, Metro, simulators, Xcode
8620
+ projects, release builds, provisioning, or store distribution.
8621
+
8622
+ Set macos.product and macos.infoPlist explicitly in .stim.json. Optional
8623
+ macos.arguments is an array of arguments passed directly to the executable:
8624
+
8625
+ { "macos": { "product": "MyApp", "infoPlist": "Support/Info-Development.plist" } }
8626
+
8627
+ The plist must contain CFBundleIdentifier and CFBundleExecutable matching the
8628
+ product. Use a development plist without shared URL schemes or an update feed.
8629
+ Stim gives the copied bundle a workspace-specific identifier. SwiftPM resource
8630
+ bundles and frameworks in the reported build directory are copied into it;
8631
+ macos.resources maps destinations under Contents/Resources to file or directory
8632
+ sources relative to the Swift Package directory, for example:
8633
+
8634
+ { "macos": { "resources": { "AppIcon.icns": "Support/AppIcon-Dev.icns" },
8635
+ "assetCatalog": "Support/Assets.xcassets" } }
8636
+
8637
+ macos.assetCatalog selects an optional .xcassets directory compiled by fixed
8638
+ xcrun actool arguments. Copies and the compiled Assets.car are added before
8639
+ ad hoc signing. If actool does not emit Assets.car, staging refuses; set
8640
+ LSMinimumSystemVersion in the plist when Xcode requires a deployment target.
8641
+ Destinations are non-empty relative paths without empty, . or
8642
+ .. segments, at most 1024 characters, and cannot overlap another resource,
8643
+ Assets.car when a catalog is set, or a SwiftPM resource bundle. At most 256
8644
+ resources are allowed. Sources must exist as regular files or directories;
8645
+ their realpaths must stay inside the git repository root, or the Swift Package
8646
+ directory when there is no git root, and a source directory cannot contain
8647
+ symbolic links. Sources can use ../ to reach other files
8648
+ inside that repository. Custom packaging scripts are not run and extra
8649
+ executables, including Stim Desktop's sim-fold helper, are not built.
8650
+
8651
+ stim macos # fixed SwiftPM Debug build, then launch
8652
+ stim macos --json # one launch record; progress goes to stderr
8653
+ stim status --json # environments[].macos and build state
8654
+ stim logs --source build
8655
+ stim logs --errors
8656
+ stim stop # stop this workspace's owned app and supervisor
8657
+
8658
+ Each invocation stops its previous owned app, rebuilds and launches. SwiftPM
8659
+ keeps incremental outputs in the workspace's runtime directory under STIM_HOME.
8660
+ Local stim macos starts the app in the background without activating it or
8661
+ changing focus: it sets STIM_BACKGROUND_LAUNCH=1 in the app's environment, which
8662
+ Stim Desktop honors. An app that activates itself at launch still takes focus.
8663
+ Hosted launches (macos --remote) do not set it.
8664
+ macOS artifacts are not cached. This prototype has no --plan, --slot, or reload command.
8665
+ A failed build records its error and compiler output without launching an app.
8666
+ Runtime stdout and stderr become client records; build output becomes build
8667
+ records, all with platform "macos". Unexpected app exits are error records.
8668
+ Stim runs the app with NSUnbufferedIO=YES, so Swift print output arrives per
8669
+ line instead of when the app exits.
8670
+
8671
+ BUILD OFFLOAD
8672
+
8673
+ --build-machine <auto|local|name> overrides STIM_OFFLOAD_MACHINE and the
8674
+ machine setting offload.machine (default auto). local builds here; a name
8675
+ requires the matching configured and paired build machine, ignoring offload.mode
8676
+ and this Mac's capacity. Any failure is STIM_OFFLOAD_REFUSED without a local xcodebuild, Gradle or
8677
+ SwiftPM compile or another machine. Invalid, unlisted or unpaired selections
8678
+ refuse before stopping the app or changing its build record. See stim guide errors STIM_OFFLOAD_REFUSED.
8679
+
8680
+ With offload.machine auto, offload.mode places these SwiftPM Debug builds:
8681
+ auto builds here while this Mac has capacity, force uses an approved build
8682
+ machine when one accepts, and off always builds here. Configure offload.machines
8683
+ and approve build access as described in stim guide settings. The worker needs
8684
+ matching Stim, CPU architecture, Xcode and macOS SDK, plus network access to
8685
+ fetch package dependencies the first time. It keeps SwiftPM dependencies in a
8686
+ per-client cache and incremental outputs per repository. It runs fixed swift
8687
+ build commands without JavaScript installs, prebuild or pods. It receives only
8688
+ the files git lists (tracked and untracked, not ignored). Resource and catalog
8689
+ sources must be among those files. The client sends resolved sources relative
8690
+ to the repository root; the worker resolves them inside its checkout.
8691
+
8692
+ The client validates the development plist and resource entries before asking a machine. It verifies
8693
+ the returned archive's sha256, bundle identifier, executable, declared resources
8694
+ and ad hoc signature
8695
+ before replacing the owned bundle, then launches locally as usual. With auto, every offload
8696
+ failure falls back to the local build, including force. Failed staging preserves
8697
+ the previous bundle. No failed artifact is promoted or cached. The build record
8698
+ is unchanged by invalid, unlisted or unpaired setup refusals. It
8699
+ carries buildMachine (selected) and builtOn (here or the machine, absent until
8700
+ a build runs), and errorCode on a typed failure. It carries offloadedTo only for a remote build, and offloadFallback when an offload
8701
+ attempt falls back here. Placement and failure reasons are in build logs.
8702
+
8703
+ OWNERSHIP AND LOCAL VIEWING
8704
+
8705
+ The supervisor owns a process-identity claim with the app as its child. Stop
8706
+ signals only verified recorded identities. An unverifiable or malformed owner
8707
+ refuses with STIM_MACOS_OWNER_UNVERIFIED; other apps remain untouched.
8708
+
8709
+ Stim Desktop shows this app in its workspace, with Build and run, Open app (local
8710
+ apps only) and Stop, and a live preview that updates itself while the app runs. Capture and Open app verify the recorded executable, bundle
8711
+ identifier, PID and process start time. Open app rechecks that the captured window
8712
+ is still the app's front window, or raises the pinned one, then activates that
8713
+ owned app for normal native input.
8714
+ The preview follows the app's front standard window, its main window with any
8715
+ attached sheet, as the app opens, switches, closes or resizes windows. It never
8716
+ captures another process's windows, menus or the desktop. Without Device Control
8717
+ and Data Access permission (Accessibility on macOS 26 and earlier) Stim cannot
8718
+ tell which window is in front, so the preview shows only an app whose one window
8719
+ contains the others. The viewer never captures its own process recursively.
8720
+ The captured view is read-only; background mouse/keyboard relay is not included.
8721
+
8722
+ Capture requires existing Screen & System Audio Recording permission (Screen
8723
+ Recording on macOS 14); Open app also requires Device Control and Data Access
8724
+ permission (Accessibility on macOS 26 and earlier). The first native viewer opening
8725
+ shows one Desktop setup screen for both permissions, named for this Mac's macOS
8726
+ version, with statuses, Request permissions, Settings and Check again. Settings
8727
+ opens the matching System Settings > Privacy & Security pane. You approve normal macOS requests; Stim never resets or grants access
8728
+ automatically. Permissions on the app card reopens setup. Builds never prompt.
8729
+ If unavailable, use the normal app window and
8730
+ read the workspace logs.
8731
+
8732
+ PHONE VIEWING AND CONTROL
8733
+
8734
+ A paired phone shows native app and build state in the workspace and home list.
8735
+ Tap the app tile to view the app's front window through stim-server. The server
8736
+ requires read access and its macos-window feature; it rechecks the recorded PID,
8737
+ process start time, executable and bundle before capture and on every frame.
8738
+ It never captures the desktop or another application, and offers no replay.
8739
+ The view follows the app's front standard window like the Desktop preview. A
8740
+ server advertising macos-windows also sends a macos-windows event on the frames
8741
+ subscription with the captured window and the app's windows (id, title and frame
8742
+ in points), after subscribing and whenever they change. Viewing starts only with
8743
+ an open window; after the app closes its last one the view reports a delay until
8744
+ another opens.
8745
+
8746
+ WINDOWS
8747
+
8748
+ The view follows the app's front window by default. A controller can pin it to
8749
+ one of the app's windows instead: with a server advertising macos-window-select,
8750
+ input.window { session, window: <id from macos-windows> } brings that window to
8751
+ the front of the app, and capture and input stay on it while another window
8752
+ comes forward on the Mac. input.window { session, window: null } follows the
8753
+ front window again. The pin also ends when its window closes or minimizes, or
8754
+ when the Control session that set it ends for any reason, including release,
8755
+ takeover, disconnect and the five-minute idle timeout, since viewers without
8756
+ Control cannot unpin; macos-windows reports pinned true while it holds. Choosing
8757
+ a window while the app shows a modal dialog, or one that just closed, is dropped
8758
+ without ending Control. The phone's Control toolbar and Stim Desktop's app card and
8759
+ hosted viewer offer the same choice as a Window menu: Follow front window, or a
8760
+ window by title. Stim Desktop's local preview pins without Control because it
8761
+ only raises the window among the app's own windows. An agent that drives the app
8762
+ through the stim-server protocol sends the same input.window.
8763
+
8764
+ A server advertising macos-window-control also supports the phone's Control
8765
+ mode on a control pairing. Tap/click and drag act on the displayed window;
8766
+ Scroll mode turns a drag into pixel scrolling. The main toolbar offers Keyboard
8767
+ and Scroll. Keyboard types printable ASCII and attaches an extra-key strip with
8768
+ Tab, Escape, Backspace, arrows and labeled Select all, Undo, Save, Copy, Paste,
8769
+ Cut and Find shortcuts. Shift, Control, Option and Command apply to the next
8770
+ supported key, then clear; dismissing the keyboard clears them too.
8771
+
8772
+ A newly built phone client with Keyboard Controller is required; this native
8773
+ change cannot be delivered to an older client by a JavaScript update. Servers
8774
+ advertising macos-keyboard-extended accept modified a-z and 0-9 one key at a
8775
+ time. Older servers retain fixed shortcuts and navigation but cannot receive
8776
+ other modified letters or digits. Multi-character modified input and symbols
8777
+ are not supported; ordinary typing keeps using input.text.
8778
+
8779
+ Shortcuts for a-z and 0-9 use the key that types the character in the Mac's
8780
+ current keyboard layout, using the Command layer when Command is held. Dvorak
8781
+ and Dvorak-QWERTY Command are supported. On Russian and similar layouts
8782
+ (Cyrillic, Greek, Hebrew, Arabic), Latin-letter shortcuts work with Command;
8783
+ Control-only or Option-only letter shortcuts are refused. With Control, the
8784
+ layout's Control table must also yield the requested character or, for letters,
8785
+ its C0 control character; otherwise the shortcut is refused. The Mac's selected
8786
+ input source is read on each key, so switching layouts takes effect on
8787
+ the next key. Letters and digits available only with Shift or Option, or through
8788
+ a dead key (for example digits on AZERTY), are refused with a reason naming the
8789
+ layout, and Control ends. Stim does not add modifiers to reach those characters.
8790
+ Ordinary typing and navigation do not depend on the layout. Symbols such as
8791
+ comma remain unsupported key names.
8792
+ Control posts input to the owned process without activating it or raising its
8793
+ 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
8794
+ attached sheet) does Stim activate the app to deliver input, waiting up to one
8795
+ second for focus. The helper then sends a controlActivated notice, which stim-server
8796
+ logs.
8797
+ Clicks on views that reject the first mouse, such as custom views and SwiftUI
8798
+ onTapGesture regions, do not land while the app is in the background. Use
8799
+ Desktop's Open app to bring the app to the front for those views.
8800
+ Control holds one exclusive server session per owned app, ends on disconnect,
8801
+ revocation, takeover or five minutes without input, and does not take a CLI
8802
+ simulator/device lock. Each action rechecks the exact owned process and that the
8803
+ captured window is still the app's front standard window, or the pinned one. The
8804
+ app's other windows are allowed; input goes to the captured window, and a sheet attached to it takes
8805
+ focus and pointer input. Input that arrives while the view moves to another
8806
+ window is dropped and Control continues. A modal dialog window refuses input. A
8807
+ sheet larger than the captured window is not supported. Existing Device
8808
+ Control and Data Access permission (Accessibility on macOS 26 and earlier) is
8809
+ required. The phone and server never request or reset it. A refusal ends Control
8810
+ with its reason while viewing and logs remain usable. Older servers remain
8811
+ view-only.
8812
+ Native Control uses dynamically resolved private CoreGraphics input SPI in the
8813
+ server helper, outside the phone and Mac App Store app binaries. A missing symbol
8814
+ or incompatible macOS version refuses Control while viewing and logs remain usable.
8815
+
8816
+ The existing capture host needs Screen & System Audio Recording permission (Screen
8817
+ Recording on macOS 14). If capture is denied,
8818
+ open Permissions in Stim Desktop on the host Mac, approve its normal OS requests
8819
+ and reconnect the phone viewer. The Desktop host presents setup on the first
8820
+ native viewer opening, including one initiated by the phone. A server started
8821
+ outside Desktop uses that launching host's permissions; granting this copy of Stim
8822
+ may not apply to it. The server never requests or resets permissions. Status and logs still work. Tap the build card for
8823
+ SwiftPM output or the logs card for native runtime stdout and stderr. Metro is not
8824
+ used.
8825
+
8826
+ ON ANOTHER MAC
8827
+
8828
+ stim macos --remote <machine> builds the Debug app on this Mac and runs it on an
8829
+ approved hosting Mac over the tailnet, without SSH. The machine must be listed
8830
+ in hosting.machines and approved: stim doctor --fix asks it, a person on that
8831
+ Mac runs stim-server devices grant <id> --device-host or stim-server setup
8832
+ with that client node, ticket and expiry (guide settings), and stim doctor then
8833
+ records the approval. Stim connects only to the machine's pinned tailnet node.
8834
+ A refusal or an unreachable host fails the command; it never launches here
8835
+ instead.
8836
+
8837
+ The --remote value is a hosting Mac name from hosting.machines. macOS refuses
8838
+ eas and proxy because it has neither backend. It also refuses auto until
8839
+ automatic placement ships. These reserved names are case-insensitive and
8840
+ trimmed; they refuse with STIM_BAD_ARG before state access or a connection.
8841
+
8842
+ Logs: the host records the hosted app's stdout, stderr and exit like a local run.
8843
+ stim logs, including --errors, --json and --follow, asks the host over the same
8844
+ approved connection, copies the records it has not copied yet into the
8845
+ workspace's logs/macos-host.ndjson and prints them with the local records.
8846
+ stim stop copies the last ones, including the exit record, before it forgets the
8847
+ placement, so the logs stay readable after stop. A host that cannot answer, because
8848
+ it is unreachable or runs a stim-server that predates this, costs one stderr
8849
+ warning (after up to 10 seconds of connecting); stdout still carries the records already copied, so logs --json stays
8850
+ valid NDJSON. The host's unified log is not collected: os.Logger output that is
8851
+ not written to stderr does not appear.
8852
+
8853
+ Phones and Stim Desktop view and control the hosted app through this Mac's
8854
+ stim-server, which relays to the host. Hosted iOS simulator tiles use the same
8855
+ relay and show on <machine>; see guide lifecycle hosted-ios. The phone sends clicks, scrolls,
8856
+ text and keys; Stim Desktop's Control sends clicks, drags and typed text.
8857
+
8858
+ stim macos --remote mini # build here, deliver, launch on mini
8859
+ stim macos --remote mini --json # { platform, product, launchId, build, host }
8860
+ stim status --json # environments[].macos.state and macos.host
8861
+ stim stop # stop the session on mini and confirm it
8862
+ stim worktree remove <path> # also stops it
8863
+
8864
+ The copy keeps the plist's own CFBundleIdentifier. The host runs it as
8865
+ <id>.hosted<slot> from a fixed slot pool, so the bundle id stays the same
8866
+ across rebuilds; whether macOS keeps the permissions the hosted app asks for
8867
+ itself also depends on how the host signs it. Running the
8868
+ command again reuses the session and delivers a new copy. macos.arguments are
8869
+ passed to the hosted launch as plain arguments, without environment injection:
8870
+ at most 32 arguments, 1024 characters each and 8192 characters total. Empty
8871
+ strings are allowed; NUL, CR and LF are refused. Values are stored in the host's
8872
+ app receipt and visible in process listings, so do not put secrets there. Older
8873
+ hosts ignore them and Stim warns to update stim-server on the host.
8874
+ status --json reports the host's applied arguments under
8875
+ environments[].macos.arguments.
8876
+ A workspace has one macOS app: a local stim macos
8877
+ refuses while it runs on a host, and --remote with a different machine refuses
8878
+ until stim stop. When the host cannot be reached or does not confirm the stop,
8879
+ the placement stays recorded; restore the connection and run stim stop again.
8880
+
8881
+ Viewing and controlling the hosted app from a phone needs Screen & System Audio
8882
+ Recording and Device Control and Data Access (Accessibility on macOS 26 and
8883
+ earlier), granted once on the host to the app that runs stim-server, not to the
8884
+ hosted app. stim-server service install runs the server under the signed Stim
8885
+ Host app (dev.stim.host, installed in ~/Applications) and shows
8886
+ macOS's own requests on that Mac's screen; a person there approves them.
8887
+ stim-server service status shows the grants, and stim doctor here reports an
8888
+ approved host that lacks them. A server started by Stim Desktop uses Desktop's
8889
+ grants.
8890
+
8891
+ Hosted delivery carries the staged bundle with declared resources and compiled assets.
8892
+ When offload built it on the host's own tailnet node, the host copies the files
8893
+ from the build it kept for this Mac instead of receiving them again, admitting
8894
+ only bytes that match the bundle's digests here. That needs both the build and
8895
+ the device-host approval for this Mac. Files the host cannot take, an older
8896
+ host, or a build fetched more than 10 minutes earlier are uploaded as before.
8897
+
8898
+ status --json reports macos.host { machine, session, appSlot, appAttempt,
8899
+ bundleId, agent }. For hosted placements only, status asks the host for the
8900
+ session state with about a 3 s timeout per connection and request and a 10 s
8901
+ cache. It reports stopped when the host says the session stopped, for example
8902
+ after a stim-server restart there, and unverified when the host is unreachable
8903
+ or cannot confirm. Run stim macos --remote <machine> to launch a stopped app
8904
+ again, or stim stop to clear or reconcile the placement. The host field stays
8905
+ in status --json for cleanup.
8906
+ While a placement is recorded, gc treats the workspace as in use. agent says
8907
+ how a coding agent drives the app: { driver: "none", setting:
8908
+ "hosting.agentDriver" } until the hosting Mac's owner sets that setting there,
8909
+ or { driver: "agent-device", remoteConfig, command }. remoteConfig is a mode
8910
+ 0600 file in the workspace directory that holds the credential; run the command
8911
+ it names and never print the file. stop, worktree remove and gc first run
8912
+ agent-device close and disconnect for the connection that agent-device reports
8913
+ as connected to that remote config (the default or active session), then delete
8914
+ the file, so the next hosted workspace needs no manual disconnect. Any other
8915
+ connection, including one under another session name, stays untouched, and a
8916
+ failure or a missing agent-device is reported without blocking the stop. When
8917
+ the hosted session ends, stim-server removes its agent-device session
8918
+ directories under its own state directory. Start with agent-device open
8919
+ <bundleId> --remote-config <path>, using macos.host.bundleId; the lease allows
8920
+ open, close, snapshot, wait, find, get, is, click, fill, press, type, focus,
8921
+ scroll, screenshot and batch on that app only. The agent-device on this Mac must
8922
+ know the macos-app lease backend. A request outside that command list, or a
8923
+ method the relay does not forward, is refused with an agent-device UNAUTHORIZED
8924
+ error whose details.reason is STIM_AGENT_REQUEST_REFUSED and whose message names
8925
+ the refused command or method; do not retry it.
8926
+
8927
+ Agents that built, launched and drove test copies on another Mac with an SSH
8928
+ script such as mini-desktop.sh use these instead:
8929
+
8930
+ script command stim
8931
+ build stim macos --remote <mac>
8932
+ launch the same command; the host launches it under the
8933
+ session's own home
8934
+ shot <out.png> agent-device screenshot --remote-config <path>
8935
+ click, rclick, key, type agent-device click, press or type --remote-config <path>
8936
+ quit stim stop (verified stop; the host frees the slot
8937
+ and removes the app's defaults)
8938
+ clean stim worktree remove
8939
+ 3 concurrent slots concurrency.maxDevices on the host; one bundle id
8940
+ slot per session
8941
+
8942
+ The agent-device rows work once the host's agent field names agent-device.
8943
+
8944
+ DESKTOP DOGFOOD
8945
+
8946
+ This repository's apps/desktop/.stim.json selects the full StimDesktop app,
8947
+ its development plist, asset catalog, fonts, branding and licences. Its sim-fold
8948
+ helper is not built. It monitors the regular Stim home alongside the installed
8949
+ app, with a workspace-specific bundle identifier and separate preferences.
8950
+ Its launch arguments disable automatic cleanup and notification alerts in this development copy.
8951
+ Use Window > SwiftUI Playground for in-memory production screen fixtures.
8952
+
8953
+ cd apps/desktop
8954
+ stim macos
8955
+ stim logs --source build
8956
+ stim stop
8957
+
8958
+ When using an unreleased CLI from this repository, build the packages first and
8959
+ set STIM_BIN to the absolute packages/stim-cli/dist/cli.mjs path before running
8960
+ that executable's macos command. The development app inherits this CLI override;
8961
+ the installed app's CLI preference and running server remain unchanged.
8962
+
8963
+ Ask an agent: "In my Swift Package app, configure the executable product and a
8964
+ development Info.plist for stim macos. Build and show its owned window in Stim
8965
+ Desktop, verify a source edit and readable failed-build logs, then stop only
8966
+ this workspace's app. Do not change permissions or use custom build scripts."
8967
+ `
6696
8968
  }
6697
8969
  };
6698
8970
  //#endregion
@@ -6774,7 +9046,7 @@ function renderIndex(version) {
6774
9046
  return lines.join("\n");
6775
9047
  }
6776
9048
  function guideCommand(program, version, status = (running) => guideStatus({
6777
- projectRoot: findProjectRoot(process.cwd()),
9049
+ projectRoot: findProjectRoot$1(process.cwd()),
6778
9050
  running
6779
9051
  })) {
6780
9052
  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) => {