stim 1.16.0 → 1.17.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 (107) hide show
  1. package/dist/{activity-BxwdD81S.mjs → activity-IRvJY_ru.mjs} +23 -3
  2. package/dist/{agent-device-usage-output-Dpih1Tfa.mjs → agent-device-usage-output-BK41vnzN.mjs} +3 -3
  3. package/dist/{android-Ift4kVvt.mjs → android--GorydsK.mjs} +679 -317
  4. package/dist/{android-B_NumVyQ.mjs → android-B_YNZy61.mjs} +7 -43
  5. package/dist/{android-r2rBL8kV.mjs → android-DE4v59Mb.mjs} +12 -3
  6. package/dist/{android-cas-8U4DPqeR.mjs → android-cas-ZFpjJBL_.mjs} +6 -5
  7. package/dist/{app-install-CKWdbXIX.mjs → app-install-VPkI5WEg.mjs} +6 -5
  8. package/dist/{budget-DjrURfQn.mjs → budget-DM984mnd.mjs} +34 -18
  9. package/dist/{build-plan-DjeSb2Xl.mjs → build-plan-CmQCoyT-.mjs} +345 -86
  10. package/dist/{build-slots-rUk77xqh.mjs → build-slots-41v_d2x9.mjs} +35 -25
  11. package/dist/{cli-BkYZ_6hy.mjs → cli-Daxug5-K.mjs} +17 -17
  12. package/dist/cli.mjs +1 -1
  13. package/dist/collector-run.mjs +3 -3
  14. package/dist/{command-output-DnJMdEY-.mjs → command-output-CDAIFD-8.mjs} +3 -3
  15. package/dist/{created-devices-D5owjN4C.mjs → created-devices-CpvzBL25.mjs} +1 -1
  16. package/dist/{dependency-state-B6cMXZab.mjs → dependency-state-CLcBHH2M.mjs} +1 -1
  17. package/dist/{deps-urbmfXig.mjs → deps-DNWaQs4V.mjs} +1 -1
  18. package/dist/{dev-client-CroAIjjj.mjs → dev-client-B-XC4uRH.mjs} +2 -2
  19. package/dist/{device-DZQjF7H5.mjs → device-C8IOoJPv.mjs} +7 -7
  20. package/dist/device-capacity-D0GWHnTM.mjs +580 -0
  21. package/dist/device-host-worker.mjs +436 -154
  22. package/dist/{device-ios-BadmeL2V.mjs → device-ios-Bc7vbGwl.mjs} +75 -44
  23. package/dist/{device-lease-D-hLqx1b.mjs → device-lease-DpoS36_u.mjs} +2 -2
  24. package/dist/{device-lease-run-CqjShqWW.mjs → device-lease-run-B8YltIH_.mjs} +3 -3
  25. package/dist/{device-pool-CUaEWMMm.mjs → device-pool-3o7s5lo4.mjs} +2 -2
  26. package/dist/{device-remote-D5WVWqOq.mjs → device-remote-hFSkpfQu.mjs} +12 -12
  27. package/dist/{doctor-Cimf-gGB.mjs → doctor-Cnk9d7Hm.mjs} +32 -51
  28. package/dist/{error-diagnostics-B9L4Ae0x.mjs → error-diagnostics-C-NMnhrk.mjs} +7 -7
  29. package/dist/{gc-Bqy4bjEu.mjs → gc-DxCL2A46.mjs} +308 -26
  30. package/dist/{gradle-BsO1MdsH.mjs → gradle-D7-L2Th3.mjs} +3 -3
  31. package/dist/{guide-B_uyKTxW.mjs → guide-DjrTQGik.mjs} +1662 -565
  32. package/dist/{guide-status-CjEZMAD4.mjs → guide-status-Du-ZQ3FD.mjs} +1 -1
  33. package/dist/hosted-android-DwLS4Mse.mjs +32 -0
  34. package/dist/{hosted-client-a-WmqE2L.mjs → hosted-client-wk1W_CNW.mjs} +2 -3
  35. package/dist/hosted-ios-aMo7Cljf.mjs +15 -0
  36. package/dist/{hosted-logs-D5g041Q3.mjs → hosted-logs-DqYmWtXc.mjs} +16 -16
  37. package/dist/{hosted-macos-BSZyIb3y.mjs → hosted-macos-dlHXEeF5.mjs} +4 -4
  38. package/dist/{hosted-ios-CIWelIqf.mjs → hosted-native-C9xJ9rpA.mjs} +77 -42
  39. package/dist/{idle-DYVymG92.mjs → idle-B2m-Ovod.mjs} +122 -58
  40. package/dist/{idle-shutdown-CTsDnAde.mjs → idle-shutdown-_3oOV1V_.mjs} +25 -17
  41. package/dist/{in-use-DvBw0Kxo.mjs → in-use-CvGOUn-x.mjs} +11 -11
  42. package/dist/{ios-aq3QoGI3.mjs → ios-DE6duQoz.mjs} +170 -88
  43. package/dist/{ios-BaInn3sW.mjs → ios-DSfnPfse.mjs} +4 -4
  44. package/dist/{ios-device-FfCN_C8p.mjs → ios-device-pAN4t54-.mjs} +1 -1
  45. package/dist/ios-state-8L3lgum6.mjs +89 -0
  46. package/dist/{launch-verify-2DBRizjo.mjs → launch-verify-lTnWZikR.mjs} +2 -2
  47. package/dist/{logs-CHEVK8zD.mjs → logs-DUoKlDoS.mjs} +36 -23
  48. package/dist/{client-BTY686R6.mjs → machines-C2XiNwh9.mjs} +278 -20
  49. package/dist/{macos-CSYEAFmF.mjs → macos-32po3kUq.mjs} +14 -14
  50. package/dist/macos-run.mjs +1 -1
  51. package/dist/maintenance-run.mjs +2 -2
  52. package/dist/{metro-BeK1ZPz1.mjs → metro-g8NqiJp-.mjs} +1 -1
  53. package/dist/{metro-gateway-BJ-rxQAB.mjs → metro-gateway-DmslUM30.mjs} +22 -3
  54. package/dist/{named-ports-Bk_YXEmE.mjs → named-ports-C8m8EQs2.mjs} +2 -2
  55. package/dist/{native-run-NaBrRFtQ.mjs → native-run-D1Q56rAD.mjs} +17 -6
  56. package/dist/{native-runtime-DBzRnEKS.mjs → native-runtime-DhJI3IVQ.mjs} +6 -6
  57. package/dist/offload-worker.mjs +8 -8
  58. package/dist/{teardown-CL6vDs2Y.mjs → ownership-BMLqyBgJ.mjs} +603 -26
  59. package/dist/{ownership-DiA16AsN.mjs → ownership-DyRQru0q.mjs} +2 -2
  60. package/dist/{ports-R2k4Knz3.mjs → ports-4nYqxPWC.mjs} +2 -2
  61. package/dist/{prebuild-D2fsca_Z.mjs → prebuild-D7RUMkwN.mjs} +3 -3
  62. package/dist/{preview-DhVbvPAZ.mjs → preview-CxPRIEht.mjs} +7 -7
  63. package/dist/{project-Du91Ilvc.mjs → project-D47Rh0ly.mjs} +7 -2
  64. package/dist/{recordings-BIXOzVnr.mjs → recordings-BNgsD1Tr.mjs} +1 -1
  65. package/dist/{reload-rM9jV1fz.mjs → reload-qZei6iWn.mjs} +62 -35
  66. package/dist/{remote-cache-C8-V13XN.mjs → remote-cache-Ci4DQYYj.mjs} +3 -3
  67. package/dist/{run-f4vI6EnK.mjs → run-CkE9y0nc.mjs} +1 -1
  68. package/dist/{server-bare-CpPGmnmO.mjs → server-bare-BkrSxSsZ.mjs} +1 -1
  69. package/dist/{server-expo-MC0rHlOC.mjs → server-expo-CHTyXD0G.mjs} +2 -2
  70. package/dist/server-expo-CeakhI8P.mjs +2 -0
  71. package/dist/{settings-oTSkkNrM.mjs → settings-Cfd-0r2d.mjs} +10 -9
  72. package/dist/{settings-ZkDzkzvB.mjs → settings-SR9OmW9h.mjs} +3 -3
  73. package/dist/settings.schema.json +42 -13
  74. package/dist/{simslim-B8-7AoTI.mjs → simslim-BWNrc3M3.mjs} +3 -3
  75. package/dist/{slot-launch-B8VrHxn1.mjs → slot-launch-DmjBmpQV.mjs} +8 -8
  76. package/dist/{start-CX8Ig-U2.mjs → start-BopZcr4n.mjs} +1 -1
  77. package/dist/{start-ClDfwXa4.mjs → start-uee1wWjY.mjs} +11 -11
  78. package/dist/{state-DcW6vOd_.mjs → state-BKa8wNN5.mjs} +1 -1
  79. package/dist/{state-3DwSQIBK.mjs → state-DyaNTgfR.mjs} +1 -1
  80. package/dist/{state-C1jB7Q8q.mjs → state-PeQiJuIu.mjs} +2 -2
  81. package/dist/{stats-CEZ_i5je.mjs → stats-Bq4fJOdB.mjs} +7 -4
  82. package/dist/{status-BuvBFeII.mjs → status-CzS4bPO9.mjs} +4 -4
  83. package/dist/{status-Cs7VLg9D.mjs → status-dXX2JX1C.mjs} +125 -35
  84. package/dist/{stop-Dt_dM16A.mjs → stop-BCwvvs7F.mjs} +2 -2
  85. package/dist/{stop-Bffozsac.mjs → stop-BjizrQwv.mjs} +48 -27
  86. package/dist/{stop-B0Yxtmiy.mjs → stop-Cr1d8DSE.mjs} +4 -4
  87. package/dist/{stop-cause-CidxIqup.mjs → stop-cause-rhmGfwvt.mjs} +1 -1
  88. package/dist/supervisor-run.d.mts +7 -4
  89. package/dist/supervisor-run.mjs +26 -28
  90. package/dist/{support-D4aYJniO.mjs → support-BJPgYCu-.mjs} +44 -19
  91. package/dist/{support-CAP8qDUF.mjs → support-F9-sr0Oi.mjs} +6 -6
  92. package/dist/{toolchain-DkUflduf.mjs → toolchain-CNIBTIOv.mjs} +7 -7
  93. package/dist/{warm-progress-CpGv3Wb6.mjs → warm-progress-DF4JRvYI.mjs} +3 -3
  94. package/dist/watchman-APYjXSQa.mjs +48 -0
  95. package/dist/{web-DWUaMp8P.mjs → web-exQtGkoc.mjs} +12 -13
  96. package/dist/web-run.mjs +4 -4
  97. package/dist/{workspace-process-lock-BL7Q0HCO.mjs → workspace-process-lock-2mNzOlaB.mjs} +3 -2
  98. package/dist/{workspace-state-B8-aJ1_5.mjs → workspace-state-DqcCbpE7.mjs} +2 -2
  99. package/dist/{workspaces-BV4pdHFz.mjs → workspaces-Bcib97-6.mjs} +3 -3
  100. package/dist/{worktree-jnG8Iph5.mjs → worktree-Dc98cE6_.mjs} +1 -1
  101. package/dist/{worktree-K-xj9Db0.mjs → worktree-ZpZR3S1k.mjs} +141 -40
  102. package/package.json +5 -5
  103. package/dist/device-capacity-BHwxNBlu.mjs +0 -136
  104. package/dist/ios-state-DS5UpYTH.mjs +0 -48
  105. package/dist/machines-BIBUdhpj.mjs +0 -265
  106. package/dist/ownership-hCkPzGmr.mjs +0 -571
  107. package/dist/server-expo-CVqO2c35.mjs +0 -2
@@ -1,15 +1,13 @@
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";
1
+ import { t as ANDROID_AVD_CONFIG_HELP } from "./settings-Cfd-0r2d.mjs";
2
+ import { s as findProjectRoot$1 } from "./project-D47Rh0ly.mjs";
3
+ import { t as RECENT_LAUNCH_MS } from "./status-CzS4bPO9.mjs";
4
+ import { t as guideStatus } from "./guide-status-Du-ZQ3FD.mjs";
5
5
  import { SETTINGS, SETTINGS_SCHEMA_URL } from "@stim-cli/core/state";
6
6
  import chalk from "chalk";
