stim 1.11.0 → 1.13.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 (82) hide show
  1. package/README.md +3 -1
  2. package/dist/{activity-BtG4Va9l.mjs → activity-DfVKZTEB.mjs} +44 -26
  3. package/dist/{android-BA2i_KAV.mjs → android-BHfAu2mf.mjs} +1 -1
  4. package/dist/{android-C1q4l523.mjs → android-CD8pr8Q2.mjs} +112 -54
  5. package/dist/{android-cas-DfJkwCXW.mjs → android-cas-B9Kv9aIy.mjs} +12 -10
  6. package/dist/{android-C4zG2Tuc.mjs → android-qx_31wJo.mjs} +414 -118
  7. package/dist/{app-install-DHut6JnV.mjs → app-install-CDFY2nUL.mjs} +1 -1
  8. package/dist/budget-E5o91NR1.mjs +1739 -0
  9. package/dist/{build-plan-CeAYgKAx.mjs → build-plan-BTa93o_r.mjs} +125 -107
  10. package/dist/{build-progress-CDwogz6R.mjs → build-progress-l7d-X-ZP.mjs} +76 -8
  11. package/dist/{cache-manifest-Pubfp8wV.mjs → cache-manifest-DWPF4RKJ.mjs} +1 -1
  12. package/dist/cache-manifest.mjs +1 -1
  13. package/dist/cdp-Dq_ypAlJ.mjs +115 -0
  14. package/dist/chrome-B5hWgQJS.mjs +61 -0
  15. package/dist/cli.mjs +17 -16
  16. package/dist/collector-run.mjs +15 -9
  17. package/dist/{command-output-D5z2_bcF.mjs → command-output-BIL7rlX7.mjs} +1 -1
  18. package/dist/{config-BEq0Qw_a.mjs → config-0jCcMM0E.mjs} +10 -1
  19. package/dist/created-devices-Cd558h31.mjs +37 -0
  20. package/dist/{dependency-state-r9UesA9a.mjs → dependency-state-DNN0yfx6.mjs} +1 -1
  21. package/dist/{deps-OF7CD5Dx.mjs → deps-2Wo81np9.mjs} +2 -2
  22. package/dist/{device-BwH2iI3O.mjs → device-Cf7vl-so.mjs} +7 -7
  23. package/dist/{device-lease-D83nqeMX.mjs → device-lease-CL9dZf73.mjs} +55 -6
  24. package/dist/{device-pool-DHj7cdPy.mjs → device-pool-MuoiyRcu.mjs} +4 -4
  25. package/dist/{device-remote-BQZSnnmE.mjs → device-remote-DT9ngZiD.mjs} +254 -579
  26. package/dist/{doctor-D8QdMRMf.mjs → doctor-D2iysNNH.mjs} +50 -20
  27. package/dist/{error-diagnostics-UYFSQwRy.mjs → error-diagnostics-BoiOhck8.mjs} +7 -7
  28. package/dist/{gc-COZteyiq.mjs → gc-eA-PRBX8.mjs} +1022 -131
  29. package/dist/{guide-NDvTmMi-.mjs → guide-BfZ0OmiP.mjs} +909 -132
  30. package/dist/{guide-status-CWvti2Lg.mjs → guide-status-BaMYu55M.mjs} +2 -2
  31. package/dist/{idle-NETbeNiQ.mjs → idle-DxLi5OkN.mjs} +43 -10
  32. package/dist/{in-use-DVhlSoyn.mjs → in-use-wYEaKQM9.mjs} +11 -7
  33. package/dist/{ios-CQLKpNfr.mjs → ios-CcfCeZ19.mjs} +103 -58
  34. package/dist/{ios-DArbmwer.mjs → ios-CmEzZiOj.mjs} +31 -22
  35. package/dist/{ios-device-CwDZo0X4.mjs → ios-device-BW-t_Lkv.mjs} +1 -1
  36. package/dist/{launch-verify-CuPDGXdr.mjs → launch-verify-CuJaBpEa.mjs} +33 -20
  37. package/dist/{logs-B44a1UeU.mjs → logs-DcSRbC2t.mjs} +12 -11
  38. package/dist/{merge-state-DDxS-XIZ.mjs → merge-state-ytFQCeh4.mjs} +38 -8
  39. package/dist/{metro-BWLOJ_Wr.mjs → metro-BPeY1YMh.mjs} +5 -5
  40. package/dist/{named-ports-Dc7neAlc.mjs → named-ports-n7tnJsI8.mjs} +78 -6
  41. package/dist/native-runtime-CiiFHt0e.mjs +74 -0
  42. package/dist/{ownership-D2xbLVoz.mjs → ownership-Bz2kETAS.mjs} +2 -2
  43. package/dist/{ownership-BiojdtAp.mjs → ownership-PUP4Dwai.mjs} +175 -217
  44. package/dist/page-BTu7I60K.mjs +29 -0
  45. package/dist/{ports-DwBaRFJs.mjs → ports-C5UutqpJ.mjs} +5 -5
  46. package/dist/{project-BXZzOABt.mjs → project-DGeclhvO.mjs} +55 -87
  47. package/dist/{reload-By9Dwsrt.mjs → reload-C-cV5yy_.mjs} +80 -21
  48. package/dist/remote-cache--aehr4Pr.mjs +526 -0
  49. package/dist/{server-bare-D_RK1RCj.mjs → server-bare-Bq8BpSIo.mjs} +1 -1
  50. package/dist/server-expo-BeYwP_Xy.mjs +2 -0
  51. package/dist/{server-expo-o6H5gcBc.mjs → server-expo-DUCg52-K.mjs} +2 -2
  52. package/dist/{settings-C0ugALSC.mjs → settings-ssVBToDR.mjs} +29 -17
  53. package/dist/{settings-B5oFZEri.mjs → settings-xB0Frkg6.mjs} +13 -4
  54. package/dist/settings.schema.json +74 -2
  55. package/dist/{simslim-CIR3IYJO.mjs → simslim-Bbg9UWrt.mjs} +13 -6
  56. package/dist/{slot-launch-rggO1Esw.mjs → slot-launch-BIWEW-wI.mjs} +4 -4
  57. package/dist/{spawn-claims-BJwXTMPt.mjs → spawn-claims-PT8aQpCa.mjs} +1 -1
  58. package/dist/{start-BSsf2tUs.mjs → start-BZhItkiL.mjs} +110 -18
  59. package/dist/{start-i8zc9VmS.mjs → start-Rw2GD3nV.mjs} +1 -1
  60. package/dist/{state-WGdIbi_d.mjs → state-DwAU2_-D.mjs} +15 -3
  61. package/dist/state-l1pYwOre.mjs +148 -0
  62. package/dist/{stats-DBYJ5bxL.mjs → stats-CaSwmsSZ.mjs} +4 -4
  63. package/dist/{stats-BAgJaLA4.mjs → stats-D8rUTPHd.mjs} +1 -1
  64. package/dist/{status-CitcBRsC.mjs → status-TJlL0HWo.mjs} +649 -116
  65. package/dist/{status-Q-WDEGCB.mjs → status-sszP4XKU.mjs} +130 -28
  66. package/dist/stim-desktop-CXt8-Z3J.mjs +40 -0
  67. package/dist/{stop-DIhrvu0B.mjs → stop-BZ2qMiBP.mjs} +50 -18
  68. package/dist/{stop-rst5mL14.mjs → stop-DskDKNTU.mjs} +2 -2
  69. package/dist/supervisor-run.mjs +11 -11
  70. package/dist/web-run.d.mts +22 -0
  71. package/dist/web-run.mjs +481 -0
  72. package/dist/web-sWXd5iek.mjs +394 -0
  73. package/dist/{worktree-DhxDTK44.mjs → worktree-3cBa-bQt.mjs} +1 -1
  74. package/dist/{worktree-KOjMX3zA.mjs → worktree-D4Do_XiQ.mjs} +229 -21
  75. package/dist/{worktree-BlizKDpZ.mjs → worktree-DeBQSVtR.mjs} +3 -1
  76. package/dist/{xcode-B5SPD--o.mjs → xcode-LM6A8cNZ.mjs} +7 -7
  77. package/package.json +5 -5
  78. package/dist/budget-De3P2rq_.mjs +0 -620
  79. package/dist/native-run-D7yB_AsB.mjs +0 -143
  80. package/dist/server-expo-B-CCUJZj.mjs +0 -2
  81. package/dist/workspace-process-lock-Cx74RSDZ.mjs +0 -57
  82. package/dist/workspaces-COD2xCz-.mjs +0 -1013
@@ -1,6 +1,7 @@
1
- import { o as findProjectRoot } from "./project-BXZzOABt.mjs";
2
- import { t as ANDROID_AVD_CONFIG_HELP } from "./settings-B5oFZEri.mjs";
3
- import { t as guideStatus } from "./guide-status-CWvti2Lg.mjs";
1
+ import { a as findProjectRoot } from "./project-DGeclhvO.mjs";
2
+ import { t as ANDROID_AVD_CONFIG_HELP } from "./settings-xB0Frkg6.mjs";
3
+ import { t as RECENT_LAUNCH_MS } from "./status-sszP4XKU.mjs";
4
+ import { t as guideStatus } from "./guide-status-BaMYu55M.mjs";
4
5
  import { SETTINGS_SCHEMA_URL } from "@stim-cli/core/state";
5
6
  import chalk from "chalk";
6
7
  //#endregion
@@ -80,7 +81,14 @@ other apps or other workspaces' devices.
80
81
  stim worktree warm
81
82
 
82
83
  stim start
83
- stim ios # or: stim android
84
+ stim ios # or: stim android, or: stim web
85
+
86
+ For the web target, stim web opens the page in a Stim-owned headless Chrome
87
+ and captures its console and errors in stim logs. It never starts a web
88
+ server: start the dev server first, then run stim web. For Expo web the dev
89
+ server is stim start, and the app needs the react-dom, react-native-web and
90
+ @expo/metro-runtime dependencies. Any other server runs on stim ports get web
91
+ and needs web.url. Read stim guide web first.
84
92
 
85
93
  For a project using EAS development builds, read stim guide lifecycle eas to
86
94
  select a profile from eas.json for the requested target, then run
@@ -177,7 +185,8 @@ RULES DURING THE LOOP
177
185
  them. Use stim doctor when a build is unexpectedly slow or the environment
178
186
  looks incomplete. If status reports a changed Android serial, rerun stim
179
187
  android with the same build options to restore forwarding, then reopen your automation
180
- session on the reported serial (guide lifecycle).
188
+ session on the reported serial (guide lifecycle). Each status issue names a
189
+ remedy to run from its workspace (guide facts status).
181
190
 
182
191
  OWNERSHIP AND DELETION
183
192
 
@@ -221,7 +230,8 @@ Ask the user before these actions:
221
230
  - worktree remove --force, because it also discards uncommitted and untracked
222
231
  files.
223
232
  - gc --delete, because it deletes orphaned resources, removes clean linked
224
- worktrees whose branch is merged, and clears the build outputs of every
233
+ worktrees whose branch or pull request is merged (or whose pull request
234
+ was closed), and clears the build outputs of every
225
235
  workspace not in use. Run stim gc --json first and show
226
236
  the user the entries its sections list (guide facts gc). gc --delete
227
237
  --cache all empties the shared build caches and those outputs instead, and
@@ -242,7 +252,11 @@ Run stim stop when you pause or finish a task, and stim worktree remove once
242
252
  it is merged or abandoned. Never create a simulator or emulator by hand;
243
253
  every device comes from Stim. Before starting a new workspace, check stim
244
254
  status for an idle workspace to reuse and confirm capacity headroom instead of
245
- adding another.
255
+ adding another. Never quit Device Hub or Simulator.app: on Xcode 27
256
+ (confirmed on 27A266a), quitting Device Hub by default shuts down every booted
257
+ simulator on the machine, including ones it never opened a window for and
258
+ ones other workspaces or agents are using. Use stim stop on workspaces you
259
+ own instead.
246
260
 
247
261
  SANDBOXES
248
262
 
@@ -266,7 +280,8 @@ Read the matching guide before acting in these situations:
266
280
  | Refusal with a CODE | stim guide errors <CODE> |
267
281
  | Running under a sandbox | stim guide errors sandbox |
268
282
  | Release configuration or ...Release variant | stim guide lifecycle release |
269
- | Web or API server ports | stim guide ports |
283
+ | Web app in an owned Chrome (stim web) | stim guide web |
284
+ | Web or API server ports | stim guide ports |
270
285
  | Remote device, custom Metro, or tunnel | stim guide metro |
271
286
  | Cache miss, bypass, or fingerprint exclusions | stim guide lifecycle builds |
272
287
  | Capacity limits | stim guide lifecycle concurrency |
@@ -310,6 +325,7 @@ FULL TOPIC LIST
310
325
  stim guide lifecycle release # Release configurations and ...Release variants
311
326
  stim guide facts # the --json payloads
312
327
  stim guide facts devmenu # the Expo dev menu or Tools button over the app
328
+ stim guide web # stim web: owned Chrome, page logs, launched, teardown
313
329
  stim guide ports # named ports for web and API servers
314
330
  stim guide metro # supervisor, custom Metro, tunnels, and remote devices
315
331
  stim guide logs # filters, record shape, and capture limits
@@ -318,7 +334,7 @@ FULL TOPIC LIST
318
334
  stim guide settings # configuration files and supported keys`
319
335
  },