7
- //#endregion
8
- //#region src/guide/index.ts
9
- const TOPICS = {
10
- agent: {
11
- summary: "The normal coding-agent workflow, safety rules, and topic routing",
12
- body: () => `AGENT WORKFLOW
7
+ //#region src/guide/agent.ts
8
+ var agent_default = {
9
+ summary: "The normal coding-agent workflow, safety rules, and topic routing",
10
+ body: () => `AGENT WORKFLOW
13
11
 
14
12
  Use Stim to run React Native and Expo apps without sharing a Metro port or
15
13
  device with another workspace. Prefer plain output: it streams each phase and
@@ -43,15 +41,23 @@ reach multiple devices. A launch counts a bundle delivery only when it can
43
41
  attribute it to its own device; read guide lifecycle options for the rule.
44
42
  Use stop --slot <name> for one slot, or plain stop for the whole workspace.
45
43
 
46
- HOSTED IOS
44
+ HOSTED IOS AND ANDROID
47
45
 
48
- For ios --remote <machine>, read guide lifecycle hosted-ios. Stop before switching
46
+ For ios --remote <machine|auto>, read guide lifecycle hosted-ios.
47
+ For android --remote <machine|auto>, read guide lifecycle hosted-android. Native
48
+ logcat records arrive through stim logs, including --errors and --json;
49
+ Desktop and the phone view/control it through the local stim-server relay.
50
+ Stop before switching
49
51
  between a local simulator and a hosting Mac. Hosted Debug requires the local
50
52
  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.
53
+ A named Mac never falls back here; auto can use the local device queue when no
54
+ host admits. Auto keeps a live local slot or recorded hosted session. Follow the
55
+ placement reason and the reported device. Stop reconciles a recorded host
56
+ before changing named placement.
57
+ For hosted iOS or Android, use agent-device <command> --remote-config <file>
58
+ from ios.host.agent or android.host.agent in stim status --json. Open the
59
+ installed app first; never use the host UDID or serial locally. See guide
60
+ lifecycle hosted-ios or hosted-android for the agent policy and setup.
55
61
 
56
62
  When driving a device yourself with agent-device, set AGENT_DEVICE_STATE_DIR
57
63
  from agentDevice.stateDir in stim ios|android|macos --json or status --json.
@@ -135,6 +141,7 @@ or hosting.* for the user, or approve requests. Read stim guide settings for
135
141
  Desktop access tickets and person-owned approval.
136
142
  Read stim guide settings to select a build machine. Keep a requested named
137
143
  placement when recovering STIM_OFFLOAD_REFUSED: report its reason and remedy.
144
+ When the user mentions slow builds, a busy Mac or wanting updates away from the desk, tell them about Stim Desktop > Settings > Build machines > Add or Phones > Pair a Phone; see stim guide settings.
138
145
 
139
146
  Read stim guide lifecycle concurrency when a build waits on another workspace
140
147
  or a build call times out. A native build can outlive a shell timeout; if the
@@ -250,6 +257,17 @@ stop and worktree remove release this workspace's leases. On a physical
250
257
  iPhone, stop also closes the app by ending its log collector; it does not
251
258
  shut down the phone or uninstall the app.
252
259
 
260
+ Owned-device caps wait in FIFO order across the Stim home for 600s by default.
261
+ Use --wait <seconds> to change the bound or --no-wait to refuse at once.
262
+ At the cap, the queue head shuts down one longest-idle eligible owned device
263
+ from another workspace, at most once every 15 seconds, then rechecks capacity.
264
+ devices.reclaimIdleMinutes is 10 by default; 0 off.
265
+ It never deletes devices or touches this workspace, physical, hosted, remote,
266
+ parked or other homes' devices. Drivers, locks, builds, viewers and recent
267
+ activity prevent reclaim. The supervisor's own idle shutdown default is 30m.
268
+ Read the holder names in progress and build.waitingFor in status; do not start
269
+ replacement runs while one is queued. See \`stim guide lifecycle concurrency\`.
270
+
253
271
  Treat a refusal as an ownership or state mismatch: read its code and remedy.
254
272
  Never reach for --force first.
255
273
  Stim leaves externally started Metro servers alone. Stop them with their original
@@ -338,6 +356,7 @@ Read the matching guide before acting in these situations:
338
356
  | Swift Package macOS development | stim guide macos |
339
357
  | macOS app on another Mac (stim macos --remote) | stim guide macos |
340
358
  | Unfamiliar state or JSON field | stim guide facts payloads |
359
+ | User asks for the Stim tutorial | stim guide tutorial |
341
360
  | Refusal without a code | stim guide errors |
342
361
 
343
362
  Use the CODE exactly as printed; codes sharing a header resolve to the same
@@ -373,11 +392,15 @@ FULL TOPIC LIST
373
392
  stim guide cleanup # what reclaims a device, and what deletes
374
393
  stim guide cleanup collector # an unproven collector pid; why the app on a phone closed
375
394
  stim guide cleanup memory # watchman and Gradle daemon memory; gc --cache watchman
395
+ stim guide tutorial # tutorial sections and manual commands
396
+ stim guide tutorial run # create the app and build in a tour worktree
376
397
  stim guide settings # configuration files and supported keys`
377
- },
378
- facts: {
379
- summary: "The --json payloads: `start`, `ios`, `android`, `web`, `ios|android --plan`, `reload`, `stop`, `status`, `doctor`, `device lock`/`unlock`, `gc`, and the error contract",
380
- preamble: () => `SLOTS
398
+ };
399
+ //#endregion
400
+ //#region src/guide/facts.ts
401
+ const facts = {
402
+ summary: "The --json payloads: `start`, `ios`, `android`, `web`, `ios|android --plan`, `reload`, `stop`, `status`, `doctor`, `device lock`/`unlock`, `gc`, and the error contract",
403
+ preamble: () => `SLOTS
381
404
  Named ios/android runs add slot to their JSON facts. Default-run fields remain
382
405
  compatible. status adds a slots array per environment with each named slot's
383
406
  ios/android device facts; top-level ios/android still describe default.
@@ -395,6 +418,14 @@ status adds archived (newest first, bounded by archive.maxCount) and
395
418
  archivedUsage { count, bytes, byKind: { logs, recordings, agentActions, record } }.
396
419
  Each archive carries id, projectRoot, project, workspace, worktree facts,
397
420
  removedAt, removedBy, lastUsedAt, builds, agents, bytes, expires and version.
421
+ When the worktree folder was already gone, as when gc prunes a dead project,
422
+ the worktree facts come from the pull request cache of that gone worktree
423
+ when Stim cached one: branch, head and pullRequest, with merged true only when
424
+ that pull request merged and null otherwise; repository and subject are null.
425
+ Without a cached entry every worktree fact is null.
426
+ Archive builds is a summary { count, last, lastErrorCount }. With read access,
427
+ server archive.detail { archive: id } returns live-shaped builds and recordings
428
+ { platform, slot, spans } for each retained recording slot with footage.
398
429
  replacedBy is the live environment path when its canonical root matches.
399
430
  Older producers may omit both fields.
400
431
  Plain status prints Archived: <count> workspace(s), <size> when count is positive;
@@ -529,10 +560,10 @@ workspace leases no physical device.
529
560
 
530
561
  Plain status prints "ios: Old iPhone (physical, iPhone 12 Pro) connected --
531
562
  leased until <time>" for each one.`,
532
- sections: {
533
- payloads: {
534
- summary: "every field of the start, ios, android, macos, web and reload payloads, the error contract, the device rules",
535
- body: () => ` stim start --json
563
+ sections: {
564
+ payloads: {
565
+ summary: "every field of the start, ios, android, macos, web and reload payloads, the error contract, the device rules",
566
+ body: () => ` stim start --json
536
567
 
537
568
  port the Metro port RESERVED for this workspace
538
569
  supervisorPid the detached supervisor's pid, or NULL when a dev server was
@@ -969,10 +1000,10 @@ RULES
969
1000
  \`android --device\` and
970
1001
  \`ios --device\`, which use a connected physical device Stim never
971
1002
  creates, boots, or deletes.`
972
- },
973
- devmenu: {
974
- summary: "why the Expo dev menu or Tools button is or is not over the app, per platform and device kind",
975
- body: () => ` EVERY DEV-CLIENT DEEP LINK CARRIES disableOnboarding=1
1003
+ },
1004
+ devmenu: {
1005
+ summary: "why the Expo dev menu or Tools button is or is not over the app, per platform and device kind",
1006
+ body: () => ` EVERY DEV-CLIENT DEEP LINK CARRIES disableOnboarding=1
976
1007
  INSIDE ITS PROJECT URL
977
1008
  (\`...?url=http%3A%2F%2Fhost%3Aport%2F%3FdisableOnboarding%3D1&disableFab=1\`),
978
1009
  and expo-dev-launcher finishes its own dev-menu ONBOARDING
@@ -1097,10 +1128,10 @@ RULES
1097
1128
  developer trust, has no API at all and is always the user's.
1098
1129
  \`guide errors unverified\` has the signature and the
1099
1130
  full commands.`
1100
- },
1101
- gc: {
1102
- summary: "the gc report payload: mode, sections, reasons, failures, results, inventory, and the gc refusals",
1103
- body: () => ` stim gc [--delete] [--older-than <days>] [--cache <name|all|workspaces|recordings|parked>]
1131
+ },
1132
+ gc: {
1133
+ summary: "the gc report payload: mode, sections, reasons, failures, results, inventory, and the gc refusals",
1134
+ body: () => ` stim gc [--delete] [--older-than <days>] [--cache <name|all|workspaces|recordings|parked>]
1104
1135
  [--worktrees] [--idle <duration>] --json
1105
1136
 
1106
1137
  The report the text prints, as one payload. Show the user its sections
@@ -1127,7 +1158,8 @@ RULES
1127
1158
  id, bytes, detail } per entry it acted on; empty on a dry
1128
1159
  run. status is "done", "kept" (left alone, detail says
1129
1160
  why) or "failed" (detail says why and what to retry).
1130
- kind: device, parkedDevice, idleDevice, deviceRecord,
1161
+ kind: device, parkedDevice, parkedHostedDevice, idleDevice,
1162
+ deviceRecord,
1131
1163
  workspaceOutputs, recording, workspaceDirectory, project,
1132
1164
  buildLock, buildSlot, deviceLease, easSession, worktree,
1133
1165
  cache.
@@ -1199,6 +1231,14 @@ RULES
1199
1231
  parkedEmulators { name, systemImage, deviceProfile, parkedAt, app,
1200
1232
  bytes, listed } app is the package name;
1201
1233
  likewise
1234
+ parkedHostedDevices { session, client, platform, id, name, parkedAt,
1235
+ listed } parked iOS/Android sessions
1236
+ on this host; client is its registry id, id is
1237
+ the UDID or AVD name, name is the device label.
1238
+ listed means its private home ledger lists the
1239
+ device; false means already removed and is not
1240
+ actionable; null means unreadable. --older-than
1241
+ filters by parkedAt; --cache scopes leave this empty
1202
1242
  orphanedDevices { kind, id, name, bytes, directory }
1203
1243
  unverifiedDevices { kind, id, name, command } stim-* devices this
1204
1244
  Stim home has no record of creating; never
@@ -1318,10 +1358,10 @@ RULES
1318
1358
  caches on this machine
1319
1359
  - --cache together with --worktrees or --idle
1320
1360
  - --cache watchman or gradle-daemons together with --older-than`
1321
- },
1322
- status: {
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",
1324
- body: () => ` stim status --json
1361
+ },
1362
+ status: {
1363
+ 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",
1364
+ body: () => ` stim status --json
1325
1365
 
1326
1366
  logs { dir, errorsSinceMarker }, or null without a log directory
1327
1367
  agentDevice { stateDir }: absolute workspace agent-device state path on
@@ -1350,6 +1390,11 @@ RULES
1350
1390
  other route to web. macOS needs Package.swift, macos.product
1351
1391
  and macos.infoPlist. Detection reads config literals and
1352
1392
  files, never runs project scripts.
1393
+ tutorial { version }: present only when app.json sets
1394
+ expo.extra.stimTutorial to a positive integer, the marker
1395
+ of the Stim tutorial app (stim guide tutorial). Status
1396
+ reports whatever version it finds; Desktop decides which
1397
+ versions it supports. Static app.json only.
1353
1398
  recording { enabled }: whether stim-server may record the workspace's
1354
1399
  device screens for replay, from recording.enabled
1355
1400
 
@@ -1502,9 +1547,10 @@ RULES
1502
1547
  lock, argent, xcodebuild, idb, maestro, appium, simctl,
1503
1548
  uiautomator or instrumentation; pid and since are null when
1504
1549
  the claim does not record them
1505
- lastActivityAt the newest of this device's app log records, this
1506
- platform's Metro bundle requests, the workspace's last
1507
- Stim run, while agent-device drives it the agent's last
1550
+ lastActivityAt the newest of this device's app log records, changes to
1551
+ the workspace's client.ndjson and device.ndjson logs,
1552
+ this platform's Metro bundle requests, the workspace's
1553
+ last Stim run, while agent-device drives it the agent's last
1508
1554
  recorded action, and now while a stim-server client views
1509
1555
  it, rounded down to the minute; absent when none is
1510
1556
  recorded
@@ -1533,12 +1579,14 @@ RULES
1533
1579
  Plain \`stim status\` appends it to each device line: "driven by
1534
1580
  agent-device for 12m", "active", "idle 3h", or "activity unknown (...)".
1535
1581
 
1536
- An owned device that is not booted after its supervisor shut it down for
1537
- devices.idleShutdownMinutes carries idleShutdown, and plain \`status\`
1582
+ An owned device that is not booted after supervisor idle shutdown or queue
1583
+ reclaim carries idleShutdown, and plain \`status\`
1538
1584
  appends "shut down after 30m idle at <at>" to its line; the next \`ios\` or
1539
1585
  \`android\` run for that slot clears it (\`guide lifecycle budget\`):
1540
1586
 
1541
- idleShutdown { at, idleMinutes }
1587
+ idleShutdown { at, idleMinutes, reason?: "idle" | "reclaimed for a waiting run" }
1588
+ Absent reason or "idle" means supervisor idle shutdown; reclaim adds
1589
+ "reclaimed for a waiting run" to plain status.
1542
1590
  \`gc --idle <duration>\` shuts down owned devices idle that long
1543
1591
  (\`guide cleanup gc\`).
1544
1592
 
@@ -1617,7 +1665,7 @@ RULES
1617
1665
  outcome, outcomeKnown, cacheLookupOutcome?, expectedMs, expectedPhaseMs,
1618
1666
  completedPhaseMs?, basis,
1619
1667
  plannedPhases, missReason?, missProvisional?, detail?, placement,
1620
- waitingOn? }
1668
+ waitingOn?, waitingFor? }
1621
1669
 
1622
1670
  state "running" while the run's own native-run claim is live;
1623
1671
  "stale" when that claim was released or its process is
@@ -1647,7 +1695,7 @@ RULES
1647
1695
  the project's most recent one
1648
1696
  cacheLookupOutcome "hit" or "miss" once an actual cache lookup resolves;
1649
1697
  absent before resolution and on runs that skip lookup,
1650
- such as --eas-profile
1698
+ such as --eas-profile or --no-build-cache
1651
1699
  expectedMs the median duration of this project's last successful
1652
1700
  runs with that outcome on that platform, or null with
1653
1701
  no history. The run estimates twice: when it starts,
@@ -1703,6 +1751,9 @@ RULES
1703
1751
  startedAt is when the offload started and phaseStartedAt
1704
1752
  when that step did. Meanwhile phase above follows it as
1705
1753
  prebuild, pods or compile.
1754
+ waitingFor optional { kind: "build-slot" | "device-slot", inUse, max,
1755
+ since } for a slot wait, independently of phase. since is
1756
+ ISO. Overlapping waits show the first, then the remaining one.
1706
1757
  waitingOn while phase is "wait" and the holder is known: { path },
1707
1758
  the workspace whose build of the same artifact this run
1708
1759
  waits for; absent otherwise. The lock is machine-wide, so
@@ -1720,7 +1771,7 @@ RULES
1720
1771
  lastBuilds { ios?, android? }, each { platform, status, cacheHit,
1721
1772
  cacheSkipped, durationMs, fingerprint, startedAt, finishedAt,
1722
1773
  errorCode?, missReason?, buildMachine?, builtOn?, offloadedTo?, offloadFallback?,
1723
- diagnostics? }
1774
+ diagnostics?, devicePlacement? }
1724
1775
 
1725
1776
  status "ok" or "failed"
1726
1777
  cacheHit "local" or "remote" for an app from that cache tier; false
@@ -1947,10 +1998,10 @@ RULES
1947
1998
  simulator is booted, no workspace is live and no build runs: status then
1948
1999
  runs neither. \`status --watch --json\` rereads both every 15 seconds while
1949
2000
  machine is not null, with no other subprocess.`
1950
- },
1951
- plan: {
1952
- summary: "the ios and android --plan payload: fingerprint, cacheHit, prebuild, missReason, expectedMs and basis",
1953
- body: () => ` stim ios --plan --json # or: stim android --plan --json
2001
+ },
2002
+ plan: {
2003
+ summary: "the ios and android --plan payload: fingerprint, cacheHit, prebuild, missReason, expectedMs and basis",
2004
+ body: () => ` stim ios --plan --json # or: stim android --plan --json
1954
2005
 
1955
2006
  { platform, slot?, fingerprint, cacheKey, cacheHit, provider,
1956
2007
  cacheSkipped, prebuild, outcome, expectedMs, basis, missReason?,
@@ -1989,10 +2040,10 @@ RULES
1989
2040
  STIM_NO_FINGERPRINT, STIM_EAS_UNAVAILABLE, or STIM_BAD_ARG for a flag that
1990
2041
  picks a device. When and why a plan and the run can differ: \`guide
1991
2042
  lifecycle builds\`.`
1992
- },
1993
- stats: {
1994
- summary: "the stats payload, what counts as a run, hit, miss and failed, timeSavedMs, the heartbeat estimate",
1995
- body: () => ` stim stats --json
2043
+ },
2044
+ stats: {
2045
+ summary: "the stats payload, what counts as a run, hit, miss and failed, timeSavedMs, the heartbeat estimate",
2046
+ body: () => ` stim stats --json
1996
2047
 
1997
2048
  { "version": 1,
1998
2049
  "project": { "key": "<path>", "ios": <bucket|null>,
@@ -2002,6 +2053,8 @@ RULES
2002
2053
  "machines": { "<machine>": { "today": <day>,
2003
2054
  "total": <totals> } },
2004
2055
  "placements": [<placement>, ...] },
2056
+ "capacityRefusals": [<capacityRefusal>, ...],
2057
+ "capacityWaits": [<capacityWait>, ...],
2005
2058
  "agentDevice": <AgentDeviceUsage|null>,
2006
2059
  "swiftpmCache": <SwiftpmCacheUsage|null> }
2007
2060
 
@@ -2014,6 +2067,25 @@ RULES
2014
2067
  average, time saved and build estimates stay local. Milliseconds are
2015
2068
  integers.
2016
2069
 
2070
+ CAPACITY REFUSALS
2071
+ capacityRefusals?: [{ at, kind: "device", platform: "ios" | "android",
2072
+ max, workspace }]
2073
+ Only device refusals with STIM_AT_CAPACITY are recorded; build caps wait.
2074
+ at is the ISO timestamp, max is concurrency.maxDevices, and workspace is
2075
+ the workspace id used by status and Desktop. The list is newest first:
2076
+ the last 50 from the last 7 days, omitted when empty. Desktop uses events
2077
+ within 6 hours for its device-limit suggestion, including terminal runs.
2078
+ Recording is best effort and does not change the refusal. Plain stats
2079
+ output is unchanged.
2080
+ capacityWaits?: [{ at, kind: "device-wait", platform: "ios" | "android",
2081
+ ms, max, workspace, reclaimed? }]
2082
+ Each owned-device slot wait records its whole elapsed milliseconds,
2083
+ including waits that time out or fail. It has the same 50-event, 7-day
2084
+ retention, newest-first order, workspace id and best-effort recording.
2085
+ reclaimed is the whole count of devices shut down for the waiting run,
2086
+ omitted when zero. Plain stats reports waits with a positive reclaim count.
2087
+ It is omitted when empty.
2088
+
2017
2089
  AGENT-DEVICE DISK USAGE
2018
2090
  agentDevice: { version: 1, measuredAt, bytes, complete, stateDir,
2019
2091
  runnerBuilds, workspaces, hosted }
@@ -2095,10 +2167,20 @@ HOW A RUN IS COUNTED (\`stats\`)
2095
2167
  ignores what it cannot read, so nothing about statistics can change a
2096
2168
  run's outcome.
2097
2169
 
2170
+ DEVICE PLACEMENT (\`ios|android --remote auto\`)
2171
+ Auto runs include devicePlacement: { decision, reason, machine? } in the run
2172
+ facts, lastBuilds and build history. decision is "local", "hosted" or
2173
+ "waited-locally" (the local run actually waited for a device slot). The same
2174
+ optional devicePlacement appears on the status device entry for each slot.
2175
+ Hosted host facts include selected: "auto" or the named machine, and reason
2176
+ for automatic placement. Plain status prints (auto: <reason>) after the host.
2177
+ Old hosted records without selected still read as the machine name.
2178
+
2098
2179
  BUILD PLACEMENT (\`offload\`)
2099
2180
  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.
2181
+ decision, reason, machine?, buildMs?, slotWaitMs?, deviceSlotWaitMs?, localEstimateMs?, failed? }.
2182
+ slotWaitMs and deviceSlotWaitMs are whole milliseconds waiting for build
2183
+ and owned-device slots respectively, present only when positive.
2102
2184
  decision is "here", "offloaded", or "fell-back" (it tried a build machine
2103
2185
  and built here). reason is why: the run's \`placement:\` reason, such as
2104
2186
  "load 0.6/core, 1 of 3 build slots busy here" (auto keeps the build here
@@ -2117,12 +2199,14 @@ BUILD PLACEMENT (\`offload\`)
2117
2199
  machine was slower. \`offload.today\` counts the day's placements by
2118
2200
  decision. A fallback counts against a machine only when the run knows
2119
2201
  which one: the machine that failed, or the only one paired.`
2120
- }
2121
2202
  }
2122
- },
2123
- metro: {
2124
- summary: "The dev server: `stim start`, the supervisor, and starting your own",
2125
- body: () => `THE DEV SERVER
2203
+ }
2204
+ };
2205
+ //#endregion
2206
+ //#region src/guide/metro.ts
2207
+ var metro_default = {
2208
+ summary: "The dev server: `stim start`, the supervisor, and starting your own",
2209
+ body: () => `THE DEV SERVER
2126
2210
 
2127
2211
  stim start
2128
2212
  stim start --remote # prepare Metro for a remote device
@@ -2156,9 +2240,10 @@ HOSTED IOS METRO
2156
2240
  Hosted Debug runs require the supervisor's private gateway support before
2157
2241
  reservation. --no-metro-check refuses; restart a missing or older supervisor
2158
2242
  with stim stop; stim start.
2159
- Native iOS logs are pulled from the host by stim logs, including --errors;
2243
+ Native iOS and Android logs are pulled from the host by stim logs, including --errors;
2160
2244
  JavaScript logs still arrive through Metro. Stop pulls native logs before
2161
- deleting the simulator. Read guide lifecycle hosted-ios for placement and cleanup.
2245
+ deleting the owned device and copies the final host tail afterwards. Read guide
2246
+ lifecycle hosted-ios or hosted-android for placement and cleanup.
2162
2247
 
2163
2248
  TAILNET-ONLY METRO
2164
2249
  Install Tailscale and sign in on this machine and the remote device or
@@ -2207,6 +2292,13 @@ BUNDLE WARMUP
2207
2292
  waits for the app's own bundle response. With a bundler started outside Stim,
2208
2293
  device logs may prove a request, but bundle completion may stay unverified.
2209
2294
 
2295
+ HOSTED ANDROID
2296
+ android --remote <machine> keeps Metro here through the private tailnet
2297
+ gateway. The host reverses the client Metro port into its loopback bridge
2298
+ on the exact owned emulator. Each run and reload restores that reverse;
2299
+ no adb command for the host serial runs here. metro.tunnel and metro.publicUrl
2300
+ are ignored. See guide lifecycle hosted-android.
2301
+
2210
2302
  REMOTE DEVICE BACKENDS
2211
2303
  Metro exposure and device selection are separate:
2212
2304
 
@@ -2302,7 +2394,7 @@ WHAT THE SUPERVISOR IS
2302
2394
  connected app alone does not keep the server. It records a
2303
2395
  supervisor_idle_stopped line in metro.ndjson and devServerStop in
2304
2396
  state.json, so \`status\` shows "stopped (idle)" rather than a crash.
2305
- Devices stay booted, unless devices.idleShutdownMinutes is set: then the
2397
+ Devices stay booted, unless devices.idleShutdownMinutes is above 0 (the default is 30): then the
2306
2398
  idle stop first shuts down the workspace's owned devices idle for the
2307
2399
  shorter of the two (\`guide lifecycle budget\`). The next \`stim start\`
2308
2400
  starts it again and clears the
@@ -2395,10 +2487,12 @@ STARTING YOUR OWN BUNDLER STILL WORKS
2395
2487
  \`stop\` leaves an externally started server running and keeps the port
2396
2488
  reserved while it answers from this project. Stop it with the tool that
2397
2489
  started it.`
2398
- },
2399
- ports: {
2400
- summary: "Workspace ports for web and API servers that Stim does not manage",
2401
- body: () => `NAMED SERVER PORTS
2490
+ };
2491
+ //#endregion
2492
+ //#region src/guide/ports.ts
2493
+ var ports_default = {
2494
+ summary: "Workspace ports for web and API servers that Stim does not manage",
2495
+ body: () => `NAMED SERVER PORTS
2402
2496
 
2403
2497
  If Stim is not installed globally, replace stim with npx stim.
2404
2498
 
@@ -2466,10 +2560,12 @@ cleanup: versions without ports do not know about named allocations.
2466
2560
 
2467
2561
  A shared API does not get a shared reservation. Pass its port by environment
2468
2562
  instead of allocating a separate label in every worktree.`
2469
- },
2470
- logs: {
2471
- summary: "Querying the merged NDJSON timeline, and what --errors means",
2472
- body: () => `LOGS
2563
+ };
2564
+ //#endregion
2565
+ //#region src/guide/logs.ts
2566
+ var logs_default = {
2567
+ summary: "Querying the merged NDJSON timeline, and what --errors means",
2568
+ body: () => `LOGS
2473
2569
 
2474
2570
  If Stim is not installed globally, replace stim with npx stim.
2475
2571
 
@@ -2836,7 +2932,7 @@ WHAT WRITES WHAT
2836
2932
  session records (~/.agent-device/sessions, or
2837
2933
  AGENT_DEVICE_STATE_DIR, plus existing workspace
2838
2934
  agent-device directories) for this workspace's owned
2839
- simulators and emulators, read-only. An action is
2935
+ simulators, emulators and native Mac app, read-only. An action is
2840
2936
  info (msg is agent-device's summary, e.g. "Tapped
2841
2937
  (201, 731)"; event agent_action with command, session,
2842
2938
  deviceId and details, and startedAt when agent-device
@@ -2847,7 +2943,13 @@ WHAT WRITES WHAT
2847
2943
  match a simulator through their runner.log, Android
2848
2944
  sessions only while agent-device's claim on the
2849
2945
  emulator is live (released claims drop their
2850
- actions from later queries). A session in an
2946
+ actions from later queries). A macOS session opened
2947
+ with explicit --surface app matches its isolated
2948
+ bundle ID and current launch (guide macos). App
2949
+ switches, unrecorded opens and rotated event logs
2950
+ clear that match; native screenshots are omitted
2951
+ because their surface is not in the event metadata.
2952
+ A session in an
2851
2953
  unrecognized format yields one warn record
2852
2954
  (agent_format_unknown) instead of its actions.
2853
2955
  build-ios.ndjson the xcodebuild / gradle transcript at level debug, the
@@ -2863,57 +2965,65 @@ WHAT WRITES WHAT
2863
2965
 
2864
2966
  A collector is killed and replaced on the next \`ios\` / \`android\` run for
2865
2967
  that platform, and reaped by \`stop\`.`
2866
- },
2867
- errors: {
2868
- summary: "Every refusal Stim can print: an index of codes, and one section for each",
2869
- sectionHint: "<CODE>",
2870
- preamble: () => `WHAT STIM REFUSES, AND WHY
2968
+ };
2969
+ //#endregion
2970
+ //#region src/guide/errors.ts
2971
+ const errors = {
2972
+ summary: "Every refusal Stim can print: an index of codes, and one section for each",
2973
+ sectionHint: "<CODE>",
2974
+ preamble: () => `WHAT STIM REFUSES, AND WHY
2871
2975
 
2872
2976
  Every refusal listed here carries a stable CODE, whichever command prints it.
2873
2977
  Branch on the code, never on the message.`,
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
2978
+ sections: {
2979
+ STIM_HOSTING_REFUSED: {
2980
+ summary: "a hosting Mac refused or could not confirm its native session; no fallback after reservation",
2981
+ body: () => `STIM_HOSTING_REFUSED
2878
2982
  The message names the hosting Mac and its reason: unreachable or changed
2879
- tailnet node, no installed simulator choice, no capacity, elevated or unknown
2983
+ tailnet node, no installed simulator or emulator choice, no capacity, elevated or unknown
2880
2984
  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
2985
+ locally and never tries another Mac after choosing a named host or creating a
2986
+ hosted session. Auto skips declined offers before building, then uses the
2987
+ local queue if none admits. A reserve refusal after an accepted offer fails
2988
+ with this code and asks to retry; it does not re-place the built architecture.
2989
+ Check stim-server and Tailscale there,
2990
+ then run stim doctor. Correct --device-type / --runtime for iOS or
2991
+ --system-image / --device-profile for Android when the offer names
2883
2992
  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.
2993
+ fails: retry stim ios|android --remote <machine>, or run stim stop to reconcile it.
2885
2994
  An unreachable stop keeps the placement; rerun stim stop when the host answers.
2886
2995
  A failed build handoff uses upload instead. If native log queries are unavailable,
2887
2996
  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).
2997
+ on the host to enable handoff and native logs (hello features hosted-ios-data
2998
+ for iOS and hosted-android-data for Android).
2889
2999
  A name outside hosting.machines is STIM_BAD_ARG. Missing or pending hosting
2890
3000
  approval keeps the doctor --fix and stim-server devices grant remedies.`
2891
- },
2892
- STIM_EAS_BUILD_MISSING: {
2893
- summary: "no completed EAS development build matches; build only with session authorization",
2894
- separator: "--- EAS BUILD CODES (`ios --eas-profile` / `android --eas-profile`) ---",
2895
- body: () => `STIM_EAS_BUILD_MISSING
3001
+ },
3002
+ STIM_EAS_BUILD_MISSING: {
3003
+ summary: "no completed EAS development build matches; build only with session authorization",
3004
+ separator: "--- EAS BUILD CODES (`ios --eas-profile` / `android --eas-profile`) ---",
3005
+ body: () => `STIM_EAS_BUILD_MISSING
2896
3006
  No compatible build matches the selected EAS project, profile, platform and
2897
3007
  native fingerprint. No device was acquired and no local or cloud build was
2898
3008
  started. The remedy prints the exact npx eas-cli build command. Check session
2899
3009
  authorization for its potential cost before running it, then retry Stim.
2900
3010
  See stim guide lifecycle eas.`
2901
- },
2902
- STIM_EAS_UNAVAILABLE: {
2903
- summary: "EAS lookup/download failed, or another run holds the artifact claim",
2904
- body: () => `STIM_EAS_UNAVAILABLE
3011
+ },
3012
+ STIM_EAS_UNAVAILABLE: {
3013
+ summary: "EAS lookup/download failed, or another run holds the artifact claim",
3014
+ body: () => `STIM_EAS_UNAVAILABLE
2905
3015
  EAS CLI is unavailable or older than 18.9.0, a profile/fingerprint/list/download
2906
3016
  operation failed, the response could not be validated, or another run holds
2907
3017
  the artifact claim.
2908
3018
  Follow the printed remedy: inspect the named EAS command or retry once the
2909
3019
  holder finishes. This is not proof that a build is missing. No native build
2910
3020
  is started. See stim guide lifecycle eas.`
2911
- },
2912
- STIM_WORKSPACE_STATE: {
2913
- summary: "$STIM_HOME/workspaces could not be prepared, or the digest directory belongs to another project",
2914
- aliases: ["STIM_WORKSPACE_COLLISION"],
2915
- separator: "--- BUILD-PATH CODES (`stim ios` / `stim android`) ---",
2916
- body: () => `STIM_WORKSPACE_STATE / STIM_WORKSPACE_COLLISION
3021
+ },
3022
+ STIM_WORKSPACE_STATE: {
3023
+ summary: "$STIM_HOME/workspaces could not be prepared, or the digest directory belongs to another project",
3024
+ aliases: ["STIM_WORKSPACE_COLLISION"],
3025
+ separator: "--- BUILD-PATH CODES (`stim ios` / `stim android`) ---",
3026
+ body: () => `STIM_WORKSPACE_STATE / STIM_WORKSPACE_COLLISION
2917
3027
  Stim could not prepare this project's global workspace directory under
2918
3028
  $STIM_HOME/workspaces. Check that STIM_HOME is writable and has free
2919
3029
  space. An EPERM on a directory the user CAN write is a sandbox, not a
@@ -2921,10 +3031,10 @@ Branch on the code, never on the message.`,
2921
3031
  readable-name-plus-digest directory already has a workspace.json for a
2922
3032
  different canonical project path; do not overwrite it until you identify
2923
3033
  which workspace owns it.`
2924
- },
2925
- STIM_NO_METRO: {
2926
- summary: "the recorded launch's port is not this workspace's live dev server (reload)",
2927
- body: () => `STIM_NO_METRO
3034
+ },
3035
+ STIM_NO_METRO: {
3036
+ summary: "the recorded launch's port is not this workspace's live dev server (reload)",
3037
+ body: () => `STIM_NO_METRO
2928
3038
  Reload requires the recorded launch's port to be this workspace's live
2929
3039
  Metro. It refuses a missing, changed, unresponsive, or foreign port. Run
2930
3040
  \`stim start\`, or run \`ios\` or \`android\` again.
@@ -2935,19 +3045,19 @@ Branch on the code, never on the message.`,
2935
3045
  (STIM_METRO_TIMEOUT, STIM_SUPERVISOR_EXITED, ...). A port held by SOMETHING
2936
3046
  ELSE, usually a bundler started from the wrong directory or another repo's
2937
3047
  Metro, gets a fresh reservation.`
2938
- },
2939
- STIM_NO_FINGERPRINT: {
2940
- summary: "@expo/fingerprint produced no hash, so the shared cache cannot be addressed",
2941
- body: () => `STIM_NO_FINGERPRINT
3048
+ },
3049
+ STIM_NO_FINGERPRINT: {
3050
+ summary: "@expo/fingerprint produced no hash, so the shared cache cannot be addressed",
3051
+ body: () => `STIM_NO_FINGERPRINT
2942
3052
  \`@expo/fingerprint\` produced no hash, so the shared build cache cannot be
2943
3053
  addressed. Stim uses its declared @expo/fingerprint dependency directly,
2944
3054
  independently of the target project's package graph. This is a refusal
2945
3055
  rather than a silent full build because an unaddressable cache means every
2946
3056
  workspace on the commit compiles from scratch, forever.`
2947
- },
2948
- STIM_PREBUILD_FAILED: {
2949
- summary: "expo prebuild could not generate or regenerate the native directory",
2950
- body: () => `STIM_PREBUILD_FAILED
3057
+ },
3058
+ STIM_PREBUILD_FAILED: {
3059
+ summary: "expo prebuild could not generate or regenerate the native directory",
3060
+ body: () => `STIM_PREBUILD_FAILED
2951
3061
  \`expo prebuild\` could not generate the missing native directory, or
2952
3062
  regenerate a stale one. The extracted output is above the code; the
2953
3063
  transcript is in the global workspace logs/build-<platform>.ndjson file.
@@ -2958,10 +3068,10 @@ Branch on the code, never on the message.`,
2958
3068
  directory from the fingerprint (.fingerprintignore), or gitignore and
2959
3069
  untrack it so Stim regenerates it. When git itself is failing, fix the
2960
3070
  checkout first.`
2961
- },
2962
- STIM_DEPS_FAILED: {
2963
- summary: "pod install or gradle sync failed; the bundler ladder and BUNDLE_FROZEN",
2964
- body: () => `STIM_DEPS_FAILED
3071
+ },
3072
+ STIM_DEPS_FAILED: {
3073
+ summary: "pod install or gradle sync failed; the bundler ladder and BUNDLE_FROZEN",
3074
+ body: () => `STIM_DEPS_FAILED
2965
3075
  \`pod install\` (iOS) or the gradle dependency sync (Android) failed. On iOS
2966
3076
  this runs only when Podfile.lock and Pods/Manifest.lock disagree, or Pods is
2967
3077
  absent -- which is exactly what a carried worktree produces.
@@ -2989,10 +3099,10 @@ Branch on the code, never on the message.`,
2989
3099
  \`yarn install\`, \`bun install\`, \`npm ci\`) or that same pod ladder. The
2990
3100
  message names the command, quotes its last lines, and nothing is copied: fix
2991
3101
  the source checkout, then warm again.`
2992
- },
2993
- STIM_BUILD_FAILED: {
2994
- summary: "xcodebuild or gradle failed; the Android missing-SDK and APK refusals; a damaged compilation-cache object",
2995
- body: () => `STIM_BUILD_FAILED
3102
+ },
3103
+ STIM_BUILD_FAILED: {
3104
+ summary: "xcodebuild or gradle failed; the Android missing-SDK and APK refusals; a damaged compilation-cache object",
3105
+ body: () => `STIM_BUILD_FAILED
2996
3106
  xcodebuild or gradle failed. The EXTRACTED diagnostics are printed (capped),
2997
3107
  not the transcript. Read the log path on the next line for the rest.
2998
3108
  Three Android refusals share this code without gradle itself failing:
@@ -3029,10 +3139,10 @@ Branch on the code, never on the message.`,
3029
3139
  object, and upgrading the CLI does not clear it. Empty that one cache with
3030
3140
  \`gc --delete --cache "compilation cache"\`, then build again. The next
3031
3141
  build is a cold one.`
3032
- },
3033
- STIM_PATH_TOO_LONG: {
3034
- summary: "Windows only: the project root leaves no room for the NDK object paths ninja must open",
3035
- body: () => `STIM_PATH_TOO_LONG (android, Windows only)
3142
+ },
3143
+ STIM_PATH_TOO_LONG: {
3144
+ summary: "Windows only: the project root leaves no room for the NDK object paths ninja must open",
3145
+ body: () => `STIM_PATH_TOO_LONG (android, Windows only)
3036
3146
  The NDK's ninja is not long-path aware, so every CMake object path has to
3037
3147
  stay under Windows' MAX_PATH. React Native's codegen objects are named
3038
3148
  after their mangled absolute source path, so each carries the project root
@@ -3048,10 +3158,10 @@ Branch on the code, never on the message.`,
3048
3158
  preflight does not inspect that configured path and may refuse it too.
3049
3159
  \`doctor --platform android\` reports the same root as a finding for the
3050
3160
  emulator's ABI.`
3051
- },
3052
- fallbacks: {
3053
- summary: "release cache-hit notes that are not codes: swap failure, asset gate, uninstall, device fallbacks",
3054
- body: () => `FALLBACK NOTES THAT ARE NOT CODES (release cache hits)
3161
+ },
3162
+ fallbacks: {
3163
+ summary: "release cache-hit notes that are not codes: swap failure, asset gate, uninstall, device fallbacks",
3164
+ body: () => `FALLBACK NOTES THAT ARE NOT CODES (release cache hits)
3055
3165
  On a release cache hit Stim regenerates this workspace's JS bundle into a
3056
3166
  COPY of the cached artifact before installing it -- \`ios --configuration
3057
3167
  Release\` into a copy of the .app, \`android --variant ...Release\` into a
@@ -3121,10 +3231,10 @@ Branch on the code, never on the message.`,
3121
3231
  The same refusal on a FRESHLY BUILT app is a code (STIM_NO_PROFILE,
3122
3232
  STIM_PROFILE_MISMATCH, STIM_NO_SIGNING_IDENTITY), not a note: building again
3123
3233
  would produce the same app and refuse again.`
3124
- },
3125
- STIM_BUILD_WAIT_TIMEOUT: {
3126
- summary: "waited ~90 minutes for another workspace's build of the same fingerprint",
3127
- body: () => `STIM_BUILD_WAIT_TIMEOUT
3234
+ },
3235
+ STIM_BUILD_WAIT_TIMEOUT: {
3236
+ summary: "waited ~90 minutes for another workspace's build of the same fingerprint",
3237
+ body: () => `STIM_BUILD_WAIT_TIMEOUT
3128
3238
  This run was waiting for ANOTHER workspace's build of the same fingerprint
3129
3239
  (see \`guide lifecycle concurrency\`), and no artifact arrived within ~90
3130
3240
  minutes.
@@ -3133,10 +3243,10 @@ Branch on the code, never on the message.`,
3133
3243
  may have failed. The message names the current pid and lock directory:
3134
3244
  check the pid, and if it is not really building, remove that directory and
3135
3245
  run the command again.`
3136
- },
3137
- STIM_CLAIM_REFUSED: {
3138
- summary: "an ownership claim blocks the operation; inspect its holder before removing an unresolved claim",
3139
- body: () => `STIM_CLAIM_REFUSED
3246
+ },
3247
+ STIM_CLAIM_REFUSED: {
3248
+ summary: "an ownership claim blocks the operation; inspect its holder before removing an unresolved claim",
3249
+ body: () => `STIM_CLAIM_REFUSED
3140
3250
  A build lock records the holder's process IDENTITY, not just its pid, so a
3141
3251
  recycled pid reads as a gone builder rather than a live one, and a builder
3142
3252
  busy in a long \`simctl\` or gradle call reads as live rather than as stale.
@@ -3174,19 +3284,19 @@ Branch on the code, never on the message.`,
3174
3284
  Legacy inline \`deletionClaim\` fields in config.json have no process
3175
3285
  identity and require separate manual inspection; \`stim guide lifecycle pool\`
3176
3286
  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
3287
+ },
3288
+ STIM_MACOS_OWNER_UNVERIFIED: {
3289
+ summary: "the macOS app owner cannot be verified; no signal is sent",
3290
+ body: () => `STIM_MACOS_OWNER_UNVERIFIED
3181
3291
  The recorded macOS process identity is unavailable or its workspace record
3182
3292
  is malformed. Stim sends no signal to an owner it cannot verify. Inspect the
3183
3293
  named process and workspace record before repairing it; do not replace a PID
3184
3294
  or token with another running app. Retry stim stop once identity inspection
3185
3295
  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
3296
+ },
3297
+ STIM_OFFLOAD_REFUSED: {
3298
+ summary: "the selected build machine cannot build this app",
3299
+ body: () => `STIM_OFFLOAD_REFUSED
3190
3300
 
3191
3301
  A named --build-machine selection is strict. The message names the machine
3192
3302
  and why it cannot take or finish the build: not configured or paired, approval
@@ -3207,10 +3317,26 @@ or runs stim-server setup on the worker with its node, ticket and expiry
3207
3317
  Rerun with --build-machine auto for normal placement and local fallback, or
3208
3318
  --build-machine local to keep the build here.
3209
3319
  `
3210
- },
3211
- STIM_CLAIM_UNAVAILABLE: {
3212
- summary: "a process identity or warm claim store is unavailable, so the protected operation refuses",
3213
- body: () => `STIM_CLAIM_UNAVAILABLE
3320
+ },
3321
+ STIM_WORKTREE_SERVICE: {
3322
+ summary: "worktree remove refuses a folder a stim-server service runs from",
3323
+ body: () => `STIM_WORKTREE_SERVICE
3324
+ A LaunchAgent that \`stim-server service install\` wrote (it carries a
3325
+ StimService marker in ~/Library/LaunchAgents/<label>.plist, default label
3326
+ dev.stim.server) runs a program from inside this worktree. Removing the
3327
+ folder would delete the running server's code. The refusal also covers a
3328
+ service that is installed but stopped, because launchd loads it again at the
3329
+ next login, and a marked plist Stim could not read. \`--force\` does not
3330
+ override it, and gc leaves the worktree in place.
3331
+ Reinstall the service from another build (stim-server service install --label
3332
+ <label> from a checkout that stays), or remove it with stim-server service
3333
+ uninstall --label <label>, then run stim worktree remove again. Nothing was
3334
+ reclaimed or removed. Other processes with their working directory in the
3335
+ folder are not detected.`
3336
+ },
3337
+ STIM_CLAIM_UNAVAILABLE: {
3338
+ summary: "a process identity or warm claim store is unavailable, so the protected operation refuses",
3339
+ body: () => `STIM_CLAIM_UNAVAILABLE
3214
3340
  Every ownership claim records the holder's process identity, captured through
3215
3341
  the \`unique-pid\` native module. This code is that capture failing: no
3216
3342
  prebuilt binary for this platform and architecture, or the OS refusing to
@@ -3230,10 +3356,10 @@ Rerun with --build-machine auto for normal placement and local fallback, or
3230
3356
  access to the existing claim store, checking parent permissions, symlink
3231
3357
  targets, mount access and sandbox rules, then retry. Do not switch STIM_HOME to evade a claim:
3232
3358
  concurrent runs must coordinate through the same store.`
3233
- },
3234
- STIM_INSTALL_FAILED: {
3235
- summary: "simctl, adb, or devicectl refused the artifact; the one signer-conflict retry",
3236
- body: () => `STIM_INSTALL_FAILED
3359
+ },
3360
+ STIM_INSTALL_FAILED: {
3361
+ summary: "simctl, adb, or devicectl refused the artifact; the one signer-conflict retry",
3362
+ body: () => `STIM_INSTALL_FAILED
3237
3363
  The artifact built or came from cache, but \`simctl install\` / \`adb install\` /
3238
3364
  \`devicectl device install app\` refused it. A signature or architecture
3239
3365
  mismatch, or a full device.
@@ -3255,10 +3381,10 @@ Rerun with --build-machine auto for normal placement and local fallback, or
3255
3381
  On a REMOTE device (\`--remote\`) agent-device refused the upload or install.
3256
3382
  With \`--remote eas\` the EAS Simulator session stays up and billed; the
3257
3383
  remedy names it. Rerun to reuse it, or run \`stim stop\` to end it.`
3258
- },
3259
- STIM_LAUNCH_FAILED: {
3260
- summary: "installed but would not start; the developer-trust tap on a phone",
3261
- body: () => `STIM_LAUNCH_FAILED
3384
+ },
3385
+ STIM_LAUNCH_FAILED: {
3386
+ summary: "installed but would not start; the developer-trust tap on a phone",
3387
+ body: () => `STIM_LAUNCH_FAILED
3262
3388
  Installed, but the app would not start. On Android this usually means no
3263
3389
  launchable activity resolved.
3264
3390
  On a local iOS simulator, a timed-out launch can mean the simulator cannot
@@ -3278,25 +3404,25 @@ Rerun with --build-machine auto for normal placement and local fallback, or
3278
3404
  signer-conflict retry performs.
3279
3405
  With \`--remote eas\` the remedy names the EAS Simulator session that is
3280
3406
  still running, as for STIM_INSTALL_FAILED.`
3281
- },
3282
- STIM_NO_SCHEME: {
3283
- summary: "Xcode schemes unavailable or no unambiguous app scheme in ios/",
3284
- body: () => `STIM_NO_SCHEME
3407
+ },
3408
+ STIM_NO_SCHEME: {
3409
+ summary: "Xcode schemes unavailable or no unambiguous app scheme in ios/",
3410
+ body: () => `STIM_NO_SCHEME
3285
3411
  Stim could not list or select an app scheme in ios/. Share the intended app
3286
3412
  scheme so xcodebuild can see it. Select an available exact name with
3287
3413
  \`stim ios --scheme <name>\`. An unknown explicit name prints available choices.
3288
3414
  Without an explicit selector, a workspace
3289
3415
  name match wins; otherwise Stim accepts a sole non-test scheme, or a listed
3290
3416
  scheme matching app.json. Unmatched ambiguous schemes are refused.`
3291
- },
3292
- STIM_NO_PROFILE: {
3293
- summary: "no or undecodable embedded.mobileprovision; build once from Xcode",
3294
- separator: "--- iOS SIGNING CODES (`ios --device`, and only there) ---",
3295
- context: `A simulator build needs no signature, which is why none of these can fire on
3417
+ },
3418
+ STIM_NO_PROFILE: {
3419
+ summary: "no or undecodable embedded.mobileprovision; build once from Xcode",
3420
+ separator: "--- iOS SIGNING CODES (`ios --device`, and only there) ---",
3421
+ context: `A simulator build needs no signature, which is why none of these can fire on
3296
3422
  the normal path. A device build carries one, and Stim re-seals any bundle it
3297
3423
  modifies with the identity the bundle already names -- so it checks, before
3298
3424
  spending a build or a bundle, that the check can succeed.`,
3299
- body: () => `STIM_NO_PROFILE
3425
+ body: () => `STIM_NO_PROFILE
3300
3426
  The built or cached .app has no embedded.mobileprovision, or
3301
3427
  \`openssl smime\` could not decode the one it has. The first means the build
3302
3428
  produced an unsigned app -- almost always a simulator-sliced artifact.
@@ -3305,14 +3431,14 @@ spending a build or a bundle, that the check can succeed.`,
3305
3431
  profile. Stim will not do that step: registering a device or minting a
3306
3432
  profile changes your Apple Developer account, so Stim never passes
3307
3433
  -allowProvisioningUpdates.`
3308
- },
3309
- STIM_PROFILE_MISMATCH: {
3310
- summary: "the profile is expired, has no ProvisionedDevices, or does not name this UDID",
3311
- context: `A simulator build needs no signature, which is why none of these can fire on
3434
+ },
3435
+ STIM_PROFILE_MISMATCH: {
3436
+ summary: "the profile is expired, has no ProvisionedDevices, or does not name this UDID",
3437
+ context: `A simulator build needs no signature, which is why none of these can fire on
3312
3438
  the normal path. A device build carries one, and Stim re-seals any bundle it
3313
3439
  modifies with the identity the bundle already names -- so it checks, before
3314
3440
  spending a build or a bundle, that the check can succeed.`,
3315
- body: () => `STIM_PROFILE_MISMATCH
3441
+ body: () => `STIM_PROFILE_MISMATCH
3316
3442
  The profile inside the app cannot admit this phone. Three shapes, and the
3317
3443
  message names which one and the profile type it found:
3318
3444
  - it expired, or carries no ExpirationDate at all;
@@ -3326,14 +3452,14 @@ spending a build or a bundle, that the check can succeed.`,
3326
3452
  With --eas-profile, follow the EAS device:create and build commands in the
3327
3453
  refusal instead. Registration, signing changes and cloud builds need session
3328
3454
  authorization. See stim guide lifecycle eas.`
3329
- },
3330
- STIM_NO_SIGNING_IDENTITY: {
3331
- summary: "no single keychain identity resolves; ios.signingIdentitySha1 for two certificates",
3332
- context: `A simulator build needs no signature, which is why none of these can fire on
3455
+ },
3456
+ STIM_NO_SIGNING_IDENTITY: {
3457
+ summary: "no single keychain identity resolves; ios.signingIdentitySha1 for two certificates",
3458
+ context: `A simulator build needs no signature, which is why none of these can fire on
3333
3459
  the normal path. A device build carries one, and Stim re-seals any bundle it
3334
3460
  modifies with the identity the bundle already names -- so it checks, before
3335
3461
  spending a build or a bundle, that the check can succeed.`,
3336
- body: () => `STIM_NO_SIGNING_IDENTITY
3462
+ body: () => `STIM_NO_SIGNING_IDENTITY
3337
3463
  No single keychain identity could be resolved to re-seal with. Either
3338
3464
  \`security find-identity -v -p codesigning\` lists nothing, or the identity
3339
3465
  the artifact's own profile names is absent, or two certificates share that
@@ -3341,14 +3467,14 @@ spending a build or a bundle, that the check can succeed.`,
3341
3467
  Open Xcode > Settings > Accounts and download your certificates, or unlock
3342
3468
  the login keychain with \`security unlock-keychain\`. For the two-certificate
3343
3469
  case, set ios.signingIdentitySha1 to the SHA-1 hash beside the one you want.`
3344
- },
3345
- STIM_CODESIGN_FAILED: {
3346
- summary: "codesign failed on the modified copy; the cache entry is untouched and the run builds fresh",
3347
- context: `A simulator build needs no signature, which is why none of these can fire on
3470
+ },
3471
+ STIM_CODESIGN_FAILED: {
3472
+ summary: "codesign failed on the modified copy; the cache entry is untouched and the run builds fresh",
3473
+ context: `A simulator build needs no signature, which is why none of these can fire on
3348
3474
  the normal path. A device build carries one, and Stim re-seals any bundle it
3349
3475
  modifies with the identity the bundle already names -- so it checks, before
3350
3476
  spending a build or a bundle, that the check can succeed.`,
3351
- body: () => `STIM_CODESIGN_FAILED
3477
+ body: () => `STIM_CODESIGN_FAILED
3352
3478
  \`codesign --force --sign\` or \`codesign --verify --strict\` exited non-zero
3353
3479
  on the modified copy. The verbatim codesign stderr is quoted, because it is
3354
3480
  the answer: a locked login keychain reports errSecInternalComponent, an
@@ -3356,14 +3482,14 @@ spending a build or a bundle, that the check can succeed.`,
3356
3482
  and confirm exactly one identity matches the name. The cache entry itself is
3357
3483
  never modified -- the failure is on a temporary copy, and the run builds
3358
3484
  fresh.`
3359
- },
3360
- STIM_NO_LAN_ADDRESS: {
3361
- summary: "the Mac has no non-internal IPv4 interface; a tunnel cannot help a phone",
3362
- separator: "--- iOS DEVICE DEBUG REACHABILITY CODES (`ios --device` in Debug) ---",
3363
- context: `A phone does not share the host's loopback and USB carries no reverse forward,
3485
+ },
3486
+ STIM_NO_LAN_ADDRESS: {
3487
+ summary: "the Mac has no non-internal IPv4 interface; a tunnel cannot help a phone",
3488
+ separator: "--- iOS DEVICE DEBUG REACHABILITY CODES (`ios --device` in Debug) ---",
3489
+ context: `A phone does not share the host's loopback and USB carries no reverse forward,
3364
3490
  so a Debug run on one is wired to a LAN origin instead of localhost. Both codes
3365
3491
  fire BEFORE the build, because a refusal that costs a build is a bad refusal.`,
3366
- body: () => `STIM_NO_LAN_ADDRESS
3492
+ body: () => `STIM_NO_LAN_ADDRESS
3367
3493
  This Mac reports no non-internal IPv4 interface, so there is no address to
3368
3494
  give the phone: it is offline, or on nothing but utun/awdl/bridge. Join a
3369
3495
  Wi-Fi or Ethernet network, or connect this Mac by cable, and run again.
@@ -3372,13 +3498,13 @@ fire BEFORE the build, because a refusal that costs a build is a bad refusal.`,
3372
3498
  ip.txt is read by RCTBundleURLProvider, which prefixes the scheme. A tunnel
3373
3499
  cannot be expressed to a phone, so --device ignores metro.publicUrl,
3374
3500
  metro.tunnel and metro.ngrokUrl and says so when one is set.`
3375
- },
3376
- STIM_LAN_METRO_UNREACHABLE: {
3377
- summary: "the LAN origin did not answer as this workspace's Metro; ios.lanHost on a multi-NIC Mac",
3378
- context: `A phone does not share the host's loopback and USB carries no reverse forward,
3501
+ },
3502
+ STIM_LAN_METRO_UNREACHABLE: {
3503
+ summary: "the LAN origin did not answer as this workspace's Metro; ios.lanHost on a multi-NIC Mac",
3504
+ context: `A phone does not share the host's loopback and USB carries no reverse forward,
3379
3505
  so a Debug run on one is wired to a LAN origin instead of localhost. Both codes
3380
3506
  fire BEFORE the build, because a refusal that costs a build is a bad refusal.`,
3381
- body: () => `STIM_LAN_METRO_UNREACHABLE
3507
+ body: () => `STIM_LAN_METRO_UNREACHABLE
3382
3508
  The chosen LAN origin did not answer as THIS workspace's Metro: no answer, a
3383
3509
  5xx, or a dev server that is not this one -- the message says which.
3384
3510
  \`stim start\` prints the port it reserved. On a Mac with several interfaces
@@ -3388,12 +3514,12 @@ fire BEFORE the build, because a refusal that costs a build is a bad refusal.`,
3388
3514
  routes a host connection to its own address over loopback, so the gate passes
3389
3515
  through a firewall that will block the phone. That evidence only ever arrives
3390
3516
  from the phone's own bundle request, which is what \`launched\` reports.`
3391
- },
3392
- unverified: {
3393
- summary: "launched: \"unverified\" with the Local Network path reason, and the routed recovery",
3394
- context: `A phone does not share the host's loopback and USB carries no reverse forward,
3517
+ },
3518
+ unverified: {
3519
+ summary: "launched: \"unverified\" with the Local Network path reason, and the routed recovery",
3520
+ context: `A phone does not share the host's loopback and USB carries no reverse forward,
3395
3521
  so a Debug run on one is wired to a LAN origin instead of localhost.`,
3396
- body: () => `LAUNCH UNVERIFIED, LOCAL NETWORK NOT GRANTED (not a code -- a routed remedy)
3522
+ body: () => `LAUNCH UNVERIFIED, LOCAL NETWORK NOT GRANTED (not a code -- a routed remedy)
3397
3523
  An app that has not been granted Local Network reaches nothing on the LAN,
3398
3524
  and CFNetwork reports each attempt as NSURLErrorDomain -1009 "The Internet
3399
3525
  connection appears to be offline." with the path reason
@@ -3479,11 +3605,11 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
3479
3605
  General > VPN & Device Management) is refused to automation by the same gate
3480
3606
  that refuses the app, agent-device's own runner included, so its remedy is
3481
3607
  "ask the user" and nothing else. An uninstall clears both.`
3482
- },
3483
- STIM_NO_DEVICE: {
3484
- summary: "no usable phone, or the owned simulator or emulator could not be created or booted",
3485
- separator: "--- DEVICE AND CAPACITY CODES ---",
3486
- body: () => `STIM_NO_DEVICE
3608
+ },
3609
+ STIM_NO_DEVICE: {
3610
+ summary: "no usable phone, or the owned simulator or emulator could not be created or booted",
3611
+ separator: "--- DEVICE AND CAPACITY CODES ---",
3612
+ body: () => `STIM_NO_DEVICE
3487
3613
  With \`--device\`, no physical device answered the selection: none connected,
3488
3614
  a named serial/UDID that is not connected, several connected with none named
3489
3615
  (the refusal lists them), or one that is connected but unusable -- an
@@ -3495,6 +3621,10 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
3495
3621
  reach a booted state. \`stim doctor\` checks the toolchain; \`stim status\` says what
3496
3622
  Stim thinks it owns. Re-running the command creates a fresh owned device
3497
3623
  when the recorded one is gone.
3624
+ With concurrency.maxDevices set, it also means Stim could not count the
3625
+ booted devices before a boot: a simctl or adb listing failed or timed out,
3626
+ usually under heavy load, or other runs held the device-admission lock for
3627
+ 5 minutes. Retry once the load falls.
3498
3628
  If Android creation says an AVD already exists on disk but is not listed,
3499
3629
  run \`npx stim gc\` to inspect orphaned owned AVDs, then \`npx stim gc --delete\`
3500
3630
  to reclaim those safe to delete before retrying. Keep anything GC cannot
@@ -3552,10 +3682,10 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
3552
3682
  The Android counterpart, from \`--device-profile\` or android.deviceProfile.
3553
3683
  Reap the AVD the same way, or pass \`--slot <name>\` to create the requested
3554
3684
  profile beside it.`
3555
- },
3556
- STIM_DEVICE_BUSY: {
3557
- summary: "another workspace holds the lease on that phone and the wait ran out",
3558
- body: () => `STIM_DEVICE_BUSY
3685
+ },
3686
+ STIM_DEVICE_BUSY: {
3687
+ summary: "another workspace holds the lease on that phone and the wait ran out",
3688
+ body: () => `STIM_DEVICE_BUSY
3559
3689
  Only on a \`--device\` run. Another workspace holds the lease on that phone,
3560
3690
  and the wait ran out: the message names the holder root, the device, and the
3561
3691
  expiry as a clock time and a remaining duration, and \`--json\` adds
@@ -3569,10 +3699,10 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
3569
3699
  no token left in its \`state.json\` (its workspace directory was recreated).
3570
3700
  The remedy for that last one is \`stim device unlock\`, which releases by
3571
3701
  holder rather than by token.`
3572
- },
3573
- STIM_DEVICE_WIRELESS_FAILED: {
3574
- summary: "an iPhone paired over Wi-Fi timed out or dropped during install or launch; use the cable",
3575
- body: () => `STIM_DEVICE_WIRELESS_FAILED
3702
+ },
3703
+ STIM_DEVICE_WIRELESS_FAILED: {
3704
+ summary: "an iPhone paired over Wi-Fi timed out or dropped during install or launch; use the cable",
3705
+ body: () => `STIM_DEVICE_WIRELESS_FAILED
3576
3706
  Only on an \`ios --device\` run whose iPhone devicectl reaches over Wi-Fi
3577
3707
  (transportType localNetwork). The \`devicectl device install app\` ran past
3578
3708
  its 15-minute Wi-Fi bound, the phone did not appear in its own process list
@@ -3584,10 +3714,10 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
3584
3714
  untrusted host, Developer Mode off, full storage, the developer-trust tap --
3585
3715
  keeps STIM_INSTALL_FAILED or STIM_LAUNCH_FAILED and its own remedy, even
3586
3716
  when the step also timed out and even during a signer-conflict reinstall.`
3587
- },
3588
- STIM_DEVICE_LOST: {
3589
- summary: "the lease was gone or re-held at the pre-install check; rerun",
3590
- body: () => `STIM_DEVICE_LOST
3717
+ },
3718
+ STIM_DEVICE_LOST: {
3719
+ summary: "the lease was gone or re-held at the pre-install check; rerun",
3720
+ body: () => `STIM_DEVICE_LOST
3591
3721
  Only on a \`--device\` run. The run held a lease, and the raise before the
3592
3722
  install found it gone or held under another token -- another workspace took
3593
3723
  the device in that window. The message names the new holder and its expiry.
@@ -3595,24 +3725,32 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
3595
3725
  AFTER the install has started this is not a failure: the app is already on
3596
3726
  the phone, so the run prints one warning, continues, and reports
3597
3727
  \`lease: null\` in \`--json\`.`
3598
- },
3599
- STIM_AT_CAPACITY: {
3600
- summary: "concurrency.maxDevices reached; a refusal, not a queue",
3601
- body: () => `STIM_AT_CAPACITY
3602
- Only when concurrency.maxDevices is set (it is UNSET by default, so this never
3603
- fires unless you opted in). Booting a NEW owned device would exceed the cap:
3604
- the machine already has that many Stim-owned devices booted. It is a refusal, not
3605
- a queue -- \`ios\`/\`android\` are interactive-shaped, so Stim does not make
3606
- you wait at a prompt. The remedy is fixed: stop an environment
3607
- (\`stim stop\`) to free a device, or raise concurrency.maxDevices. A
3608
- workspace whose OWN device is already booted is never refused -- re-running
3609
- \`ios\` on an environment you already have is idempotent. (The build cap
3610
- behaves differently: a compile WAITS for a free slot rather than refusing.
3611
- See \`guide lifecycle concurrency\`.)`
3612
- },
3613
- STIM_LOW_DISK: {
3614
- summary: "free disk stayed below budget.hardFloorDiskGb after reclaiming",
3615
- body: () => `STIM_LOW_DISK
3728
+ },
3729
+ STIM_AT_CAPACITY: {
3730
+ summary: "owned-device slot wait timed out, or --no-wait reached the cap or queue",
3731
+ body: () => `STIM_AT_CAPACITY
3732
+ Only when concurrency.maxDevices is set (unset by default). New owned
3733
+ devices queue in FIFO order across $STIM_HOME for 600 seconds by default.
3734
+ This refusal means the wait expired, or \`--no-wait\` / \`--wait 0\`
3735
+ found the cap full or another run queued. The message gives the current
3736
+ count and, on timeout, how long it waited. iOS waits beside its build,
3737
+ so a rerun can reuse the artifact. A workspace's own booted or booting
3738
+ device bypasses the queue. At the cap, the FIFO head shuts down the
3739
+ longest-idle eligible owned device from another workspace, one per attempt,
3740
+ at most once every 15 seconds, then rechecks capacity.
3741
+ devices.reclaimIdleMinutes defaults to 10; 0 off. The same idle checks as supervisor shutdown (default 30 minutes) protect drivers,
3742
+ locks, builds, viewers and recent activity. Reclaim excludes this workspace,
3743
+ physical, hosted, remote, parked and other homes' devices and never deletes.
3744
+ A failed reclaim is logged and skipped; the run keeps waiting.
3745
+ Stop an environment (\`stim stop\`), pass a longer \`--wait <seconds>\`,
3746
+ or raise concurrency.maxDevices. Waiting prints holder names and elapsed
3747
+ time; status JSON exposes build.waitingFor independently of phase.
3748
+ Stats records capacityWaits for waits and capacityRefusals for this code.
3749
+ See \`guide lifecycle concurrency\`.`
3750
+ },
3751
+ STIM_LOW_DISK: {
3752
+ summary: "free disk stayed below budget.hardFloorDiskGb after reclaiming",
3753
+ body: () => `STIM_LOW_DISK
3616
3754
  \`start\`, \`ios\` and \`android\` check free disk on the volumes holding
3617
3755
  the app and $STIM_HOME before they build or boot anything. Below
3618
3756
  budget.minFreeDiskGb (20 GB by default) they first reclaim what Stim can
@@ -3633,10 +3771,10 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
3633
3771
  Ask before deleting anything outside Stim, such as Xcode DerivedData or
3634
3772
  simulators Stim did not create. Lowering budget.hardFloorDiskGb (0 never
3635
3773
  refuses) only removes the protection.`
3636
- },
3637
- STIM_BUILD_SLOT_TIMEOUT: {
3638
- summary: "the maxBuilds wait gave up with every slot held by a running process",
3639
- body: () => `STIM_BUILD_SLOT_TIMEOUT
3774
+ },
3775
+ STIM_BUILD_SLOT_TIMEOUT: {
3776
+ summary: "the maxBuilds wait gave up with every slot held by a running process",
3777
+ body: () => `STIM_BUILD_SLOT_TIMEOUT
3640
3778
  Only when concurrency.maxBuilds is set. The build cap does not refuse, it
3641
3779
  WAITS -- this code is that wait giving up: ~90 minutes elapsed and every one
3642
3780
  of the N slots was still held by a running process, or by a holder Stim could
@@ -3650,44 +3788,44 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
3650
3788
  the message names the directory: remove the slot of a builder that is not
3651
3789
  building, or raise concurrency.maxBuilds
3652
3790
  (\`guide lifecycle concurrency\`).`
3653
- },
3654
- STIM_NO_REMOTE_SESSION: {
3655
- summary: "the backend could not use agent-device, or metro.tunnel names an unusable provider",
3656
- separator: "--- REMOTE-DEVICE CODES (`ios --remote <proxy|eas>` / `android --remote <proxy|eas>`) ---",
3657
- body: () => `STIM_NO_REMOTE_SESSION
3791
+ },
3792
+ STIM_NO_REMOTE_SESSION: {
3793
+ summary: "the backend could not use agent-device, or metro.tunnel names an unusable provider",
3794
+ separator: "--- REMOTE-DEVICE CODES (`ios --remote <proxy|eas>` / `android --remote <proxy|eas>`) ---",
3795
+ body: () => `STIM_NO_REMOTE_SESSION
3658
3796
  The selected backend could not use agent-device, or metro.tunnel names a
3659
3797
  provider or mode this workspace cannot use (e.g. "expo" on a bare RN
3660
3798
  project). The remedy line says which. Nothing was created yet.`
3661
- },
3662
- STIM_REMOTE_PROXY_CONFIG: {
3663
- summary: "--remote proxy needs AGENT_DEVICE_DAEMON_BASE_URL and AGENT_DEVICE_DAEMON_AUTH_TOKEN",
3664
- body: () => `STIM_REMOTE_PROXY_CONFIG
3799
+ },
3800
+ STIM_REMOTE_PROXY_CONFIG: {
3801
+ summary: "--remote proxy needs AGENT_DEVICE_DAEMON_BASE_URL and AGENT_DEVICE_DAEMON_AUTH_TOKEN",
3802
+ body: () => `STIM_REMOTE_PROXY_CONFIG
3665
3803
  \`--remote proxy\` requires AGENT_DEVICE_DAEMON_BASE_URL and
3666
3804
  AGENT_DEVICE_DAEMON_AUTH_TOKEN. These variables provide credentials after
3667
3805
  proxy is selected. They never select the backend.`
3668
- },
3669
- STIM_REMOTE_EAS_UNAVAILABLE: {
3670
- summary: "--remote eas needs eas-cli 21.6.0 or later",
3671
- body: () => `STIM_REMOTE_EAS_UNAVAILABLE
3806
+ },
3807
+ STIM_REMOTE_EAS_UNAVAILABLE: {
3808
+ summary: "--remote eas needs eas-cli 21.6.0 or later",
3809
+ body: () => `STIM_REMOTE_EAS_UNAVAILABLE
3672
3810
  \`--remote eas\` requires eas-cli 21.6.0 or later, the first release with
3673
3811
  the \`eas simulator:*\` commands Stim runs. Stim reads \`eas --version\` and
3674
3812
  refuses before any build or session work when eas-cli is missing, older, or
3675
3813
  reports no version. Upgrade it (\`npm install --global eas-cli@latest\`, or
3676
3814
  the project's eas-cli dependency). Proxy environment variables do not change
3677
3815
  this selection and are not passed to EAS.`
3678
- },
3679
- STIM_REMOTE_PLATFORM_MISMATCH: {
3680
- summary: "the recorded remote session belongs to the other platform; stop, then rerun",
3681
- body: () => `STIM_REMOTE_PLATFORM_MISMATCH
3816
+ },
3817
+ STIM_REMOTE_PLATFORM_MISMATCH: {
3818
+ summary: "the recorded remote session belongs to the other platform; stop, then rerun",
3819
+ body: () => `STIM_REMOTE_PLATFORM_MISMATCH
3682
3820
  This workspace already has a recorded remote session, and it belongs to the
3683
3821
  OTHER platform ("Session <id> belongs to android, not ios"). A workspace
3684
3822
  holds one remote session, and Stim will not end the recorded one to make
3685
3823
  room -- it may be mid-run for whoever started it. Run \`stim stop\` for this
3686
3824
  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
3825
+ },
3826
+ STIM_REMOTE_DEVICE_MISMATCH: {
3827
+ summary: "the recorded EAS session runs another model than --device-type asks for; stop, then rerun",
3828
+ body: () => `STIM_REMOTE_DEVICE_MISMATCH
3691
3829
  \`stim ios --remote eas --device-type <name>\` found this workspace's
3692
3830
  recorded EAS Simulator session still running another model, or the model
3693
3831
  EAS chose when no --device-type was given. EAS cannot change a running
@@ -3696,20 +3834,20 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
3696
3834
  workspace, then rerun with the model you want. A recorded session that has
3697
3835
  already ended is replaced on the requested model instead, and without
3698
3836
  --device-type a live one is reused as it is. Nothing was created here.`
3699
- },
3700
- STIM_REMOTE_SESSION_STATE: {
3701
- summary: "the EAS session was created but its state could not be recorded, so Stim stopped it",
3702
- body: () => `STIM_REMOTE_SESSION_STATE
3837
+ },
3838
+ STIM_REMOTE_SESSION_STATE: {
3839
+ summary: "the EAS session was created but its state could not be recorded, so Stim stopped it",
3840
+ body: () => `STIM_REMOTE_SESSION_STATE
3703
3841
  The EAS session was created and is healthy, but recording it in this
3704
3842
  workspace's state failed (an unwritable STIM_HOME, a full disk). A session
3705
3843
  nothing references is a session nothing will ever stop, so Stim stopped the
3706
3844
  one it had just created and removed its ownership claim before reporting:
3707
3845
  this code means nothing is running and nothing is still billing. Repair the
3708
3846
  state storage the message names, then run the remote command again.`
3709
- },
3710
- STIM_REMOTE_SESSION_CLEANUP: {
3711
- summary: "Stim could not prove an EAS session ended; eas simulator:stop --id",
3712
- body: () => `STIM_REMOTE_SESSION_CLEANUP
3847
+ },
3848
+ STIM_REMOTE_SESSION_CLEANUP: {
3849
+ summary: "Stim could not prove an EAS session ended; eas simulator:stop --id",
3850
+ body: () => `STIM_REMOTE_SESSION_CLEANUP
3713
3851
  Stim tried to end an EAS session and could not PROVE it ended: \`eas
3714
3852
  simulator:stop\` failed, or its output did not confirm the stop, or the
3715
3853
  session stopped but its claim in the machine ledger could not be removed.
@@ -3718,10 +3856,10 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
3718
3856
  \`eas simulator:stop --id <id>\` -- and for a ledger that outlived its
3719
3857
  session, the ledger path to repair. The same code covers a recorded session
3720
3858
  that could not be verified before replacement: inspect it, then \`stim stop\`.`
3721
- },
3722
- STIM_REMOTE_METRO_WRONG: {
3723
- summary: "the tunnel reaches a Metro that is not this workspace's",
3724
- body: () => `STIM_REMOTE_METRO_WRONG
3859
+ },
3860
+ STIM_REMOTE_METRO_WRONG: {
3861
+ summary: "the tunnel reaches a Metro that is not this workspace's",
3862
+ body: () => `STIM_REMOTE_METRO_WRONG
3725
3863
  The gate that proves a tunnel still reaches THIS workspace's Metro failed --
3726
3864
  before a session or a build, whether the tunnel is Expo's own, one Stim
3727
3865
  started (metro.tunnel: cloudflared/ngrok/auto), or a named metro.publicUrl.
@@ -3731,32 +3869,32 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
3731
3869
  port), and it now serves ANOTHER workspace's dev server -- healthy, and
3732
3870
  wrong. Re-run \`stim start\` (it prints the port it reserved) and, for a
3733
3871
  manual tunnel, rebuild it against that port.`
3734
- },
3735
- STIM_REMOTE_METRO_UNREACHABLE: {
3736
- summary: "a remote start could not create its managed tunnel or tell the device where Metro is",
3737
- body: () => `STIM_REMOTE_METRO_UNREACHABLE
3872
+ },
3873
+ STIM_REMOTE_METRO_UNREACHABLE: {
3874
+ summary: "a remote start could not create its managed tunnel or tell the device where Metro is",
3875
+ body: () => `STIM_REMOTE_METRO_UNREACHABLE
3738
3876
  A remote start could not create its selected managed tunnel, or the device
3739
3877
  could not be told where Metro is. Follows the same remedy as
3740
3878
  STIM_NO_REMOTE_SESSION's tunnel guidance -- set metro.tunnel, or use
3741
3879
  metro.publicUrl for an existing endpoint.`
3742
- },
3743
- STIM_RELOAD_AMBIGUOUS: {
3744
- summary: "more than one owned app or the owned Chrome is live; name the platform",
3745
- separator: "--- RELOAD CODES (`stim reload [ios|android|web]`) ---",
3746
- body: () => `STIM_RELOAD_AMBIGUOUS
3880
+ },
3881
+ STIM_RELOAD_AMBIGUOUS: {
3882
+ summary: "more than one owned app or the owned Chrome is live; name the platform",
3883
+ separator: "--- RELOAD CODES (`stim reload [ios|android|web]`) ---",
3884
+ body: () => `STIM_RELOAD_AMBIGUOUS
3747
3885
  More than one of the owned iOS app, Android app and Chrome page is live.
3748
3886
  Name ios, android or web; Stim never guesses.`
3749
- },
3750
- STIM_RELOAD_RELEASE: {
3751
- summary: "the live app has embedded JS; run a Debug build first",
3752
- body: () => `STIM_RELOAD_RELEASE
3887
+ },
3888
+ STIM_RELOAD_RELEASE: {
3889
+ summary: "the live app has embedded JS; run a Debug build first",
3890
+ body: () => `STIM_RELOAD_RELEASE
3753
3891
  The live app was launched with embedded JavaScript. Run the platform command
3754
3892
  with a Debug configuration or variant first.`
3755
- },
3756
- STIM_RELOAD_STOPPED: {
3757
- summary: "the recorded app is gone, its device is not live and owned, or the process could not be proven",
3758
- aliases: ["STIM_RELOAD_UNOWNED", "STIM_RELOAD_PROBE_FAILED"],
3759
- body: () => `STIM_RELOAD_STOPPED / STIM_RELOAD_UNOWNED / STIM_RELOAD_PROBE_FAILED
3893
+ },
3894
+ STIM_RELOAD_STOPPED: {
3895
+ summary: "the recorded app is gone, its device is not live and owned, or the process could not be proven",
3896
+ aliases: ["STIM_RELOAD_UNOWNED", "STIM_RELOAD_PROBE_FAILED"],
3897
+ body: () => `STIM_RELOAD_STOPPED / STIM_RELOAD_UNOWNED / STIM_RELOAD_PROBE_FAILED
3760
3898
  The recorded app is gone, its exact device is not live and owned by this
3761
3899
  workspace, or simctl/adb could not prove the process exists. For web,
3762
3900
  STOPPED means no owned Chrome is running (run stim web) and PROBE_FAILED
@@ -3764,10 +3902,10 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
3764
3902
  only when no native launch is recorded. No launch or
3765
3903
  device lifecycle action is taken; follow the printed platform-command or
3766
3904
  process-probe remedy.`
3767
- },
3768
- STIM_RELOAD_FAILED: {
3769
- summary: "the reload failed; the remedy differs by shape -- read it before acting",
3770
- body: () => `STIM_RELOAD_FAILED
3905
+ },
3906
+ STIM_RELOAD_FAILED: {
3907
+ summary: "the reload failed; the remedy differs by shape -- read it before acting",
3908
+ body: () => `STIM_RELOAD_FAILED
3771
3909
  The Metro websocket reload did not reach a peer Stim could identify. Two
3772
3910
  shapes reach this code and the remedy differs. Read it rather than assuming.
3773
3911
 
@@ -3798,72 +3936,72 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
3798
3936
  MORE THAN ONE MATCHING PEER IS NOT A FAILURE. A workspace Metro serves one
3799
3937
  app, so several matching peers are that app on several devices. Stim reloads
3800
3938
  every one of them and reports the count in the facts as targets.`
3801
- },
3802
- STIM_WEB_NO_CHROME: {
3803
- summary: "stim web found no installed Chrome or Chromium",
3804
- separator: "--- WEB CODES (`stim web`) ---",
3805
- body: () => `STIM_WEB_NO_CHROME
3939
+ },
3940
+ STIM_WEB_NO_CHROME: {
3941
+ summary: "stim web found no installed Chrome or Chromium",
3942
+ separator: "--- WEB CODES (`stim web`) ---",
3943
+ body: () => `STIM_WEB_NO_CHROME
3806
3944
  stim web drives the installed Google Chrome (or Chromium) with a profile
3807
3945
  Stim creates. It looks in /Applications and ~/Applications on macOS, in
3808
3946
  Program Files on Windows, and for google-chrome or chromium on PATH. Stim
3809
3947
  never installs a browser. Install Chrome, then run stim doctor.`
3810
- },
3811
- STIM_WEB_NO_URL: {
3812
- summary: "not an Expo app and web.url is unset, so Stim does not know which page to open",
3813
- body: () => `STIM_WEB_NO_URL
3948
+ },
3949
+ STIM_WEB_NO_URL: {
3950
+ summary: "not an Expo app and web.url is unset, so Stim does not know which page to open",
3951
+ body: () => `STIM_WEB_NO_URL
3814
3952
  Only Expo serves web from Metro, so for any other app Stim needs web.url.
3815
3953
  stim web never starts a web server, for any framework. Start yours on a
3816
3954
  named port and point web.url at it:
3817
3955
  pnpm exec vite --port "$(stim ports get web)" --strictPort
3818
3956
  stim settings set web.url 'http://localhost:{port:web}/' --scope workspace
3819
3957
  See stim guide web.`
3820
- },
3821
- STIM_WEB_DEPS_MISSING: {
3822
- summary: "an Expo app without react-native-web cannot render on the web",
3823
- body: () => `STIM_WEB_DEPS_MISSING
3958
+ },
3959
+ STIM_WEB_DEPS_MISSING: {
3960
+ summary: "an Expo app without react-native-web cannot render on the web",
3961
+ body: () => `STIM_WEB_DEPS_MISSING
3824
3962
  The Expo app does not resolve react-native-web, so Metro cannot build a web
3825
3963
  bundle. Install the web dependencies, start Metro, then run stim web again:
3826
3964
  npx expo install react-dom react-native-web @expo/metro-runtime
3827
3965
  stim start`
3828
- },
3829
- STIM_WEB_BROWSER_HELD: {
3830
- summary: "the previous owned Chrome could not be stopped or verified; it was left running",
3831
- body: () => `STIM_WEB_BROWSER_HELD
3966
+ },
3967
+ STIM_WEB_BROWSER_HELD: {
3968
+ summary: "the previous owned Chrome could not be stopped or verified; it was left running",
3969
+ body: () => `STIM_WEB_BROWSER_HELD
3832
3970
  stim web replaces the workspace's owned Chrome when its options change, and
3833
3971
  the previous one could not be stopped: its supervisor or Chrome did not
3834
3972
  exit, or their process identities could not be verified. Stim never signals
3835
3973
  a process it cannot verify. Check stim status for browser-unverified, then
3836
3974
  follow stim guide errors teardown.`
3837
- },
3838
- STIM_WEB_LAUNCH_FAILED: {
3839
- summary: "the owned Chrome did not start or did not open DevTools on its reserved port",
3840
- body: () => `STIM_WEB_LAUNCH_FAILED
3975
+ },
3976
+ STIM_WEB_LAUNCH_FAILED: {
3977
+ summary: "the owned Chrome did not start or did not open DevTools on its reserved port",
3978
+ body: () => `STIM_WEB_LAUNCH_FAILED
3841
3979
  The browser supervisor exited before Chrome answered on its reserved
3842
3980
  DevTools port. The remedy names the supervisor log; web.ndjson carries the
3843
3981
  failure as web_browser_failed, and browser.log holds Chrome's own output.
3844
3982
  A port another process bound first also lands here: run stim web again to
3845
3983
  reserve a fresh one.`
3846
- },
3847
- STIM_WORKTREE_REMOVAL_IN_PROGRESS: {
3848
- summary: "a managed remote start found worktree remove holding the lock; wait, then rerun",
3849
- separator: "--- DEV-SERVER CODES (`stim start`) ---",
3850
- body: () => `STIM_WORKTREE_REMOVAL_IN_PROGRESS
3984
+ },
3985
+ STIM_WORKTREE_REMOVAL_IN_PROGRESS: {
3986
+ summary: "a managed remote start found worktree remove holding the lock; wait, then rerun",
3987
+ separator: "--- DEV-SERVER CODES (`stim start`) ---",
3988
+ body: () => `STIM_WORKTREE_REMOVAL_IN_PROGRESS
3851
3989
  A managed remote start found that \`stim worktree remove\` owns the
3852
3990
  worktree lock. The start did not register the project or create a tunnel.
3853
3991
  Wait for removal to finish, then run \`stim start --remote\` again.`
3854
- },
3855
- STIM_REMOTE_START_REQUIRED: {
3856
- summary: "a running server cannot gain a remote tunnel; stop, then start --remote, or metro.publicUrl",
3857
- body: () => `STIM_REMOTE_START_REQUIRED
3992
+ },
3993
+ STIM_REMOTE_START_REQUIRED: {
3994
+ summary: "a running server cannot gain a remote tunnel; stop, then start --remote, or metro.publicUrl",
3995
+ body: () => `STIM_REMOTE_START_REQUIRED
3858
3996
  A healthy bare or Expo server was started without its required remote
3859
3997
  tunnel. A running server cannot gain that option. For a Stim supervisor,
3860
3998
  run \`stim stop\`, then \`stim start --remote\`. For an external server,
3861
3999
  configure metro.publicUrl or let Stim supervise the server.`
3862
- },
3863
- STIM_BARE_DEPS: {
3864
- summary: "the supervisor cannot host Metro from the project's node_modules; the @stim-cli/metro capture note",
3865
- aliases: ["STIM_BARE_LOAD", "STIM_BARE_API"],
3866
- body: () => `STIM_BARE_DEPS / STIM_BARE_LOAD / STIM_BARE_API (bare RN)
4000
+ },
4001
+ STIM_BARE_DEPS: {
4002
+ summary: "the supervisor cannot host Metro from the project's node_modules; the @stim-cli/metro capture note",
4003
+ aliases: ["STIM_BARE_LOAD", "STIM_BARE_API"],
4004
+ body: () => `STIM_BARE_DEPS / STIM_BARE_LOAD / STIM_BARE_API (bare RN)
3867
4005
  The supervisor hosts Metro out of the PROJECT's node_modules, so metro,
3868
4006
  @react-native/dev-middleware and @react-native-community/cli-server-api must
3869
4007
  be installed there and must match the project's React Native. DEPS = not
@@ -3875,15 +4013,15 @@ captured" (in metro.ndjson, bare RN)
3875
4013
  The dev server is serving; only capture is missing, so \`logs\` would report
3876
4014
  a quiet timeline for a broken build. Install \`@stim-cli/metro\` as a
3877
4015
  devDependency of the project.`
3878
- },
3879
- STIM_EXPO_BIN: {
3880
- summary: "node_modules/.bin/expo is missing; install dependencies",
3881
- body: () => `STIM_EXPO_BIN (Expo)
4016
+ },
4017
+ STIM_EXPO_BIN: {
4018
+ summary: "node_modules/.bin/expo is missing; install dependencies",
4019
+ body: () => `STIM_EXPO_BIN (Expo)
3882
4020
  node_modules/.bin/expo does not exist. Install the project's dependencies.`
3883
- },
3884
- STIM_METRO_TIMEOUT: {
3885
- summary: "the supervisor is alive but Metro or the tunnel was not ready within the wait; --wait 180",
3886
- body: () => `STIM_METRO_TIMEOUT
4021
+ },
4022
+ STIM_METRO_TIMEOUT: {
4023
+ summary: "the supervisor is alive but Metro or the tunnel was not ready within the wait; --wait 180",
4024
+ body: () => `STIM_METRO_TIMEOUT
3887
4025
  "The dev server did not answer on port <n> within <s>s."
3888
4026
  The supervisor is alive, but Metro or its requested Expo tunnel is not ready.
3889
4027
  A Debug \`ios\` or \`android\` run that starts the dev server reports the
@@ -3892,10 +4030,10 @@ captured" (in metro.ndjson, bare RN)
3892
4030
  printed the last lines of the global workspace logs/supervisor.log above this -- read
3893
4031
  them. A cold Metro on a large graph can genuinely need more than the default
3894
4032
  60s: re-run with \`--wait 180\`. Otherwise \`stim stop\`, then \`start\`.`
3895
- },
3896
- STIM_SUPERVISOR_EXITED: {
3897
- summary: "the dev server failed outright; the quoted supervisor.log tail is the real error",
3898
- body: () => `STIM_SUPERVISOR_EXITED
4033
+ },
4034
+ STIM_SUPERVISOR_EXITED: {
4035
+ summary: "the dev server failed outright; the quoted supervisor.log tail is the real error",
4036
+ body: () => `STIM_SUPERVISOR_EXITED
3899
4037
  "The supervisor exited (<code|signal>) before the dev server came up"
3900
4038
  The dev server failed outright, and the quoted evidence is the real error:
3901
4039
  the supervisor.log tail if it wrote one, plus this attempt's error records
@@ -3910,11 +4048,11 @@ captured" (in metro.ndjson, bare RN)
3910
4048
  Stim's processes. For a legacy record, stop the server with the tool that
3911
4049
  started it before retrying. Never reconstruct ownership from a process
3912
4050
  name, port, or wall-clock timestamp.`
3913
- },
3914
- STIM_BAD_ARG: {
3915
- summary: "an argument, setting, directory, flavor, or device name refused before anything starts",
3916
- aliases: ["STIM_NO_PROJECT"],
3917
- body: () => `STIM_BAD_ARG / STIM_NO_PROJECT
4051
+ },
4052
+ STIM_BAD_ARG: {
4053
+ summary: "an argument, setting, directory, flavor, or device name refused before anything starts",
4054
+ aliases: ["STIM_NO_PROJECT"],
4055
+ body: () => `STIM_BAD_ARG / STIM_NO_PROJECT
3918
4056
  The command refused before doing anything: an unusable --wait value, a known
3919
4057
  setting with the wrong type ("Invalid <key> setting <value>. Expected <shape>."
3920
4058
  -- \`guide settings\` names the type each key takes), an invalid
@@ -3932,8 +4070,9 @@ captured" (in metro.ndjson, bare RN)
3932
4070
  on an eas/proxy run (\`--remote\` or ios.remote; that backend picks the
3933
4071
  iOS version), \`ios --device-type\` on the proxy backend or with an
3934
4072
  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
4073
+ \`--device-profile\` on an eas/proxy run (\`--remote\` or android.remote), a
4074
+ \`android --remote auto\`, a changed hosted placement, or hosted Debug
4075
+ with \`--no-metro-check\` (see lifecycle hosted-android), a working directory
3937
4076
  with no package.json above it, or one whose nearest package.json does not
3938
4077
  parse or depends on neither react-native nor expo, so the directory is not
3939
4078
  an app (the refusal names that package.json and says which of the two it
@@ -3982,11 +4121,11 @@ captured" (in metro.ndjson, bare RN)
3982
4121
  list device -c\`) runs only when
3983
4122
  a name was actually given, and a listing that fails is reported as
3984
4123
  STIM_NO_DEVICE naming the tool, never as a crash.`
3985
- },
3986
- STIM_LOCK_REFUSED: {
3987
- summary: "a directory lock is held by a removal, which is never waited out",
3988
- separator: "--- COORDINATION CODES (any command that shares a resource) ---",
3989
- body: () => `STIM_LOCK_REFUSED
4124
+ },
4125
+ STIM_LOCK_REFUSED: {
4126
+ summary: "a directory lock is held by a removal, which is never waited out",
4127
+ separator: "--- COORDINATION CODES (any command that shares a resource) ---",
4128
+ body: () => `STIM_LOCK_REFUSED
3990
4129
  A directory lock that serialises two commands over the same thing -- this
3991
4130
  workspace's managed tunnel, its managed remote worktree, the machine's EAS
3992
4131
  project ledger -- is held by a REMOVAL, and a removal is never waited out:
@@ -3995,10 +4134,10 @@ captured" (in metro.ndjson, bare RN)
3995
4134
  \`workspace removal\` -- both are \`stim worktree remove\`). Let it finish,
3996
4135
  then run the command again. \`start --remote\` reports this same case as
3997
4136
  STIM_WORKTREE_REMOVAL_IN_PROGRESS instead.`
3998
- },
3999
- STIM_CANCELLED: {
4000
- summary: "an ios or android run was interrupted by `stim stop` or Ctrl-C before it finished",
4001
- body: () => `STIM_CANCELLED (ios, android; exit 130)
4137
+ },
4138
+ STIM_CANCELLED: {
4139
+ summary: "an ios or android run was interrupted by `stim stop` or Ctrl-C before it finished",
4140
+ body: () => `STIM_CANCELLED (ios, android; exit 130)
4002
4141
  "The ios run was cancelled by \`stim stop\` (pid <n>) before it finished."
4003
4142
  The run received SIGINT: from \`stim stop\`, which interrupts a build it
4004
4143
  leaves with nothing to deploy to, or from Ctrl-C. It forwarded the interrupt
@@ -4010,10 +4149,10 @@ captured" (in metro.ndjson, bare RN)
4010
4149
  second SIGINT exits at once, and so does a run that holds a physical-device
4011
4150
  lease; those exits print no payload. Run the same command again when you want the
4012
4151
  app on a device.`
4013
- },
4014
- STIM_STOP_BLOCKED: {
4015
- summary: "`stop` could not interrupt the ios or android run holding this workspace; names its pid and claim",
4016
- body: () => `STIM_STOP_BLOCKED (stop)
4152
+ },
4153
+ STIM_STOP_BLOCKED: {
4154
+ summary: "`stop` could not interrupt the ios or android run holding this workspace; names its pid and claim",
4155
+ body: () => `STIM_STOP_BLOCKED (stop)
4017
4156
  \`stop\` found a live \`ios\` or \`android\` run holding this workspace's
4018
4157
  native-run claim and could not end it:
4019
4158
  - "... did not exit within 60s of SIGINT": the run was interrupted but is
@@ -4030,10 +4169,10 @@ captured" (in metro.ndjson, bare RN)
4030
4169
  session never waits on a build. The JSON payload carries the session
4031
4170
  outcome under device.remote. Devices, collectors and the dev server are left
4032
4171
  as they were.`
4033
- },
4034
- STIM_LOCK_TIMEOUT: {
4035
- summary: "a lock held past the wait; workspace-process and short directory locks",
4036
- body: () => `STIM_LOCK_TIMEOUT
4172
+ },
4173
+ STIM_LOCK_TIMEOUT: {
4174
+ summary: "a lock held past the wait; workspace-process and short directory locks",
4175
+ body: () => `STIM_LOCK_TIMEOUT
4037
4176
  The same locks, held by an ordinary command that is still running, for
4038
4177
  longer than the wait -- 60s by default, 4 minutes for the remote-session lock
4039
4178
  and for \`gc\` deleting EAS sessions under the EAS project lock (its sweep
@@ -4078,11 +4217,11 @@ captured" (in metro.ndjson, bare RN)
4078
4217
  using it: if none is running, remove the named directory with that command.
4079
4218
  Standalone Metro and Expo cache packages use the
4080
4219
  same core protocol and do not require the Stim CLI.`
4081
- },
4082
- teardown: {
4083
- summary: "an unmanaged port, an unverified supervisor, and a failed device teardown",
4084
- separator: "--- TEARDOWN AND WORKSPACE REFUSALS ---",
4085
- body: () => `"metro refusing to kill port <n>: ... runs from <dir>, outside
4220
+ },
4221
+ teardown: {
4222
+ summary: "an unmanaged port, an unverified supervisor, and a failed device teardown",
4223
+ separator: "--- TEARDOWN AND WORKSPACE REFUSALS ---",
4224
+ body: () => `"metro refusing to kill port <n>: ... runs from <dir>, outside
4086
4225
  <project>" (stop)
4087
4226
  Stim only signals processes it launched and whose saved identity still
4088
4227
  matches. Stop an externally started server with the tool that started it.
@@ -4140,10 +4279,10 @@ emulator process <pid> is still running"
4140
4279
  no process-table check and Stim signals nothing there: it waits for the pid
4141
4280
  in the AVD's process lock after \`adb emu kill\`, and refuses at once when
4142
4281
  adb cannot reach the emulator.`
4143
- },
4144
- remove: {
4145
- summary: "worktree remove refused a dirty tree: what it restores itself and what --force discards",
4146
- body: () => `"Refusing to remove <path>: uncommitted changes / untracked files / commits
4282
+ },
4283
+ remove: {
4284
+ summary: "worktree remove refused a dirty tree: what it restores itself and what --force discards",
4285
+ body: () => `"Refusing to remove <path>: uncommitted changes / untracked files / commits
4147
4286
  not on any remote" (worktree remove)
4148
4287
  A native build rewrites tracked files, and Stim now RESTORES the one class
4149
4288
  it can prove is not work: when the only dirt left is \`pod install\` churn
@@ -4167,10 +4306,10 @@ not on any remote" (worktree remove)
4167
4306
  refusal actually named.
4168
4307
  Use --force only when you genuinely intend to discard work; it deletes
4169
4308
  uncommitted and untracked files permanently.`
4170
- },
4171
- STIM_MAIN_DIRTY: {
4172
- summary: "warm --refresh will not move a source checkout with local work or an operation in progress",
4173
- body: () => `STIM_MAIN_DIRTY
4309
+ },
4310
+ STIM_MAIN_DIRTY: {
4311
+ summary: "warm --refresh will not move a source checkout with local work or an operation in progress",
4312
+ body: () => `STIM_MAIN_DIRTY
4174
4313
  \`worktree warm --refresh\` writes to the SOURCE CHECKOUT, and it refuses one
4175
4314
  it cannot move: tracked files with uncommitted changes (the refusal names
4176
4315
  them), or a rebase or merge in progress. Untracked files are not a reason to
@@ -4180,28 +4319,28 @@ not on any remote" (worktree remove)
4180
4319
  or \`git -C <source-checkout> rebase --abort\`. Nothing was installed or
4181
4320
  copied. Plain \`stim worktree warm\` does not care: it copies from a dirty
4182
4321
  source checkout exactly as it always has.`
4183
- },
4184
- STIM_MAIN_DETACHED: {
4185
- summary: "warm --refresh needs a branch to fast-forward, not a detached HEAD",
4186
- body: () => `STIM_MAIN_DETACHED
4322
+ },
4323
+ STIM_MAIN_DETACHED: {
4324
+ summary: "warm --refresh needs a branch to fast-forward, not a detached HEAD",
4325
+ body: () => `STIM_MAIN_DETACHED
4187
4326
  The source checkout's HEAD is detached, so there is no branch to fast-forward
4188
4327
  and no upstream to fast-forward it to. Run
4189
4328
  \`git -C <source-checkout> checkout <branch>\` and warm again. \`--refresh\`
4190
4329
  never picks a branch for you; a checkout whose job is to seed worktrees
4191
4330
  should sit on a branch someone chose.`
4192
- },
4193
- STIM_MAIN_DIVERGED: {
4194
- summary: "the source checkout is both ahead of and behind its upstream; warm --refresh will not merge",
4195
- body: () => `STIM_MAIN_DIVERGED
4331
+ },
4332
+ STIM_MAIN_DIVERGED: {
4333
+ summary: "the source checkout is both ahead of and behind its upstream; warm --refresh will not merge",
4334
+ body: () => `STIM_MAIN_DIVERGED
4196
4335
  The source checkout's branch has commits its upstream does not, AND its
4197
4336
  upstream has commits it does not. A fast-forward is impossible, and
4198
4337
  \`--refresh\` will not merge or reset someone else's checkout to make one:
4199
4338
  that decision is yours. Rebase or merge it yourself, then warm again. The
4200
4339
  message reports both counts. Nothing was installed or copied.`
4201
- },
4202
- STIM_DEPS_INCOMPLETE: {
4203
- summary: "the last install recorded for the lockfile on disk now did not finish, so warm will not copy it",
4204
- body: () => `STIM_DEPS_INCOMPLETE
4340
+ },
4341
+ STIM_DEPS_INCOMPLETE: {
4342
+ summary: "the last install recorded for the lockfile on disk now did not finish, so warm will not copy it",
4343
+ body: () => `STIM_DEPS_INCOMPLETE
4205
4344
  \`worktree warm\` refuses to copy dependencies the source checkout never
4206
4345
  finished installing. \`--refresh\` records a COMPLETED install of the lockfile
4207
4346
  it read under ~/.stim/warm-installs; an install that failed, or whose process
@@ -4219,20 +4358,20 @@ not on any remote" (worktree remove)
4219
4358
  repository before its first \`--refresh\` -- copies as it always has.
4220
4359
  In a monorepo the record is keyed on the directory that owns the lockfile,
4221
4360
  which is usually the repository root, so every app of it reads the same one.`
4222
- },
4223
- warm: {
4224
- summary: "worktree warm claim storage refusals and recovery",
4225
- body: () => `Both plain warm and --refresh require an ownership claim.
4361
+ },
4362
+ warm: {
4363
+ summary: "worktree warm claim storage refusals and recovery",
4364
+ body: () => `Both plain warm and --refresh require an ownership claim.
4226
4365
  Permission-denied or read-only claim storage reports STIM_CLAIM_UNAVAILABLE
4227
4366
  and names the path. Restore access to the existing claim store and retry.
4228
4367
  A non-directory claim ancestor reports STIM_CLAIM_REFUSED and names the
4229
4368
  blocking file. Inspect it and use the printed move-aside command to preserve
4230
4369
  its contents before retrying. See guide errors STIM_CLAIM_UNAVAILABLE and
4231
4370
  guide errors STIM_CLAIM_REFUSED for the recovery details.`
4232
- },
4233
- carry: {
4234
- summary: "worktree warm copy results, lockfile mismatches, and remedies",
4235
- body: () => `"carry incomplete: ... ignored entries copied, ... kept, ... failed"
4371
+ },
4372
+ carry: {
4373
+ summary: "worktree warm copy results, lockfile mismatches, and remedies",
4374
+ body: () => `"carry incomplete: ... ignored entries copied, ... kept, ... failed"
4236
4375
  (worktree warm)
4237
4376
  At least one entry could not be copied. The command exits 1 and names each
4238
4377
  failure. Existing entries stay untouched; any files already published remain.
@@ -4273,11 +4412,11 @@ not on any remote" (worktree remove)
4273
4412
  the app is ready, unless \`--refresh\` installed them in the SOURCE CHECKOUT
4274
4413
  first; even then the copy can still carry a lockfile this branch does not
4275
4414
  have, which is exactly what these carry warnings report.`
4276
- },
4277
- environment: {
4278
- summary: "npx registry E401/E404, the Node floor, no free Metro port, the reservation race",
4279
- separator: "--- ENVIRONMENT ---",
4280
- body: () => `"npm error code E401 / E404" while \`npx\` resolves the stim package
4415
+ },
4416
+ environment: {
4417
+ summary: "npx registry E401/E404, the Node floor, no free Metro port, the reservation race",
4418
+ separator: "--- ENVIRONMENT ---",
4419
+ body: () => `"npm error code E401 / E404" while \`npx\` resolves the stim package
4281
4420
  The repo probably pins a private registry in \`.npmrc\`, so \`npx\` looked for
4282
4421
  the package there instead of on npm. Use the public registry for this command:
4283
4422
 
@@ -4298,10 +4437,10 @@ not on any remote" (worktree remove)
4298
4437
  "Could not reserve a Metro port after 5 attempts"
4299
4438
  Several commands raced for the same ports and each one lost. Nothing is
4300
4439
  wrong; retry.`
4301
- },
4302
- sandbox: {
4303
- summary: "running under a sandboxing harness: EPERM under STIM_HOME, CoreSimulatorService, adb",
4304
- body: () => `RUNNING UNDER A SANDBOX
4440
+ },
4441
+ sandbox: {
4442
+ summary: "running under a sandboxing harness: EPERM under STIM_HOME, CoreSimulatorService, adb",
4443
+ body: () => `RUNNING UNDER A SANDBOX
4305
4444
 
4306
4445
  An agent harness that sandboxes shell commands typically permits writes
4307
4446
  inside the project and blocks the rest. Three things Stim needs sit outside
@@ -4348,10 +4487,10 @@ not on any remote" (worktree remove)
4348
4487
 
4349
4488
  A git credential helper is often blocked too. It prints \`failed to store\`
4350
4489
  on a fetch that otherwise succeeded, and is safe to ignore.`
4351
- },
4352
- STIM_CONFIG_CORRUPT: {
4353
- summary: "~/.stim/config.json is not a valid JSON object and Stim never resets it",
4354
- body: () => `STIM_CONFIG_CORRUPT ("Stim config at <path> is not valid JSON",
4490
+ },
4491
+ STIM_CONFIG_CORRUPT: {
4492
+ summary: "~/.stim/config.json is not a valid JSON object and Stim never resets it",
4493
+ body: () => `STIM_CONFIG_CORRUPT ("Stim config at <path> is not valid JSON",
4355
4494
  "... is not a JSON object",
4356
4495
  "... has a projects that is not an object",
4357
4496
  "... has a projects entry "<key>" that is not an object")
@@ -4362,19 +4501,19 @@ not on any remote" (worktree remove)
4362
4501
  resets it for you -- a silent reset would orphan every simulator it names.
4363
4502
  Repair the file, or move it aside (\`mv <path> <path>.broken\`) and accept
4364
4503
  that the devices it recorded become orphans you delete by hand.`
4365
- },
4366
- STIM_RELATIVE_PATH: {
4367
- summary: "STIM_HOME, STIM_BUILD_CACHE or STIM_METRO_CACHE is set to a relative path",
4368
- body: () => `STIM_RELATIVE_PATH ("<NAME>=<value> is not an absolute path")
4504
+ },
4505
+ STIM_RELATIVE_PATH: {
4506
+ summary: "STIM_HOME, STIM_BUILD_CACHE or STIM_METRO_CACHE is set to a relative path",
4507
+ body: () => `STIM_RELATIVE_PATH ("<NAME>=<value> is not an absolute path")
4369
4508
  Any command refuses before it reads or writes state. A relative value would
4370
4509
  resolve against each process's working directory, so the CLI, Metro and the
4371
4510
  Expo build-cache provider would each use a different store. Set the named
4372
4511
  variable to an absolute path, or unset it to use the default. Metro and the
4373
4512
  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>")
4513
+ },
4514
+ STIM_NODE_UNSUPPORTED: {
4515
+ summary: "stim or stim-server started on a Node older than 22.12.0, often a project pin",
4516
+ body: () => `STIM_NODE_UNSUPPORTED ("Stim needs Node <floor> or later; this is Node <version> at <path>")
4378
4517
  stim and stim-server refuse before loading anything else when the Node that
4379
4518
  runs them is older than their engines floor. Both start through
4380
4519
  \`#!/usr/bin/env node\`, so the working directory can choose that Node: asdf,
@@ -4395,12 +4534,14 @@ not on any remote" (worktree remove)
4395
4534
  is set for command stim" instead of running Stim. When that version is
4396
4535
  older than 22.12.0, first install Stim with npm under a supported version.
4397
4536
  The tools Stim starts inherit the override.`
4398
- }
4399
4537
  }
4400
- },
4401
- lifecycle: {
4402
- summary: "The full worktree -> start -> ios/android -> logs -> teardown flow, with sections for builds, devices and flags",
4403
- preamble: () => `ENVIRONMENT LIFECYCLE
4538
+ }
4539
+ };
4540
+ //#endregion
4541
+ //#region src/guide/lifecycle.ts
4542
+ const lifecycle = {
4543
+ summary: "The full worktree -> start -> ios/android -> logs -> teardown flow, with sections for builds, devices and flags",
4544
+ preamble: () => `ENVIRONMENT LIFECYCLE
4404
4545
 
4405
4546
  Two workflows share steps 2 through 6.
4406
4547
 
@@ -4651,10 +4792,152 @@ WAITING FOR A CHANGE
4651
4792
  nothing changes, except a closed stdout pipe on Linux whose starter is
4652
4793
  still running, which it notices at the next change.
4653
4794
  Without --json it reprints the human view on change.`,
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
4795
+ sections: {
4796
+ "hosted-android": {
4797
+ summary: "Android on a named approved Mac: strict placement, private Metro, status and stop",
4798
+ body: () => `ANDROID ON A HOSTING MAC
4799
+
4800
+ If Stim is not installed globally, replace stim with npx stim.
4801
+
4802
+ stim settings set hosting.machines '["janics-mac-mini"]'
4803
+ stim doctor --fix
4804
+ stim doctor
4805
+ stim android --remote janics-mac-mini --device-profile pixel_7
4806
+ stim status --json
4807
+ stim reload android
4808
+ stim stop
4809
+
4810
+ A person on the host approves device-host access, separately from build, read
4811
+ and control. The setup in hosted-ios also serves Android. android.remote sets
4812
+ a workspace default. eas and proxy remain backends. --remote auto or
4813
+ android.remote = auto places automatically; see hosted-ios for the shared
4814
+ policy. No flag or setting runs on this Mac.
4815
+
4816
+ A named Mac is strict: refusal, unreachable, declined or failed preparation
4817
+ returns STIM_HOSTING_REFUSED without fallback. --device, a different recorded
4818
+ machine, a running local owned emulator in the slot, and --no-metro-check for
4819
+ hosted Debug refuse STIM_BAD_ARG. Run stim stop before switching placement.
4820
+ Unreadable android.host blocks only its slot; restore its recorded machine and
4821
+ session from the host before stopping that slot.
4822
+
4823
+ --system-image and --device-profile select installed choices on the host.
4824
+ The build targets the offered ABI and --build-machine independently selects a
4825
+ compatible build worker. Stim builds before reserving, records the session as
4826
+ soon as it exists, then delivers one App.apk. The emulator boots headless;
4827
+ androidEmulatorApp is ignored with a note.
4828
+
4829
+ Debug requires the local Metro supervisor. metro.publicUrl and metro.tunnel
4830
+ are ignored with a note. A private tailnet gateway reaches the host loopback
4831
+ bridge; the host reverses the client's Metro port into that bridge on its exact
4832
+ ledger-owned serial and sets debug_http_host to localhost:<clientMetroPort>.
4833
+ Each run and stim reload android restores the reverse, including after a host
4834
+ adb server restart. Reload broadcasts through the local Metro and never runs
4835
+ adb against the host serial on this Mac. Release variants skip Metro. Launch
4836
+ is true only with client bundle evidence or a live host release process;
4837
+ bundling requires a bundle request and unverified has no launch evidence.
4838
+
4839
+ Status adds android.host { machine, session, selected, agent, device: { name,
4840
+ systemImage, api }, state } per slot. The public name is the profile and API
4841
+ level, never the host serial or AVD name. ready, stopped and unverified are
4842
+ session probe states; a conflicting local emulator remains visible with a
4843
+ warning. A recorded placement keeps the workspace in use and blocks idle stop.
4844
+
4845
+ stop and worktree remove wait for device-host.stop, park the host's eligible owned
4846
+ emulator within pool.androidParkedMax (delete otherwise), close the gateway and clear placement. stop --json reports
4847
+ outcomes.device["android:host:<slot>"]. Forbidden or unknown-session also clears
4848
+ placement; an unreachable host keeps it and fails cleanup. A clean host restart
4849
+ parks eligible devices; a later reserve by the same client adopts a compatible
4850
+ parked emulator with all third-party apps removed. Running sessions left by a
4851
+ crash stay unknown until explicit stop; booted devices are never re-attached
4852
+ after restart. See hosted-ios for retention and eviction. JavaScript logs
4853
+ arrive through local Metro.
4854
+
4855
+ AGENT CONTROL
4856
+
4857
+ Set hosting.agentDriver to agent-device on the hosting Mac. Both Macs need
4858
+ agent-device 0.21.22 or later. The host advertises hosted-android-agent and
4859
+ starts one loopback daemon per installed Android session. Its policy allows
4860
+ only the exact ledger-owned emulator serial and denies device shutdown.
4861
+ The host verifies the policy digest and android-instance backend before granting
4862
+ control. An older host, missing binary or unsupported policy reports driver: none;
4863
+ the host's notice explains unavailable control.
4864
+
4865
+ android.host.agent in stim status --json provides driver, remoteConfig and
4866
+ command, never its token. Each slot has a separate 0600 file under the workspace
4867
+ state directory at hosted-android/<slot>/agent-device-remote.json. Use that file:
4868
+
4869
+ agent-device open <packageId> --remote-config <file>
4870
+ agent-device snapshot --remote-config <file>
4871
+ agent-device click <ref> --remote-config <file>
4872
+ agent-device screenshot --remote-config <file>
4873
+
4874
+ The command shape is agent-device <command> --remote-config <file>. Open leases
4875
+ the emulator automatically with android-instance, the proxy provider and this
4876
+ session's tenant. devices is permitted for the client's open flow; inventory
4877
+ contains only this emulator. Never use its serial with a local agent-device.
4878
+ Allowed commands are devices, open, close, snapshot, diff, wait, find, get, is,
4879
+ click, fill, press, type, focus, scroll, screenshot, longpress, swipe, back, home,
4880
+ orientation, appstate, alert and batch. Stim installs; install, reinstall,
4881
+ uninstall, boot, shutdown, close --shutdown, record, logs, uploads and device-wide
4882
+ actions are refused. Other serials in commands, flags, inputs, nested batches or
4883
+ lease requests are refused. Ambient client paths and owner fields are stripped;
4884
+ the host pins platform, serial, tenant, runId, clientId, deviceKey and backend.
4885
+ Host paths and launch inputs are refused. Screenshots use only remote artifacts.
4886
+
4887
+ Stop closes the matching agent-device connection and removes the slot file only
4888
+ once the host confirms stop or revocation. Reinstall, stop, revocation and server
4889
+ close stop the daemon before native work or device deletion. An unresolved
4890
+ proxy, daemon or helper retains the child-aware daemon claim and refuses
4891
+ replacement or deletion, including after a server restart. Agent-device uses
4892
+ one-shot Android snapshots without helper adb forwards; teardown stops its
4893
+ snapshot and IME helper processes only on the pinned serial. Helper APKs remain
4894
+ on the owned emulator until Stim deletes it. A daemon exit rotates only that
4895
+ session's grant; rerun stim android to refresh its config.
4896
+
4897
+ Stim Desktop and the phone app view and control the emulator through this
4898
+ Mac's local stim-server relay, with an "on <machine>" label. Turn on Serve to
4899
+ phones in Desktop and pair the phone with this Mac, not the hosting Mac.
4900
+ frames.subscribe and control.begin address workspace, platform android and
4901
+ slot, including named slots. The host verifies its exact ledger-owned serial,
4902
+ AVD identity and ABI before capture or input; clients never use that serial
4903
+ locally. Capture uses the host's stim-frames helper and scrcpy server jar over
4904
+ adb; stopping capture removes its jar and forward before emulator teardown.
4905
+ Stopped sessions show a rerun remedy; unavailable hosts report why frames
4906
+ cannot arrive. Replay (at/rate), duoFrame and physical hosted targets are
4907
+ refused. Desktop hides local emulator windows, rotation, clipboard and emulator
4908
+ options; touch, text and hardware buttons use the relay. Rotation and posture
4909
+ are unavailable through the scrcpy path.
4910
+
4911
+ Reruns send only missing manifest and APK digests from the session-scoped blob
4912
+ store. The host verifies reused bytes; each new attempt still installs the APK.
4913
+ When an Android build ran on the hosting Mac's pinned node, the host takes only
4914
+ the single App.apk matching the client's manifest. The build and hosting approvals
4915
+ must belong to the same tailnet node. Handoff failure falls back to upload,
4916
+ retrying a busy host for at most one minute.
4917
+
4918
+ stim logs, including --errors and --json, pulls native logcat records as src: device.
4919
+ The host rechecks the exact ledger-owned serial and AVD name, resolves the app's
4920
+ pid on each collection and also drains its last observed pid after a crash or
4921
+ restart. A successful drain retires the exited pid. A failed pid lookup preserves
4922
+ the checkpoint without querying logcat, so the next collection can retry.
4923
+ If no pid was ever observed,
4924
+ native logs cannot be attributed to the app.
4925
+ PID-based logcat cannot distinguish historical reuse of an exited pid.
4926
+ Queries use bounded logcat -d -v epoch -T <epoch> --pid <pid> output, a ten-second
4927
+ budget, a persisted checkpoint and five-second overlap to deduplicate records.
4928
+ Logcat retains a finite buffer; records already evicted or persisted beyond the
4929
+ overlap may be unavailable. An oversized query retries the recent tail and writes
4930
+ a device warning naming any dropped interval. Followers share a child-aware log
4931
+ claim; install and stop cancel and settle the worker before native operations.
4932
+ Stop copies logs before and after the host's final collection and teardown, with
4933
+ each client drain bounded to 30 seconds and progress on stderr. Collected logs
4934
+ remain readable after stop. The host advertises hosted-android-data for Android
4935
+ handoff and native logs. Older hosts get a newer stim-server note, use upload
4936
+ and show records already copied here.`
4937
+ },
4938
+ "hosted-ios": {
4939
+ summary: "iOS on a named approved Mac: strict placement, private Metro, status and stop",
4940
+ body: () => `IOS ON A HOSTING MAC
4658
4941
 
4659
4942
  If Stim is not installed globally, replace stim with npx stim.
4660
4943
 
@@ -4671,9 +4954,36 @@ If Stim is not installed globally, replace stim with npx stim.
4671
4954
  The same setup serves macos --remote. Approval is separate from build, read
4672
4955
  and phone control. Set ios.remote to the machine name for a workspace default;
4673
4956
  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.
4957
+ --remote auto (or ios.remote = auto / android.remote = auto) stays here when
4958
+ there is a free concurrency.maxDevices slot (0 means unlimited), no device
4959
+ waiter ahead, normal host memory pressure, no budget shortfall,
4960
+ and 5-minute load per core below offload.maxLoadPerCore. Without hosting.machines,
4961
+ auto is local. An unknown local device count also stays local; boot admission
4962
+ still decides. The default without a flag or setting stays local.
4963
+
4964
+ Otherwise Stim asks every approved hosting.machines host in parallel, with a
4965
+ 3-second probe timeout and this run's model/runtime or image/profile selectors.
4966
+ A compatible choice, no declined reason, normal memory pressure and available
4967
+ capacity are required. With a free local slot, a host must report a lower load;
4968
+ with a full cap or a queue ahead, an admitted host with unknown load is allowed.
4969
+ Hosts rank by the build preference (flag > STIM_OFFLOAD_MACHINE > offload.machine)
4970
+ when it names a machine, then lowest load, most free memory and hosting.machines order. Automatic build
4971
+ offload resolves later, after the device architecture is known.
4972
+
4973
+ When no host admits, auto runs here and may wait in the existing FIFO device
4974
+ slot queue; --no-wait and --wait 0 refuse with STIM_AT_CAPACITY. The placement
4975
+ line explains the decision and the skipped hosts. JSON progress goes to stderr.
4976
+ A recorded hosted session wins over load; a live local owned slot stays here.
4977
+ A stopped recorded session places again; unreachable or unknown sessions refuse.
4978
+
4979
+ A reserve refusal after a successful offer can race with another run. It fails
4980
+ with STIM_HOSTING_REFUSED naming the host and asking to retry. Stim does not
4981
+ try the next host or local placement after the build has targeted that host's
4982
+ architecture, and never falls back once a hosted session exists. A named machine remains strict. --device
4983
+ cannot combine with auto. eas/proxy and macOS placement are unchanged.
4984
+ --simulator-app is honoured locally with auto and ignored with a note on a host.
4985
+ Placement measures budgets without reclaiming; only a new local run reclaims.
4986
+ Android --remote <machine> uses an owned emulator on that Mac; see hosted-android.
4677
4987
 
4678
4988
  A named Mac is strict: STIM_HOSTING_REFUSED names its reason; nothing boots
4679
4989
  here or elsewhere. --device cannot use a hosting Mac, and --simulator-app
@@ -4683,6 +4993,24 @@ refuses. The host boots headless and ignores this Mac's iosSimulatorApp setting.
4683
4993
  the offered simulator architecture, not this Mac's. --build-machine remains
4684
4994
  independent and selects a compatible build worker for a Debug cache miss.
4685
4995
 
4996
+ The host's own owned devices count toward concurrency.maxDevices.
4997
+
4998
+ The host's person sees Hosted here rows in stim-server devices; --json adds
4999
+ hostedSessions beside devices. Rows show client, platform, device, installed
5000
+ app, state and since, newest first. Plain stopped sessions are omitted;
5001
+ parked and unknown sessions stay visible, with parked rows using the parking
5002
+ time. An unowned active session reads unknown on this server.
5003
+ Loopback device-host.sessions takes no params or {} and returns { sessions };
5004
+ rows carry id, client { id, name }, platform, device and app (nullable), state,
5005
+ parked, since (ISO), and workspace. device-host.sessions.stop takes { session }
5006
+ and returns { id, state: stopping | stopped }, stopping any client's session
5007
+ through the existing stop path. Both require an authenticated local Desktop
5008
+ control connection; read-only local connections, phones and tailnet clients
5009
+ get forbidden. An absent session id gets unknown-session; stopped is
5010
+ idempotent. The approved-client device-host.stop still stops only its own
5011
+ sessions.
5012
+
5013
+
4686
5014
  Stim builds or fetches before reserving, records the session immediately, then
4687
5015
  uploads the app on every run. Debug keeps Metro here; its supervisor owns a
4688
5016
  private gateway bound to this Mac's Tailscale address, pinned to the host peer
@@ -4703,10 +5031,12 @@ agent, state } per slot. A shutdown local simulator is replaced in status; a
4703
5031
  booted or unknown local simulator stays visible alongside ios.host with a warning.
4704
5032
  The default slot appears only in ios, never in slots[]. No host UDID or gateway secret enters local device state or
4705
5033
  status. Ready is normal; stopped has a rerun hint; unknown or unreachable is
4706
- unverified. A host server restart stops its sessions.
5034
+ unverified. A clean host server restart stops its sessions and retains eligible
5035
+ parked devices. A running session left by a crash stays unknown until explicit
5036
+ stop; the restarted server never re-attaches to a booted device.
4707
5037
 
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
5038
+ stop and worktree remove wait for the host to stop, park its eligible owned
5039
+ simulator within pool.iosParkedMax (delete otherwise), close the gateway and clear placement. stop --json
4710
5040
  reports each hosted slot under outcomes.device["ios:host:<slot>"], retaining
4711
5041
  local device outcomes and successful siblings when another slot fails. Revoked or
4712
5042
  missing sessions clear placement too. An unreachable host retains placement
@@ -4725,18 +5055,47 @@ ran on the hosting Mac's pinned node, it takes matching files from that build.
4725
5055
  A refused or timed-out handoff falls back to upload, retrying a still-busy host
4726
5056
  for at most one minute.
4727
5057
 
5058
+ HOSTED PARKING
5059
+
5060
+ The host reads pool.iosParkedMax and pool.androidParkedMax from its own home,
5061
+ with STIM_POOL_IOS_PARKED_MAX and STIM_POOL_ANDROID_PARKED_MAX overrides.
5062
+ Each platform has a separate hosted limit across all clients; 0 disables parking.
5063
+ With STIM_HOME set, parking requires the explicit platform environment override.
5064
+ Only healthy devices of still-approved clients are parked. The journal reports
5065
+ stopped to clients, and the device stays ledger-owned in its session's private
5066
+ home, outside the host's local parked pool. Parked devices persist across a clean
5067
+ stim-server restart.
5068
+
5069
+ A new reserve adopts the oldest compatible parked device of the same client.
5070
+ iOS matches resolved device type, runtime and architecture; Android matches
5071
+ system image, device profile and architecture. Adoption clears app data by
5072
+ removing every third-party app; iOS also resets privacy and keychain. Parking and
5073
+ adoption remove session app copies but keep the content-addressed upload store,
5074
+ so the first run after adoption uploads only changed files. Devices are never reused across clients.
5075
+ The oldest parked devices beyond the host's platform limit are deleted through
5076
+ owned-device teardown, also after a limit decrease. Adoption-time reconciliation
5077
+ retires missing, renamed, running or unowned candidates; failed retirement keeps
5078
+ the record. Revocation retires that client's parked devices.
5079
+ On the hosting Mac, unscoped \`stim gc\` lists parked hosted devices and
5080
+ \`stim gc --delete\` deletes them through their session homes under a session
5081
+ claim. Held claims and unverified ownership keep the device. Android deletion
5082
+ requires visibility from this shell's Android environment; otherwise run gc with
5083
+ the server's ANDROID_AVD_HOME/HOME. Records no longer listed in their session
5084
+ ledger are already removed and skipped. Reconciliation
5085
+ clears its parked marker after the session ledger becomes empty.
5086
+
4728
5087
  stim logs and stim logs --errors pull native device records from bounded host
4729
5088
  queries. Concurrent followers share a collection, throttled per session, without
4730
5089
  blocking app delivery, viewing or control. Stop limits each log drain to 30 seconds with
4731
5090
  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
5091
+ tail before parking or deletion, including on revocation or server close; stop copies it
4733
5092
  back afterwards. If the final collection drops a backlog interval and eventually
4734
5093
  succeeds, a device warning record names that interval. A damaged collection checkpoint is ignored and
4735
5094
  rebuilt; it never blocks reading collected records. The client waits up to
4736
5095
  180 seconds for stop. The final collection and worker termination paths fit within
4737
5096
  that wait; an in-flight handoff copy and closing Metro or view transports are
4738
5097
  outside those worker bounds.
4739
- Collected records remain in the session home after deletion;
5098
+ Collected records remain in the session home after parking or deletion;
4740
5099
  app blobs and materialized bundles are removed. Native queries read persisted
4741
5100
  entries, overlap by five seconds and de-duplicate; info-level or later-persisted
4742
5101
  entries may be unavailable. JavaScript logs arrive through Metro.
@@ -4811,10 +5170,10 @@ frames cannot arrive. Replay, duoFrame and physical targets are refused for
4811
5170
  hosted subscriptions. Desktop hides local Simulator.app, rotation, hardware
4812
5171
  buttons and simulator options; touch and text use the relay. The phone can also
4813
5172
  send the host's supported button, rotation and posture inputs.`
4814
- },
4815
- eas: {
4816
- summary: "download a matching EAS development build; explicit profile, costs, cache and miss remedies",
4817
- body: () => `EAS DEVELOPMENT BUILDS
5173
+ },
5174
+ eas: {
5175
+ summary: "download a matching EAS development build; explicit profile, costs, cache and miss remedies",
5176
+ body: () => `EAS DEVELOPMENT BUILDS
4818
5177
 
4819
5178
  stim ios --eas-profile ios-simulator
4820
5179
  stim android --eas-profile development
@@ -4890,10 +5249,10 @@ An eas-cli older than 18.9.0 cannot run build:download --build-id; Stim reads
4890
5249
  eas --version first and refuses with STIM_EAS_UNAVAILABLE and the upgrade
4891
5250
  command before any EAS lookup. stim doctor reports it in a project with eas.json.
4892
5251
  If another run holds the artifact claim, wait for it to finish and retry.`
4893
- },
4894
- readiness: {
4895
- summary: "implement optional pending/ready app logs, deadlines, errors, and platform isolation",
4896
- body: () => `OPTIONAL APP READINESS
5252
+ },
5253
+ readiness: {
5254
+ summary: "implement optional pending/ready app logs, deadlines, errors, and platform isolation",
5255
+ body: () => `OPTIONAL APP READINESS
4897
5256
 
4898
5257
  No package or SDK is needed. This applies to debug ios/android launches with
4899
5258
  Metro verification enabled, not release builds or --no-metro-check.
@@ -4953,10 +5312,10 @@ including when device logs are unavailable, the default check applies.
4953
5312
  The app declares readiness; Stim does not inspect a rendered frame. This does
4954
5313
  not change launched JSON semantics or the need to inspect the expected UI and
4955
5314
  stim logs --errors. A reload request does not run this launch check.`
4956
- },
4957
- verification: {
4958
- summary: "reproduce the affected behavior, verify the change on the reported device, and retain proof",
4959
- body: () => `VERIFY THE CHANGE
5315
+ },
5316
+ verification: {
5317
+ summary: "reproduce the affected behavior, verify the change on the reported device, and retain proof",
5318
+ body: () => `VERIFY THE CHANGE
4960
5319
 
4961
5320
  For a UI change or bug fix, decide what observable result would prove the task
4962
5321
  is complete. A successful build, a live process, or an empty log query does
@@ -4986,10 +5345,10 @@ not prove that result.
4986
5345
 
4987
5346
  For a change without a UI effect, use the relevant runtime output or test
4988
5347
  result as proof instead of requiring an unrelated screenshot.`
4989
- },
4990
- progress: {
4991
- summary: "phase lines, the label set, heartbeats and their ~ estimate, what warm, start, stop and remove print",
4992
- body: () => `PROGRESS ON A LONG RUN
5348
+ },
5349
+ progress: {
5350
+ summary: "phase lines, the label set, heartbeats and their ~ estimate, what warm, start, stop and remove print",
5351
+ body: () => `PROGRESS ON A LONG RUN
4993
5352
  Native build progress goes to stderr. In \`--json\` mode, stdout carries only
4994
5353
  the result payload. Plain \`start\` also prints progress on stdout. Every
4995
5354
  progress line has the same shape -- two spaces, a label padded to eleven
@@ -5137,10 +5496,10 @@ result as proof instead of requiring an unrelated screenshot.`
5137
5496
  until it returns; the next heartbeat then lands on the grid, which is why an
5138
5497
  elapsed value can jump. Read the phase lines, not the wall clock, before
5139
5498
  killing a run.`
5140
- },
5141
- pool: {
5142
- summary: "parked and adopted simulators and emulators: what park and adoption clear or keep, the model and runtime match, emulator storage, gc erase",
5143
- body: () => ` THE SIMULATOR POOL
5499
+ },
5500
+ pool: {
5501
+ summary: "parked and adopted simulators and emulators: what park and adoption clear or keep, the model and runtime match, emulator storage, gc erase",
5502
+ body: () => ` THE SIMULATOR POOL
5144
5503
  \`worktree remove\` PARKS this workspace's owned simulator instead of
5145
5504
  deleting it, and the next workspace that wants the same model and runtime
5146
5505
  ADOPTS it. Adoption reuses the simulator and the app installed on it, but
@@ -5312,10 +5671,10 @@ result as proof instead of requiring an unrelated screenshot.`
5312
5671
  reports their system image, hardware profile, age, recorded app and disk
5313
5672
  size.
5314
5673
  `
5315
- },
5316
- builds: {
5317
- summary: "optional cache warm-up, build optimizations, fingerprints, .fingerprintignore, install unchanged, running app restart, runtime state",
5318
- body: () => `OPTIONAL CACHE WARM-UP FOR REPEATED NATIVE WORK
5674
+ },
5675
+ builds: {
5676
+ summary: "optional cache warm-up, build optimizations, fingerprints, .fingerprintignore, install unchanged, running app restart, runtime state",
5677
+ body: () => `OPTIONAL CACHE WARM-UP FOR REPEATED NATIVE WORK
5319
5678
  When several native worktrees are coming, build the source checkout once to
5320
5679
  seed the shared caches before warming the linked worktrees. Skip this extra
5321
5680
  build for one-off or JavaScript-only work. For local simulator or emulator
@@ -5466,7 +5825,7 @@ with machine or project optimization settings; see \`guide settings\`:
5466
5825
  no org.gradle.caching=true in gradle.properties. Debug builds also
5467
5826
  carry -PreactNativeArchitectures=<target ABI>, using the owned
5468
5827
  emulator system-image ABI or the physical device's primary ABI.
5469
- Unknown targets and Release builds stay universal.
5828
+ Unknown targets and local Release builds stay universal.
5470
5829
  ccache the same gradlew run carries an absolute
5471
5830
  CMAKE_C_COMPILER_LAUNCHER / CMAKE_CXX_COMPILER_LAUNCHER plus
5472
5831
  CCACHE_DIR, CCACHE_BASEDIR, CCACHE_NOHASHDIR, CCACHE_SLOPPINESS
@@ -5721,10 +6080,10 @@ WHAT MAKES THE CACHE ACTUALLY HIT: .FINGERPRINTIGNORE
5721
6080
  mismatch naming the differing sources. Untracked, non-gitignored files under
5722
6081
  ios/ or android/ count too -- they are hashed like any other source, so a
5723
6082
  stray file there moves the key on your machine and nowhere else.`
5724
- },
5725
- concurrency: {
5726
- summary: "waiting on another workspace's build, --no-build-cache, concurrency.maxBuilds and maxDevices",
5727
- body: () => `ONE COMPILE PER FINGERPRINT, ACROSS EVERY WORKSPACE
6083
+ },
6084
+ concurrency: {
6085
+ summary: "waiting on another workspace's build, --no-build-cache, concurrency.maxBuilds and maxDevices",
6086
+ body: () => `ONE COMPILE PER FINGERPRINT, ACROSS EVERY WORKSPACE
5728
6087
  The cache makes the SECOND workspace on a commit free -- but only once the
5729
6088
  first has finished. Three agents starting within the same minute all miss it,
5730
6089
  and without this all three compile the same app at once, fighting for the
@@ -5790,12 +6149,50 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
5790
6149
  lock does, and a dead builder frees its slot within
5791
6150
  a poll (process identity, like the lock).
5792
6151
 
5793
- concurrency.maxDevices how many Stim-owned devices are BOOTED at once. Checked
5794
- at device time, before a sim is created or booted.
5795
- At the cap, a NEW device is REFUSED with
5796
- STIM_AT_CAPACITY (interactive-shaped: it does not
5797
- queue). A workspace whose own device is already
5798
- booted is never refused.
6152
+ concurrency.maxDevices how many Stim-owned devices are BOOTED at once,
6153
+ counting sims that are booting, emulators adb lists
6154
+ in any state, and devices other runs are booting.
6155
+ Admission and the boot claim are taken under one
6156
+ lock in $STIM_HOME. At the cap, a new owned device
6157
+ waits in FIFO order across this Stim home, for
6158
+ 600 seconds by default. \`--wait <seconds>\` changes
6159
+ the bound; \`--no-wait\` or \`--wait 0\` refuses at
6160
+ once with STIM_AT_CAPACITY and cannot jump queued
6161
+ runs. A workspace's own booted or booting device
6162
+ bypasses the queue. iOS waits beside its build.
6163
+ Waiting prints the count, holder workspace names
6164
+ and elapsed time at once and about every 10s, then
6165
+ the queue position, head workspace name and
6166
+ $STIM_HOME/device-waits path. Timeout names the
6167
+ same head and path. Stop cancels a queued run and
6168
+ releases its ticket without booting its device.
6169
+ Status JSON adds build.waitingFor independently
6170
+ of phase: { kind: "device-slot" | "build-slot",
6171
+ inUse, max, since }. Overlapping waits show the
6172
+ one that started first, then the remaining wait.
6173
+ Positive deviceSlotWaitMs is recorded beside
6174
+ slotWaitMs in compiling run placements; every
6175
+ wait also records a capacityWaits event in stats,
6176
+ with reclaimed when one or more devices were
6177
+ shut down for it.
6178
+ While at the cap, only the queue head reclaims:
6179
+ it shuts down the longest-idle eligible owned
6180
+ device in another workspace of this Stim home,
6181
+ one per attempt, at most once every 15 seconds,
6182
+ then rechecks queue and capacity.
6183
+ devices.reclaimIdleMinutes defaults to 10; 0 off.
6184
+ The waiter's effective setting applies.
6185
+ Idle checks are the same as supervisor
6186
+ idle shutdown below: no driver, lock, build,
6187
+ viewer or recent activity. Physical, hosted,
6188
+ remote, parked and other homes' devices, and
6189
+ the waiting workspace itself, are excluded.
6190
+ Shutdown never deletes. Reclaim failures are
6191
+ logged and skipped without failing the run.
6192
+ A listing failure leaves the count unknown and
6193
+ admission refuses with STIM_NO_DEVICE. Unresolved
6194
+ wait or boot claims refuse with the claim code
6195
+ and the command to remove that exact claim.
5799
6196
  See \`guide errors STIM_AT_CAPACITY\`.
5800
6197
 
5801
6198
  \`stim doctor\` prints one note echoing the caps and the current live count,
@@ -5803,10 +6200,10 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
5803
6200
  reports stale build locks, and \`gc --delete\` clears them. Set the caps
5804
6201
  with \`stim settings set concurrency.maxBuilds 2\`, by editing
5805
6202
  ~/.stim/config.json, or via the two env vars (see \`guide settings\`).`
5806
- },
5807
- budget: {
5808
- summary: "disk and memory budgets: what start, ios and android reclaim first, and STIM_LOW_DISK",
5809
- body: () => `DISK AND MEMORY BUDGETS (ON BY DEFAULT)
6203
+ },
6204
+ budget: {
6205
+ summary: "disk and memory budgets: what start, ios and android reclaim first, and STIM_LOW_DISK",
6206
+ body: () => `DISK AND MEMORY BUDGETS (ON BY DEFAULT)
5810
6207
  Before \`start\`, \`ios\` and \`android\` build or boot anything, they
5811
6208
  compare the machine against four MACHINE-level settings:
5812
6209
 
@@ -5847,8 +6244,8 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
5847
6244
  4. trim shared cache entries nothing has used for 14 days
5848
6245
  (\`gc --delete --older-than 14\` for the caches)
5849
6246
 
5850
- IDLE SHUTDOWN (OFF BY DEFAULT): with devices.idleShutdownMinutes set, a
5851
- workspace's supervisor shuts down that workspace's owned simulators and
6247
+ IDLE SHUTDOWN (30 MINUTES BY DEFAULT): with devices.idleShutdownMinutes above
6248
+ 0, a workspace's supervisor shuts down that workspace's owned simulators and
5852
6249
  emulators once they have been idle that long, without waiting for a run
5853
6250
  to go over budget. Idle is step 1 with that many minutes in place of 10:
5854
6251
  booted, no driver, no Stim or agent-device lock, no build in progress, and
@@ -5862,11 +6259,35 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
5862
6259
  status\` shows "shut down after 30m idle" on the device until the next
5863
6260
  \`ios\` or \`android\` run boots it again. When metro.idleStopMinutes is
5864
6261
  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\`.
6262
+ long, because no supervisor is left to check afterwards. The supervisor
6263
+ path does not check without a supervisor: release runs, and after
6264
+ \`stim stop\`. The setting is read when the supervisor starts. Stim Desktop's simulator view is not a
6265
+ stim-server client and does not count as a viewer. Change it with
6266
+ \`stim settings set devices.idleShutdownMinutes <minutes> --scope machine\`;
6267
+ 0 turns it off, and an explicit 0 stays 0.
6268
+
6269
+ QUEUE RECLAIM (10 MINUTES BY DEFAULT): a run waiting at concurrency.maxDevices
6270
+ uses devices.reclaimIdleMinutes from its own effective workspace settings.
6271
+ Only the FIFO head shuts down the longest-idle eligible device across this
6272
+ Stim home's other workspaces, one per attempt, at most once every 15 seconds,
6273
+ rechecking capacity before another. 0 disables it. The supervisor's own idle
6274
+ shutdown defaults to 30 minutes.
6275
+ Reclaim uses the same idle check: no driver, Stim or agent-device lock,
6276
+ build, device lock or viewer, and no Stim command, app or device log,
6277
+ Metro bundle or agent action within that interval. It rechecks under the
6278
+ target workspace's native-run lock and re-resolves ownership in centralized
6279
+ teardown. It shuts down, never deletes, and excludes the waiting workspace,
6280
+ physical, hosted, remote, parked and other homes' devices. Failed locks or
6281
+ teardown are reported once per device and skipped for the rest of the wait.
6282
+ Progress on stderr names the reclaimed
6283
+ device and workspace; the target's status and device_idle_shutdown log
6284
+ record "reclaimed for a waiting run", and the wait's stats carry reclaimed.
6285
+ Any workspace of this Stim home with a device idle for
6286
+ devices.reclaimIdleMinutes is eligible, even with no supervisor and even
6287
+ when its own devices.idleShutdownMinutes is 0. Stim Desktop's simulator view
6288
+ and manual input in Simulator.app do not count as activity.
6289
+ \`stim settings set devices.reclaimIdleMinutes 0 --scope machine\` opts out
6290
+ machine-wide; the waiter's effective setting decides.
5870
6291
 
5871
6292
  Steps 3 and 4 run only for disk. Memory and workspace limits never refuse;
5872
6293
  a run still over them prints one \`budget\` warning and continues. The
@@ -5881,13 +6302,18 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
5881
6302
  memory against the budget, and a finding with the reclaim plan when over it.
5882
6303
  Change a limit with \`stim settings set budget.minFreeDiskGb 30\`.
5883
6304
  See \`guide errors STIM_LOW_DISK\`.`
5884
- },
5885
- options: {
5886
- summary: "every flag per command, Android variants and flavors, the per-run simulator model, runtime and system image",
5887
- body: () => `THE OPTION SURFACE, IN FULL
6305
+ },
6306
+ options: {
6307
+ summary: "every flag per command, Android variants and flavors, the per-run simulator model, runtime and system image",
6308
+ body: () => `THE OPTION SURFACE, IN FULL
5888
6309
  start --json --wait <seconds> --remote --reset-cache
5889
6310
  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
6311
  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>
6312
+ ios/android --wait bounds owned-device slot waits (default 600s), or physical
6313
+ leases with --device (default 60s). Remote targets do not join this local
6314
+ slot queue. --no-wait refuses a full/queued owned
6315
+ slot at once; its existing physical lease bypass is unchanged.
6316
+
5891
6317
  reload [ios|android] --json
5892
6318
  device lock <ios|android> [id] --slot <name> --for <duration> --wait <seconds> --json;
5893
6319
  unlock [ios|android] --slot <name> --json
@@ -6140,9 +6566,9 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
6140
6566
  A named hosting Mac uses both selectors and its installed choices instead;
6141
6567
  an unavailable choice is STIM_HOSTING_REFUSED. See hosted-ios.
6142
6568
  \`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.
6569
+ STIM_BAD_ARG on eas/proxy runs (\`--remote\` or the android.remote
6570
+ setting), which ignore android.systemImage and android.deviceProfile.
6571
+ A named hosting Mac honors both selectors; see hosted-android.
6146
6572
 
6147
6573
  These flags describe a device that does not exist yet. When this workspace
6148
6574
  ALREADY owns a simulator and \`--device-type\` names a different model, or
@@ -6164,10 +6590,10 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
6164
6590
  The --json payload reports what was actually used: \`deviceType\` and
6165
6591
  \`runtime\` on iOS, \`systemImage\` and \`deviceProfile\` on Android, read
6166
6592
  from the device itself, so a settings-driven run reports them too.`
6167
- },
6168
- devices: {
6169
- summary: "ios --device and android --device on a phone: what the run skips, signing, the LAN wiring, the collector",
6170
- body: () => ` \`android --device [serial]\` installs and launches on a physical device
6593
+ },
6594
+ devices: {
6595
+ summary: "ios --device and android --device on a phone: what the run skips, signing, the LAN wiring, the collector",
6596
+ body: () => ` \`android --device [serial]\` installs and launches on a physical device
6171
6597
  connected to this machine instead of this workspace's owned emulator. With
6172
6598
  no serial it takes the first device it can lease (\`guide lifecycle lease\`).
6173
6599
  It cannot be combined with --remote.
@@ -6306,10 +6732,10 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
6306
6732
  by hashing the installed container; there is no cheap equivalent through
6307
6733
  devicectl, so a device run always installs. It is an upgrade install: the
6308
6734
  app's data, and the Local Network permission the phone granted it, survive.`
6309
- },
6310
- lease: {
6311
- summary: "run-scoped leases, --wait and --no-wait, device lock and unlock, which phone an id-less --device picks",
6312
- body: () => `THE DEVICE LEASE ON A \`--device\` RUN
6735
+ },
6736
+ lease: {
6737
+ summary: "run-scoped leases, --wait and --no-wait, device lock and unlock, which phone an id-less --device picks",
6738
+ body: () => `THE DEVICE LEASE ON A \`--device\` RUN
6313
6739
  A physical device is shared, so a \`--device\` run takes a lease on it. The
6314
6740
  lease step sits AFTER the build (a build touches no device) and before the
6315
6741
  install, and the run releases what it took when the command exits: on
@@ -6334,8 +6760,9 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
6334
6760
  holder's running app, a different one means the launch only backgrounds it,
6335
6761
  and when Stim cannot read the holder's app id it says so rather than
6336
6762
  guessing. A free device is leased as usual under \`--no-wait\`. The two flags
6337
- together are STIM_BAD_ARG, and so is either one without \`--device\`, because
6338
- an owned simulator or emulator has no contention.
6763
+ together are STIM_BAD_ARG. Without \`--device\`, they instead bound the
6764
+ FIFO owned-device slot wait (default 600s); \`--no-wait\` refuses at once
6765
+ when the cap is full or another run is queued. See \`guide lifecycle concurrency\`.
6339
6766
 
6340
6767
  A successful \`--device\` run reports \`lease: { kind, expiresAt }\` in its
6341
6768
  \`--json\`; a run that proceeded without one, or lost one after the install,
@@ -6411,10 +6838,10 @@ THE POOL: WHICH DEVICE AN ID-LESS \`--device\` PICKS
6411
6838
  The chosen device is on the phase line and in \`--json\` (\`udid\` or
6412
6839
  \`serial\`, plus \`deviceName\`), so an agent can hand the same id to its
6413
6840
  device tool.`
6414
- },
6415
- release: {
6416
- summary: "Release configurations and ...Release variants: Metro skipped, process-proven launch, the JS swap",
6417
- body: () => ` A VARIANT WHOSE NAME ENDS IN "Release" IS A RELEASE BUILD (\`release\`,
6841
+ },
6842
+ release: {
6843
+ summary: "Release configurations and ...Release variants: Metro skipped, process-proven launch, the JS swap",
6844
+ body: () => ` A VARIANT WHOSE NAME ENDS IN "Release" IS A RELEASE BUILD (\`release\`,
6418
6845
  \`productionRelease\`), and that is the whole opt-in -- there is no second
6419
6846
  flag. It is the Android half of \`ios --configuration Release\` and behaves
6420
6847
  the same way: AGP's bundle task embeds the JS, so Metro is skipped ENTIRELY
@@ -6499,10 +6926,10 @@ THE POOL: WHICH DEVICE AN ID-LESS \`--device\` PICKS
6499
6926
  builds the \`iphoneos\` slice for a cabled iPhone and keys its cache
6500
6927
  \`-device\` instead of \`-sim\`, but does not install it yet. Archives,
6501
6928
  \`.ipa\` export, store signing and distribution stay out of scope.`
6502
- },
6503
- simslim: {
6504
- summary: "recommended SimSlim profiles and recovery from host memory pressure",
6505
- body: () => `SIMSLIM FOR PARALLEL IOS WORK
6929
+ },
6930
+ simslim: {
6931
+ summary: "recommended SimSlim profiles and recovery from host memory pressure",
6932
+ body: () => `SIMSLIM FOR PARALLEL IOS WORK
6506
6933
  SimSlim is recommended as an optional way to reduce simulator background
6507
6934
  services and memory use, especially with several workspaces. Review which
6508
6935
  services your app and tests need; a slim profile can disable those features.
@@ -6579,12 +7006,14 @@ HOST MEMORY PRESSURE AND STALLED SIMULATORS
6579
7006
  Rebooting a simulator under the same pressure
6580
7007
  can repeat the stall. Consider fewer concurrent builds/devices (guide lifecycle
6581
7008
  concurrency) and a reviewed SimSlim profile for future runs.`
6582
- }
6583
7009
  }
6584
- },
6585
- cleanup: {
6586
- summary: "Where simulators come from, and how they get reclaimed",
6587
- preamble: () => `DEVICE SLOTS
7010
+ }
7011
+ };
7012
+ //#endregion
7013
+ //#region src/guide/cleanup.ts
7014
+ const cleanup = {
7015
+ summary: "Where simulators come from, and how they get reclaimed",
7016
+ preamble: () => `DEVICE SLOTS
6588
7017
 
6589
7018
  Cleanup enumerates every slot. stop --slot <name> keeps the shared server and
6590
7019
  other slots; plain stop and worktree remove handle the whole workspace.
@@ -6595,7 +7024,7 @@ versions cannot reliably manage their assignments.
6595
7024
 
6596
7025
  ARCHIVE
6597
7026
 
6598
- Linked-worktree removal and gc dead-project pruning keep history under
7027
+ Worktree removal and gc dead-project pruning keep history under
6599
7028
  $STIM_HOME/archive by default. See stim guide cleanup archive.
6600
7029
 
6601
7030
  MAINTENANCE
@@ -6711,20 +7140,27 @@ start the counters over. A file this version cannot read -- unparseable, or
6711
7140
  written by a newer Stim -- costs one dim line on stderr and is otherwise left
6712
7141
  alone; only the next \`ios\` or \`android\` run moves an unparseable one aside
6713
7142
  to stats.json.corrupt-<unix ms> and starts a new one.`,
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
7143
+ sections: {
7144
+ archive: {
7145
+ summary: "retained workspace history and explicit archive cleanup",
7146
+ body: () => `ARCHIVE
7147
+ Worktree removal, including the environment reclaim of a source checkout, and
7148
+ gc pruning a registered root that is gone keep
6719
7149
  state, ended agents, the error index, logs, closed recording segments and
6720
7150
  workspace-local agent-device sessions under $STIM_HOME/archive.
6721
7151
  Build outputs and open recording segments are deleted with the workspace.
6722
7152
  Archives have no checkout, devices, ports or running processes. A recreated
6723
7153
  path starts fresh; status links its earlier archives with replacedBy.
6724
7154
  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.
7155
+ workspace for server log queries and replay.
7156
+ archive.detail takes { archive: id } and returns { builds, recordings }:
7157
+ live-shaped iOS/Android build history and { platform, slot, spans } for every
7158
+ retained recording slot with spans. Missing state gives builds {}; no footage
7159
+ gives recordings []. status keeps archive build summaries.
7160
+ Older servers refuse archive.detail with unknown-method.
7161
+ Archived log subscriptions send retained records, then logs-ended.
7162
+ Archived frame subscriptions need at and cannot be physical or go live.
7163
+ Replay ranges report recording disabled.
6728
7164
  Reads stay on this Mac.
6729
7165
  An updated server is required; older servers refuse with bad-request when
6730
7166
  workspace is omitted. There is no CLI archive log flag.
@@ -6758,10 +7194,23 @@ to stats.json.corrupt-<unix ms> and starts a new one.`,
6758
7194
  Directories are 0700 and files 0600. No redaction is applied; logs and agent
6759
7195
  actions can contain secrets. Use archive.enabled false for a sensitive repo.
6760
7196
  Archived logs and replay are not exposed through CLI or app readers yet.`
6761
- },
6762
- gc: {
6763
- summary: "what gc and worktree remove delete, keep and refuse: orphans, stale records, locks, leases, EAS sessions",
6764
- body: () => `AGENT-DEVICE (REPORT ONLY)
7197
+ },
7198
+ gc: {
7199
+ summary: "what gc and worktree remove delete, keep and refuse: orphans, stale records, locks, leases, EAS sessions",
7200
+ body: () => `PARKED HOSTED DEVICES
7201
+ On a hosting Mac, unscoped gc lists parked hosted iOS/Android devices by session,
7202
+ client id, device label, parked time and private-home ledger ownership.
7203
+ gc --delete takes each session claim and deletes through the packaged stop worker
7204
+ in that session's home; held claims, changed sessions and unverified ownership
7205
+ keep the device. Android deletion also requires the AVD to be visible from this
7206
+ shell; otherwise run gc with the server's ANDROID_AVD_HOME/HOME. Devices no longer
7207
+ listed in their session ledger are already removed and skipped. An unreadable
7208
+ journal keeps every hosted device. --older-than
7209
+ filters by parked time; --cache scopes omit hosted sessions. The CLI never writes
7210
+ the server journal: stim-server reconciliation clears the parked marker after
7211
+ successful deletion leaves the session ledger empty.
7212
+
7213
+ AGENT-DEVICE (REPORT ONLY)
6765
7214
  Unscoped gc reports agent-device runner builds by platform and entry, last use,
6766
7215
  agent-device and Xcode versions, sessions, logs, other state, workspace state
6767
7216
  and hosted driver state. Stim never selects this state as a cache.
@@ -6806,7 +7255,9 @@ IN USE
6806
7255
  that names no workspace blocks nothing, because every build also holds its
6807
7256
  own workspace's native-run.lock; gc lists it with the command that removes
6808
7257
  it. \`worktree remove\` re-checks uncommitted and unpushed work under its
6809
- removal locks, just before it reclaims anything. A project root whose
7258
+ removal locks, just before it reclaims anything. It refuses, even with
7259
+ --force, a worktree that an installed stim-server service runs from
7260
+ (STIM_WORKTREE_SERVICE; see guide errors). A project root whose
6810
7261
  existence cannot be read (a permission error) is never treated as deleted.
6811
7262
 
6812
7263
  SWEEPING FINISHED WORKTREES
@@ -6955,10 +7406,18 @@ ON THE SOURCE CHECKOUT
6955
7406
  (including nested monorepo app dirs) dropped, and the global workspace
6956
7407
  directory deleted. The tree itself is never touched, which is also why the
6957
7408
  dirty-tree and unpushed guards do not apply on that path.
7409
+ Run from the checkout root it reclaims every registered project under the
7410
+ checkout. Run from a subfolder (for example apps/mobile) it reclaims only
7411
+ the nearest registered project at or above that folder and the projects nested
7412
+ under it, leaves the other
7413
+ projects' devices, ports and records alone, and refuses with exit 1 when no
7414
+ project is registered at or above it.
6958
7415
  It ends with:
6959
7416
  Reclaimed the environment; the working tree stays (it is the source checkout).
6960
7417
  A registered project directory that is not a git repo at all gets the same
6961
7418
  environment reclaim -- there is nothing else remove could mean there.
7419
+ Either reclaim archives each workspace it removes, as removal of a linked
7420
+ worktree does.
6962
7421
 
6963
7422
  The delete paths and \`stop\` do not check simulator occupancy. An explicit
6964
7423
  \`stim stop\` shuts down this workspace's Stim-owned simulator, including a
@@ -7074,10 +7533,10 @@ THE ONE CASE GC WILL NOT REAP
7074
7533
  it found, so you can judge. Delete them yourself:
7075
7534
  xcrun simctl delete <udid>
7076
7535
  avdmanager delete avd -n <name>`
7077
- },
7078
- collector: {
7079
- summary: "log collector reaping: an unproven collector pid, and why the app on a phone closed",
7080
- body: () => `WHAT ELSE STOP REAPS
7536
+ },
7537
+ collector: {
7538
+ summary: "log collector reaping: an unproven collector pid, and why the app on a phone closed",
7539
+ body: () => `WHAT ELSE STOP REAPS
7081
7540
  The device-log collectors (\`simctl log stream\` / \`adb logcat\`) that
7082
7541
  \`ios\` / \`android\` attach after launch. They are recorded in
7083
7542
  the global workspace state.json, and nothing outside this workspace can name them,
@@ -7131,10 +7590,10 @@ THE ONE CASE GC WILL NOT REAP
7131
7590
  is gone, and the unrelated process is never signalled. A missing, malformed,
7132
7591
  or unreadable identity leaves the record unverified and kept for a retry.
7133
7592
  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
7593
+ },
7594
+ memory: {
7595
+ summary: "watchman and Gradle and Kotlin daemon memory in gc, and when gc --delete --cache watchman or gradle-daemons stops them",
7596
+ body: () => `MEMORY
7138
7597
  Every \`gc\` without --cache, and \`gc --cache watchman\` or
7139
7598
  \`gc --cache gradle-daemons\`, reports the long-lived helpers that grow while
7140
7599
  they run: the shared watchman daemon, Gradle daemons and Kotlin compile
@@ -7187,10 +7646,10 @@ THE ONE CASE GC WILL NOT REAP
7187
7646
  --older-than does not apply to these kinds and is refused with
7188
7647
  STIM_BAD_ARG. With STIM_HOME set, gc skips them: they are machine-global.
7189
7648
  \`stim doctor\` notes a watchman footprint over 2 GiB.`
7190
- },
7191
- disk: {
7192
- summary: "disk usage, workspace build outputs, logs and device recordings, AVD and build-log sizes, the data partition, trimming the shared caches",
7193
- body: () => `DISK
7649
+ },
7650
+ disk: {
7651
+ summary: "disk usage, workspace build outputs, logs and device recordings, AVD and build-log sizes, the data partition, trimming the shared caches",
7652
+ body: () => `DISK
7194
7653
  Logs, state, pidfiles and Xcode DerivedData are under the global workspace
7195
7654
  directory, and \`worktree remove\` reclaims them. \`gc --delete\` clears the
7196
7655
  build outputs of workspaces nobody is using (WORKSPACE BUILD OUTPUTS
@@ -7331,12 +7790,14 @@ SHARED BUILD CACHES
7331
7790
 
7332
7791
  Trim rather than empty. Emptying costs the next build in every project the
7333
7792
  time the cache was saving.`
7334
- }
7335
7793
  }
7336
- },
7337
- settings: {
7338
- summary: "Settings Stim reads, and where they can live",
7339
- body: () => `SETTINGS
7794
+ }
7795
+ };
7796
+ //#endregion
7797
+ //#region src/guide/settings.ts
7798
+ var settings_default = {
7799
+ summary: "Settings Stim reads, and where they can live",
7800
+ body: () => `SETTINGS
7340
7801
 
7341
7802
  Settings are JSON: machine layers in ~/.stim/config.json and a committed
7342
7803
  .stim.json. Command-line selectors override their matching settings.
@@ -7379,8 +7840,8 @@ Resolution order, first match wins:
7379
7840
  3. committed .stim.json beside the app's package.json or Package.swift
7380
7841
  4. machine ~/.stim/config.json, top-level optimizations,
7381
7842
  ios.deviceType, ios.runtime, android.systemImage,
7382
- android.deviceProfile and devices.idleShutdownMinutes
7383
- and archive.enabled
7843
+ android.deviceProfile, devices.idleShutdownMinutes,
7844
+ devices.reclaimIdleMinutes and archive.enabled
7384
7845
  5. Stim default
7385
7846
  An environment override, where a setting has one, wins over every layer.
7386
7847
 
@@ -7440,8 +7901,8 @@ KEYS STIM READS
7440
7901
  flag overrides this per invocation. Unset means Debug.
7441
7902
  ios.remote "proxy", "eas", or a named approved Mac from
7442
7903
  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.
7904
+ "auto" places on an approved Mac when this Mac is full or
7905
+ busy. Unset runs here. See lifecycle hosted-ios.
7445
7906
  ios.simslimProfile a SimSlim JSON profile under the app directory,
7446
7907
  at most 64 KiB. Install the
7447
7908
  external tool once with
@@ -7581,7 +8042,10 @@ ${ANDROID_AVD_CONFIG_HELP.map((line) => ` ${line}`).joi
7581
8042
  committed .stim.json avoids carrying a secret; a
7582
8043
  bare string is used as the literal password. Unset
7583
8044
  means the debug keystore's fixed "android".
7584
- android.remote "proxy" or "eas"; the Android half of ios.remote
8045
+ android.remote "proxy", "eas", or a named approved Mac in
8046
+ hosting.machines; "auto" places on an approved Mac when
8047
+ this Mac is full or busy. Unset runs here.
8048
+ See lifecycle hosted-android.
7585
8049
  metro.tunnel selects how a remote device reaches this workspace's
7586
8050
  Metro after remote intent exists. Plain \`start\` stays
7587
8051
  local. For Expo and bare React Native, "auto" (default)
@@ -7652,9 +8116,17 @@ ${ANDROID_AVD_CONFIG_HELP.map((line) => ` ${line}`).joi
7652
8116
  minutes an owned simulator or emulator stays idle
7653
8117
  (\`gc --idle\` conditions; an open stim-server viewer
7654
8118
  counts as activity) before its workspace's supervisor
7655
- shuts it down, never deletes it. Default 0, never.
8119
+ shuts it down, never deletes it. Default 30; 0 never.
7656
8120
  Machine or project layers. Read when the supervisor
7657
8121
  starts; see \`guide lifecycle budget\`.
8122
+ devices.reclaimIdleMinutes
8123
+ minutes idle before the head of the device slot queue
8124
+ shuts down the longest-idle eligible owned device in
8125
+ another workspace of this Stim home. Default 10; 0 off.
8126
+ Machine or project layers, resolved for the waiting
8127
+ workspace on each poll. Same idle checks as supervisor
8128
+ shutdown, one device per poll, never deleted. See
8129
+ \`guide lifecycle concurrency\`.
7658
8130
  macos.product the explicit Swift Package executable product built in
7659
8131
  Debug by stim macos
7660
8132
  macos.infoPlist development Info.plist relative to Package.swift,
@@ -7722,7 +8194,8 @@ ${ANDROID_AVD_CONFIG_HELP.map((line) => ` ${line}`).joi
7722
8194
  them from the environment or the machine layers.
7723
8195
 
7724
8196
  Each setting takes its documented type: string, array of strings, number,
7725
- boolean, or object. android.remote, metro.tunnel, web.viewport,
8197
+ boolean, or object. ios.remote and android.remote accept backend names or
8198
+ tailnet machine names. metro.tunnel, web.viewport,
7726
8199
  optimizations.android.compilerCache and optimizations.android.pch take only
7727
8200
  their listed choices. A value of the wrong
7728
8201
  type or outside those choices is refused by name on every command that resolves
@@ -7878,6 +8351,13 @@ platform bounds. Android adoption matches the system image and AVD creation
7878
8351
  settings, preserves the APK, and clears app data before launch. See
7879
8352
  \`guide lifecycle pool\` for cleanup and the system state that remains.
7880
8353
 
8354
+ The host also uses these bounds for hosted iOS and Android parking on stop,
8355
+ separately from its local pool and across all hosted clients per platform.
8356
+ It adopts only a compatible device of the same client, clears app data and
8357
+ removes other apps. Parked devices persist across a clean stim-server restart;
8358
+ eviction, adoption-time reconciliation and revocation delete them through
8359
+ owned-device teardown. See guide lifecycle hosted-ios.
8360
+
7881
8361
  When STIM_HOME is set, parking and adoption are OFF unless
7882
8362
  the corresponding STIM_POOL_IOS_PARKED_MAX or STIM_POOL_ANDROID_PARKED_MAX
7883
8363
  is set too. A redirected home is a scoped config --
@@ -7896,6 +8376,15 @@ ticket; matching hashes and approved credentials are preserved.
7896
8376
  Approved doctor entries under buildMachines and deviceHosts include host
7897
8377
  { name, screenRecording, accessibility } when the worker reports it.
7898
8378
 
8379
+ BUILD MACHINES AND PHONE APP
8380
+ When the user mentions slow builds, a busy Mac, a full disk or device limit, or
8381
+ wanting updates away from the desk, tell them about Stim Desktop > Settings >
8382
+ Build machines > Add (a Mac on the tailnet that builds or hosts simulators for
8383
+ this one) or Phones > Pair a Phone (the Stim phone app). Stim Desktop also
8384
+ suggests these itself, at most once a day and never during a build. Agents only
8385
+ tell the user: they never run \`stim-server setup\`, never edit \`offload.*\` or
8386
+ \`hosting.*\` for the user, and never approve requests.
8387
+
7899
8388
  HOSTING MACHINES ARE MACHINE-LEVEL
7900
8389
  \`hosting.machines\` names Macs that may host owned simulator sessions,
7901
8390
  by MagicDNS name with an optional serve port (default 7443). Name each node
@@ -7915,9 +8404,22 @@ unless every chosen capability already has a matching approval. Desktop reuse
7915
8404
  requires an existing tailnet route. Ctrl-C or SIGTERM completes the journal,
7916
8405
  releases the setup claim and exits 1; a typed N also exits 1. Hosting grants
7917
8406
  include no read, control or build capability.
8407
+ On the hosting Mac, Stim Desktop > Settings > Phones > Hosted here lists the
8408
+ simulators, emulators and apps approved Macs run there, below Device hosting
8409
+ approvals. Stop asks for confirmation, ends the session and deletes or parks
8410
+ its device on that Mac. Parked sessions remain listed without Stop. The list
8411
+ refreshes every five seconds and stays hidden when the local server does not
8412
+ support it.
7918
8413
  $STIM_HOME/device-host-machines.json stores a private token and pinned tailnet
7919
8414
  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,
8415
+ in JSON. Desktop uses placement set by config or agents, such as
8416
+ stim settings set ios.remote auto --scope workspace (or android.remote),
8417
+ or per run stim ios --remote auto / stim ios --remote <machine>.
8418
+ New Desktop runs pass no --remote flag; recorded hosted sessions keep their
8419
+ machine until stim stop. Only devices outside this Mac show "on <machine>"
8420
+ on tiles, workspace pages, viewer toolbars and sidebar rows, with the reason
8421
+ as hover text.
8422
+ Stim sends tokens only to the pinned node's own tailnet address,
7921
8423
  with its MagicDNS name for TLS and Host routing. A changed node refuses
7922
8424
  access, and uncertain replies or unreadable credentials preserve the pin.
7923
8425
  Invalid hosting settings report an error and preserve every saved credential.
@@ -7933,14 +8435,16 @@ Access (Accessibility on macOS 26 and earlier) for Stim Host, the app
7933
8435
  \`stim-server service install\` runs the server under.
7934
8436
 
7935
8437
  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.
8438
+ client's coding agent can drive the macOS apps, iOS simulators and Android emulators it hosts for that client.
7937
8439
  The default, \`none\`, starts nothing. For macOS, \`agent-device\` starts its
7938
8440
  shared daemon only when that agent-device can lease a single app (its
7939
8441
  \`macos-app\` lease backend); otherwise agent control reports \`none\` with a
7940
8442
  notice, and no client is handed the Mac's desktop. STIM_AGENT_DEVICE_BIN in
7941
8443
  stim-server's environment names an agent-device binary to use instead of
7942
8444
  ~/.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
8445
+ its UDID policy; both Macs need agent-device 0.21.20 or later. Hosted Android
8446
+ uses one daemon per emulator, pinned by its serial policy, with agent-device
8447
+ 0.21.22 or later on both Macs. See guide lifecycle
7944
8448
  hosted-ios. \`doctor\` on that Mac notes an installed hosted app that runs while
7945
8449
  the setting is \`none\`.
7946
8450
 
@@ -8019,7 +8523,8 @@ emulator debug build or a stim macos SwiftPM Debug build compiles:
8019
8523
  Load per core is the 5-minute load average divided by the CPU count; a Mac's
8020
8524
  native builds are its Stim runs in prebuild, pods or compile on that Mac, not
8021
8525
  the ones it offloaded.
8022
- \`offload.maxLoadPerCore\` (default 2) is the load per core at which a Mac
8526
+ \`offload.maxLoadPerCore\` (default 2) is also the busy threshold for
8527
+ automatic iOS and Android device placement. It is the load per core at which a Mac
8023
8528
  counts as saturated, both here and on a build machine.
8024
8529
 
8025
8530
  STIM_OFFLOAD_MODE overrides it for one command. Device, Release and
@@ -8170,7 +8675,7 @@ be setup steps are supplied by Stim on the command lines it composes itself:
8170
8675
  gradle.properties. Debug builds add
8171
8676
  -PreactNativeArchitectures=<target ABI> when the owned
8172
8677
  emulator system image or physical device proves the ABI;
8173
- unknown targets and Release builds stay universal. The same run
8678
+ unknown targets and local Release builds stay universal. The same run
8174
8679
  carries the ccache launcher and CCACHE_BASEDIR /
8175
8680
  CCACHE_NOHASHDIR when ccache is on PATH -- so no
8176
8681
  externalNativeBuild cmake arguments in a committed
@@ -8284,8 +8789,8 @@ The example shows the defaults. Full setting names and behavior:
8284
8789
  optimizations.android.gradleBuildCache
8285
8790
  false passes --no-build-cache to Gradle, overriding org.gradle.caching=true.
8286
8791
  optimizations.android.targetAbiOnly
8287
- false stops narrowing Debug builds to the device ABI. Release is always
8288
- universal; project ABI filters still apply.
8792
+ false stops narrowing Debug and hosted Android builds to the device ABI.
8793
+ Local Release builds are universal; project ABI filters still apply.
8289
8794
 
8290
8795
  Android CAS, explicit PCH modes, and changed iOS compiler options use separate
8291
8796
  native artifact keys. Android ccache and none share an artifact key when their
@@ -8383,10 +8888,12 @@ platform instead of one entry. Pass prune: 'atomic' for a cache whose index
8383
8888
  references its own data (an LLVM CAS): it is then left alone by --older-than
8384
8889
  and emptied whole only by 'gc --delete --cache all'.
8385
8890
  Registration is idempotent and keyed on the directory.`
8386
- },
8387
- web: {
8388
- summary: "stim web: an owned headless Chrome per workspace, its logs, launched, and teardown",
8389
- body: () => `WEB: AN OWNED CHROME PER WORKSPACE
8891
+ };
8892
+ //#endregion
8893
+ //#region src/guide/web.ts
8894
+ var web_default = {
8895
+ summary: "stim web: an owned headless Chrome per workspace, its logs, launched, and teardown",
8896
+ body: () => `WEB: AN OWNED CHROME PER WORKSPACE
8390
8897
 
8391
8898
  If Stim is not installed globally, replace stim with npx stim.
8392
8899
 
@@ -8604,10 +9111,12 @@ LIMITS
8604
9111
 
8605
9112
  Chrome and Chromium only. The owned page is one tab: a page the app opens in
8606
9113
  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
9114
+ };
9115
+ //#endregion
9116
+ //#region src/guide/macos.ts
9117
+ var macos_default = {
9118
+ summary: "Swift Package macOS Debug apps: owned bundle, logs, local window viewing, and --remote on another Mac",
9119
+ body: () => `MACOS: SWIFT PACKAGE DEVELOPMENT PROTOTYPE
8611
9120
 
8612
9121
  If Stim is not installed globally, replace stim with npx stim.
8613
9122
 
@@ -8700,6 +9209,33 @@ carries buildMachine (selected) and builtOn (here or the machine, absent until
8700
9209
  a build runs), and errorCode on a typed failure. It carries offloadedTo only for a remote build, and offloadFallback when an offload
8701
9210
  attempt falls back here. Placement and failure reasons are in build logs.
8702
9211
 
9212
+ AGENT ACTIONS
9213
+
9214
+ Launch through stim macos first. Use its isolated bundleId from the JSON
9215
+ launch record or status --json:
9216
+
9217
+ agent-device open <bundleId> --platform macos --surface app --foreground
9218
+ agent-device click <ref> --settle
9219
+ stim logs --source agent
9220
+
9221
+ Stim reads agent-device's recorded app-scoped actions and failures into the
9222
+ native workspace's agent feed in Desktop and the phone viewer. Text stays
9223
+ redacted by agent-device. Matching uses the workspace-specific bundle ID and
9224
+ the current launch's start time; open/close attempts, app switches and event
9225
+ log rotation clear the binding. Open must explicitly include --surface app:
9226
+ agent-device can otherwise inherit a previous desktop or menubar surface.
9227
+ An unrecorded open needs a new recorded app open before actions appear.
9228
+ Native screenshot events are omitted because their surface override is not
9229
+ recorded. Generic computer-use tools and phone Control are not agent-device
9230
+ actions. Manually duplicated bundles sharing the isolated identifier are
9231
+ unsupported; leave app launch and stop to Stim. No native replay is added.
9232
+ The feed requires agent-device to record the explicit surface in its open
9233
+ event. agent-device 0.21.23 is the minimum; older versions such as 0.21.12 omit
9234
+ it, so native actions remain unavailable with them. Stim does not infer the
9235
+ surface from the bundle ID. Actions are attributed only while the launch is
9236
+ recorded as running: after the app exits or is relaunched, earlier launches'
9237
+ actions are no longer returned, and a failed open is not shown.
9238
+
8703
9239
  OWNERSHIP AND LOCAL VIEWING
8704
9240
 
8705
9241
  The supervisor owns a process-identity claim with the app as its child. Stop
@@ -8939,6 +9475,8 @@ script such as mini-desktop.sh use these instead:
8939
9475
  3 concurrent slots concurrency.maxDevices on the host; one bundle id
8940
9476
  slot per session
8941
9477
 
9478
+ The host's own owned devices count toward concurrency.maxDevices.
9479
+
8942
9480
  The agent-device rows work once the host's agent field names agent-device.
8943
9481
 
8944
9482
  DESKTOP DOGFOOD
@@ -8965,6 +9503,565 @@ development Info.plist for stim macos. Build and show its owned window in Stim
8965
9503
  Desktop, verify a source edit and readable failed-build logs, then stop only
8966
9504
  this workspace's app. Do not change permissions or use custom build scripts."
8967
9505
  `
9506
+ };
9507
+ //#endregion
9508
+ //#region src/guide/tutorial-data.ts
9509
+ const TUTORIAL_PINS = {
9510
+ createExpoApp: "5.0.0",
9511
+ template: "expo-template-blank@58.0.15"
9512
+ };
9513
+ const TUTORIAL_FILES = {
9514
+ "App.js": `import { useEffect, useState } from 'react';
9515
+ import { Pressable, StyleSheet, Text, View } from 'react-native';
9516
+ import { StatusBar } from 'expo-status-bar';
9517
+ import { TITLE_COLOR } from './theme';
9518
+
9519
+ const TAG = '[stim:tutorial]';
9520
+
9521
+ function Button({ label, onPress }) {
9522
+ return (
9523
+ <Pressable accessibilityRole="button" onPress={onPress} style={styles.button}>
9524
+ <Text style={styles.buttonText}>{label}</Text>
9525
+ </Pressable>
9526
+ );
9527
+ }
9528
+
9529
+ export default function App() {
9530
+ const [note, setNote] = useState('');
9531
+
9532
+ useEffect(() => {
9533
+ console.log(\`\${TAG} title color=\${TITLE_COLOR}\`);
9534
+ }, [TITLE_COLOR]);
9535
+
9536
+ const logError = () => {
9537
+ console.error(\`\${TAG} error-button test error\`);
9538
+ setNote('Logged an error.');
9539
+ };
9540
+
9541
+ const crash = () => {
9542
+ setTimeout(() => {
9543
+ throw new Error(\`\${TAG} crash-button uncaught test error\`);
9544
+ }, 0);
9545
+ };
9546
+
9547
+ const slowRequest = async () => {
9548
+ const started = Date.now();
9549
+ setNote('Waiting 3 seconds...');
9550
+ await new Promise((resolve) => setTimeout(resolve, 3000));
9551
+ const elapsed = Date.now() - started;
9552
+ console.warn(\`\${TAG} slow-request \${elapsed}ms\`);
9553
+ setNote(\`Slow request took \${elapsed}ms.\`);
9554
+ };
9555
+
9556
+ return (
9557
+ <View style={styles.container}>
9558
+ <Text style={[styles.title, { color: TITLE_COLOR }]}>Stim Tutorial</Text>
9559
+ <Text style={styles.body}>Tap a button, then look at Stim Desktop &gt; Logs.</Text>
9560
+ <Button label="Log an error" onPress={logError} />
9561
+ <Button label="Crash me" onPress={crash} />
9562
+ <Button label="Slow request" onPress={slowRequest} />
9563
+ <Text style={styles.note}>{note}</Text>
9564
+ <StatusBar style="auto" />
9565
+ </View>
9566
+ );
9567
+ }
9568
+
9569
+ const styles = StyleSheet.create({
9570
+ container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', padding: 24 },
9571
+ title: { fontSize: 32, fontWeight: '700', marginBottom: 8 },
9572
+ body: { fontSize: 16, color: '#4b5563', textAlign: 'center', marginBottom: 24 },
9573
+ button: { backgroundColor: '#111827', borderRadius: 10, paddingVertical: 14, paddingHorizontal: 28, marginBottom: 12 },
9574
+ buttonText: { color: '#fff', fontSize: 17, fontWeight: '600' },
9575
+ note: { marginTop: 12, fontSize: 15, color: '#374151' },
9576
+ });
9577
+ `,
9578
+ "theme.js": `export const TITLE_COLOR = '#1f2937';
9579
+ `,
9580
+ "app.json": `{
9581
+ "expo": {
9582
+ "name": "Stim Tutorial",
9583
+ "slug": "stim-tutorial",
9584
+ "version": "1.0.0",
9585
+ "orientation": "portrait",
9586
+ "icon": "./assets/icon.png",
9587
+ "userInterfaceStyle": "light",
9588
+ "ios": {
9589
+ "supportsTablet": true,
9590
+ "bundleIdentifier": "dev.stim.tutorial"
9591
+ },
9592
+ "android": {
9593
+ "adaptiveIcon": {
9594
+ "backgroundColor": "#E6F4FE",
9595
+ "foregroundImage": "./assets/android-icon-foreground.png",
9596
+ "backgroundImage": "./assets/android-icon-background.png",
9597
+ "monochromeImage": "./assets/android-icon-monochrome.png"
9598
+ },
9599
+ "package": "dev.stim.tutorial"
9600
+ },
9601
+ "web": {
9602
+ "favicon": "./assets/favicon.png"
9603
+ },
9604
+ "extra": {
9605
+ "stimTutorial": 1
9606
+ }
9607
+ }
9608
+ }
9609
+ `,
9610
+ ".gitignore": `/ios
9611
+ /android
9612
+ /tutorial*.ad
9613
+ /tutorial*.png
9614
+ `
9615
+ };
9616
+ const TUTORIAL_PROMPTS = {
9617
+ begin: "Run the Stim tutorial.",
9618
+ rebuild: "Continue the Stim tutorial: rebuild",
9619
+ agent: "Continue the Stim tutorial: agent",
9620
+ refresh: "Continue the Stim tutorial: refresh",
9621
+ machine: "Continue the Stim tutorial: machine",
9622
+ finish: "Continue the Stim tutorial: finish"
9623
+ };
9624
+ const TUTORIAL_RESTART_PROMPT = "Restart the Stim tutorial.";
9625
+ const TUTORIAL_STEPS = [
9626
+ {
9627
+ id: "begin",
9628
+ title: "Create the tutorial",
9629
+ who: "agent",
9630
+ optional: false,
9631
+ prompt: TUTORIAL_PROMPTS.begin,
9632
+ section: "run",
9633
+ manual: [
9634
+ "base=\"{base}\"",
9635
+ "mkdir -p \"${base%/*}\"",
9636
+ "if [ ! -e \"$base\" ]; then",
9637
+ "cd \"${base%/*}\"",
9638
+ "if git -C \"${base%/*}\" rev-parse --is-inside-work-tree >/dev/null 2>&1; then echo \"Stop: inside another repository. Ask for another folder; never git add in the user repository.\"; exit 1; fi",
9639
+ `npx --yes create-expo-app@${TUTORIAL_PINS.createExpoApp} stim-tutorial --template ${TUTORIAL_PINS.template} --no-install --no-agents-md --yes`,
9640
+ "cd \"$base\"",
9641
+ "stim guide tutorial app",
9642
+ ...Object.entries(TUTORIAL_FILES).flatMap(([name, content]) => [`cat ${name === ".gitignore" ? ">>" : ">"} ${name} <<'STIM_TUTORIAL_EOF'`].concat(content.trimEnd().split("\n"), "STIM_TUTORIAL_EOF")),
9643
+ "npm install --prefer-offline",
9644
+ "npm pkg set scripts.ios=\"expo run:ios\" scripts.android=\"expo run:android\"",
9645
+ "git init",
9646
+ "git add -A",
9647
+ "git -c user.name=Stim -c user.email=stim@localhost -c commit.gpgsign=false commit -m \"Stim tutorial\"",
9648
+ "else",
9649
+ "cd \"$base\"",
9650
+ `node -e 'const fs = require("node:fs"); const app = fs.existsSync("app.json") ? JSON.parse(fs.readFileSync("app.json", "utf8")) : null; if (app?.expo?.extra?.stimTutorial !== ${JSON.parse(TUTORIAL_FILES["app.json"]).expo.extra.stimTutorial}) { console.error("Stop: existing non-tutorial folder or another version. Ask for another folder; never overwrite or delete it."); process.exit(1); }'`,
9651
+ `git rev-parse --show-toplevel | node -e 'const fs = require("node:fs"); const root = fs.readFileSync(0, "utf8").trim(); if (fs.realpathSync(root) !== fs.realpathSync(process.cwd())) { console.error("Stop: inside another repository. Ask for another folder; never git add in the user repository."); process.exit(1); }'`,
9652
+ "fi"
9653
+ ]
9654
+ },
9655
+ {
9656
+ id: "sidebar",
9657
+ title: "Workspace in sidebar",
9658
+ who: "you",
9659
+ optional: false,
9660
+ prompt: null,
9661
+ section: null,
9662
+ manual: []
9663
+ },
9664
+ {
9665
+ id: "build",
9666
+ title: "First iOS build",
9667
+ who: "you",
9668
+ optional: false,
9669
+ prompt: null,
9670
+ section: null,
9671
+ manual: [
9672
+ "cd \"{base}\"",
9673
+ "git worktree add -B stim-tutorial/tour \"{tour}\" HEAD",
9674
+ "cd \"{tour}\"",
9675
+ "stim worktree warm",
9676
+ "stim guide agent",
9677
+ "stim doctor --platform ios",
9678
+ "stim start",
9679
+ "stim ios"
9680
+ ]
9681
+ },
9682
+ {
9683
+ id: "rebuild",
9684
+ title: "Rebuild from cache",
9685
+ who: "agent",
9686
+ optional: false,
9687
+ prompt: TUTORIAL_PROMPTS.rebuild,
9688
+ section: "rebuild",
9689
+ manual: [
9690
+ "cd \"{tour}\"",
9691
+ "stim ios",
9692
+ "stim status --json"
9693
+ ]
9694
+ },
9695
+ {
9696
+ id: "device",
9697
+ title: "Live view and control",
9698
+ who: "you",
9699
+ optional: false,
9700
+ prompt: null,
9701
+ section: null,
9702
+ manual: ["stim status"]
9703
+ },
9704
+ {
9705
+ id: "logs",
9706
+ title: "App logs",
9707
+ who: "you",
9708
+ optional: false,
9709
+ prompt: null,
9710
+ section: null,
9711
+ manual: ["stim logs --errors", "stim logs --grep '\\[stim:tutorial\\]'"]
9712
+ },
9713
+ {
9714
+ id: "agent",
9715
+ title: "Agent actions and replay",
9716
+ who: "agent",
9717
+ optional: false,
9718
+ prompt: TUTORIAL_PROMPTS.agent,
9719
+ section: "agent",
9720
+ manual: [
9721
+ "cd \"{tour}\"",
9722
+ "export AGENT_DEVICE_STATE_DIR=\"{stateDir}\"",
9723
+ "stim status --json",
9724
+ "read -r iosUdid",
9725
+ "agent-device open dev.stim.tutorial --platform ios --udid \"$iosUdid\" --save-script=tutorial.ad",
9726
+ "agent-device react-native dismiss-overlay || true",
9727
+ `agent-device press 'label="Log an error"' --settle`,
9728
+ "agent-device screenshot tutorial.png",
9729
+ "agent-device close",
9730
+ "grep -v -e 'target-v1' -e 'dismiss-overlay' tutorial.ad > tutorial-replay.ad",
9731
+ "agent-device replay tutorial-replay.ad --platform ios --udid \"$iosUdid\"",
9732
+ "stim logs --source agent --tail 10"
9733
+ ]
9734
+ },
9735
+ {
9736
+ id: "refresh",
9737
+ title: "Fast Refresh",
9738
+ who: "agent",
9739
+ optional: false,
9740
+ prompt: TUTORIAL_PROMPTS.refresh,
9741
+ section: "refresh",
9742
+ manual: [
9743
+ "cd \"{tour}\"",
9744
+ "cat > theme.js <<'STIM_TUTORIAL_EOF'",
9745
+ "export const TITLE_COLOR = '#7c3aed';",
9746
+ "STIM_TUTORIAL_EOF",
9747
+ "sleep 6",
9748
+ "stim logs --errors",
9749
+ "stim logs --grep 'title color'"
9750
+ ]
9751
+ },
9752
+ {
9753
+ id: "phone",
9754
+ title: "Watch on your phone",
9755
+ who: "you",
9756
+ optional: true,
9757
+ prompt: null,
9758
+ section: null,
9759
+ manual: []
9760
+ },
9761
+ {
9762
+ id: "machine",
9763
+ title: "Build on another Mac",
9764
+ who: "both",
9765
+ optional: true,
9766
+ prompt: TUTORIAL_PROMPTS.machine,
9767
+ section: "machine",
9768
+ manual: ["cd \"{tour}\"", "stim ios --build-machine \"{machine}\" --no-build-cache"]
9769
+ },
9770
+ {
9771
+ id: "finish",
9772
+ title: "Finish and archive",
9773
+ who: "agent",
9774
+ optional: false,
9775
+ prompt: TUTORIAL_PROMPTS.finish,
9776
+ section: "finish",
9777
+ manual: [
9778
+ "git -C \"{tour}\" checkout -- theme.js",
9779
+ "cd \"{tour}\"",
9780
+ "stim stop",
9781
+ "cd \"{base}\"",
9782
+ "stim worktree remove \"{tour}\""
9783
+ ]
9784
+ }
9785
+ ];
9786
+ //#endregion
9787
+ //#region src/guide/tutorial.ts
9788
+ const paths = `Use {base} = ~/stim-tutorial and {tour} = ~/stim-tutorial-tour unless the
9789
+ user named another parent folder. Expand ~ to the absolute home path when
9790
+ substituting inside quotes. Keep the tutorial outside the user's project.`;
9791
+ function commands(id) {
9792
+ return TUTORIAL_STEPS.find((step) => step.id === id).manual.join("\n");
9793
+ }
9794
+ //#endregion
9795
+ //#region src/guide/index.ts
9796
+ const TOPICS = {
9797
+ agent: agent_default,
9798
+ facts,
9799
+ metro: metro_default,
9800
+ ports: ports_default,
9801
+ logs: logs_default,
9802
+ errors,
9803
+ lifecycle,
9804
+ cleanup,
9805
+ settings: settings_default,
9806
+ web: web_default,
9807
+ macos: macos_default,
9808
+ tutorial: {
9809
+ summary: "An iOS tutorial: isolated worktree, builds, devices, logs, agent actions, and cleanup",
9810
+ sectionHint: "run",
9811
+ preamble: () => `STIM TUTORIAL
9812
+
9813
+ Create a small Expo app in its own repository and tour worktree. Follow the
9814
+ normal Stim flow on iOS, then inspect builds, cache reuse, device control,
9815
+ logs, agent actions, Fast Refresh, and cleanup.
9816
+
9817
+ Run one section per user request. Then end the turn, say what to look at in
9818
+ Stim Desktop, and give the next prompt. Pause never means stim stop.
9819
+ "${TUTORIAL_PROMPTS.begin}" means stim guide tutorial run.
9820
+ "Continue the Stim tutorial: <section>" means stim guide tutorial <section>.
9821
+ "${TUTORIAL_RESTART_PROMPT}" means stim guide tutorial restart.
9822
+
9823
+ Without Stim Desktop, use the simulator, stim status for the workspace and
9824
+ device, stim logs --errors for errors, and stim stats for build performance.
9825
+ Follow stim guide agent for doctor, errors, consent, and cleanup. Never pair
9826
+ phones, approve machines, or grant access on the user's behalf.
9827
+
9828
+ ${paths}
9829
+
9830
+ Start with stim guide tutorial run. For commands to type yourself, read
9831
+ stim guide tutorial manual. The first run needs network access unless the
9832
+ required npm and native dependencies are already cached.`,
9833
+ sections: {
9834
+ run: {
9835
+ summary: "Create or reuse the app, warm a tour worktree, and build on iOS",
9836
+ body: () => `RUN THE TUTORIAL
9837
+
9838
+ ${paths}
9839
+
9840
+ Folder safety: create the base folder only if absent. Reuse an existing
9841
+ folder only if app.json's expo.extra.stimTutorial equals this guide's version,
9842
+ ${JSON.parse(TUTORIAL_FILES["app.json"]).expo.extra.stimTutorial}; skip creation and file writes when reusing it. For an existing non-tutorial
9843
+ folder or another tutorial version, stop and ask the user for another folder.
9844
+ Never overwrite or delete it.
9845
+
9846
+ For a new app, first check the selected parent folder: create it if absent,
9847
+ then run git -C <parent> rev-parse --is-inside-work-tree. If it succeeds, the
9848
+ folder is inside another repository: stop and ask the user for another folder.
9849
+ Never git add in the user's repo. Then, from the parent folder, run:
9850
+
9851
+ npx --yes create-expo-app@${TUTORIAL_PINS.createExpoApp} stim-tutorial --template ${TUTORIAL_PINS.template} --no-install --no-agents-md --yes
9852
+
9853
+ Enter the base folder. Run stim guide tutorial app, write its three app files
9854
+ verbatim, and append its .gitignore lines. Then run:
9855
+
9856
+ npm install --prefer-offline
9857
+ npm pkg set scripts.ios="expo run:ios" scripts.android="expo run:android"
9858
+ git init
9859
+ git add -A
9860
+ git -c user.name=Stim -c user.email=stim@localhost -c commit.gpgsign=false commit -m "Stim tutorial"
9861
+
9862
+ On reuse, run git rev-parse --show-toplevel in the base folder before adding
9863
+ a worktree; if it does not equal the base folder, the folder is inside another
9864
+ repository: stop and ask the user for another folder.
9865
+
9866
+ Set the scripts before the commit because Expo prebuild rewrites them to
9867
+ expo run:ios and expo run:android; otherwise the dirty worktree blocks removal.
9868
+ On npm or network failure, report stderr and stop.
9869
+
9870
+ From the base folder, add the tour worktree and run:
9871
+
9872
+ git worktree add -B stim-tutorial/tour "{tour}" HEAD
9873
+ cd "{tour}"
9874
+ stim worktree warm
9875
+ stim guide agent
9876
+
9877
+ Apply the guide agent doctor rule before native work: if its STATUS block
9878
+ says doctor is due, run stim doctor --platform ios from the tour worktree and
9879
+ resolve its findings. No STATUS block means doctor is current. Then run:
9880
+
9881
+ stim start
9882
+ stim ios
9883
+
9884
+ Tell the user the first build can take about four minutes on a cold cache.
9885
+ Relay the Open in Stim Desktop link printed by stim ios once.
9886
+
9887
+ PAUSE: end the turn. Ask the user to look at the Build section, then the
9888
+ device, then Logs in Stim Desktop. Without Desktop, use stim status,
9889
+ stim stats, and stim logs --errors. Give the next prompt:
9890
+ "${TUTORIAL_PROMPTS.rebuild}". Leave the workspace running.`
9891
+ },
9892
+ app: {
9893
+ summary: "Pinned template, verbatim app files, and .gitignore additions",
9894
+ body: () => `TUTORIAL APP
9895
+
9896
+ create-expo-app: ${TUTORIAL_PINS.createExpoApp}
9897
+ Template: ${TUTORIAL_PINS.template}
9898
+ Write the app files verbatim. Append the .gitignore lines to the template's
9899
+ existing file. App log lines start with [stim:tutorial]. Crash me raises an
9900
+ uncaught JavaScript error, and Slow request times a local three-second timer;
9901
+ Stim does not capture native network requests.
9902
+
9903
+ ${Object.entries(TUTORIAL_FILES).map(([name, content]) => `${name}\n\n\`\`\`${name.endsWith(".json") ? "json" : name.endsWith(".js") ? "js" : "text"}\n${content}\`\`\``).join("\n\n")}`
9904
+ },
9905
+ rebuild: {
9906
+ summary: "Repeat the iOS build and explain the cache result",
9907
+ body: () => `REBUILD
9908
+
9909
+ ${paths}
9910
+
9911
+ Run the same build again, never with --no-build-cache:
9912
+
9913
+ ${commands("rebuild")}
9914
+
9915
+ Read this workspace's environments[].lastBuilds.ios.cacheHit from
9916
+ stim status --json, and the miss reason printed by stim ios. Explain a local or remote hit, or the actual miss
9917
+ reason when cacheHit is false. A repeat run can miss; report the evidence.
9918
+
9919
+ PAUSE: end the turn. Point at the Build section and cache badge, or stim stats.
9920
+ Ask the user to open the device's live view and tap Log an error; without
9921
+ Desktop use the simulator. Then inspect Logs or stim logs --errors. Crash me
9922
+ and Slow request are optional: the former shows a red box, the latter prints
9923
+ a timing line. Give the next prompt: "${TUTORIAL_PROMPTS.agent}".`
9924
+ },
9925
+ agent: {
9926
+ summary: "Record a tap and screenshot, replay it, and inspect agent logs",
9927
+ body: () => `AGENT ACTIONS
9928
+
9929
+ ${paths}
9930
+
9931
+ Set {stateDir} to this workspace's agentDevice.stateDir from stim ios or
9932
+ stim status --json. Replace <ios.udid> with this workspace's ios.udid from
9933
+ stim status --json. Use that exact owned simulator, not a guessed one.
9934
+ Run from the tour worktree:
9935
+
9936
+ ${commands("agent").replace("read -r iosUdid", "iosUdid=\"<ios.udid>\"")}
9937
+
9938
+ Use --save-script=tutorial.ad with the equals sign: agent-device treats a
9939
+ separate path as a URL and refuses. The dismiss-overlay step clears a red box that would cover the buttons.
9940
+ Replay with the same --platform and --udid so its steps reach the agent log.
9941
+ Remove the target-v1 evidence and dismiss-overlay lines before replay: replaying
9942
+ them can fail with REPLAY_DIVERGENCE on the recorded button identity.
9943
+ The scripts and screenshot stay in the tour worktree and are git-ignored.
9944
+
9945
+ PAUSE: end the turn. Point at Agent actions, or the agent log records just
9946
+ printed. Expect another error-button line after replay. When screen recording
9947
+ is enabled, the user can scrub Replay in Desktop. Give the next prompt:
9948
+ "${TUTORIAL_PROMPTS.refresh}".`
9949
+ },
9950
+ refresh: {
9951
+ summary: "Change the title to purple and verify Fast Refresh through logs",
9952
+ body: () => `FAST REFRESH
9953
+
9954
+ ${paths}
9955
+
9956
+ Set TITLE_COLOR in theme.js to '#7c3aed' (purple). Wait a few seconds for
9957
+ Fast Refresh, without reloading the app:
9958
+
9959
+ ${commands("refresh")}
9960
+
9961
+ The line [stim:tutorial] title color=#7c3aed is the proof that the edit reached
9962
+ the app. The intentional button errors may still be in the error log; check
9963
+ whether the edit introduced a new error.
9964
+
9965
+ PAUSE: end the turn. Point at the purple title in the device view or simulator
9966
+ and the title color log line. Phone viewing and an approved build machine are
9967
+ optional user steps; skip them if unwanted. Never pair, approve, or grant
9968
+ anything. If the user names an approved machine, the next prompt is
9969
+ "${TUTORIAL_PROMPTS.machine}". Otherwise give
9970
+ "${TUTORIAL_PROMPTS.finish}".`
9971
+ },
9972
+ machine: {
9973
+ summary: "Optionally build using a machine the user names and has approved",
9974
+ body: () => `BUILD MACHINE (OPTIONAL)
9975
+
9976
+ ${paths}
9977
+
9978
+ Proceed only when the user names an approved build machine. Substitute that
9979
+ name for {machine}. Never approve, pair, or grant anything. If none is named,
9980
+ ask for the name or let the user skip this step.
9981
+
9982
+ ${commands("machine")}
9983
+
9984
+ This step bypasses the artifact cache so the build can use the named machine.
9985
+ A named machine refuses without a local fallback. If it refuses, report the
9986
+ refusal and offer --build-machine auto or local; do not retry silently.
9987
+
9988
+ PAUSE: end the turn. Point at the build's machine in Desktop or its report in
9989
+ stim status --json. Give the next prompt: "${TUTORIAL_PROMPTS.finish}".`
9990
+ },
9991
+ finish: {
9992
+ summary: "Revert the tutorial edit, stop, and remove only the tour worktree",
9993
+ body: () => `FINISH
9994
+
9995
+ ${paths}
9996
+
9997
+ The user's finish request authorizes removing this tour worktree only.
9998
+ Revert the refresh edit before removal; worktree remove refuses dirty trees.
9999
+ Stop from the tour path, then remove from the base checkout:
10000
+
10001
+ ${commands("finish")}
10002
+
10003
+ Never use --force. On a refusal, report it and stop. Keep the base folder
10004
+ and branch. Print these optional cleanup commands for the user; do not run. Deleting
10005
+ the base folder also removes the branch, so they are alternatives:
10006
+
10007
+ rm -rf "{base}"
10008
+ git -C "{base}" branch -D stim-tutorial/tour
10009
+
10010
+ If archive is enabled, tell the user the tour appears under Archived in Stim
10011
+ Desktop. With archive disabled, report removal without promising an archive.
10012
+ Without Desktop, inspect stim status --json for the removed environment and,
10013
+ when enabled, its archived entry. End the turn.`
10014
+ },
10015
+ restart: {
10016
+ summary: "Remove the existing tour safely and repeat from the worktree step",
10017
+ body: () => `RESTART
10018
+
10019
+ ${paths}
10020
+
10021
+ For "${TUTORIAL_RESTART_PROMPT}", if the tour worktree is present, revert
10022
+ the refresh edit, stop, and remove it using the finish section's commands:
10023
+
10024
+ ${commands("finish")}
10025
+
10026
+ Never use --force. On a refusal, report it and stop. Keep the base repository.
10027
+ Read stim guide tutorial run, recheck its folder and repository safety rules,
10028
+ and redo run from the worktree step. Pause after the build as run instructs.`
10029
+ },
10030
+ manual: {
10031
+ summary: "Commands for every step, including heredocs for the app files",
10032
+ body: () => `MANUAL TUTORIAL
10033
+
10034
+ ${paths}
10035
+
10036
+ These commands are for a person typing them, one step at a time,
10037
+ as scripts: save a block to a file and run it with sh -e, so it stops on the
10038
+ first failure. Read stderr and do not continue to later commands. Follow stim guide tutorial run for folder and repository
10039
+ safety. Reuse only this version's tutorial app, never overwrite another folder.
10040
+ The creation block skips writes on reuse and checks the repository root.
10041
+
10042
+ Replace {base} and {tour} with absolute paths; {base} must end in stim-tutorial. For Agent actions, replace
10043
+ {stateDir} with agentDevice.stateDir from stim ios or stim status --json;
10044
+ when read -r iosUdid waits, type this workspace's ios.udid from that status.
10045
+ For the optional machine step,
10046
+ replace {machine} with a machine you have already approved, or skip it.
10047
+
10048
+ Look at the sidebar during warm and Build during the first build (about four
10049
+ minutes on a cold cache). Compare cacheHit and missReason on rebuild. At Live
10050
+ view and control, open the device viewer and tap Log an error; without Desktop
10051
+ use the simulator. At App logs, try Crash me or Slow request if wanted. A JS
10052
+ crash shows a red box; the slow request is a local timer, not network capture.
10053
+ At Watch on your phone, optionally open an already paired Stim phone to see
10054
+ the tour workspace; phone setup and machine approval stay with you.
10055
+ Use stim status, stim logs --errors, and stim stats without Desktop.
10056
+
10057
+ ${TUTORIAL_STEPS.map((step) => `${step.title}${step.optional ? " (optional)" : ""}\n\n${step.manual.length ? `\`\`\`sh\n${step.manual.join("\n")}\n\`\`\`` : "Observe this step in Stim Desktop, or skip it without Desktop."}`).join("\n\n")}
10058
+
10059
+ Finish removes only the tour worktree. Never use --force; report a refusal.
10060
+ With archive enabled, find the tour under Archived in Desktop or archived[]
10061
+ in stim status --json. Keep the base repository and branch. To delete them,
10062
+ read stim guide tutorial finish for commands to review and run yourself.`
10063
+ }
10064
+ }
8968
10065
  }
8969
10066
  };
8970
10067
  //#endregion