320
336
  facts: {
321
- summary: "The --json payloads: `start`, `ios`, `android`, `ios|android --plan`, `reload`, `stop`, `status`, `doctor`, `device lock`/`unlock`, `gc`, and the error contract",
337
+ summary: "The --json payloads: `start`, `ios`, `android`, `web`, `ios|android --plan`, `reload`, `stop`, `status`, `doctor`, `device lock`/`unlock`, `gc`, and the error contract",
322
338
  preamble: () => `SLOTS
323
339
  Named ios/android runs add slot to their JSON facts. Default-run fields remain
324
340
  compatible. status adds a slots array per environment with each named slot's
@@ -327,7 +343,7 @@ Named collector, lease-holder, and launch keys use platform:slot internally.
327
343
 
328
344
  FACTS CONTRACT
329
345
 
330
- \`start\`, \`ios\`, \`android\`, \`reload\`, \`stop\`, \`status\`, \`stats\`, \`doctor\`,
346
+ \`start\`, \`ios\`, \`android\`, \`web\`, \`reload\`, \`stop\`, \`status\`, \`stats\`, \`doctor\`,
331
347
  \`gc\`, and \`device lock\`/\`device unlock\` each print exactly ONE line of JSON on
332
348
  stdout for \`--json\`. Every other line goes to stderr, so it is always safe
333
349
  to pipe. \`logs --json\` is the one exception: it is NDJSON, one record per
@@ -366,8 +382,11 @@ status runs \`git status --porcelain=v2 --branch\` in every worktree in
366
382
  parallel, and caches the merge verdict by HEAD and default-branch commit
367
383
  under $STIM_HOME/git-merge. One call starts no new merge check 250 ms after
368
384
  its first; later calls judge the rest, and until then a verdict for the same
369
- HEAD at an older default-branch commit stands in. \`status --watch\` reuses a worktree's git read
370
- for 5 s. Plain status prints "git: 2 changed, 1 untracked, ahead 3" under
385
+ HEAD at an older default-branch commit stands in; a check that timed out
386
+ is retried after 5 minutes. \`status --watch\` reuses a worktree's git read
387
+ until its index, HEAD, reflog or the branch, upstream or default-branch refs
388
+ change, and for at most 60 s, so a file edit, creation or deletion that is
389
+ not staged can take up to a minute to show. Plain status prints "git: 2 changed, 1 untracked, ahead 3" under
371
390
  each environment, and the same after each worktree with no environment.
372
391
 
373
392
  status's remoteDevices lists each environment's recorded EAS Simulator
@@ -390,7 +409,7 @@ Plain status prints one "remote <platform>: EAS session <id> billable" line
390
409
  per session, with the preview URL.`,
391
410
  sections: {
392
411
  payloads: {
393
- summary: "every field of the start, ios, android and reload payloads, the error contract, the device rules",
412
+ summary: "every field of the start, ios, android, web and reload payloads, the error contract, the device rules",
394
413
  body: () => ` stim start --json
395
414
 
396
415
  port the Metro port RESERVED for this workspace
@@ -431,13 +450,18 @@ per session, with the preview URL.`,
431
450
  hit 6564e2.. (post-prebuild key)\`), so a cold tree -- a
432
451
  fresh worktree or clone of a CNG app -- installs an entry
433
452
  another workspace already built instead of compiling
434
- beside it. Android also fingerprints after Gradle because
435
- Gradle plugins can rewrite native inputs while they build;
436
- its artifact is stored only under that post-build hash. A
437
- stable second fingerprint prints no shift line. If the iOS
438
- fingerprint after prebuild or pod install, or the Android
439
- fingerprint after Gradle, cannot be computed, the build is
440
- installed but not cached, and fingerprint and cacheKey are null
453
+ beside it. Both platforms fingerprint again after the
454
+ compile because Gradle plugins can rewrite native inputs
455
+ while they build; a change under node_modules/ or the
456
+ native directory stores the artifact only under that
457
+ post-build hash. A stable fingerprint prints no shift line.
458
+ If any other input changed while the build ran (the app
459
+ config or a config plugin since the first lookup, other
460
+ than the bundle id or package prebuild adds, or any
461
+ other source during the compile), or a fingerprint after
462
+ prebuild, pod install or the compile cannot be computed,
463
+ the build is installed but not cached, and fingerprint and
464
+ cacheKey are null
441
465
  configuration the Xcode configuration that was built ("Release" from
442
466
  --configuration or the ios.configuration setting); null for
443
467
  the default Debug
@@ -518,6 +542,12 @@ per session, with the preview URL.`,
518
542
  finishing
519
543
  "unverified" nothing was observed at all: usually a
520
544
  dev-client server picker awaiting a tap
545
+ Neither "bundling" nor "unverified" is reported for an app
546
+ whose process is gone when the bundle timeout closes: the
547
+ run fails as FATAL, "the app process exited", with the
548
+ device log's errors. An iOS crash report can land a minute
549
+ after the crash, so \`logs --errors\` may show the native
550
+ stack only later
521
551
  See \`guide facts devmenu\` for the dev menu and its button.
522
552
  metroPort the port the app was wired to; NULL on a non-Debug
523
553
  configuration, whose JS is embedded and which is launched
@@ -615,18 +645,20 @@ per session, with the preview URL.`,
615
645
  reason is "not running" or "stopped (idle)" when the
616
646
  supervisor had stopped it after metro.idleStopMinutes
617
647
 
618
- stim reload [ios|android] --json
648
+ stim reload [ios|android|web] --json
619
649
 
620
650
  Exit 0 and this payload confirm that the reload request was sent. They do
621
651
  not prove that new JavaScript loaded or that the screen recovered. The
622
652
  command does not observe completion. Verify the expected UI on deviceId
623
653
  and inspect stim logs --errors before claiming recovery.
624
654
 
625
- platform "ios" | "android"
626
- deviceId the exact owned simulator UDID or emulator serial targeted
627
- deviceName the owned simulator or AVD name
628
- appId the live bundle id or Android package
629
- metroPort the workspace's verified Metro port
655
+ platform "ios" | "android" | "web"
656
+ deviceId the exact owned simulator UDID or emulator serial targeted;
657
+ for web, the owned Chrome's DevTools endpoint
658
+ deviceName the owned simulator or AVD name; for web, the Chrome version
659
+ appId the live bundle id or Android package; for web, the page URL
660
+ metroPort the workspace's verified Metro port; for web, the reserved
661
+ Metro port or null
630
662
  strategy how the reload was addressed.
631
663
  "metro-websocket" -- Metro named its clients and Stim
632
664
  addressed every peer matching this platform. A workspace
@@ -636,11 +668,35 @@ per session, with the preview URL.`,
636
668
  the reload went to all of them and Stim cannot confirm appId
637
669
  was among them. Verify the UI on deviceId; if it did not
638
670
  change, reload from the app's own error screen or dev menu
671
+ "cdp" -- web: Page.reload on the owned Chrome page, sent
672
+ over a DevTools connection verified to reach that Chrome
639
673
  targets how many peers the reload was addressed to, or null when
640
674
  broadcast. Greater than 1 means several devices are running
641
675
  this app on that Metro and the request addressed all of
642
676
  them, not only deviceId. Completion is not observed
643
677
 
678
+ stim web --json
679
+
680
+ platform "web"
681
+ browser "chrome"
682
+ version the Chrome product, such as "Chrome/153.0.8010.49", or null
683
+ running true: the owned Chrome answered and holds the page
684
+ pid the owned Chrome's browser process; null when not running
685
+ supervisorPid the Stim process holding its DevTools session, or null
686
+ url the page opened, web.url with its ports filled in
687
+ headless false only with --headed
688
+ viewport "desktop" | "phone" (web.viewport)
689
+ profile the Stim-owned Chrome user data directory
690
+ cdpEndpoint http://127.0.0.1:<port>, the reserved DevTools endpoint, or
691
+ null when not running
692
+ reused true when the running Chrome navigated again instead of
693
+ starting: same --headed, viewport and certificate options
694
+ launched true | "bundling" | "unverified" (see \`guide web\`)
695
+ metroPort the Metro port the page loads from, or null for a web.url
696
+ that does not use {port:metro}
697
+ logs { dir }: web.ndjson holds the page records
698
+ durationMs wall time of the run
699
+
644
700
  stim doctor --json
645
701
 
646
702
  project the resolved app root
@@ -664,7 +720,7 @@ per session, with the preview URL.`,
664
720
  costs-time finding with a PATH or installation remedy
665
721
 
666
722
  ON FAILURE
667
- \`start\`, \`ios\` and \`android\` all print the error contract instead,
723
+ \`start\`, \`ios\`, \`android\` and \`web\` all print the error contract instead,
668
724
  still one line on stdout, and exit 1:
669
725
 
670
726
  { "code": "STIM_METRO_TIMEOUT", "message": "...", "remedy": "..." }
@@ -689,6 +745,12 @@ ON FAILURE
689
745
 
690
746
  { "root": "...", "ok": false, "code": "STIM_STOP_BLOCKED", "message": "...", "remedy": "..." }
691
747
 
748
+ device.web is present when the workspace had an owned Chrome:
749
+ { status, label: "Chrome", kind?, reason?, remedy? }; kind is null when no
750
+ kind applies. status is "shut-down"
751
+ when Chrome closed (its profile is kept), else "skipped" (its identity could
752
+ not be verified) or "failed", and ok is then false.
753
+
692
754
  Branch on \`code\`, never on the message text. \`guide errors\` enumerates
693
755
  every code.
694
756
 
@@ -805,13 +867,13 @@ RULES
805
867
  full commands.`
806
868
  },
807
869
  gc: {
808
- summary: "the gc report payload: mode, sections, reasons, failures, and the gc refusals",
870
+ summary: "the gc report payload: mode, sections, reasons, failures, results, inventory, and the gc refusals",
809
871
  body: () => ` stim gc [--delete] [--older-than <days>] [--cache <name|all|workspaces>]
810
872
  [--worktrees] [--idle <duration>] --json
811
873
 
812
874
  The report the text prints, as one payload. Show the user its sections
813
875
  before you run \`gc --delete\`. Under --delete it is the report that run
814
- acted on: each entry's outcome is a stderr line, \`failures\` counts the
876
+ acted on: \`results\` lists each entry's outcome, \`failures\` counts the
815
877
  entries it could not delete, and a nonzero count exits 1. Run
816
878
  \`stim gc --json\` again to see what is left.
817
879
 
@@ -828,6 +890,46 @@ RULES
828
890
  actionable true when --delete with the same flags reclaims something
829
891
  failures null on a dry run without --idle; otherwise the entries it
830
892
  could not delete or shut down
893
+ results what --delete or --idle did, one { kind, status, label,
894
+ id, bytes, detail } per entry it acted on; empty on a dry
895
+ run. status is "done", "kept" (left alone, detail says
896
+ why) or "failed" (detail says why and what to retry).
897
+ kind: device, parkedDevice, idleDevice, deviceRecord,
898
+ workspaceOutputs, workspaceDirectory, project, buildLock,
899
+ buildSlot, deviceLease, easSession, worktree, cache.
900
+ label is a device, path or cache name; id is the UDID,
901
+ AVD name or path behind it, or null
902
+ inventory null except on a dry run without --cache or --idle. Report
903
+ only: gc never acts on it, even under --delete.
904
+ { devices, runtimes, systemImages, notices }
905
+ devices { kind, id, name, model, runtime, state, lastUsedAt, bytes,
906
+ directory, owner, project, slot } every available iOS
907
+ simulator and registered AVD. runtime is the simctl
908
+ runtime identifier or the AVD's system image package.
909
+ lastUsedAt is simctl's last use, or when the emulator
910
+ last wrote the AVD's hardware-qemu.ini. bytes is the
911
+ simulator's data size from simctl; null for AVDs.
912
+ owner is workspace (project and slot say which),
913
+ parked, orphaned (this Stim home created it and no
914
+ workspace holds it), otherStimHome (a stim-* device this
915
+ home has no record of creating, even when a workspace
916
+ names it) or user. Off macOS no simulator is listed
917
+ runtimes { identifier, runtimeIdentifier, version, build, bytes,
918
+ lastUsedAt, deviceCount, command } iOS simulator
919
+ runtimes from \`xcrun simctl runtime list -j\`, then
920
+ any other \`simctl list runtimes\` shows, with bytes
921
+ and command null. deviceCount is the simulators on
922
+ it; command is the \`xcrun simctl runtime delete\`
923
+ line for a deletable runtime, else null. Stim never
924
+ runs it
925
+ systemImages { package, directory, avdCount, command } installed
926
+ Android system images; avdCount is the AVDs whose
927
+ image.sysdir.1 names it; command is the
928
+ \`sdkmanager --uninstall\` line. Stim never runs it
929
+ notices why a listing is missing or partial, such as simctl
930
+ timing out or an AVD or system image folder Stim
931
+ cannot read. A macOS privacy denial (EPERM) names the
932
+ Privacy & Security setting to grant
831
933
  sections one array per report section, in the text order. Every key
832
934
  is present, empty when there is nothing to report:
833
935
  deadProjects { path }
@@ -835,26 +937,42 @@ RULES
835
937
  orphanedPorts { project, label, port }
836
938
  orphanedWorkspaces { dir, projectRoot, bytes } --delete removes the
837
939
  whole workspace directory
838
- linkedWorktrees { path, idleDays, mergedInto, willRemove,
839
- reason, detail } mergedInto is the default
840
- branch HEAD is merged into ("origin/main"),
841
- or null; detail says why it is removed
842
- ("merged into origin/main", "idle 9d") or kept.
843
- Without --worktrees, the source checkout and
844
- roots outside git are left out
940
+ linkedWorktrees { path, idleDays, mergedInto, pullRequest,
941
+ pullRequestUnknown, willRemove, reason,
942
+ detail, eligibleAt } mergedInto is the
943
+ default branch HEAD is merged into
944
+ ("origin/main"), or null. pullRequest is the
945
+ branch's pull request { number, state:
946
+ "open" | "merged" | "closed", url,
947
+ containsHead }, or null; pullRequestUnknown
948
+ says why gh could not answer, else null.
949
+ detail says why it is removed ("merged into
950
+ origin/main", "PR #12 closed", "idle 9d") or
951
+ kept. eligibleAt is the ISO time a
952
+ recent-activity worktree becomes removable,
953
+ else null. Without --worktrees, the source
954
+ checkout and roots outside git are left out
845
955
  parkedSimulators { udid, name, model, runtime, parkedAt, bytes,
846
956
  listed } with --older-than, only those parked
847
957
  at least that long
848
958
  parkedEmulators { name, systemImage, deviceProfile, parkedAt, bytes, listed }
849
959
  likewise
850
960
  orphanedDevices { kind, id, name, bytes, directory }
851
- unverifiedDevices { kind, id, name, command } stim-* devices Stim
852
- has no record of creating; never deleted, run
853
- command yourself
961
+ unverifiedDevices { kind, id, name, command } stim-* devices this
962
+ Stim home has no record of creating; never
963
+ deleted, run command yourself (rm -rf <dir>
964
+ for AVD data with no registration)
854
965
  staleDevices { kind, id, name, project, slot, idleDays,
855
966
  bytes } only with --older-than
856
967
  staleDeviceRecords { kind, id, project, slot } --delete clears
857
968
  the record only
969
+ staleLedgerEntries { kind: "ios" | "android" | "web", id }
970
+ simulators this Stim home created that a
971
+ complete simctl listing no longer shows, AVD
972
+ names with no registration or data in any AVD
973
+ root, and browser profile paths whose
974
+ directory is gone; --delete forgets the
975
+ ledger entry only
858
976
  idleDevices { kind, id, name, project, slot, lastActivityAt,
859
977
  idleForMs, buildInProgress } booted owned
860
978
  devices whose status activity is "idle";
@@ -872,6 +990,12 @@ RULES
872
990
  deviceSweepNotices { message }
873
991
  easSessionSweepNotices { message }
874
992
  skipped { path, detail } not classified as dead
993
+ workspaceLogs { dir, projectRoot, bytes, trimBytes, willTrim,
994
+ reason, detail } logs/ of each workspace;
995
+ trimBytes is what --delete would drop from
996
+ Metro, client and device logs over twice the
997
+ 8 MiB cap; willTrim marks the ones it would
998
+ trim
875
999
  workspaceBuildOutputs { dir, projectRoot, bytes, idleDays, willClear,
876
1000
  reason, detail } derived-data, gradle-build,
877
1001
  android-cas and cache-provider of each
@@ -890,12 +1014,14 @@ RULES
890
1014
  A linked worktree's detail is never null.
891
1015
  workspaceBuildOutputs unresolved | in-use | last-use-unknown |
892
1016
  recently-used
1017
+ workspaceLogs unresolved | in-use | collector
893
1018
  linkedWorktrees not-a-worktree | bare-repository |
894
1019
  source-checkout-unknown | source-checkout |
895
1020
  locked | in-use | status-unreadable | dirty |
896
1021
  unpushed-unchecked | unpushed | submodules |
897
1022
  not-merged | merge-unknown | last-use-unknown |
898
- recently-used
1023
+ recently-used | activity-unknown |
1024
+ recent-activity
899
1025
 
900
1026
  These print the error contract instead and exit 1:
901
1027
 
@@ -906,9 +1032,66 @@ RULES
906
1032
  - --cache together with --worktrees or --idle`
907
1033
  },
908
1034
  status: {
909
- summary: "the status payload's build and device activity fields: a running build, its estimate, each platform's last build, and who drives each device",
1035
+ summary: "the status payload's issues and their codes, build and device activity fields: a running build, its estimate, each platform's last build, who drives each device, whether the app runs on it, and what uses CPU and memory now",
910
1036
  body: () => ` stim status --json
911
1037
 
1038
+ Each environment carries issues, the things in that workspace that need
1039
+ the user, and warnings, the same issues as text ("<slot>: " when not the
1040
+ default slot, then "<message>; run \`<remedy>\`"):
1041
+
1042
+ issues [{ code, severity, message, remedy, workspace, slot? }]
1043
+
1044
+ code port-not-ours something other than this workspace's
1045
+ Metro answers its reserved port
1046
+ sim-missing the recorded simulator no longer exists
1047
+ sim-without-metro the simulator is booted and no Metro
1048
+ serves the workspace
1049
+ avd-serial-changed the owned emulator came back on another
1050
+ serial, so Metro forwarding is lost
1051
+ avd-missing the recorded AVD no longer exists
1052
+ avd-not-detected adb does not see the owned emulator while
1053
+ the workspace expects it: it holds an
1054
+ unexpired lease on its serial, or it
1055
+ launched onto it and has not stopped
1056
+ that slot since, and its dev server runs
1057
+ or the launch was in the last
1058
+ ${RECENT_LAUNCH_MS / 6e4} minutes. An idle workspace's
1059
+ shut-down emulator is android.state
1060
+ "not-detected" with no issue.
1061
+ avd-unchecked the emulator listing could not be read
1062
+ supervisor-unverified a supervisor record whose process status
1063
+ cannot prove gone or ours; stop refuses to
1064
+ signal it
1065
+ browser-unverified the owned Chrome's supervisor or Chrome
1066
+ process cannot be proven gone or ours;
1067
+ stop and stim web refuse to signal it
1068
+ browser-orphaned the owned Chrome runs but its supervisor
1069
+ exited, so page logs are not captured;
1070
+ stim stop closes it
1071
+ severity "error" when stop or start refuses until it is resolved, else
1072
+ "warning"
1073
+ remedy a command to run from workspace, such as "stim android --slot
1074
+ fold"
1075
+ slot absent for the default slot
1076
+
1077
+ A supervisor record whose pid is gone or reused is not an issue: status
1078
+ reports supervisor null, and the next stop or start clears the record.
1079
+ \`stop --slot <name>\` forgets that slot's launch, so a device stopped on
1080
+ purpose is not reported while the shared dev server keeps running.
1081
+
1082
+ An environment where \`stim web\` ran carries web, its Stim-owned Chrome:
1083
+
1084
+ web { browser, version, running, pid, supervisorPid, url, headless,
1085
+ viewport, profile, cdpEndpoint }
1086
+
1087
+ running the browser supervisor and Chrome are both verified live;
1088
+ pid, supervisorPid and cdpEndpoint are null when false
1089
+ cdpEndpoint http://127.0.0.1:<port>, the reserved loopback DevTools
1090
+ endpoint of that Chrome. Attach Playwright MCP
1091
+ (--cdp-endpoint) or agent-browser (--cdp <port>) to it; it
1092
+ reaches only the Stim profile, never your own browser
1093
+ profile the Stim-owned user data directory under STIM_HOME
1094
+
912
1095
  Each booted simulator and detected emulator in environments (and in
913
1096
  slots) carries activity; a shut-down or physical device has none:
914
1097
 
@@ -925,7 +1108,8 @@ RULES
925
1108
  the claim does not record them
926
1109
  lastActivityAt the newest of this device's app log records, this
927
1110
  platform's Metro bundle requests and the workspace's last
928
- Stim run; absent when none is recorded
1111
+ Stim run, rounded down to the minute; absent when none is
1112
+ recorded
929
1113
  basis the evidence behind state, strongest first:
930
1114
  agent-device-claim, agent-device-lease agent-device state,
931
1115
  read only; live only when every recorded process is alive
@@ -941,6 +1125,29 @@ RULES
941
1125
  \`gc --idle <duration>\` shuts down owned devices idle that long
942
1126
  (\`guide cleanup gc\`).
943
1127
 
1128
+ An owned booted simulator or detected emulator also carries app, whether
1129
+ the workspace's app process is alive on it now:
1130
+
1131
+ app { id, state }
1132
+
1133
+ id the bundle identifier or package checked: the one launched on this
1134
+ device, else the project's
1135
+ state "running" a process of that app runs on the device
1136
+ "stopped" no such process: it crashed, was killed or never launched
1137
+ "unknown" the process listing or the app's Info.plist could not be
1138
+ read; never "stopped"
1139
+
1140
+ app is absent when the device is not owned or no app id is known: the
1141
+ process was not checked.
1142
+
1143
+ This is current process state, read from one host ps (simulator apps are
1144
+ host processes) and the same adb shell ps as activity, so a status --watch
1145
+ refresh notices an exit within its 30-second fallback. It is not launch
1146
+ evidence: launched in an ios or android result keeps its own meaning. For
1147
+ "stopped", run \`stim ios\` or \`stim android\` to launch it again. Plain
1148
+ status appends "<id> running", "<id> not running" or "<id> process
1149
+ unknown" to the device line.
1150
+
944
1151
  An environment's metro carries idleStop { reason: "idle", at, idleMinutes }
945
1152
  when its supervisor stopped the dev server for idleness and nothing serves
946
1153
  the port since; plain \`status\` prints "stopped (idle)" (\`guide metro\`).
@@ -980,7 +1187,7 @@ RULES
980
1187
 
981
1188
  lastBuilds { ios?, android? }, each { platform, status, cacheHit,
982
1189
  cacheSkipped, durationMs, fingerprint, startedAt, finishedAt,
983
- errorCode?, missReason? }
1190
+ errorCode?, missReason?, diagnostics? }
984
1191
 
985
1192
  status "ok" or "failed"
986
1193
  cacheHit "local" or "remote" for an app from that cache tier; false
@@ -998,19 +1205,87 @@ RULES
998
1205
  baseline is { fingerprint, from: "workspace" | "project" }, the
999
1206
  cached build compared with; rekeyedBy lists "prebuild" or
1000
1207
  "pod install" when those steps moved the key.
1208
+ diagnostics only on a failed run whose compiler reported errors: up to
1209
+ 5 { file, line, column, message }, null where the compiler
1210
+ gave no position.
1001
1211
 
1002
1212
  Plain status prints "last build: ios local cache in 12s, android compiled
1003
1213
  in 7m02s". To predict the next run instead, see \`guide facts plan\`.
1004
1214
 
1215
+ An environment with a recorded run also carries builds, each platform's
1216
+ last 10 runs, newest first. Its newest entry that is not "interrupted" is
1217
+ the run lastBuilds reports, once a run has recorded builds.
1218
+
1219
+ builds { ios?, android? }, each a list of lastBuilds entries
1220
+ with { result, slot, configuration, cacheKey, phases }
1221
+ result "succeeded", "failed", "cancelled" (Stim stopped the run
1222
+ after an interrupt or \`stim stop\`) or "interrupted": its
1223
+ process ended without recording a result, such as a kill or a
1224
+ second interrupt, so the next run recorded it with
1225
+ null durationMs and finishedAt, no cache facts, and 0 for the
1226
+ phase it stopped in
1227
+ slot the device slot, "default" without --slot
1228
+ configuration the iOS configuration or Android variant the run built,
1229
+ "Debug" or "debug" by default; null when the run ended
1230
+ before resolving it
1231
+ cacheKey the cache key the run looked up or stored under
1232
+ phases milliseconds spent in each phase the run entered, among
1233
+ prepare, cache-lookup, wait, prebuild, pods, compile,
1234
+ install and launch
1235
+
1236
+ Only runs that record a last build are listed: a run that stops before
1237
+ looking up a build, such as a bad flag, is not. Estimates (expectedMs) come from run statistics, not builds.
1238
+
1005
1239
  There is no completion fraction: a compile's log volume depends on what
1006
- is already built, so it does not measure progress.`
1240
+ is already built, so it does not measure progress.
1241
+
1242
+ An environment's memoryMb is an estimate, not a measurement: a fixed amount
1243
+ per booted simulator, detected emulator, running Metro and running Chrome.
1244
+ capacity.committedMb sums it, and the memory budget uses the same estimate.
1245
+ What is using CPU and memory now is the top-level machine section:
1246
+
1247
+ machine null, or { owners: [{ kind, name, workspace, slot?, id, owned,
1248
+ cpuPercent, residentMb, processes }] }
1249
+
1250
+ kind simulator a booted simulator's launchd_sim tree
1251
+ emulator an emulator's launcher and qemu tree, by its -avd
1252
+ metro a workspace's supervisor and Metro trees
1253
+ build a running ios or android run's process tree,
1254
+ xcodebuild, Gradle and compilers included
1255
+ browser the Chrome of \`stim web\` and its supervisor
1256
+ server stim-server
1257
+ shared machine-wide processes no workspace owns:
1258
+ CoreSimulator services, adb server, Gradle and
1259
+ Kotlin daemons, Watchman, the emulator's netsimd
1260
+ workspace the workspace that records the device or runs the process;
1261
+ null for a device no workspace records and for server and
1262
+ shared
1263
+ id the simulator's UDID, the AVD name, Metro's port or the
1264
+ build's platform; null otherwise
1265
+ owned true for what Stim started and stops: a workspace's owned
1266
+ device (\`stim stop --slot <slot>\`), its Metro (\`stim
1267
+ stop\`), build or Chrome. Never act on an owner with false.
1268
+ cpuPercent ps %CPU summed over the owner's processes; 100 is one core
1269
+ residentMb summed resident set size. Pages shared between processes
1270
+ count once per process, so a simulator reports well above
1271
+ its physical footprint.
1272
+
1273
+ Each process counts in exactly one owner, the one whose root process is
1274
+ its nearest ancestor, so the supervisor a build started counts as Metro,
1275
+ not as the build, and a Gradle or Kotlin daemon counts in the build that
1276
+ started it until that build exits, then as shared. Processes with no owner
1277
+ are left out. machine comes from one host ps, the one status reads for
1278
+ device activity, and is null when no simulator is booted, no workspace is
1279
+ live and no build runs: status then reads no process table. \`status --watch --json\` rereads it every 15 seconds while
1280
+ machine is not null, with no other subprocess.`
1007
1281
  },
1008
1282
  plan: {
1009
- summary: "the ios and android --plan payload: fingerprint, cacheHit, prebuild, expectedMs and basis",
1283
+ summary: "the ios and android --plan payload: fingerprint, cacheHit, prebuild, missReason, expectedMs and basis",
1010
1284
  body: () => ` stim ios --plan --json # or: stim android --plan --json
1011
1285
 
1012
1286
  { platform, slot?, fingerprint, cacheKey, cacheHit, provider,
1013
- cacheSkipped, prebuild, outcome, expectedMs, basis, refusal? }
1287
+ cacheSkipped, prebuild, outcome, expectedMs, basis, missReason?,
1288
+ refusal? }
1014
1289
 
1015
1290
  fingerprint the fingerprint the run would look up first; with
1016
1291
  --eas-profile, the one EAS CLI computes
@@ -1025,6 +1300,15 @@ RULES
1025
1300
  expectedMs the median wall time of this project's last successful runs
1026
1301
  with that outcome on that platform, or null with none
1027
1302
  basis how many runs expectedMs comes from (at most 10)
1303
+ missReason on a predicted cold build with cache reads on, why the
1304
+ cache has no app, in the shape of lastBuilds.<platform>
1305
+ .missReason (\`guide facts status\`), compared with the same
1306
+ baseline the run would use. kind is "changed",
1307
+ "no-baseline", "same-sources" or "prebuild-pending": a
1308
+ baseline exists and the run would prebuild first, which the
1309
+ plan does not, so changes compare the fingerprint before that
1310
+ prebuild; changeCount 0 then means those inputs match the
1311
+ baseline. rekeyedBy is empty.
1028
1312
  refusal { code, message, remedy } when the run would refuse:
1029
1313
  STIM_PREBUILD_FAILED for a tracked native dir the fingerprint
1030
1314
  leaves out, STIM_EAS_BUILD_MISSING for an EAS miss
@@ -1298,7 +1582,16 @@ their owning workspaces.
1298
1582
 
1299
1583
  The machine registry, under STIM_HOME, serializes allocation and cleanup.
1300
1584
  The workspace is the nearest package.json directory, resolved through
1301
- symlinks. Use the same package directory for every command in a monorepo.
1585
+ symlinks. In a monorepo, a package that depends on neither react-native nor
1586
+ expo, such as a Vite web app, uses the one Stim app registered in the same
1587
+ git worktree instead, and says so on stderr. ports, web, settings, logs,
1588
+ reload, stop and status follow this rule (status stars the app instead of
1589
+ printing the note); other commands use the nearest package.json. Register the app first: run stim ports get <label>, stim start,
1590
+ stim ios or stim android from the app directory. Every ports command then
1591
+ acts on the app's ports, so ports stop without a label there also stops the
1592
+ app's other labels. A package that already holds ports keeps them; release
1593
+ them to move to the app. With no registered app, or more than one, the
1594
+ nearest package.json stays the workspace; run ports from the app directory.
1302
1595
  The allocation reserves a number, not a listening socket. Another process
1303
1596
  can bind it before your server does. Use strict-port behavior when supported
1304
1597
  and verify the server bound the number supplied. Stim does not start,
@@ -1340,7 +1633,10 @@ discovered, not enumerated.
1340
1633
  metro.ndjson, client.ndjson and device.ndjson are size-capped. At about 8 MiB a file
1341
1634
  becomes <name>.1, replacing the previous .1, and a new file starts. Queries read
1342
1635
  both generations, so the oldest records drop off first. History and --since
1343
- reach back only as far as those records.
1636
+ reach back only as far as those records. A file written before the cap
1637
+ existed can be larger; \`gc\` reports each workspace's log size and \`gc
1638
+ --delete\` trims a file over 16 MiB to its newest 8 MiB in a workspace not in use
1639
+ (\`guide cleanup disk\`).
1344
1640
 
1345
1641
  EXIT 0 MEANS THE QUERY SUCCEEDED, whether or not records matched. A clean
1346
1642
  \`stim logs --errors\` check requires exit code 0 AND no matching errors in
@@ -1370,18 +1666,23 @@ FLAGS
1370
1666
  --tail <n> only the last n MATCHING records (applied after filtering,
1371
1667
  so --level error --tail 5 is the last five ERRORS)
1372
1668
  --errors errors and fatals since the last marker, from metro, client
1373
- and build, plus confirmed native app-crash reports.
1669
+ and build, plus confirmed native app-crash reports and the
1670
+ owned Chrome's device errors (platform web: failed
1671
+ requests, browser errors).
1374
1672
  Capped at 20 printed records.
1375
1673
  --follow keep streaming until interrupted (Ctrl+C is exit 0)
1376
1674
  --json the raw records, one per line, so stdout is valid NDJSON.
1377
1675
  ZERO matches is ZERO bytes on stdout (an empty NDJSON
1378
1676
  stream), exit 0 -- parse stdout line by line, never as one
1379
1677
  JSON document. The "No matching log records" note is human
1380
- mode only, on stderr.
1678
+ mode only, on stderr. With --errors, an Expo error record
1679
+ also carries context (see THE RECORD).
1381
1680
 
1382
1681
  --ERRORS, PRECISELY
1383
1682
  Level error or fatal, from metro, client and build, plus device records with
1384
- event native_crash (app/device/time-correlated OS reports or fatal app console output), timestamped after the
1683
+ event native_crash (app/device/time-correlated OS reports or fatal app console output)
1684
+ and device records with platform web (the owned page's failed requests and
1685
+ browser errors, from stim web), timestamped after the
1385
1686
  marker that closes their window. Three rules, and a field test
1386
1687
  caught all three wrong at once -- it returned 3,004 iOS syslog lines on a
1387
1688
  healthy app while hiding a real startup crash.
@@ -1390,7 +1691,10 @@ FLAGS
1390
1691
  \`simctl log stream\` is predicated on the app's PROCESS, and inside that
1391
1692
  process Apple's frameworks log thousands of Error-typed lines (nw_socket,
1392
1693
  SecTrust, WebKit, CoreUI) that have nothing to do with your app. The proven
1393
- ones are demoted to info by the collector; the scope rule covers the rest.
1694
+ ones are demoted to info by the collector, and iOS main-thread
1695
+ "Synchronous URL loading" performance diagnostics (Expo icons served by
1696
+ Metro raise them) to warn, so a launch does not count them as app errors;
1697
+ the scope rule covers the rest.
1394
1698
  The metro stream carries exactly one demotion of its own, and it is Stim's
1395
1699
  doing: the dev-client deep link \`ios\`/\`android\` open to wire the app to
1396
1700
  your port arrives inside the app as a link, and React Navigation logs at
@@ -1416,8 +1720,15 @@ FLAGS
1416
1720
  errors are reported -- and nothing else. A failed attempt's own summary
1417
1721
  and details land at or after its marker, so they stay reported.
1418
1722
  a LAUNCH marker (src build, written before \`ios\` / \`android\` attempts
1419
- launch) resets EVERYTHING. It precedes the tool call so an immediate
1420
- native crash is not hidden by a marker written after the process died.
1723
+ launch) resets EVERYTHING except the web page's records. It precedes the
1724
+ tool call so an immediate native crash is not hidden by a marker written
1725
+ after the process died.
1726
+ a PAGE-LOAD marker (the web_navigation record \`stim web\` writes for each
1727
+ load of the page's top-level document) resets only the page's records,
1728
+ those with platform web. It is written when the load starts, so a
1729
+ failure logged in the same millisecond belongs to that load, and so does
1730
+ anything the old page logs before it is replaced. Until the first page
1731
+ load is logged, the page's records follow the launch windows.
1421
1732
  A finished bundle is not evidence that the app which loaded it is fine.
1422
1733
  In the field case the app threw at 16:03:54 and Metro wrote its marker at
1423
1734
  16:03:55, one second later, because the bundler finishes accounting for a
@@ -1438,11 +1749,18 @@ FLAGS
1438
1749
  not stack depth. For untouched captured records use:
1439
1750
  stim logs --source all --json
1440
1751
  Neither form can restore text the runtime truncated before capture.
1441
- In non-follow human output, an Expo error includes its immediately
1442
- following code frame and Call Stack lines. Bare React Native symbolication is
1443
- shown as separate context because Metro does not provide an error correlation
1444
- identifier. Context does not change the error count or the raw error records
1445
- returned by --json. --json is never capped, and neither is an explicit --tail.
1752
+ With --errors, an Expo error includes the code frame and stack lines Expo
1753
+ printed after it, in human and --json output, with or without --follow.
1754
+ Stim's own Metro records that land between those lines, such as bundle
1755
+ responses, do not cut the stack short. Human output joins the lines to the
1756
+ message. --json leaves msg as captured and adds them to the error record as
1757
+ a context array of strings; each record is still one line. --follow holds an
1758
+ Expo error until a poll, about every 500 ms, adds no more of its lines, so
1759
+ it can print up to about a second after Expo does. Non-follow human output
1760
+ also shows bare React Native symbolication as separate context, because
1761
+ Metro does not provide an error correlation identifier. Context never
1762
+ changes the error count or adds records to --json. --json is never capped,
1763
+ and neither is an explicit --tail.
1446
1764
 
1447
1765
  In --follow mode the marker window is dropped -- every error arriving from
1448
1766
  then on is by definition after the last marker seen.
@@ -1464,6 +1782,10 @@ THE RECORD
1464
1782
  A collector_clock warning then says timestamps retain device time.
1465
1783
  raw true when the level was inferred from a line of text rather than
1466
1784
  reported by the producer (every expo-child record)
1785
+ context --errors --json only: the code frame and stack lines Expo
1786
+ printed after this error, as an array of strings. Absent when
1787
+ there are none. Stim adds it at query time; it is not in the
1788
+ log files.
1467
1789
 
1468
1790
  WHAT WRITES WHAT
1469
1791
  metro.ndjson the bundler, in both supervisor modes
@@ -1479,7 +1801,8 @@ WHAT WRITES WHAT
1479
1801
  is where a native crash that never reached JS shows up
1480
1802
  -- and, on iOS, where every Apple framework running in
1481
1803
  the app's process also logs. The proven noise sources
1482
- are recorded at info rather than error; the rest is why
1804
+ are recorded at info rather than error (Synchronous
1805
+ URL loading diagnostics at warn); the rest is why
1483
1806
  --errors leaves this source out unless asked. A VERIFIED
1484
1807
  LAUNCH counts these records and prints one line:
1485
1808
 
@@ -1741,11 +2064,17 @@ Branch on the code, never on the message.`,
1741
2064
  the source checkout, then warm again.`
1742
2065
  },
1743
2066
  STIM_BUILD_FAILED: {
1744
- summary: "xcodebuild or gradle failed; the two Android APK refusals; a damaged compilation-cache object",
2067
+ summary: "xcodebuild or gradle failed; the Android missing-SDK and APK refusals; a damaged compilation-cache object",
1745
2068
  body: () => `STIM_BUILD_FAILED
1746
2069
  xcodebuild or gradle failed. The EXTRACTED diagnostics are printed (capped),
1747
2070
  not the transcript. Read the log path on the next line for the rest.
1748
- Two Android refusals share this code without gradle itself failing:
2071
+ Three Android refusals share this code without gradle itself failing:
2072
+ - NO ANDROID SDK, before gradle starts: nothing exists at the SDK path
2073
+ Stim resolves (ANDROID_HOME, else ANDROID_SDK_ROOT, else the default
2074
+ location), and there is no android/local.properties. Set ANDROID_HOME to the SDK or write sdk.dir
2075
+ into android/local.properties; \`stim doctor --platform android\` reports
2076
+ the same condition. When the SDK is found and neither variable is set,
2077
+ Stim passes its path to gradle as ANDROID_HOME.
1749
2078
  - MORE THAN ONE debug APK under android/app/build/outputs/apk and nothing
1750
2079
  configured to pick one (a project with product flavors, several flavors
1751
2080
  already built). Stim will not guess which flavor to install: the
@@ -2248,6 +2577,12 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
2248
2577
  \`gc --delete\`) and run \`stim ios\` again to create the requested model.
2249
2578
  That loses the old sim's app state.
2250
2579
 
2580
+ "this project's sim runs iOS X, but --runtime asked for Y"
2581
+ The same refusal for an explicit \`--runtime\` that names another installed
2582
+ iOS version than the project's sim runs. Reap the sim the same way, or pass
2583
+ \`--slot <name>\` to create one on the requested runtime beside it. The
2584
+ ios.runtime setting alone never refuses; it applies at creation.
2585
+
2251
2586
  "this project's emulator uses device profile X, but Y was requested"
2252
2587
  The Android counterpart, from \`--device-profile\` or android.deviceProfile.
2253
2588
  Reap the AVD the same way, or pass \`--slot <name>\` to create the requested
@@ -2428,10 +2763,11 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
2428
2763
  metro.publicUrl for an existing endpoint.`
2429
2764
  },
2430
2765
  STIM_RELOAD_AMBIGUOUS: {
2431
- summary: "both owned apps are live; name the platform",
2432
- separator: "--- RELOAD CODES (`stim reload [ios|android]`) ---",
2766
+ summary: "more than one owned app or the owned Chrome is live; name the platform",
2767
+ separator: "--- RELOAD CODES (`stim reload [ios|android|web]`) ---",
2433
2768
  body: () => `STIM_RELOAD_AMBIGUOUS
2434
- Both owned apps are live. Name ios or android; Stim never guesses.`
2769
+ More than one of the owned iOS app, Android app and Chrome page is live.
2770
+ Name ios, android or web; Stim never guesses.`
2435
2771
  },
2436
2772
  STIM_RELOAD_RELEASE: {
2437
2773
  summary: "the live app has embedded JS; run a Debug build first",
@@ -2444,7 +2780,10 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
2444
2780
  aliases: ["STIM_RELOAD_UNOWNED", "STIM_RELOAD_PROBE_FAILED"],
2445
2781
  body: () => `STIM_RELOAD_STOPPED / STIM_RELOAD_UNOWNED / STIM_RELOAD_PROBE_FAILED
2446
2782
  The recorded app is gone, its exact device is not live and owned by this
2447
- workspace, or simctl/adb could not prove the process exists. No launch or
2783
+ workspace, or simctl/adb could not prove the process exists. For web,
2784
+ STOPPED means no owned Chrome is running (run stim web) and PROBE_FAILED
2785
+ that its identity could not be verified. A bare reload picks the Chrome page
2786
+ only when no native launch is recorded. No launch or
2448
2787
  device lifecycle action is taken; follow the printed platform-command or
2449
2788
  process-probe remedy.`
2450
2789
  },
@@ -2476,6 +2815,51 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
2476
2815
  MORE THAN ONE MATCHING PEER IS NOT A FAILURE. A workspace Metro serves one
2477
2816
  app, so several matching peers are that app on several devices. Stim reloads
2478
2817
  every one of them and reports the count in the facts as targets.`
2818
+ },
2819
+ STIM_WEB_NO_CHROME: {
2820
+ summary: "stim web found no installed Chrome or Chromium",
2821
+ separator: "--- WEB CODES (`stim web`) ---",
2822
+ body: () => `STIM_WEB_NO_CHROME
2823
+ stim web drives the installed Google Chrome (or Chromium) with a profile
2824
+ Stim creates. It looks in /Applications and ~/Applications on macOS, in
2825
+ Program Files on Windows, and for google-chrome or chromium on PATH. Stim
2826
+ never installs a browser. Install Chrome, then run stim doctor.`
2827
+ },
2828
+ STIM_WEB_NO_URL: {
2829
+ summary: "not an Expo app and web.url is unset, so Stim does not know which page to open",
2830
+ body: () => `STIM_WEB_NO_URL
2831
+ Only Expo serves web from Metro, so for any other app Stim needs web.url.
2832
+ stim web never starts a web server, for any framework. Start yours on a
2833
+ named port and point web.url at it:
2834
+ pnpm exec vite --port "$(stim ports get web)" --strictPort
2835
+ stim settings set web.url 'http://localhost:{port:web}/' --scope workspace
2836
+ See stim guide web.`
2837
+ },
2838
+ STIM_WEB_DEPS_MISSING: {
2839
+ summary: "an Expo app without react-native-web cannot render on the web",
2840
+ body: () => `STIM_WEB_DEPS_MISSING
2841
+ The Expo app does not resolve react-native-web, so Metro cannot build a web
2842
+ bundle. Install the web dependencies, start Metro, then run stim web again:
2843
+ npx expo install react-dom react-native-web @expo/metro-runtime
2844
+ stim start`
2845
+ },
2846
+ STIM_WEB_BROWSER_HELD: {
2847
+ summary: "the previous owned Chrome could not be stopped or verified; it was left running",
2848
+ body: () => `STIM_WEB_BROWSER_HELD
2849
+ stim web replaces the workspace's owned Chrome when its options change, and
2850
+ the previous one could not be stopped: its supervisor or Chrome did not
2851
+ exit, or their process identities could not be verified. Stim never signals
2852
+ a process it cannot verify. Check stim status for browser-unverified, then
2853
+ follow stim guide errors teardown.`
2854
+ },
2855
+ STIM_WEB_LAUNCH_FAILED: {
2856
+ summary: "the owned Chrome did not start or did not open DevTools on its reserved port",
2857
+ body: () => `STIM_WEB_LAUNCH_FAILED
2858
+ The browser supervisor exited before Chrome answered on its reserved
2859
+ DevTools port. The remedy names the supervisor log; web.ndjson carries the
2860
+ failure as web_browser_failed, and browser.log holds Chrome's own output.
2861
+ A port another process bound first also lands here: run stim web again to
2862
+ reserve a fresh one.`
2479
2863
  },
2480
2864
  STIM_WORKTREE_REMOVAL_IN_PROGRESS: {
2481
2865
  summary: "a managed remote start found worktree remove holding the lock; wait, then rerun",
@@ -2574,7 +2958,11 @@ captured" (in metro.ndjson, bare RN)
2574
2958
  ios.deviceType, ios.runtime, android.systemImage or android.deviceProfile
2575
2959
  setting is checked the same way, and the check applies
2576
2960
  even when this workspace ALREADY owns a device, so a name that could never
2577
- create anything is caught rather than left to a later run.
2961
+ create anything is caught rather than left to a later run. The
2962
+ \`pixel_fold\` and \`resizable\` profiles on a system image without
2963
+ foldable support (SupportPixelFold in its advancedFeatures.ini) refuse the
2964
+ same way, since the emulator quits on boot; the remedy names an installed
2965
+ image that has it, or an sdkmanager install when none does.
2578
2966
  \`gc --json --cache <name>\` refuses with STIM_BAD_ARG when no shared cache
2579
2967
  carries the name; the remedy names the caches on this machine. \`gc\`
2580
2968
  refuses --cache together with --worktrees the same way.
@@ -2643,9 +3031,17 @@ captured" (in metro.ndjson, bare RN)
2643
3031
  summary: "a lock held past the wait; workspace-process and short directory locks",
2644
3032
  body: () => `STIM_LOCK_TIMEOUT
2645
3033
  The same locks, held by an ordinary command that is still running, for
2646
- longer than the wait -- 60s by default, 4 minutes for the remote-session and
2647
- EAS project locks, and ~90 minutes for the \`worktree warm\` lock, which one
2648
- \`--refresh\` can hold for a whole dependency install. That wait prints its
3034
+ longer than the wait -- 60s by default, 4 minutes for the remote-session lock
3035
+ and for \`gc\` deleting EAS sessions under the EAS project lock (its sweep
3036
+ skips at once while that lock is held), and ~90 minutes for the \`worktree warm\` lock, which one
3037
+ \`--refresh\` can hold for a whole dependency install. The machine-wide EAS
3038
+ project lock admits one EAS session start at a time. A \`--remote eas\` run
3039
+ waits on it after its build with no overall limit, printing \`lock waiting
3040
+ for EAS remote start (pid 41233, in <workspace>, running for 1m12s) to
3041
+ release the EAS project lock\` at once and \`still waiting for ...\` every 30
3042
+ seconds. It refuses only when one holder has held the lock for longer than
3043
+ the slowest EAS session start (39 minutes), naming that pid.
3044
+ The \`worktree warm\` wait prints its
2649
3045
  elapsed waiting time and holder every 30 seconds (\`lock waiting 40s
2650
3046
  for stim worktree warm --refresh (pid 41233)\`) and the refusal names the same holder
2651
3047
  and the lock directory under ~/.stim/warm-locks.
@@ -2658,8 +3054,10 @@ captured" (in metro.ndjson, bare RN)
2658
3054
  "Timed out waiting for the lock at <path>."
2659
3055
  Short directory locks serialize writes to config, workspace state, device
2660
3056
  leases, ownership records, metadata, and cache manifests. The path identifies
2661
- the lock. These locks wait up to 12s and never expire based on age. Wait for
2662
- the command holding it; if none is running, remove the named directory.
3057
+ the lock. These locks wait up to 12s and never expire based on age. When
3058
+ another Stim command holds the lock, the message says so: wait for it and
3059
+ retry. A failed marker write, such as ENOSPC on a full disk, removes the
3060
+ directory it just created before Stim reports the write error.
2663
3061
  Short locks use the same process-identity claims as long operations, stored
2664
3062
  beside the visible directory at <path>.claims. An opaque marker in the visible
2665
3063
  directory also excludes older Stim versions. Only the exclusive claim holder
@@ -2669,9 +3067,12 @@ captured" (in metro.ndjson, bare RN)
2669
3067
  STIM_CLAIM_REFUSED and names the claim to inspect; an unavailable native
2670
3068
  identity reports STIM_CLAIM_UNAVAILABLE without running the protected work.
2671
3069
  Older lock directories have no process identity. A visible directory left
2672
- empty before marker publication or during final removal also cannot prove it is free.
2673
- These paths still time out; verify that no holder is running before removing
2674
- only the named directory. Standalone Metro and Expo cache packages use the
3070
+ empty before marker publication (a process killed between creating it and
3071
+ writing its marker) or during final removal also cannot prove it is free.
3072
+ These paths still time out, and the message says no current Stim holds the
3073
+ lock and ends with \`rm -rf '<path>'\`. An older Stim version may still be
3074
+ using it: if none is running, remove the named directory with that command.
3075
+ Standalone Metro and Expo cache packages use the
2675
3076
  same core protocol and do not require the Stim CLI.`
2676
3077
  },
2677
3078
  teardown: {
@@ -2708,7 +3109,23 @@ the supervisor could not be verified" (stop)
2708
3109
  "teardown failed: <reason>"
2709
3110
  Stim could not release the owned device and keeps its record for a retry.
2710
3111
  \`worktree remove\` exits 1 without removing the worktree while the device is
2711
- still tracked. Fix the reported cause and re-run.`
3112
+ still tracked. Fix the reported cause and re-run.
3113
+
3114
+ "teardown failed: Owned AVD <name> did not finish shutting down after <n>s:
3115
+ emulator process <pid> is still running"
3116
+ An owned emulator counts as stopped only when no process launched for its
3117
+ AVD (\`qemu-system-*\` or \`emulator\` with \`-avd <name>\`) is left in the
3118
+ process table. Stim asks it to quit with \`adb emu kill\` and waits 60s.
3119
+ Then it sends SIGTERM and, 5s later, SIGKILL. When adb cannot reach the
3120
+ emulator, Stim sends SIGTERM at once and waits 60s before SIGKILL. It signals
3121
+ only a pid whose command line names the AVD and whose process identity,
3122
+ recorded before shutdown, still matches. "(Stim could not verify the
3123
+ identity of <pid>, so it sent no signal)" means that identity could not be
3124
+ read or no longer matches. Check \`ps -p <pid> -o command=\`, stop the
3125
+ process yourself, then re-run the command or \`gc --delete\`. Windows has
3126
+ no process-table check and Stim signals nothing there: it waits for the pid
3127
+ in the AVD's process lock after \`adb emu kill\`, and refuses at once when
3128
+ adb cannot reach the emulator.`
2712
3129
  },
2713
3130
  remove: {
2714
3131
  summary: "worktree remove refused a dirty tree: what it restores itself and what --force discards",
@@ -3111,13 +3528,13 @@ ANDROID EMULATOR RESTARTS
3111
3528
  started is alive. Stim keeps waiting on the same emulator; it never
3112
3529
  launches a duplicate. See \`stim guide errors STIM_NO_DEVICE\` for the
3113
3530
  wait windows, diagnostics, and recovery steps.
3114
- On macOS, top-level androidEmulatorApp "stim-desktop" in the machine config
3115
- boots owned emulators without a window and shows them in Stim Desktop; see
3116
- guide settings. An emulator that is already running keeps its current
3531
+ On macOS, androidEmulatorApp "stim-desktop", the default while Stim Desktop
3532
+ is installed, boots owned emulators without a window and shows them in Stim
3533
+ Desktop; see guide settings. An emulator that is already running keeps its current
3117
3534
  display until it next boots.
3118
3535
 
3119
3536
  DESTRUCTIVE COMMANDS -- ask the user first
3120
- gc --delete deletes orphaned devices Stim created, tens of GB
3537
+ gc --delete deletes orphaned devices this Stim home created, tens of GB
3121
3538
  gc --delete --cache all empties the shared build caches every project uses
3122
3539
  gc --delete --cache <name>
3123
3540
  empties only the caches that carry <name>
@@ -3170,8 +3587,10 @@ WAITING FOR A CHANGE
3170
3587
  status payload per line: one at once, then one each time the payload
3171
3588
  changes, never two identical ones in a row. It reacts to Stim state files,
3172
3589
  the EAS session ledger, adb device arrivals and departures, and simulator
3173
- state, and recomputes every 30 seconds as a fallback. Ctrl+C, SIGTERM or
3174
- closing its stdout ends it with exit 0. Without --json it reprints the human view on change.`,
3590
+ state, and recomputes every 30 seconds as a fallback. A log append updates
3591
+ only the log error count and device activity, no sooner than 15 seconds
3592
+ after the previous refresh. Ctrl+C, SIGTERM or closing its stdout ends it
3593
+ with exit 0. Without --json it reprints the human view on change.`,
3175
3594
  sections: {
3176
3595
  eas: {
3177
3596
  summary: "download a matching EAS development build; explicit profile, costs, cache and miss remedies",
@@ -3657,7 +4076,11 @@ PREDICTING THE NEXT BUILD (--plan)
3657
4076
  the cache.provider tier into a temporary directory the plan removes, the
3658
4077
  app config's provider wherever it keeps its downloads, as during a run.
3659
4078
  On a miss it adds the prebuild decision the run would make (generate,
3660
- regenerate, none, or refuse with STIM_PREBUILD_FAILED). With --eas-profile it asks EAS for a matching
4079
+ regenerate, none, or refuse with STIM_PREBUILD_FAILED) and, with cache
4080
+ reads on, the miss reason the run would print, such as "native dependency
4081
+ added: expo-clipboard". A plan never prebuilds, so when the run would and a
4082
+ baseline exists, the reason's kind is prebuild-pending and it compares the
4083
+ fingerprint before that prebuild. With --eas-profile it asks EAS for a matching
3661
4084
  build (config, fingerprint:generate, build:list) and downloads nothing.
3662
4085
  expectedMs is the median of this project's recorded runs with that outcome;
3663
4086
  the payload is in \`guide facts plan\`.
@@ -3895,7 +4318,10 @@ THE BUILD CACHE HAS THREE LEVELS
3895
4318
  records the reason as lastBuilds.<platform>.missReason in status --json
3896
4319
  (see \`guide facts status\`), and writes the changed sources (capped at 20
3897
4320
  names) to the build log as a fingerprint_diff record. With no baseline the
3898
- line says there was nothing to compare with.
4321
+ line says there was nothing to compare with. status --json also lists each
4322
+ platform's last 10 runs in builds.<platform>, with their result, cache
4323
+ outcome, miss reason, phase timings and diagnostics, so a hit that turned
4324
+ into a miss shows which run changed what.
3899
4325
 
3900
4326
  THE KEY CAN MOVE MID-RUN, and the run says so in two facts rather than two
3901
4327
  explanations. \`expo prebuild\` and \`pod install\` rewrite fingerprinted
@@ -3932,11 +4358,28 @@ THE BUILD CACHE HAS THREE LEVELS
3932
4358
  \`expo prebuild --clean\` yourself or delete the directory so the next build
3933
4359
  regenerates it.
3934
4360
 
3935
- If the iOS fingerprint after prebuild or pod install is unavailable, Stim
3936
- installs the build but skips local storage and remote uploads. fingerprint
3937
- and cacheKey are null in the result and lastBuild; the old key is not reused.
3938
- Android does the same if its post-Gradle fingerprint cannot be computed.
3939
- These null fields mean unavailable cache information, not an install failure.
4361
+ Both platforms fingerprint once more after the compile. A change the build
4362
+ itself makes to a fingerprinted file under node_modules/ or the native
4363
+ directory moves the key the same way, printed as \`(after Gradle)\` or
4364
+ \`(after xcodebuild)\`. Any other input that changed while the build ran
4365
+ -- the app config or a config plugin at any point after the first lookup,
4366
+ or any other source during the compile -- means the artifact may not match
4367
+ the key, so Stim installs what it built and stores nothing. The one config
4368
+ change exempted is the ios.bundleIdentifier or android.package that
4369
+ \`expo prebuild\` adds to a static app.json that lacks one:
4370
+
4371
+ fingerprint expoConfig changed while the build ran, so the artifact may not match its key; the build will be installed but not cached
4372
+
4373
+ The prebuild record never takes a hash that includes the edit, so the next
4374
+ run regenerates a CNG directory from the edited config and compiles it
4375
+ instead of reusing the stale one. An edit under node_modules/ or the native
4376
+ directory during the compile cannot be told apart from the build's own
4377
+ writes and moves the key. Stim also skips storing when the fingerprint
4378
+ after prebuild, pod install or the compile cannot be computed. In every
4379
+ skipped case nothing goes to the local cache, the cache provider or a
4380
+ remote upload, fingerprint and cacheKey are null in the result and
4381
+ lastBuild, and the old key is not reused. These null fields mean
4382
+ unavailable cache information, not an install failure.
3940
4383
 
3941
4384
  WHAT MAKES THE CACHE ACTUALLY HIT: .FINGERPRINTIGNORE
3942
4385
  Every entry is keyed on what the tree hashes, so two workspaces share an
@@ -4357,13 +4800,22 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
4357
4800
  (\`iOS 26.5\`), exactly; no prefix or suffix matches.
4358
4801
 
4359
4802
  These flags describe a device that does not exist yet. When this workspace
4360
- ALREADY owns a simulator and \`--device-type\` names a different model,
4361
- Stim refuses rather than silently booting the wrong one: reap the current
4362
- sim with \`stim worktree remove\` (or \`stim gc --delete\`), then run
4363
- \`stim ios\` again to create the requested one. \`--device-profile\` does
4364
- the same for an AVD of another profile. To keep both devices, give the new
4365
- one its own \`--slot\`. \`--runtime\` and \`--system-image\` apply at
4366
- creation only, so an existing device keeps the version it was made with.
4803
+ ALREADY owns a simulator and \`--device-type\` names a different model, or
4804
+ \`--runtime\` a different iOS version, Stim refuses rather than silently
4805
+ booting the wrong one: reap the current sim with \`stim worktree remove\`
4806
+ (or \`stim gc --delete\`), then run \`stim ios\` again to create the
4807
+ requested one. \`--device-profile\` does the same for an AVD of another
4808
+ profile. To keep both devices, give the new one its own \`--slot\`. The
4809
+ ios.runtime and android.systemImage settings apply at creation only, so an
4810
+ existing device keeps the version it was made with. An explicit \`--system-image\` that
4811
+ names another image than this workspace's AVD refuses the same way, unless
4812
+ that AVD never finished a boot (for example one made from an image its
4813
+ profile cannot boot): Stim then deletes it through owned-device teardown and
4814
+ creates the requested one. \`--device-profile pixel_fold\` or
4815
+ \`resizable\` on a system image without foldable support (SupportPixelFold
4816
+ in its advancedFeatures.ini) refuses with STIM_BAD_ARG before anything is
4817
+ created, since the emulator quits on boot; the remedy names an installed
4818
+ image that has it.
4367
4819
  The --json payload reports what was actually used: \`deviceType\` and
4368
4820
  \`runtime\` on iOS, \`systemImage\` and \`deviceProfile\` on Android, read
4369
4821
  from the device itself, so a settings-driven run reports them too.`
@@ -4723,9 +5175,10 @@ HOST MEMORY PRESSURE AND STALLED SIMULATORS
4723
5175
  boot (including the initial boot request) up to 10 minutes, and app installation
4724
5176
  up to 5 minutes. Process termination confirmation can take another 10 seconds;
4725
5177
  a final boot-state query can take 30 seconds. After boot, Stim opens the owned
4726
- simulator in the selected Xcode's Device Hub on Xcode 27, or Simulator on
4727
- older Xcode. Top-level iosSimulatorApp in the machine config can select
4728
- Siniulator or Stim Desktop instead; see guide settings. Override it for one
5178
+ simulator in Stim Desktop while it is installed, otherwise in the selected
5179
+ Xcode's Device Hub on Xcode 27, or Simulator on older Xcode. Top-level
5180
+ iosSimulatorApp in the machine config can select another viewer; see guide
5181
+ settings. Override it for one
4729
5182
  local run with \`stim ios --simulator-app siniulator\`,
4730
5183
  \`--simulator-app stim-desktop\`, or \`--simulator-app xcode\`. This also
4731
5184
  opens an already running owned simulator without rebooting it or saving the
@@ -4734,6 +5187,8 @@ HOST MEMORY PRESSURE AND STALLED SIMULATORS
4734
5187
  and takes at most 10 seconds. A timeout is not proof of an app crash or OOM.
4735
5188
  During boot, Stim reports elapsed time, the simulator name, last boot output,
4736
5189
  current pressure and the highest observed pressure roughly every 15 seconds.
5190
+ Warning or critical pressure adds recovery advice to the first report at
5191
+ that level; later reports repeat it only after the level changes.
4737
5192
  Failure diagnostics retain the highest pressure and unavailable sample count;
4738
5193
  they do not infer the cause of a timeout. Monitoring stops when boot ends.
4739
5194
  Monitoring and timeout handling are best-effort: synchronous CLI work can
@@ -4745,7 +5200,11 @@ HOST MEMORY PRESSURE AND STALLED SIMULATORS
4745
5200
 
4746
5201
  If pressure is elevated, free host memory before retrying. Use \`stim stop\`
4747
5202
  only in workspaces you own and have finished using; ask before closing other
4748
- agents' simulators or heavy apps. Rebooting a simulator under the same pressure
5203
+ agents' simulators or heavy apps. Never quit Device Hub or Simulator.app to
5204
+ free memory: on Xcode 27 (confirmed on 27A266a), quitting Device Hub by
5205
+ default shuts down every booted simulator on the machine, including ones it
5206
+ never opened a window for and ones other workspaces or agents are using.
5207
+ Rebooting a simulator under the same pressure
4749
5208
  can repeat the stall. Consider fewer concurrent builds/devices (guide lifecycle
4750
5209
  concurrency) and a reviewed SimSlim profile for future runs.`
4751
5210
  }
@@ -4768,11 +5227,14 @@ WHAT RECLAIMS AN OWNED DEVICE
4768
5227
  stim worktree remove parks eligible owned simulators and emulators
4769
5228
  (\`guide lifecycle pool\`); deletes them when
4770
5229
  parking is disabled or their setup cannot be verified
4771
- stim gc --delete sweeps devices Stim created that no project
5230
+ stim gc --delete sweeps devices this Stim home created that no project
4772
5231
  references (\`guide cleanup gc\`), clears
4773
5232
  verified parked simulators and emulators, and
4774
5233
  runs \`stim worktree remove\` on every clean,
4775
- Stim-managed linked worktree whose branch is merged
5234
+ Stim-managed linked worktree whose branch is merged,
5235
+ or whose pull request was merged or closed, and
5236
+ that shows no activity within
5237
+ gc.worktreeGraceMinutes
4776
5238
  stim gc --delete --older-than <days>
4777
5239
  also reaps the device of a workspace no Stim
4778
5240
  command has used in that long, even though the
@@ -4788,15 +5250,20 @@ WHAT RECLAIMS AN OWNED DEVICE
4788
5250
 
4789
5251
  \`worktree remove\` and \`gc --delete\` are the only two commands that delete;
4790
5252
  \`gc --delete\` deletes worktrees only through \`worktree remove\`. \`gc
4791
- --delete\` also clears workspace build outputs (\`guide cleanup disk\`) and
4792
- orphaned workspace directories, never a checkout. \`stim stop\` shuts a device
5253
+ --delete\` also clears workspace build outputs, trims oversized workspace logs
5254
+ (\`guide cleanup disk\`) and removes orphaned workspace directories, never a
5255
+ checkout. \`stim stop\` shuts a device
4793
5256
  DOWN and leaves it assigned, which is what makes returning to a branch cost a
4794
5257
  boot rather than a create, a provision and a reinstall.
4795
5258
 
4796
5259
  Before shutting down, parking or deleting an owned device, Stim best-effort
4797
5260
  closes local agent-device sessions bound to its exact iOS UDID or live Android
4798
- serial. It rechecks device ownership and uses agent-device's rejecting session
4799
- target guard; sessions on other devices stay open. Physical devices are outside
5261
+ serial. An Android session must also name the owned AVD, because the next
5262
+ emulator on a console port reuses its serial. \`stop\` closes a session only
5263
+ when agent-device's claim on the device names it and was taken inside this
5264
+ workspace; sessions from another workspace or claim stay open. Stim rechecks
5265
+ device ownership and uses agent-device's rejecting session target guard;
5266
+ sessions on other devices stay open. Physical devices are outside
4800
5267
  this cleanup. agent-device is optional: a missing binary skips this step, and a
4801
5268
  failed or timed-out list/close prints a device line on stderr while teardown
4802
5269
  continues. Cleanup allows at most 15 seconds of agent-device calls per device.
@@ -4843,7 +5310,8 @@ SWEEPING FINISHED WORKTREES
4843
5310
  Every \`gc\` without --cache looks at every registered project root and
4844
5311
  every workspace.json root, grouped by git worktree, and reports each linked
4845
5312
  worktree with the reason it is removed or kept. A worktree is finished when
4846
- its branch is merged into the default branch. \`--worktrees\` also removes
5313
+ its branch is merged into the default branch, or its pull request was
5314
+ merged or closed (PULL REQUESTS below). \`--worktrees\` also removes
4847
5315
  one that is idle: no recorded use for --older-than days, 7 without it; a
4848
5316
  worktree whose last use is unknown is kept. Both need the same clean state;
4849
5317
  a worktree is kept when it is the source checkout, bare, locked, in use,
@@ -4852,6 +5320,27 @@ SWEEPING FINISHED WORKTREES
4852
5320
  initialized submodules. Without --worktrees, the report leaves out the
4853
5321
  source checkout and roots outside git.
4854
5322
 
5323
+ IN USE means its dev server supervisor runs, a stim ios, android or stop
5324
+ run holds its native-run lock, a live build lock or slot names it, a
5325
+ managed tunnel or remote lock is held, an owned simulator or emulator of
5326
+ the workspace is booted (an agent-device session needs one), or it holds
5327
+ an unexpired device lease. When the simulator or emulator list cannot be
5328
+ read, the worktree counts as in use. A device left booted keeps the
5329
+ worktree until \`stim stop\` in it, or \`gc --idle <duration>\`, shuts
5330
+ the device down.
5331
+
5332
+ RECENT ACTIVITY keeps a worktree gc would otherwise remove until
5333
+ gc.worktreeGraceMinutes (120 by default, \`guide settings\`) have passed
5334
+ since the latest of: a write to its git index, HEAD or HEAD reflog, a write
5335
+ to its Stim workspace state or log files, and, for a merged branch, the
5336
+ committer date of the commit that brought it into the default branch, or
5337
+ the time its pull request was merged or closed. That
5338
+ window is when an agent that just merged runs \`stim stop\` and \`stim
5339
+ worktree remove\` itself. The reason is recent-activity, and the report
5340
+ and the JSON eligibleAt field say when it becomes removable. When that
5341
+ activity cannot be read, the worktree is kept (reason activity-unknown).
5342
+ 0 turns the grace period off.
5343
+
4855
5344
  MERGED means, with the default branch taken from origin/HEAD, after a
4856
5345
  \`git fetch origin <default>\` per repository (30s timeout, no credential
4857
5346
  prompt; skipped when that checkout fetched in the last 10 minutes):
@@ -4864,24 +5353,42 @@ SWEEPING FINISHED WORKTREES
4864
5353
  commit on the default branch (a rebase merge), or its whole diff since the
4865
5354
  merge base has the same verbatim patch id as a commit there, compared on
4866
5355
  the files the branch changes (a squash merge).
4867
- Merge state comes from git alone, not from a hosting service. A squash
4868
- merge whose content changed during the merge (a conflict resolution, a
4869
- suggested edit, even whitespace) does not match and is kept, and so is a
4870
- fast-forwarded branch. When origin/HEAD is not set, the fetch fails, or git
5356
+ From git alone, a squash merge whose content changed during the merge (a
5357
+ conflict resolution, a suggested edit, even whitespace) does not match, and
5358
+ neither does a fast-forwarded branch or a stacked pull request merged after
5359
+ the one below it; the pull request check covers those. When origin/HEAD is not set, the fetch fails, or git
4871
5360
  cannot answer, the state is unknown and never counts as merged (reason
4872
5361
  merge-unknown, with the remedy); with --worktrees an idle worktree is still
4873
5362
  removed. A squash- or rebase-merged branch whose upstream is gone (deleted
4874
5363
  after the merge and pruned locally) has commits only it reaches; they do
4875
5364
  not block removal, because their change is on the default branch, and the
4876
5365
  branch is kept.
5366
+
5367
+ PULL REQUESTS: gc runs one \`gh api graphql\` query per repository that
5368
+ asks for the 20 newest pull requests of each linked worktree's branch, as
5369
+ \`gh pr list --head <branch> --state all\` would, and for each worktree
5370
+ takes the pull request whose head is HEAD, else one whose head contains
5371
+ HEAD. A pull request whose head HEAD is past ("PR #12 merged, and HEAD has
5372
+ commits it does not"), one unrelated to HEAD (an older use of the branch
5373
+ name), and one from a fork do not count. A merged or closed one makes the worktree finished
5374
+ under the same clean state. Merged, its commits are kept on GitHub, so local
5375
+ commits whose remote branch was deleted do not block removal. Closed, they
5376
+ do: a closed pull request whose remote branch is gone keeps the worktree as
5377
+ unpushed. An open pull request never makes a worktree finished. When gh is
5378
+ not installed, not signed in, or fails, the JSON pullRequestUnknown field
5379
+ and the report say why, and gc judges from git alone. A detached HEAD is
5380
+ not looked up. \`stim worktree remove\` makes the same check when
5381
+ local-only commits alone would refuse the removal, and accepts a merged
5382
+ pull request whose head is or contains HEAD.
4877
5383
  stim gc # report merged worktrees
4878
5384
  stim gc --delete # remove them
4879
5385
  stim gc --delete --worktrees --older-than 3 # also the clean idle ones
4880
5386
  --cache with --worktrees is refused with STIM_BAD_ARG; run them separately.
4881
5387
  With --delete it runs the \`stim worktree remove\` pipeline, never --force,
4882
5388
  on each removable worktree. That pipeline re-inspects the worktree and
4883
- re-checks use under the removal locks, and idleness for an idle worktree
4884
- or an unchanged HEAD for a merged one, then parks devices and handles the
5389
+ re-checks use and recent activity under the removal locks, and idleness
5390
+ for an idle worktree or an unchanged HEAD for a merged one, then parks
5391
+ devices through the same teardown and handles the
4885
5392
  branch exactly as a manual \`stim worktree remove\`, which keeps the
4886
5393
  checkout when its HEAD moves while devices are reclaimed. A worktree that
4887
5394
  changed since the report is kept with the reason. A worktree that fails is
@@ -4941,15 +5448,16 @@ it. A record is what makes the device findable again, so it outlives a failed
4941
5448
  teardown rather than turning it into an orphan.
4942
5449
 
4943
5450
  WHICH stim-* DEVICES gc DELETES
4944
- A \`stim-\` prefix alone is not proof that Stim made a device. gc deletes an
4945
- unreferenced device only when Stim created it: the device is listed in
5451
+ A \`stim-\` name is not proof that this Stim home made a device: every
5452
+ STIM_HOME names its devices the same way. gc deletes an unreferenced device
5453
+ only when this home created it: the device is listed in
4946
5454
  $STIM_HOME/created-devices.json, where Stim records every simulator and AVD
4947
- it creates, or it predates that ledger and matches Stim's own format
4948
- exactly -- an iOS name \`stim-<label> (<model> <runtime>)\`, or an AVD
4949
- named \`stim-<label>\` whose config.ini holds the byte-valued
4950
- disk.dataPartition.size Stim writes. gc lists any other stim-* device under
5455
+ it creates, or it predates that ledger and this home's config records it in
5456
+ the device pool or in a project as owned. gc lists any other stim-* device,
5457
+ including those another STIM_HOME created, under
4951
5458
  "Unrecognized stim-* devices" with the command that deletes it
4952
- (\`xcrun simctl delete <udid>\` or \`avdmanager delete avd -n <name>\`), and
5459
+ (\`xcrun simctl delete <udid>\`, \`avdmanager delete avd -n <name>\`, or
5460
+ \`rm -rf <dir>\` for AVD data with no registration), and
4953
5461
  never runs it. Boot, shutdown and teardown re-check ownership by the same
4954
5462
  rule, so a device that stops matching is left alone.
4955
5463
 
@@ -5018,6 +5526,26 @@ THE MIRROR IMAGE: A STALE DEVICE RECORD
5018
5526
  at simctl or avdmanager, and the project keeps its entry, its label and its
5019
5527
  Metro port. The next \`ios\` / \`android\` creates a fresh owned device.
5020
5528
 
5529
+ The ledger of devices Stim created (created-devices.json) keeps a
5530
+ simulator's UDID after the simulator is deleted outside Stim. \`gc\`
5531
+ reports such UDIDs under "Stale device ledger entries" when a complete
5532
+ simctl listing, unavailable simulators included, does not show them, and
5533
+ \`gc --delete\` forgets them under the ledger lock. If the listing fails,
5534
+ nothing is reported or forgotten, and like the device sweep it is skipped
5535
+ without a config or under a scoped STIM_HOME. UDIDs are never reused, so a
5536
+ forgotten entry cannot belong to a later simulator.
5537
+
5538
+ Android ledger entries are AVD names, and names are reused: another
5539
+ STIM_HOME can later create an AVD with the same name. \`gc\` reports an
5540
+ AVD name as stale only when \`emulator -list-avds\` answered, the first AVD
5541
+ root exists (a missing one may be an unmounted volume), no AVD root holds
5542
+ <name>.ini or <name>.avd, and no unfinished setup reserves the name.
5543
+ \`gc --delete\` takes that AVD's claim, then re-checks and forgets the name
5544
+ under the config and ledger locks, so a setup in progress or an AVD
5545
+ recreated since the report keeps its entry. A name reused before \`gc\`
5546
+ runs still counts as this home's; run \`gc\` after deleting a Stim AVD by
5547
+ hand.
5548
+
5021
5549
  THE ONE CASE GC WILL NOT REAP
5022
5550
  If the config is gone entirely (deleted ~/.stim, or a throwaway
5023
5551
  STIM_HOME), gc cannot tell your stale devices from another config's LIVE
@@ -5084,7 +5612,7 @@ THE ONE CASE GC WILL NOT REAP
5084
5612
  Wall-clock timestamps and command names are not ownership proof.`
5085
5613
  },
5086
5614
  disk: {
5087
- summary: "disk usage, workspace build outputs, AVD and build-log sizes, the data partition, trimming the shared caches",
5615
+ summary: "disk usage, workspace build outputs and logs, AVD and build-log sizes, the data partition, trimming the shared caches",
5088
5616
  body: () => `DISK
5089
5617
  Logs, state, pidfiles and Xcode DerivedData are under the global workspace
5090
5618
  directory, and \`worktree remove\` reclaims them. \`gc --delete\` clears the
@@ -5100,12 +5628,16 @@ THE ONE CASE GC WILL NOT REAP
5100
5628
  disabled. The first boot and a boot after the emulator, system image, or AVD
5101
5629
  settings change are cold, while later supported boots load the one automatic
5102
5630
  snapshot saved on exit. \`stop\` waits for the emulator process and, when
5103
- enabled, the snapshot save to finish.
5631
+ enabled, the snapshot save to finish. An emulator that ignores \`adb emu
5632
+ kill\` or is unreachable over adb is signalled once its identity is verified;
5633
+ one that still runs fails the teardown and keeps its record (see \`stim guide
5634
+ errors teardown\`).
5104
5635
  New owned AVDs default to an 8 GiB data partition, though project settings can
5105
5636
  change it. When enabled, Quick Boot keeps one automatic snapshot until the AVD is
5106
5637
  parked or deleted.
5107
5638
  \`gc\` prints the on-disk size beside an orphaned or stale owned Android AVD
5108
- when its content directory can be read.
5639
+ when its content directory can be read, and beside an orphaned or stale
5640
+ owned simulator the data size simctl reports for it.
5109
5641
 
5110
5642
  So are the logs, and one of them is not small: build-ios.ndjson /
5111
5643
  build-android.ndjson hold the whole xcodebuild or gradle transcript at debug
@@ -5154,6 +5686,19 @@ WORKSPACE BUILD OUTPUTS
5154
5686
  React Native 0.86 Swift does not use it (explicit modules are off), so that
5155
5687
  build recompiles Swift.
5156
5688
 
5689
+ WORKSPACE LOGS
5690
+ \`gc\` reports the size of each workspace's logs/ (the workspaceLogs
5691
+ section of \`gc --json\`). metro.ndjson, client.ndjson and device.ndjson and
5692
+ their .1 generations rotate at 8 MiB (\`guide logs\`), but a file written by
5693
+ a Stim version before the cap can be hundreds of MB. \`gc --delete\` trims
5694
+ each of those six files that is over 16 MiB to its newest 8 MiB, cut at a
5695
+ record boundary; a file just past 8 MiB is normal rotation and stays.
5696
+ \`--older-than\` does not limit it. It skips a workspace that is in use (a
5697
+ dev server, a native run, a build or a held tunnel) or that has a device
5698
+ log collector recorded; \`stim stop\`
5699
+ stops the collector. Build transcripts keep the whole run and are never
5700
+ trimmed, and nothing else under logs/ is touched.
5701
+
5157
5702
  SHARED BUILD CACHES
5158
5703
  The caches that make a second workspace fast are alive by design and never
5159
5704
  included in a plain \`gc --delete\`. Every \`gc\` run reports them anyway,
@@ -5202,7 +5747,9 @@ shape, before anything is written. Writes to the machine file take its lock
5202
5747
  and replace it atomically; a committed write keeps the file's other keys and
5203
5748
  indentation. Run \`stim settings\` from the app directory: workspace and
5204
5749
  committed resolve from the nearest package.json, repo from its Git
5205
- repository. worktree.exclude and worktree.defaultBranch are read only from
5750
+ repository. From a monorepo web package that resolves to its worktree's app
5751
+ (see stim guide ports), workspace and committed are that app's entry and
5752
+ .stim.json. worktree.exclude and worktree.defaultBranch are read only from
5206
5753
  the repo layer and the repository root's .stim.json.
5207
5754
 
5208
5755
  android.keystorePassword is sensitive: its value prints as ******** in every
@@ -5332,6 +5879,12 @@ KEYS STIM READS
5332
5879
  profile. When this workspace already owns an AVD of
5333
5880
  another profile, a requested profile refuses rather
5334
5881
  than booting it; reap the AVD to create the new one.
5882
+ "pixel_fold" and "resizable" need a system image
5883
+ whose advancedFeatures.ini turns on SupportPixelFold
5884
+ (recent images such as API 34 google_apis do); the
5885
+ emulator quits on boot without it, so Stim refuses
5886
+ the pair with
5887
+ STIM_BAD_ARG and names an installed image that has it.
5335
5888
  Existing AVDs keep their display settings; parked AVDs
5336
5889
  from the old generic profile are not adopted.
5337
5890
  android.dataPartitionSizeGb
@@ -5360,6 +5913,10 @@ ${ANDROID_AVD_CONFIG_HELP.map((line) => ` ${line}`).joi
5360
5913
  keys are protected. The emulator may normalize a valid
5361
5914
  value. These overrides apply only to a newly created
5362
5915
  AVD; existing and recovered AVDs are never rewritten.
5916
+ Unless these settings set hw.keyboard, every AVD Stim
5917
+ creates gets hw.keyboard=yes, so the emulator takes
5918
+ gRPC key events. The one rewrite: a parked AVD created
5919
+ before that default gets it when a workspace adopts it.
5363
5920
  On displayless Linux, Stim launches with
5364
5921
  -gpu swiftshader_indirect -noaudio; those arguments
5365
5922
  override hw.gpu.enabled, hw.gpu.mode, hw.audioInput,
@@ -5442,6 +5999,21 @@ ${ANDROID_AVD_CONFIG_HELP.map((line) => ` ${line}`).joi
5442
5999
  driven. Read
5443
6000
  when \`start\` spawns the supervisor; see
5444
6001
  \`guide metro\`.
6002
+ web.url the page \`stim web\` opens in the owned Chrome, an
6003
+ http:// or https:// URL. {port:<label>} becomes the
6004
+ workspace's named port (allocated like \`stim ports
6005
+ get <label>\`), and {port:metro} its Metro port.
6006
+ Unset, Expo apps open http://localhost:<metroPort>/
6007
+ and other apps refuse until it is set. Example:
6008
+ http://localhost:{port:web}/apps/groups/.
6009
+ See \`guide web\`.
6010
+ web.ignoreCertificateErrors
6011
+ true starts the owned Chrome with
6012
+ --ignore-certificate-errors, for dev servers with
6013
+ self-signed certificates (default false). It applies
6014
+ only to Stim's own profile, never to your browser.
6015
+ web.viewport desktop (default, 1280x800) or phone (390x844 at 3x
6016
+ with touch events) for the owned Chrome page.
5445
6017
  worktree.exclude ignored-path skip list for worktree warm. Settings
5446
6018
  come from the source checkout's repository-root
5447
6019
  .stim.json. A nonempty .worktreeexclude in the source
@@ -5484,7 +6056,7 @@ ${ANDROID_AVD_CONFIG_HELP.map((line) => ` ${line}`).joi
5484
6056
  them from the environment or the machine layers.
5485
6057
 
5486
6058
  Each setting takes its documented type: string, array of strings, number,
5487
- boolean, or object. ios.remote, android.remote, metro.tunnel,
6059
+ boolean, or object. ios.remote, android.remote, metro.tunnel, web.viewport,
5488
6060
  optimizations.android.compilerCache and optimizations.android.pch take only
5489
6061
  their listed choices. A value of the wrong
5490
6062
  type or outside those choices is refused by name on every command that resolves
@@ -5543,8 +6115,13 @@ STIM_BAD_ARG. See \`guide lifecycle budget\` for what each limit reclaims.
5543
6115
  THE IOS SIMULATOR APP IS MACHINE-LEVEL
5544
6116
  Top-level \`iosSimulatorApp\` in ~/.stim/config.json selects the macOS app that
5545
6117
  displays Stim's owned local iOS simulator after boot. It is not a project
5546
- setting. Unset or \`"xcode"\` opens the selected Xcode's Device Hub on Xcode 27,
5547
- or Simulator on older Xcode. To use an installed Siniulator instead:
6118
+ setting. Unset, it is \`"stim-desktop"\` while Stim Desktop is installed and
6119
+ \`"xcode"\` otherwise. \`"xcode"\` opens the selected Xcode's Device Hub on Xcode 27,
6120
+ or Simulator on older Xcode. On Xcode 27 (confirmed on 27A266a), quitting
6121
+ Device Hub by default shuts down every booted simulator on the machine,
6122
+ including ones it never opened a window for and ones other workspaces or
6123
+ agents are using; never quit it to free memory or clean up. To use an
6124
+ installed Siniulator instead:
5548
6125
 
5549
6126
  { "iosSimulatorApp": "siniulator" }
5550
6127
 
@@ -5554,6 +6131,10 @@ To show the booted simulator in Stim Desktop and open no simulator window:
5554
6131
 
5555
6132
  Stim Desktop selects the workspace that owns the simulator and focuses that
5556
6133
  device. It only displays the simulator; it never boots or shuts it down.
6134
+ When Stim Desktop is not running, Stim starts it without the command's
6135
+ \`STIM_HOME\`, so it reads the same Stim home as when you open it yourself.
6136
+ It shows only devices from that home: under another \`STIM_HOME\`, pick
6137
+ \`--simulator-app xcode\` to see the simulator.
5557
6138
 
5558
6139
  Override the preference for one local launch with
5559
6140
  \`stim ios --simulator-app siniulator\`, \`stim ios --simulator-app stim-desktop\`,
@@ -5570,9 +6151,10 @@ window behavior.
5570
6151
 
5571
6152
  THE ANDROID EMULATOR APP IS MACHINE-LEVEL
5572
6153
  Top-level \`androidEmulatorApp\` in ~/.stim/config.json selects how an owned
5573
- Android emulator that Stim boots on macOS is displayed. Unset or
5574
- \`"emulator"\` opens the emulator's own window. To boot it headlessly and show
5575
- it in Stim Desktop instead:
6154
+ Android emulator that Stim boots on macOS is displayed. Unset, it is
6155
+ \`"stim-desktop"\` while Stim Desktop is installed and \`"emulator"\`
6156
+ otherwise. \`"emulator"\` opens the emulator's own window. To boot it
6157
+ headlessly and show it in Stim Desktop:
5576
6158
 
5577
6159
  { "androidEmulatorApp": "stim-desktop" }
5578
6160
 
@@ -5581,8 +6163,17 @@ Stim then starts the emulator with \`-no-window -gpu host\` and opens
5581
6163
  frames and sends input over the emulator's gRPC endpoint. The setting applies
5582
6164
  only when Stim boots the emulator: one that is already running keeps its
5583
6165
  current display until it next boots, and physical devices are unaffected.
5584
- An invalid value refuses before boot. On Linux and Windows the setting has
5585
- no effect.
6166
+ Like the iOS viewer, it starts Stim Desktop without the command's
6167
+ \`STIM_HOME\`. An invalid value refuses before boot. On Linux and Windows the
6168
+ setting has no effect.
6169
+
6170
+ Stim finds Stim Desktop by its bundle id, dev.stim.desktop, in Launch Services.
6171
+ A command Stim Desktop runs skips that lookup: Desktop sets \`STIM_DESKTOP_APP\`
6172
+ to its app path for every command it starts.
6173
+ \`stim settings\` shows that default as \`(default: Stim Desktop installed)\`,
6174
+ \`settings get\` prints it on stderr, and \`--json\` adds
6175
+ \`"defaultReason": "Stim Desktop installed"\`. A set value always wins; set
6176
+ \`"xcode"\` or \`"emulator"\` to keep the device windows.
5586
6177
 
5587
6178
  THE DEVICE POOL BOUNDS ARE MACHINE-LEVEL TOO
5588
6179
  \`pool.iosParkedMax\` caps how many parked simulators \`worktree remove\` may
@@ -5614,6 +6205,22 @@ test suites and the end-to-end harness use one -- and a scoped config must not
5614
6205
  leave simulators on the machine it cannot account for. A redirected home that
5615
6206
  wants a pool says so with the variable.
5616
6207
 
6208
+ THE GC WORKTREE GRACE PERIOD IS MACHINE-LEVEL
6209
+ \`gc.worktreeGraceMinutes\` is how long \`gc --delete\` waits before it removes
6210
+ a merged or idle linked worktree. The clock starts at the worktree's latest
6211
+ activity: a write to its git index, HEAD or HEAD reflog, its Stim workspace
6212
+ state or logs, the merge of its branch into the default branch, or when its
6213
+ pull request was merged or closed.
6214
+
6215
+ {
6216
+ "gc": { "worktreeGraceMinutes": 120 }
6217
+ }
6218
+
6219
+ in ~/.stim/config.json, or STIM_GC_WORKTREE_GRACE_MINUTES in the environment,
6220
+ which overrides the file. Absent means 120. \`0\` removes a finished worktree at
6221
+ once. A value that is not a whole number 0 or more is warned about by \`gc\`,
6222
+ which then uses 120. See \`guide cleanup\` for what else keeps a worktree.
6223
+
5617
6224
  STIM NEEDS NO PROJECT CHANGES TO RUN
5618
6225
  Nothing above is required to use Stim. The performance caches that used to
5619
6226
  be setup steps are supplied by Stim on the command lines it composes itself:
@@ -5841,6 +6448,176 @@ platform instead of one entry. Pass prune: 'atomic' for a cache whose index
5841
6448
  references its own data (an LLVM CAS): it is then left alone by --older-than
5842
6449
  and emptied whole only by 'gc --delete --cache all'.
5843
6450
  Registration is idempotent and keyed on the directory.`
6451
+ },
6452
+ web: {
6453
+ summary: "stim web: an owned headless Chrome per workspace, its logs, launched, and teardown",
6454
+ body: () => `WEB: AN OWNED CHROME PER WORKSPACE
6455
+
6456
+ If Stim is not installed globally, replace stim with npx stim.
6457
+
6458
+ stim web opens this workspace's page in a Chrome that Stim owns, captures the
6459
+ page's console, uncaught errors and failed requests in stim logs, and reports
6460
+ whether the page loaded. It uses the installed Google Chrome (or Chromium)
6461
+ with a profile Stim creates under STIM_HOME. Stim never installs a browser;
6462
+ stim doctor reports a missing Chrome.
6463
+
6464
+ stim web never starts a web server, for any framework. It opens, reuses or
6465
+ reloads the owned Chrome at the page URL. Every web project follows the same
6466
+ two steps: start your dev server, then run stim web.
6467
+
6468
+ stim web # headless, the default
6469
+ stim web --headed # show the Chrome window
6470
+ stim web --json # one payload on stdout; progress on stderr
6471
+ stim logs --errors # page errors land here with platform "web"
6472
+ stim reload web # Page.reload on the owned page
6473
+ stim stop # closes Chrome with the rest; the profile stays
6474
+
6475
+ WHICH PAGE AND WHICH DEV SERVER
6476
+
6477
+ Expo web runs on the workspace's Metro. With web.url unset, an Expo app
6478
+ opens http://localhost:<metroPort>/. Its dev server is stim start:
6479
+
6480
+ npx expo install react-dom react-native-web @expo/metro-runtime # once
6481
+ stim start
6482
+ stim web
6483
+
6484
+ Any other web server (Vite, Next, webpack) starts with its own command.
6485
+ Reserve its port with stim ports get, start it with strict-port behavior, and
6486
+ point web.url at it. {port:<label>} in web.url becomes that named port,
6487
+ {port:metro} the Metro port:
6488
+
6489
+ pnpm exec vite --port "$(stim ports get web)" --strictPort
6490
+ stim settings set web.url 'http://localhost:{port:web}/' --scope workspace
6491
+ stim web
6492
+
6493
+ When nothing serves the page, stim web still opens Chrome, reports launched
6494
+ "unverified", and prints the remedy for this workspace: stim start for an
6495
+ Expo web app, the stim ports get web recipe for any other server. A Metro
6496
+ port held by another process is also "unverified", and the page it serves
6497
+ still reaches stim logs; stim start then reserves a free port for this
6498
+ workspace. When this workspace's supervisor is still recorded, run stim stop
6499
+ first.
6500
+
6501
+ In a monorepo whose web app is its own package (apps/web beside apps/mobile),
6502
+ stim ports, web, settings, logs, reload, stop and status run from the web
6503
+ package resolve to the one Stim app registered in the same Git worktree; each
6504
+ names the app on stderr, and status stars it. The port, the browser, web.url
6505
+ and the logs all belong to that app's workspace, so every command can run from
6506
+ the web package. Register the app first, from its directory. See stim guide
6507
+ ports for the exact rule.
6508
+
6509
+ HTTPS dev servers with a self-signed certificate, such as Vite with
6510
+ @vitejs/plugin-basic-ssl, fail with net::ERR_CERT_AUTHORITY_INVALID until
6511
+ web.ignoreCertificateErrors is true, which accepts the certificate in the
6512
+ owned profile only:
6513
+ stim settings set web.url 'https://localhost:{port:web}/' --scope workspace
6514
+ stim settings set web.ignoreCertificateErrors true --scope workspace
6515
+ An https:// URL on a plain HTTP server fails with ERR_SSL_PROTOCOL_ERROR, and
6516
+ an http:// URL on an HTTPS server with ERR_EMPTY_RESPONSE; the remedy line
6517
+ prints the settings command that switches the scheme. web.viewport phone
6518
+ gives the page a 390x844 touch screen at 3x instead of the 1280x800 desktop
6519
+ window. See stim guide settings.
6520
+
6521
+ MONOREPO RECIPE: A VITE PACKAGE WITH A BASE PATH AND HTTPS
6522
+
6523
+ apps/web runs Vite through its dev script, serves the app under /apps/groups/,
6524
+ and uses @vitejs/plugin-basic-ssl; apps/mobile is the Stim app. The flow is
6525
+ the same as for any web project: start the dev server, then run stim web.
6526
+
6527
+ cd apps/mobile && stim ports get web # registers the app; no Metro
6528
+ cd ../web
6529
+ stim settings set web.url 'https://localhost:{port:web}/apps/groups/' --scope repo
6530
+ stim settings set web.ignoreCertificateErrors true --scope repo
6531
+ pnpm dev --port "$(stim ports get web)" --strictPort # keep it running
6532
+ stim web
6533
+ stim logs --errors
6534
+ stim reload web
6535
+ stim stop # closes Chrome; stim ports stop web stops Vite
6536
+ stim worktree remove # in a linked worktree: Chrome, Vite and profile
6537
+
6538
+ The repo layer is shared by every worktree of the repository, so a new
6539
+ worktree skips the two settings lines; the certificate and scheme remedies
6540
+ print --scope workspace, which overrides it for one worktree only. Run ports
6541
+ get from the app directory before any ports or web command in the web
6542
+ package: a reservation made there first keeps the web package as its own
6543
+ workspace until you release it and stop. Pass --port and --strictPort
6544
+ through the dev script: Vite otherwise binds its config default and moves to
6545
+ the next free port when that one is taken. Put the base path in web.url: a
6546
+ path outside it can reach the dev server's proxy instead of the app. API
6547
+ calls the dev server proxies to a backend that is not running fail as device
6548
+ errors in stim logs --errors; the page still loads.
6549
+
6550
+ LAUNCHED
6551
+
6552
+ The web payload's launched keeps its native meaning, from evidence in the
6553
+ owned page rather than a shared port:
6554
+ true the page's document answered and its load event fired; on
6555
+ Metro, the page also fetched a web bundle
6556
+ "bundling" Metro was still building the web bundle when the check ended
6557
+ "unverified" the document failed (for example ERR_CONNECTION_REFUSED:
6558
+ the dev server is not running, or ERR_CERT_AUTHORITY_INVALID: a
6559
+ self-signed certificate), loaded without a Metro bundle, or
6560
+ did not finish: 20 seconds with no answer, 60 once a server
6561
+ other than Metro answered. The remedy line names the fix
6562
+ false is never produced. A page that loads and then throws is still launched
6563
+ true; read stim logs --errors.
6564
+
6565
+ A second stim web with the same options reuses the running Chrome and
6566
+ navigates it again. Changing --headed, web.viewport or the certificate
6567
+ setting restarts it.
6568
+
6569
+ LOGS
6570
+
6571
+ Records go to web.ndjson in the workspace log directory, all with
6572
+ platform "web":
6573
+ src client console calls at their level (console.error is error) and
6574
+ uncaught errors and rejections, with stack frames
6575
+ src device failed requests, which stim logs --errors includes: a failed
6576
+ page document is error whatever its
6577
+ status; another request's network failure or HTTP 5xx is
6578
+ error, a 4xx warn, a canceled request debug; browser messages such
6579
+ as CSP violations; the browser's own lifecycle
6580
+ Expo also prints web console calls on Metro ("Web LOG"), so they can appear
6581
+ twice: once from the page (client), once from Metro (metro, level info).
6582
+ Each load of the page's top-level document is a page-load marker: stim web,
6583
+ stim reload, and a reload or navigation the page makes itself. logs --errors
6584
+ and the status error count report the page's records (platform web) from the
6585
+ latest load only, the way Chrome DevTools clears its console when the page
6586
+ navigates. A page load does not hide the native app's errors, and an ios or
6587
+ android launch does not hide the page's.
6588
+
6589
+ AGENTS AND OTHER TOOLS ON THE SAME BROWSER
6590
+
6591
+ stim status --json shows environments[].web.cdpEndpoint, a reserved loopback
6592
+ DevTools endpoint on 127.0.0.1. Point browser tools at it instead of letting
6593
+ them start their own browser:
6594
+ Playwright MCP --cdp-endpoint http://127.0.0.1:<port>
6595
+ agent-browser --cdp <port>
6596
+ The port reaches only the Stim profile: Chrome refuses remote debugging on
6597
+ your default profile, and Stim never attaches to a browser it did not start.
6598
+ The port is managed like metro: stim ports lists it as web-cdp (managed),
6599
+ and ports get, stop and release refuse it.
6600
+
6601
+ OWNERSHIP AND CLEANUP
6602
+
6603
+ A browser supervisor process holds the DevTools session and an ownership
6604
+ claim for its lifetime; Chrome is the claim's child. Stim signals the
6605
+ supervisor or Chrome only after verifying the process identity it recorded.
6606
+ stim stop closes Chrome and keeps the profile, so cookies and
6607
+ storage survive the next stim web
6608
+ stim worktree remove closes Chrome and deletes the profile
6609
+ stim gc --delete does the same for a workspace whose path is gone,
6610
+ and forgets ledger entries of deleted profiles
6611
+ The profile is deleted only when the created-devices ledger lists it. After
6612
+ Chrome is gone, Stim removes the SingletonLock files a killed Chrome leaves,
6613
+ which would otherwise block the next launch. status reports
6614
+ browser-unverified when an identity cannot be proven; follow stim guide
6615
+ errors teardown.
6616
+
6617
+ LIMITS
6618
+
6619
+ Chrome and Chromium only. No Stim Desktop or phone viewer yet. The owned page
6620
+ is one tab: a page the app opens in a new window is not captured.`
5844
6621
  }
5845
6622
  };
5846
6623
  //#endregion