stim 1.9.0 → 1.10.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 (83) hide show
  1. package/dist/{android-_y4Ohi5z.mjs → android-787HhP5w.mjs} +119 -29
  2. package/dist/{android-BPGizezh.mjs → android-DBkeHwpm.mjs} +168 -95
  3. package/dist/{android-DEgo4oCo.mjs → android-DPov-fv2.mjs} +2 -2
  4. package/dist/{android-cas-eiExNLf7.mjs → android-cas-CITmIeWY.mjs} +9 -8
  5. package/dist/android-cas-compiler.mjs +1 -1
  6. package/dist/{app-install-Uf3a83Tv.mjs → app-install-CrJ6t_bv.mjs} +2 -2
  7. package/dist/budget-BP4jZZL4.mjs +620 -0
  8. package/dist/build-progress-BXx-ZBe4.mjs +512 -0
  9. package/dist/{cache-manifest-Dg7OnMrp.mjs → cache-manifest-Pubfp8wV.mjs} +1 -1
  10. package/dist/cache-manifest.mjs +1 -1
  11. package/dist/cli.mjs +16 -16
  12. package/dist/collector-run.d.mts +5 -1
  13. package/dist/collector-run.mjs +7 -7
  14. package/dist/{command-output-gByVmIjy.mjs → command-output-D5z2_bcF.mjs} +1 -1
  15. package/dist/{config-Cp9lM3wX.mjs → config-BEq0Qw_a.mjs} +14 -131
  16. package/dist/{dependency-state-Bq5DcLGn.mjs → dependency-state-r9UesA9a.mjs} +2 -2
  17. package/dist/{deps-Cip5vUm1.mjs → deps-DP4eWqea.mjs} +3 -3
  18. package/dist/{detached-entry-KEu0_AXL.mjs → detached-entry-DJHoca49.mjs} +1 -1
  19. package/dist/{dev-client-HyqISM0h.mjs → dev-client-92U593KT.mjs} +137 -250
  20. package/dist/{device-D4U-xLty.mjs → device-DhTZe7L4.mjs} +7 -7
  21. package/dist/{build-lock-DPXPLgTc.mjs → device-lease-CbEaIk7b.mjs} +370 -12
  22. package/dist/{device-pool-CnTAz8X1.mjs → device-pool-Di8Q-9Ro.mjs} +4 -4
  23. package/dist/{device-remote-AQ5rQPqr.mjs → device-remote-Dy1jGj3U.mjs} +547 -468
  24. package/dist/{doctor-B_Ei3bZu.mjs → doctor-B-inAJmt.mjs} +58 -22
  25. package/dist/{error-diagnostics-Vfz83GCF.mjs → error-diagnostics-xxWKNO3-.mjs} +11 -12
  26. package/dist/{exec-jIAlgilk.mjs → exec-DK8KaVHK.mjs} +3 -2
  27. package/dist/{gc-Ckw8RQHi.mjs → gc-ChPFZMRC.mjs} +500 -1114
  28. package/dist/{guide-BiAbhs1J.mjs → guide-DwmaD_Wa.mjs} +882 -528
  29. package/dist/guide-status-CWvti2Lg.mjs +125 -0
  30. package/dist/idle-DpRLp5lj.mjs +157 -0
  31. package/dist/in-use-CjUluqIX.mjs +245 -0
  32. package/dist/{ios-BL2NFckr.mjs → ios-B2Fpqvlb.mjs} +25 -59
  33. package/dist/{ios-BzUJWk1K.mjs → ios-Dd-hYJBh.mjs} +130 -89
  34. package/dist/{ios-device-DPsSm0v_.mjs → ios-device-BiOsSVzd.mjs} +1 -1
  35. package/dist/{ios-device-D972i9wf.mjs → ios-device-CtRdEVjx.mjs} +3 -3
  36. package/dist/{launch-verify-Q_OtW1Rf.mjs → launch-verify-CQS_Kg-C.mjs} +5 -5
  37. package/dist/{logs-CI9NQSK7.mjs → logs-MARIy6m9.mjs} +9 -9
  38. package/dist/{metro-DrLHaMn2.mjs → metro-BWLOJ_Wr.mjs} +116 -10
  39. package/dist/{named-ports-CmLQSIRj.mjs → named-ports-DiTfSz6p.mjs} +4 -4
  40. package/dist/{ndjson-9uFNRwfe.mjs → ndjson-CDY9IkVC.mjs} +3 -56
  41. package/dist/{ndjson-DvHSsNU9.d.mts → ndjson-Du3j0AQ9.d.mts} +1 -0
  42. package/dist/{ownership-D15Op8vN.mjs → ownership-D2xbLVoz.mjs} +2 -3
  43. package/dist/ownership-DZjdx8Gq.mjs +1180 -0
  44. package/dist/{ports-BNKoWVWZ.mjs → ports-CLSSFOuz.mjs} +3 -3
  45. package/dist/{project-DS112DYB.mjs → project-BXZzOABt.mjs} +2 -2
  46. package/dist/{reload-ZNJUo1pl.mjs → reload-DoETJ3rl.mjs} +9 -10
  47. package/dist/{server-bare-DWkkcIxV.mjs → server-bare-DVbb7kXH.mjs} +1 -1
  48. package/dist/{server-expo-B_lctBR3.mjs → server-expo-BvOCeakp.mjs} +4 -4
  49. package/dist/server-expo-DBVTdult.mjs +2 -0
  50. package/dist/{settings-D030k0t4.mjs → settings-DOugPlPz.mjs} +16 -428
  51. package/dist/{settings-BKNh5rl4.mjs → settings-EWYNwe72.mjs} +16 -14
  52. package/dist/settings.schema.json +75 -0
  53. package/dist/{simslim-B_LsGLuJ.mjs → simslim-Ca3487H6.mjs} +182 -5
  54. package/dist/{slot-launch-DU9A-gYw.mjs → slot-launch-C5cg0fmk.mjs} +4 -4
  55. package/dist/{spawn-claims-Dn3C_N6w.mjs → spawn-claims-CUleC2cM.mjs} +1 -1
  56. package/dist/{start-DYkkKF0s.mjs → start-F9gv5dcC.mjs} +147 -98
  57. package/dist/start-VoFsPSes.mjs +2 -0
  58. package/dist/{state-NcJzMEBF.mjs → state-WGdIbi_d.mjs} +2 -2
  59. package/dist/{stats-DG9e66Rz.mjs → stats-BAgJaLA4.mjs} +1 -1
  60. package/dist/{stats-B63-JVAY.mjs → stats-DBYJ5bxL.mjs} +4 -4
  61. package/dist/{status-S6ehL4Ui.mjs → status-DLCD-o3s.mjs} +63 -231
  62. package/dist/status-Dr4fRkym.mjs +216 -0
  63. package/dist/{guide-status-BAMVU0ZA.mjs → stim-installations-CINXy_su.mjs} +3 -124
  64. package/dist/{stop-BO28HGqF.mjs → stop-Dj4pfmyv.mjs} +22 -18
  65. package/dist/{stop-T1R-cZRz.mjs → stop-ZvOeTmKb.mjs} +2 -2
  66. package/dist/supervisor-run.d.mts +11 -2
  67. package/dist/supervisor-run.mjs +185 -17
  68. package/dist/{workspace-process-lock-DWaiRD0e.mjs → workspace-process-lock-BXA96myD.mjs} +2 -2
  69. package/dist/workspaces-BTUyFfkv.mjs +1003 -0
  70. package/dist/{worktree-DKhSF0UX.mjs → worktree-4aZg2NnI.mjs} +1 -1
  71. package/dist/{worktree-LGB-UwgG.mjs → worktree-BMS7GeqB.mjs} +40 -122
  72. package/dist/{worktree-R_cUlE7s.mjs → worktree-BlizKDpZ.mjs} +143 -18
  73. package/dist/{xcode-BZqyxh3e.mjs → xcode-B2tBkf0m.mjs} +9 -158
  74. package/package.json +6 -5
  75. package/dist/build-progress-CVqkfPC6.mjs +0 -188
  76. package/dist/device-lease-Crs-HkJY.mjs +0 -366
  77. package/dist/logs-query-Cqma3WIu.mjs +0 -298
  78. package/dist/ownership-DCQPuWEc.mjs +0 -489
  79. package/dist/public-http-probe-CyMDrQ3c.mjs +0 -61
  80. package/dist/server-expo-CeILEu86.mjs +0 -2
  81. package/dist/sim-pool-KaIsp2Ft.mjs +0 -723
  82. package/dist/spawn-entry-nCE6m770.mjs +0 -14
  83. package/dist/workspace-state-DF2gCFhx.mjs +0 -169
@@ -1,11 +1,14 @@
1
- import { o as findProjectRoot } from "./project-DS112DYB.mjs";
2
- import { t as ANDROID_AVD_CONFIG_HELP } from "./settings-D030k0t4.mjs";
3
- import { t as guideStatus } from "./guide-status-BAMVU0ZA.mjs";
1
+ import { o as findProjectRoot } from "./project-BXZzOABt.mjs";
2
+ import { t as ANDROID_AVD_CONFIG_HELP } from "./settings-DOugPlPz.mjs";
3
+ import { t as guideStatus } from "./guide-status-CWvti2Lg.mjs";
4
+ import { SETTINGS_SCHEMA_URL } from "@stim-cli/core/state";
4
5
  import chalk from "chalk";
5
- //#region src/guide/agent.ts
6
- var agent_default = {
7
- summary: "The normal coding-agent workflow, safety rules, and topic routing",
8
- body: () => `AGENT WORKFLOW
6
+ //#endregion
7
+ //#region src/guide/index.ts
8
+ const TOPICS = {
9
+ agent: {
10
+ summary: "The normal coding-agent workflow, safety rules, and topic routing",
11
+ body: () => `AGENT WORKFLOW
9
12
 
10
13
  Use Stim to run React Native and Expo apps without sharing a Metro port or
11
14
  device with another workspace. Prefer plain output: it streams each phase and
@@ -119,8 +122,10 @@ RULES DURING THE LOOP
119
122
  - Put runtime .stim.json beside that app's package.json. Monorepo apps do not
120
123
  inherit a repository-root runtime file. Keep repository-wide worktree-copy
121
124
  rules at the source checkout root; see guide settings for the two scopes.
122
- - Run start before a debug ios or android build. If it returns STIM_NO_METRO,
123
- run stim start and retry.
125
+ - A debug ios or android run starts this workspace's dev server when it is
126
+ not running, including after it stopped itself for an hour with no use
127
+ (guide metro). Running start first is optional; it lets Metro warm up while
128
+ you do other work.
124
129
  - Run ios or android again after a native input changes. A JavaScript-only
125
130
  change does not need one.
126
131
  - For stale Metro transforms or file-map state, use stim start --reset-cache.
@@ -211,8 +216,9 @@ Ask the user before these actions:
211
216
  existing Stim ownership record is deleted only when it has no unique commits.
212
217
  - worktree remove --force, because it also discards uncommitted and untracked
213
218
  files.
214
- - gc --delete, because it deletes orphaned resources and clears the build
215
- outputs of every workspace not in use. Run stim gc --json first and show
219
+ - gc --delete, because it deletes orphaned resources, removes clean linked
220
+ worktrees whose branch is merged, and clears the build outputs of every
221
+ workspace not in use. Run stim gc --json first and show
216
222
  the user the entries its sections list (guide facts gc). gc --delete
217
223
  --cache all empties the shared build caches and those outputs instead, and
218
224
  gc --delete --cache workspaces clears only the outputs; both inspect
@@ -224,6 +230,14 @@ Ask the user before these actions:
224
230
  delete it. An explicit stop shuts down a Stim-owned simulator even when
225
231
  another process uses it. It never shuts down an unowned simulator.
226
232
 
233
+ LEAVE THE MACHINE CLEAN
234
+
235
+ Run stim stop when you pause or finish a task, and stim worktree remove once
236
+ it is merged or abandoned. Never create a simulator or emulator by hand;
237
+ every device comes from Stim. Before starting a new workspace, check stim
238
+ status for an idle workspace to reuse and confirm capacity headroom instead of
239
+ adding another.
240
+
227
241
  SANDBOXES
228
242
 
229
243
  An agent harness that sandboxes shell commands usually permits writes inside
@@ -250,6 +264,7 @@ Read the matching guide before acting in these situations:
250
264
  | Remote device, custom Metro, or tunnel | stim guide metro |
251
265
  | Cache miss, bypass, or fingerprint exclusions | stim guide lifecycle builds |
252
266
  | Capacity limits | stim guide lifecycle concurrency |
267
+ | budget lines, reclaimed field, or low disk | stim guide lifecycle budget |
253
268
  | Cache statistics from stim stats | stim guide facts stats |
254
269
  | Worktree carry-over | stim guide lifecycle options |
255
270
  | Doctor finding: seed checkout or cross-volume copy | stim guide lifecycle options |
@@ -283,6 +298,7 @@ FULL TOPIC LIST
283
298
  stim guide lifecycle readiness # add optional app readiness logs; no package required
284
299
  stim guide lifecycle builds # build optimizations, optional cache warm-up, fingerprints
285
300
  stim guide lifecycle concurrency # shared builds, wait timeouts, capacity limits
301
+ stim guide lifecycle budget # disk and memory budgets, automatic reclaim
286
302
  stim guide lifecycle options # every flag, Android variants, --device-type, --system-image
287
303
  stim guide lifecycle devices # ios --device and android --device on a physical phone
288
304
  stim guide lifecycle release # Release configurations and ...Release variants
@@ -294,12 +310,10 @@ FULL TOPIC LIST
294
310
  stim guide cleanup # what reclaims a device, and what deletes
295
311
  stim guide cleanup collector # an unproven collector pid; why the app on a phone closed
296
312
  stim guide settings # configuration files and supported keys`
297
- };
298
- //#endregion
299
- //#region src/guide/facts.ts
300
- const facts = {
301
- summary: "The --json payloads: `start`, `ios`, `android`, `reload`, `stop`, `status`, `doctor`, `device lock`/`unlock`, `gc`, and the error contract",
302
- preamble: () => `SLOTS
313
+ },
314
+ facts: {
315
+ summary: "The --json payloads: `start`, `ios`, `android`, `reload`, `stop`, `status`, `doctor`, `device lock`/`unlock`, `gc`, and the error contract",
316
+ preamble: () => `SLOTS
303
317
  Named ios/android runs add slot to their JSON facts. Default-run fields remain
304
318
  compatible. status adds a slots array per environment with each named slot's
305
319
  ios/android device facts; top-level ios/android still describe default.
@@ -313,8 +327,15 @@ stdout for \`--json\`. Every other line goes to stderr, so it is always safe
313
327
  to pipe. \`logs --json\` is the one exception: it is NDJSON, one record per
314
328
  line by design (see \`guide logs\`), not this single-payload contract.
315
329
 
316
- status's unprovisionedWorktrees lists this repository's linked worktrees with
317
- no Stim environment. \`worktree warm\` does not create one; \`start\`, \`ios\`,
330
+ status's unprovisionedWorktrees lists the linked worktrees with no Stim
331
+ environment in every repository with a registered environment, plus the
332
+ repository status runs from. A worktree counts as having an
333
+ environment when one is registered at it or inside it. Each entry is
334
+ { path, branch, repository }; repository is the main checkout, or the git
335
+ directory of a bare repository. status reads git's own worktree records and
336
+ never opens the worktree directories, so a worktree under a macOS-protected
337
+ folder such as ~/Documents is listed without a privacy prompt.
338
+ \`worktree warm\` does not create an environment; \`start\`, \`ios\`,
318
339
  \`android\` and \`doctor\` register it.
319
340
 
320
341
  status's remoteDevices lists each environment's recorded EAS Simulator
@@ -335,10 +356,10 @@ is still running.
335
356
 
336
357
  Plain status prints one "remote <platform>: EAS session <id> billable" line
337
358
  per session, with the preview URL.`,
338
- sections: {
339
- payloads: {
340
- summary: "every field of the start, ios, android and reload payloads, the error contract, the device rules",
341
- body: () => ` stim start --json
359
+ sections: {
360
+ payloads: {
361
+ summary: "every field of the start, ios, android and reload payloads, the error contract, the device rules",
362
+ body: () => ` stim start --json
342
363
 
343
364
  port the Metro port RESERVED for this workspace
344
365
  supervisorPid the detached supervisor's pid, or NULL when a dev server was
@@ -543,6 +564,20 @@ per session, with the preview URL.`,
543
564
  error, and this field is separate from cacheHit
544
565
  logs the workspace log directory
545
566
  durationMs wall time for the whole run
567
+ reclaimed present only when the run was over its disk or memory
568
+ budget and reclaimed first (\`start\`, \`ios\` and
569
+ \`android\`, on success and failure alike). One entry per
570
+ step that acted, in order:
571
+ { step, targets, failures, freedMb }
572
+ step is "idle-devices", "idle-dev-servers",
573
+ "workspace-outputs" or "stale-cache-entries"; targets names
574
+ the devices, workspaces or caches it acted on; freedMb is the
575
+ free disk the step added. See \`guide lifecycle budget\`
576
+ devServer present only when this run started the workspace's dev
577
+ server because none was running (\`ios\` and \`android\`):
578
+ { started: true, reason }
579
+ reason is "not running" or "stopped (idle)" when the
580
+ supervisor had stopped it after metro.idleStopMinutes
546
581
 
547
582
  stim reload [ios|android] --json
548
583
 
@@ -581,6 +616,14 @@ per session, with the preview URL.`,
581
616
  PATH and the version each reports. resolvedIsOlder is true
582
617
  only when that first executable is below the highest version
583
618
  available from this invocation or PATH
619
+ budget { budget, volumes, memory, plan } or null when a budget
620
+ setting is invalid. budget echoes minFreeDiskGb,
621
+ hardFloorDiskGb, maxCommittedMemoryGb and maxLiveWorkspaces;
622
+ volumes is { volume, freeGb } for the volumes holding the app
623
+ and $STIM_HOME; memory is { committedGb, liveWorkspaces } or
624
+ null when off; plan lists what the next start, ios or
625
+ android would reclaim, in the \`reclaimed\` shape without
626
+ freedMb, and is empty while under budget
584
627
  findings the diagnostic findings; a lower resolved Stim is a
585
628
  costs-time finding with a PATH or installation remedy
586
629
 
@@ -588,12 +631,14 @@ ON FAILURE
588
631
  \`start\`, \`ios\` and \`android\` all print the error contract instead,
589
632
  still one line on stdout, and exit 1:
590
633
 
591
- { "code": "STIM_NO_METRO", "message": "...", "remedy": "..." }
634
+ { "code": "STIM_METRO_TIMEOUT", "message": "...", "remedy": "..." }
592
635
 
593
636
  If a native build returned before the failure, this payload also carries
594
637
  \`ccache\` (Android) or \`compilationCache\` (iOS), with the status and
595
638
  counters described above. This includes failed builds and later install
596
639
  or launch failures. The field is absent when no native build returned.
640
+ \`reclaimed\` is present the same way as on success when the run reclaimed
641
+ before failing, including a STIM_LOW_DISK refusal.
597
642
 
598
643
  Branch on \`code\`, never on the message text. \`guide errors\` enumerates
599
644
  every code.
@@ -615,10 +660,10 @@ RULES
615
660
  \`android --device\` and
616
661
  \`ios --device\`, which use a connected physical device Stim never
617
662
  creates, boots, or deletes.`
618
- },
619
- devmenu: {
620
- summary: "why the Expo dev menu or Tools button is or is not over the app, per platform and device kind",
621
- body: () => ` EVERY DEV-CLIENT DEEP LINK CARRIES disableOnboarding=1
663
+ },
664
+ devmenu: {
665
+ summary: "why the Expo dev menu or Tools button is or is not over the app, per platform and device kind",
666
+ body: () => ` EVERY DEV-CLIENT DEEP LINK CARRIES disableOnboarding=1
622
667
  INSIDE ITS PROJECT URL
623
668
  (\`...?url=http%3A%2F%2Fhost%3Aport%2F%3FdisableOnboarding%3D1&disableFab=1\`),
624
669
  and expo-dev-launcher finishes its own dev-menu ONBOARDING
@@ -709,11 +754,11 @@ RULES
709
754
  developer trust, has no API at all and is always the user's.
710
755
  \`guide errors unverified\` has the signature and the
711
756
  full commands.`
712
- },
713
- gc: {
714
- summary: "the gc report payload: mode, sections, reasons, failures, and the gc refusals",
715
- body: () => ` stim gc [--delete] [--older-than <days>] [--cache <name|all|workspaces>]
716
- [--worktrees] --json
757
+ },
758
+ gc: {
759
+ summary: "the gc report payload: mode, sections, reasons, failures, and the gc refusals",
760
+ body: () => ` stim gc [--delete] [--older-than <days>] [--cache <name|all|workspaces>]
761
+ [--worktrees] [--idle <duration>] --json
717
762
 
718
763
  The report the text prints, as one payload. Show the user its sections
719
764
  before you run \`gc --delete\`. Under --delete it is the report that run
@@ -722,16 +767,18 @@ RULES
722
767
  \`stim gc --json\` again to see what is left.
723
768
 
724
769
  mode "dry-run" | "delete"
770
+ idle the --idle duration in milliseconds, or null
725
771
  cacheScope the --cache name, or null. When set, devices, project
726
772
  entries and locks were not inspected and their sections
727
773
  are empty
728
774
  olderThan the --older-than days, or null
729
775
  worktreeSweep null without --worktrees; otherwise { olderThan, defaulted }:
730
776
  the idle days a linked worktree needs, and whether that is
731
- the default 7 because --older-than was not given
777
+ the default 7 because --older-than was not given. Merged
778
+ worktrees are swept either way
732
779
  actionable true when --delete with the same flags reclaims something
733
- failures null on a dry run; under --delete, the entries it could
734
- not delete
780
+ failures null on a dry run without --idle; otherwise the entries it
781
+ could not delete or shut down
735
782
  sections one array per report section, in the text order. Every key
736
783
  is present, empty when there is nothing to report:
737
784
  deadProjects { path }
@@ -739,16 +786,31 @@ RULES
739
786
  orphanedPorts { project, label, port }
740
787
  orphanedWorkspaces { dir, projectRoot, bytes } --delete removes the
741
788
  whole workspace directory
742
- linkedWorktrees { path, idleDays, willRemove, reason, detail }
743
- only with --worktrees
789
+ linkedWorktrees { path, idleDays, mergedInto, willRemove,
790
+ reason, detail } mergedInto is the default
791
+ branch HEAD is merged into ("origin/main"),
792
+ or null; detail says why it is removed
793
+ ("merged into origin/main", "idle 9d") or kept.
794
+ Without --worktrees, the source checkout and
795
+ roots outside git are left out
744
796
  parkedSimulators { udid, name, model, runtime, parkedAt, bytes,
745
- listed }
797
+ listed } with --older-than, only those parked
798
+ at least that long
746
799
  parkedEmulators { name, systemImage, parkedAt, bytes, listed }
800
+ likewise
747
801
  orphanedDevices { kind, id, name, bytes, directory }
802
+ unverifiedDevices { kind, id, name, command } stim-* devices Stim
803
+ has no record of creating; never deleted, run
804
+ command yourself
748
805
  staleDevices { kind, id, name, project, slot, idleDays,
749
806
  bytes } only with --older-than
750
807
  staleDeviceRecords { kind, id, project, slot } --delete clears
751
808
  the record only
809
+ idleDevices { kind, id, name, project, slot, lastActivityAt,
810
+ idleForMs, buildInProgress } booted owned
811
+ devices whose status activity is "idle";
812
+ --idle shuts down those idle long enough,
813
+ --delete never touches them
752
814
  orphanedEasSessions { id, name, platform, status, projectScope }
753
815
  staleBuildLocks { path, platform, key, pid, projectRoot }
754
816
  staleBuildSlots { path, index, pid, projectRoot }
@@ -776,13 +838,15 @@ RULES
776
838
 
777
839
  reason is null for an entry --delete acts on, and otherwise a stable code
778
840
  to branch on; detail is the text line, for the user. Never parse detail.
841
+ A linked worktree's detail is never null.
779
842
  workspaceBuildOutputs unresolved | in-use | last-use-unknown |
780
843
  recently-used
781
844
  linkedWorktrees not-a-worktree | bare-repository |
782
845
  source-checkout-unknown | source-checkout |
783
846
  locked | in-use | status-unreadable | dirty |
784
847
  unpushed-unchecked | unpushed | submodules |
785
- last-use-unknown | recently-used
848
+ not-merged | merge-unknown | last-use-unknown |
849
+ recently-used
786
850
 
787
851
  These print the error contract instead and exit 1:
788
852
 
@@ -790,13 +854,49 @@ RULES
790
854
 
791
855
  - a --cache name that no shared cache carries; the remedy names the
792
856
  caches on this machine
793
- - --cache together with --worktrees`
794
- },
795
- status: {
796
- summary: "the status payload's build field: a running ios or android build, its phase, and its estimate",
797
- body: () => ` stim status --json
798
-
799
- Each entry of environments carries build: null, or the ios or android
857
+ - --cache together with --worktrees or --idle`
858
+ },
859
+ status: {
860
+ summary: "the status payload's build and device activity fields: a running build, its estimate, and who drives each device",
861
+ body: () => ` stim status --json
862
+
863
+ Each booted simulator and detected emulator in environments (and in
864
+ slots) carries activity; a shut-down or physical device has none:
865
+
866
+ activity { state, driver?, lastActivityAt?, basis }
867
+
868
+ state "driven" a live claim or driver holds the device now
869
+ "active" no driver, but activity in the last 10 minutes
870
+ "idle" no driver and no activity for 10 minutes or more
871
+ "unknown" a claim or driver check could not be read;
872
+ never treated as idle
873
+ driver { tool, pid, since } for "driven": agent-device, stim device
874
+ lock, xcodebuild, idb, maestro, appium, simctl,
875
+ uiautomator or instrumentation; pid and since are null when
876
+ the claim does not record them
877
+ lastActivityAt the newest of this device's app log records, this
878
+ platform's Metro bundle requests and the workspace's last
879
+ Stim run; absent when none is recorded
880
+ basis the evidence behind state, strongest first:
881
+ agent-device-claim, agent-device-lease agent-device state,
882
+ read only; live only when every recorded process is alive
883
+ with its recorded start time, so a reused pid is dead
884
+ device-lock an unexpired \`stim device lock\` lease
885
+ driver-process a host process naming the UDID or serial
886
+ instrumentation an on-device uiautomator or androidx.test
887
+ process (one adb shell ps per emulator)
888
+ device-log, metro-bundle, workspace-use recency
889
+
890
+ Plain \`stim status\` appends it to each device line: "driven by
891
+ agent-device for 12m", "active", "idle 3h", or "activity unknown (...)".
892
+ \`gc --idle <duration>\` shuts down owned devices idle that long
893
+ (\`guide cleanup gc\`).
894
+
895
+ An environment's metro carries idleStop { reason: "idle", at, idleMinutes }
896
+ when its supervisor stopped the dev server for idleness and nothing serves
897
+ the port since; plain \`status\` prints "stopped (idle)" (\`guide metro\`).
898
+
899
+ Each entry of environments also carries build: null, or the ios or android
800
900
  run that holds this workspace's native-run.lock:
801
901
 
802
902
  build { platform, slot, state, phase, startedAt, phaseStartedAt,
@@ -828,10 +928,10 @@ RULES
828
928
 
829
929
  There is no completion fraction: a compile's log volume depends on what
830
930
  is already built, so it does not measure progress.`
831
- },
832
- stats: {
833
- summary: "the stats payload, what counts as a run, hit, miss and failed, timeSavedMs, the heartbeat estimate",
834
- body: () => ` stim stats --json
931
+ },
932
+ stats: {
933
+ summary: "the stats payload, what counts as a run, hit, miss and failed, timeSavedMs, the heartbeat estimate",
934
+ body: () => ` stim stats --json
835
935
 
836
936
  { "version": 1,
837
937
  "project": { "key": "<path>", "ios": <bucket|null>,
@@ -885,14 +985,12 @@ HOW A RUN IS COUNTED (\`stats\`)
885
985
  and a warm run has no long phase to size. That read takes no lock and
886
986
  ignores what it cannot read, so nothing about statistics can change a
887
987
  run's outcome.`
988
+ }
888
989
  }
889
- }
890
- };
891
- //#endregion
892
- //#region src/guide/metro.ts
893
- var metro_default = {
894
- summary: "The dev server: `stim start`, the supervisor, and starting your own",
895
- body: () => `THE DEV SERVER
990
+ },
991
+ metro: {
992
+ summary: "The dev server: `stim start`, the supervisor, and starting your own",
993
+ body: () => `THE DEV SERVER
896
994
 
897
995
  stim start
898
996
  stim start --remote # prepare Metro for a remote device
@@ -952,11 +1050,12 @@ REMOTE DEVICE BACKENDS
952
1050
  requires AGENT_DEVICE_DAEMON_BASE_URL and
953
1051
  AGENT_DEVICE_DAEMON_AUTH_TOKEN. Stim creates no remote session for it.
954
1052
 
955
- The EAS backend needs eas-cli and an account with EAS Simulator access. An
956
- EAS session is billable. EAS does not inherit the proxy credentials. Always
957
- tear the session down: \`stop\`, \`worktree remove\`, and \`gc --delete\`
958
- can end sessions that Stim proves it owns. \`stim status\` lists each
959
- recorded session with its preview URL (see \`guide facts\`).
1053
+ The EAS backend needs eas-cli 21.6.0 or later and an account with EAS
1054
+ Simulator access. An EAS session is billable. EAS does not inherit the proxy
1055
+ credentials. Always tear the session down: \`stop\`, \`worktree remove\`,
1056
+ and \`gc --delete\` can end sessions that Stim proves it owns.
1057
+ \`stim status\` lists each recorded session with its preview URL (see
1058
+ \`guide facts\`).
960
1059
 
961
1060
  IDEMPOTENT
962
1061
  A healthy dev server on the reserved port is a no-op: \`start\` prints the
@@ -995,6 +1094,31 @@ WHAT THE SUPERVISOR IS
995
1094
  the device-log collectors, shuts the owned device down (never deletes it)
996
1095
  and frees the port.
997
1096
 
1097
+ IDLE STOP: the supervisor stops its dev server after metro.idleStopMinutes
1098
+ (default 60; 0 never stops) with no bundle request, no client log record
1099
+ (in-app console logs; for Expo, any stdout line of the Expo CLI) and no Stim
1100
+ command in the workspace (start, ios, android, reload, worktree warm). It
1101
+ checks once a minute. It keeps running while a bundle response is in
1102
+ flight, while a build in the workspace is in progress, while a stim ios,
1103
+ android or stop run holds the workspace's native-run lock, while the
1104
+ workspace holds an unexpired device lease (\`stim device lock\` or a
1105
+ --device run, physical devices included), while one of the workspace's
1106
+ devices is driven or its activity is unknown (\`guide facts status\`; on
1107
+ Windows, which has no ps, the host driver-process probe is skipped), and
1108
+ while a \`stim start\` holds the workspace's metro-start lock. It checks
1109
+ all of this again under the native-run and metro-start locks before it
1110
+ stops the server. A Fast Refresh update is not a bundle request:
1111
+ an open app that only hot-reloads and logs nothing counts as idle, and a
1112
+ connected app alone does not keep the server. It records a
1113
+ supervisor_idle_stopped line in metro.ndjson and devServerStop in
1114
+ state.json, so \`status\` shows "stopped (idle)" rather than a crash.
1115
+ Devices stay booted. The next \`stim start\` starts it again and clears the
1116
+ record, also when it reuses a dev server that another process started. A
1117
+ Debug \`ios\` or \`android\` run starts it the same way and reports
1118
+ devServer.reason "stopped (idle)". The setting
1119
+ is read when \`start\` spawns the supervisor; a value that is not a whole
1120
+ number of 0 or more is refused, as \`stim settings set\` refuses it.
1121
+
998
1122
  ENVIRONMENT: the supervisor -- and through it the dev server, including a
999
1123
  metro.config.js evaluated inside the expo child -- inherits the environment
1000
1124
  of the \`start\` call that SPAWNED it. A later \`start\` that finds a healthy
@@ -1038,12 +1162,10 @@ STARTING YOUR OWN BUNDLER STILL WORKS
1038
1162
  \`stop\` leaves an externally started server running and keeps the port
1039
1163
  reserved while it answers from this project. Stop it with the tool that
1040
1164
  started it.`
1041
- };
1042
- //#endregion
1043
- //#region src/guide/ports.ts
1044
- var ports_default = {
1045
- summary: "Workspace ports for web and API servers that Stim does not manage",
1046
- body: () => `NAMED SERVER PORTS
1165
+ },
1166
+ ports: {
1167
+ summary: "Workspace ports for web and API servers that Stim does not manage",
1168
+ body: () => `NAMED SERVER PORTS
1047
1169
 
1048
1170
  If Stim is not installed globally, replace stim with npx stim.
1049
1171
 
@@ -1095,12 +1217,10 @@ cleanup: versions without ports do not know about named allocations.
1095
1217
 
1096
1218
  A shared API does not get a shared reservation. Pass its port by environment
1097
1219
  instead of allocating a separate label in every worktree.`
1098
- };
1099
- //#endregion
1100
- //#region src/guide/logs.ts
1101
- var logs_default = {
1102
- summary: "Querying the merged NDJSON timeline, and what --errors means",
1103
- body: () => `LOGS
1220
+ },
1221
+ logs: {
1222
+ summary: "Querying the merged NDJSON timeline, and what --errors means",
1223
+ body: () => `LOGS
1104
1224
 
1105
1225
  stim logs [filters]
1106
1226
 
@@ -1386,41 +1506,40 @@ WHAT WRITES WHAT
1386
1506
 
1387
1507
  A collector is killed and replaced on the next \`ios\` / \`android\` run for
1388
1508
  that platform, and reaped by \`stop\`.`
1389
- };
1390
- //#endregion
1391
- //#region src/guide/errors.ts
1392
- const errors = {
1393
- summary: "Every refusal Stim can print: an index of codes, and one section for each",
1394
- sectionHint: "<CODE>",
1395
- preamble: () => `WHAT STIM REFUSES, AND WHY
1509
+ },
1510
+ errors: {
1511
+ summary: "Every refusal Stim can print: an index of codes, and one section for each",
1512
+ sectionHint: "<CODE>",
1513
+ preamble: () => `WHAT STIM REFUSES, AND WHY
1396
1514
 
1397
1515
  Every refusal listed here carries a stable CODE, whichever command prints it.
1398
1516
  Branch on the code, never on the message.`,
1399
- sections: {
1400
- STIM_EAS_BUILD_MISSING: {
1401
- summary: "no completed EAS development build matches; build only with session authorization",
1402
- separator: "--- EAS BUILD CODES (`ios --eas-profile` / `android --eas-profile`) ---",
1403
- body: () => `STIM_EAS_BUILD_MISSING
1517
+ sections: {
1518
+ STIM_EAS_BUILD_MISSING: {
1519
+ summary: "no completed EAS development build matches; build only with session authorization",
1520
+ separator: "--- EAS BUILD CODES (`ios --eas-profile` / `android --eas-profile`) ---",
1521
+ body: () => `STIM_EAS_BUILD_MISSING
1404
1522
  No compatible build matches the selected EAS project, profile, platform and
1405
1523
  native fingerprint. No device was acquired and no local or cloud build was
1406
1524
  started. The remedy prints the exact npx eas-cli build command. Check session
1407
1525
  authorization for its potential cost before running it, then retry Stim.
1408
1526
  See stim guide lifecycle eas.`
1409
- },
1410
- STIM_EAS_UNAVAILABLE: {
1411
- summary: "EAS lookup/download failed, or another run holds the artifact claim",
1412
- body: () => `STIM_EAS_UNAVAILABLE
1413
- EAS CLI is unavailable, a profile/fingerprint/list/download operation failed,
1414
- the response could not be validated, or another run holds the artifact claim.
1527
+ },
1528
+ STIM_EAS_UNAVAILABLE: {
1529
+ summary: "EAS lookup/download failed, or another run holds the artifact claim",
1530
+ body: () => `STIM_EAS_UNAVAILABLE
1531
+ EAS CLI is unavailable or older than 18.9.0, a profile/fingerprint/list/download
1532
+ operation failed, the response could not be validated, or another run holds
1533
+ the artifact claim.
1415
1534
  Follow the printed remedy: inspect the named EAS command or retry once the
1416
1535
  holder finishes. This is not proof that a build is missing. No native build
1417
1536
  is started. See stim guide lifecycle eas.`
1418
- },
1419
- STIM_WORKSPACE_STATE: {
1420
- summary: "$STIM_HOME/workspaces could not be prepared, or the digest directory belongs to another project",
1421
- aliases: ["STIM_WORKSPACE_COLLISION"],
1422
- separator: "--- BUILD-PATH CODES (`stim ios` / `stim android`) ---",
1423
- body: () => `STIM_WORKSPACE_STATE / STIM_WORKSPACE_COLLISION
1537
+ },
1538
+ STIM_WORKSPACE_STATE: {
1539
+ summary: "$STIM_HOME/workspaces could not be prepared, or the digest directory belongs to another project",
1540
+ aliases: ["STIM_WORKSPACE_COLLISION"],
1541
+ separator: "--- BUILD-PATH CODES (`stim ios` / `stim android`) ---",
1542
+ body: () => `STIM_WORKSPACE_STATE / STIM_WORKSPACE_COLLISION
1424
1543
  Stim could not prepare this project's global workspace directory under
1425
1544
  $STIM_HOME/workspaces. Check that STIM_HOME is writable and has free
1426
1545
  space. An EPERM on a directory the user CAN write is a sandbox, not a
@@ -1428,44 +1547,47 @@ Branch on the code, never on the message.`,
1428
1547
  readable-name-plus-digest directory already has a workspace.json for a
1429
1548
  different canonical project path; do not overwrite it until you identify
1430
1549
  which workspace owns it.`
1431
- },
1432
- STIM_NO_METRO: {
1433
- summary: "nothing provably this workspace's dev server holds the reserved port (ios, android, reload)",
1434
- body: () => `STIM_NO_METRO
1435
- Nothing that could be proven to be THIS workspace's dev server holds the
1436
- reserved port -- or no port is reserved at all. The gate fires in about a
1437
- second, before the device is even booted, rather than after four minutes of
1438
- compiling an app that could not load a bundle. Run \`stim start\` first.
1439
- \`--no-metro-check\` overrides it and wires the app to the reservation (or to
1440
- 8081 when there is none). A non-Debug \`ios --configuration\` never emits
1441
- this: a release-shaped build embeds its JS, so the gate does not run at all.
1442
- A port held by SOMETHING ELSE reports what: usually a bundler started from
1443
- the wrong directory (the repo root instead of the app dir in a monorepo), or
1444
- another repo's Metro. Restart it from inside the project, or free the port
1445
- and run \`stim start\` to get a fresh reservation.
1446
-
1550
+ },
1551
+ STIM_NO_METRO: {
1552
+ summary: "the recorded launch's port is not this workspace's live dev server (reload)",
1553
+ body: () => `STIM_NO_METRO
1447
1554
  Reload requires the recorded launch's port to be this workspace's live
1448
- Metro. It refuses a missing, changed, unresponsive, or foreign port.`
1449
- },
1450
- STIM_NO_FINGERPRINT: {
1451
- summary: "@expo/fingerprint produced no hash, so the shared cache cannot be addressed",
1452
- body: () => `STIM_NO_FINGERPRINT
1555
+ Metro. It refuses a missing, changed, unresponsive, or foreign port. Run
1556
+ \`stim start\`, or run \`ios\` or \`android\` again.
1557
+
1558
+ \`ios\` and \`android\` do not emit this code. A Debug run with no healthy
1559
+ dev server on the reserved port starts one, as \`stim start\` would, and
1560
+ refuses only when that start fails, with the start's code
1561
+ (STIM_METRO_TIMEOUT, STIM_SUPERVISOR_EXITED, ...). A port held by SOMETHING
1562
+ ELSE, usually a bundler started from the wrong directory or another repo's
1563
+ Metro, gets a fresh reservation.`
1564
+ },
1565
+ STIM_NO_FINGERPRINT: {
1566
+ summary: "@expo/fingerprint produced no hash, so the shared cache cannot be addressed",
1567
+ body: () => `STIM_NO_FINGERPRINT
1453
1568
  \`@expo/fingerprint\` produced no hash, so the shared build cache cannot be
1454
1569
  addressed. Stim uses its declared @expo/fingerprint dependency directly,
1455
1570
  independently of the target project's package graph. This is a refusal
1456
1571
  rather than a silent full build because an unaddressable cache means every
1457
1572
  workspace on the commit compiles from scratch, forever.`
1458
- },
1459
- STIM_PREBUILD_FAILED: {
1460
- summary: "expo prebuild could not generate the native directory",
1461
- body: () => `STIM_PREBUILD_FAILED
1462
- \`expo prebuild\` could not generate the missing native directory. The
1463
- extracted output is above the code; the transcript is in
1464
- the global workspace logs/build-<platform>.ndjson file.`
1465
- },
1466
- STIM_DEPS_FAILED: {
1467
- summary: "pod install or gradle sync failed; the bundler ladder and BUNDLE_FROZEN",
1468
- body: () => `STIM_DEPS_FAILED
1573
+ },
1574
+ STIM_PREBUILD_FAILED: {
1575
+ summary: "expo prebuild could not generate or regenerate the native directory",
1576
+ body: () => `STIM_PREBUILD_FAILED
1577
+ \`expo prebuild\` could not generate the missing native directory, or
1578
+ regenerate a stale one. The extracted output is above the code; the
1579
+ transcript is in the global workspace logs/build-<platform>.ndjson file.
1580
+ The same code refuses a native directory that the fingerprint leaves out of
1581
+ the cache key while git tracks its files, or when git cannot answer: Stim
1582
+ cannot show those files came from the current app config, and will not
1583
+ delete tracked files with \`expo prebuild --clean\`. Stop excluding the
1584
+ directory from the fingerprint (.fingerprintignore), or gitignore and
1585
+ untrack it so Stim regenerates it. When git itself is failing, fix the
1586
+ checkout first.`
1587
+ },
1588
+ STIM_DEPS_FAILED: {
1589
+ summary: "pod install or gradle sync failed; the bundler ladder and BUNDLE_FROZEN",
1590
+ body: () => `STIM_DEPS_FAILED
1469
1591
  \`pod install\` (iOS) or the gradle dependency sync (Android) failed. On iOS
1470
1592
  this runs only when Podfile.lock and Pods/Manifest.lock disagree, or Pods is
1471
1593
  absent -- which is exactly what a carried worktree produces.
@@ -1493,10 +1615,10 @@ Branch on the code, never on the message.`,
1493
1615
  \`yarn install\`, \`bun install\`, \`npm ci\`) or that same pod ladder. The
1494
1616
  message names the command, quotes its last lines, and nothing is copied: fix
1495
1617
  the source checkout, then warm again.`
1496
- },
1497
- STIM_BUILD_FAILED: {
1498
- summary: "xcodebuild or gradle failed; the two Android APK refusals; a damaged compilation-cache object",
1499
- body: () => `STIM_BUILD_FAILED
1618
+ },
1619
+ STIM_BUILD_FAILED: {
1620
+ summary: "xcodebuild or gradle failed; the two Android APK refusals; a damaged compilation-cache object",
1621
+ body: () => `STIM_BUILD_FAILED
1500
1622
  xcodebuild or gradle failed. The EXTRACTED diagnostics are printed (capped),
1501
1623
  not the transcript. Read the log path on the next line for the rest.
1502
1624
  Two Android refusals share this code without gradle itself failing:
@@ -1527,10 +1649,10 @@ Branch on the code, never on the message.`,
1527
1649
  object, and upgrading the CLI does not clear it. Empty that one cache with
1528
1650
  \`gc --delete --cache "compilation cache"\`, then build again. The next
1529
1651
  build is a cold one.`
1530
- },
1531
- STIM_PATH_TOO_LONG: {
1532
- summary: "Windows only: the project root leaves no room for the NDK object paths ninja must open",
1533
- body: () => `STIM_PATH_TOO_LONG (android, Windows only)
1652
+ },
1653
+ STIM_PATH_TOO_LONG: {
1654
+ summary: "Windows only: the project root leaves no room for the NDK object paths ninja must open",
1655
+ body: () => `STIM_PATH_TOO_LONG (android, Windows only)
1534
1656
  The NDK's ninja is not long-path aware, so every CMake object path has to
1535
1657
  stay under Windows' MAX_PATH. React Native's codegen objects are named
1536
1658
  after their mangled absolute source path, so each carries the project root
@@ -1546,10 +1668,10 @@ Branch on the code, never on the message.`,
1546
1668
  preflight does not inspect that configured path and may refuse it too.
1547
1669
  \`doctor --platform android\` reports the same root as a finding for the
1548
1670
  emulator's ABI.`
1549
- },
1550
- fallbacks: {
1551
- summary: "release cache-hit notes that are not codes: swap failure, asset gate, uninstall, device fallbacks",
1552
- body: () => `FALLBACK NOTES THAT ARE NOT CODES (release cache hits)
1671
+ },
1672
+ fallbacks: {
1673
+ summary: "release cache-hit notes that are not codes: swap failure, asset gate, uninstall, device fallbacks",
1674
+ body: () => `FALLBACK NOTES THAT ARE NOT CODES (release cache hits)
1553
1675
  On a release cache hit Stim regenerates this workspace's JS bundle into a
1554
1676
  COPY of the cached artifact before installing it -- \`ios --configuration
1555
1677
  Release\` into a copy of the .app, \`android --variant ...Release\` into a
@@ -1615,10 +1737,10 @@ Branch on the code, never on the message.`,
1615
1737
  The same refusal on a FRESHLY BUILT app is a code (STIM_NO_PROFILE,
1616
1738
  STIM_PROFILE_MISMATCH, STIM_NO_SIGNING_IDENTITY), not a note: building again
1617
1739
  would produce the same app and refuse again.`
1618
- },
1619
- STIM_BUILD_WAIT_TIMEOUT: {
1620
- summary: "waited ~90 minutes for another workspace's build of the same fingerprint",
1621
- body: () => `STIM_BUILD_WAIT_TIMEOUT
1740
+ },
1741
+ STIM_BUILD_WAIT_TIMEOUT: {
1742
+ summary: "waited ~90 minutes for another workspace's build of the same fingerprint",
1743
+ body: () => `STIM_BUILD_WAIT_TIMEOUT
1622
1744
  This run was waiting for ANOTHER workspace's build of the same fingerprint
1623
1745
  (see \`guide lifecycle concurrency\`), and no artifact arrived within ~90
1624
1746
  minutes.
@@ -1627,10 +1749,10 @@ Branch on the code, never on the message.`,
1627
1749
  may have failed. The message names the current pid and lock directory:
1628
1750
  check the pid, and if it is not really building, remove that directory and
1629
1751
  run the command again.`
1630
- },
1631
- STIM_CLAIM_REFUSED: {
1632
- summary: "an ownership claim blocks the operation; inspect its holder before removing an unresolved claim",
1633
- body: () => `STIM_CLAIM_REFUSED
1752
+ },
1753
+ STIM_CLAIM_REFUSED: {
1754
+ summary: "an ownership claim blocks the operation; inspect its holder before removing an unresolved claim",
1755
+ body: () => `STIM_CLAIM_REFUSED
1634
1756
  A build lock records the holder's process IDENTITY, not just its pid, so a
1635
1757
  recycled pid reads as a gone builder rather than a live one, and a builder
1636
1758
  busy in a long \`simctl\` or gradle call reads as live rather than as stale.
@@ -1668,10 +1790,10 @@ Branch on the code, never on the message.`,
1668
1790
  Legacy inline \`deletionClaim\` fields in config.json have no process
1669
1791
  identity and require separate manual inspection; \`stim guide lifecycle pool\`
1670
1792
  describes their field-only recovery.`
1671
- },
1672
- STIM_CLAIM_UNAVAILABLE: {
1673
- summary: "a process identity or warm claim store is unavailable, so the protected operation refuses",
1674
- body: () => `STIM_CLAIM_UNAVAILABLE
1793
+ },
1794
+ STIM_CLAIM_UNAVAILABLE: {
1795
+ summary: "a process identity or warm claim store is unavailable, so the protected operation refuses",
1796
+ body: () => `STIM_CLAIM_UNAVAILABLE
1675
1797
  Every ownership claim records the holder's process identity, captured through
1676
1798
  the \`unique-pid\` native module. This code is that capture failing: no
1677
1799
  prebuilt binary for this platform and architecture, or the OS refusing to
@@ -1691,10 +1813,10 @@ Branch on the code, never on the message.`,
1691
1813
  access to the existing claim store, checking parent permissions, symlink
1692
1814
  targets, mount access and sandbox rules, then retry. Do not switch STIM_HOME to evade a claim:
1693
1815
  concurrent runs must coordinate through the same store.`
1694
- },
1695
- STIM_INSTALL_FAILED: {
1696
- summary: "simctl, adb, or devicectl refused the artifact; the one signer-conflict retry",
1697
- body: () => `STIM_INSTALL_FAILED
1816
+ },
1817
+ STIM_INSTALL_FAILED: {
1818
+ summary: "simctl, adb, or devicectl refused the artifact; the one signer-conflict retry",
1819
+ body: () => `STIM_INSTALL_FAILED
1698
1820
  The artifact built or came from cache, but \`simctl install\` / \`adb install\` /
1699
1821
  \`devicectl device install app\` refused it. A signature or architecture
1700
1822
  mismatch, or a full device.
@@ -1712,11 +1834,14 @@ Branch on the code, never on the message.`,
1712
1834
  installed -- the built APK's applicationId, which on a flavored project is
1713
1835
  the flavor's id and not the gradle namespace -- and gives you the
1714
1836
  \`adb -s <serial> uninstall <applicationId>\` that clears it. Re-running after
1715
- that is a cache hit: one install, no build.`
1716
- },
1717
- STIM_LAUNCH_FAILED: {
1718
- summary: "installed but would not start; the developer-trust tap on a phone",
1719
- body: () => `STIM_LAUNCH_FAILED
1837
+ that is a cache hit: one install, no build.
1838
+ On a REMOTE device (\`--remote\`) agent-device refused the upload or install.
1839
+ With \`--remote eas\` the EAS Simulator session stays up and billed; the
1840
+ remedy names it. Rerun to reuse it, or run \`stim stop\` to end it.`
1841
+ },
1842
+ STIM_LAUNCH_FAILED: {
1843
+ summary: "installed but would not start; the developer-trust tap on a phone",
1844
+ body: () => `STIM_LAUNCH_FAILED
1720
1845
  Installed, but the app would not start. On Android this usually means no
1721
1846
  launchable activity resolved.
1722
1847
  On a local iOS simulator, a timed-out launch can mean the simulator cannot
@@ -1733,26 +1858,28 @@ Branch on the code, never on the message.`,
1733
1858
  VPN & Device Management, tap the developer profile under DEVELOPER APP, tap
1734
1859
  Trust, then run the command again. It is a per-developer-certificate tap, not
1735
1860
  a per-build one -- but an uninstall clears it, including the one Stim's own
1736
- signer-conflict retry performs.`
1737
- },
1738
- STIM_NO_SCHEME: {
1739
- summary: "Xcode schemes unavailable or no unambiguous app scheme in ios/",
1740
- body: () => `STIM_NO_SCHEME
1861
+ signer-conflict retry performs.
1862
+ With \`--remote eas\` the remedy names the EAS Simulator session that is
1863
+ still running, as for STIM_INSTALL_FAILED.`
1864
+ },
1865
+ STIM_NO_SCHEME: {
1866
+ summary: "Xcode schemes unavailable or no unambiguous app scheme in ios/",
1867
+ body: () => `STIM_NO_SCHEME
1741
1868
  Stim could not list or select an app scheme in ios/. Share the intended app
1742
1869
  scheme so xcodebuild can see it. Select an available exact name with
1743
1870
  \`stim ios --scheme <name>\`. An unknown explicit name prints available choices.
1744
1871
  Without an explicit selector, a workspace
1745
1872
  name match wins; otherwise Stim accepts a sole non-test scheme, or a listed
1746
1873
  scheme matching app.json. Unmatched ambiguous schemes are refused.`
1747
- },
1748
- STIM_NO_PROFILE: {
1749
- summary: "no or undecodable embedded.mobileprovision; build once from Xcode",
1750
- separator: "--- iOS SIGNING CODES (`ios --device`, and only there) ---",
1751
- context: `A simulator build needs no signature, which is why none of these can fire on
1874
+ },
1875
+ STIM_NO_PROFILE: {
1876
+ summary: "no or undecodable embedded.mobileprovision; build once from Xcode",
1877
+ separator: "--- iOS SIGNING CODES (`ios --device`, and only there) ---",
1878
+ context: `A simulator build needs no signature, which is why none of these can fire on
1752
1879
  the normal path. A device build carries one, and Stim re-seals any bundle it
1753
1880
  modifies with the identity the bundle already names -- so it checks, before
1754
1881
  spending a build or a bundle, that the check can succeed.`,
1755
- body: () => `STIM_NO_PROFILE
1882
+ body: () => `STIM_NO_PROFILE
1756
1883
  The built or cached .app has no embedded.mobileprovision, or
1757
1884
  \`openssl smime\` could not decode the one it has. The first means the build
1758
1885
  produced an unsigned app -- almost always a simulator-sliced artifact.
@@ -1761,14 +1888,14 @@ spending a build or a bundle, that the check can succeed.`,
1761
1888
  profile. Stim will not do that step: registering a device or minting a
1762
1889
  profile changes your Apple Developer account, so Stim never passes
1763
1890
  -allowProvisioningUpdates.`
1764
- },
1765
- STIM_PROFILE_MISMATCH: {
1766
- summary: "the profile is expired, has no ProvisionedDevices, or does not name this UDID",
1767
- context: `A simulator build needs no signature, which is why none of these can fire on
1891
+ },
1892
+ STIM_PROFILE_MISMATCH: {
1893
+ summary: "the profile is expired, has no ProvisionedDevices, or does not name this UDID",
1894
+ context: `A simulator build needs no signature, which is why none of these can fire on
1768
1895
  the normal path. A device build carries one, and Stim re-seals any bundle it
1769
1896
  modifies with the identity the bundle already names -- so it checks, before
1770
1897
  spending a build or a bundle, that the check can succeed.`,
1771
- body: () => `STIM_PROFILE_MISMATCH
1898
+ body: () => `STIM_PROFILE_MISMATCH
1772
1899
  The profile inside the app cannot admit this phone. Three shapes, and the
1773
1900
  message names which one and the profile type it found:
1774
1901
  - it expired, or carries no ExpirationDate at all;
@@ -1782,14 +1909,14 @@ spending a build or a bundle, that the check can succeed.`,
1782
1909
  With --eas-profile, follow the EAS device:create and build commands in the
1783
1910
  refusal instead. Registration, signing changes and cloud builds need session
1784
1911
  authorization. See stim guide lifecycle eas.`
1785
- },
1786
- STIM_NO_SIGNING_IDENTITY: {
1787
- summary: "no single keychain identity resolves; ios.signingIdentitySha1 for two certificates",
1788
- context: `A simulator build needs no signature, which is why none of these can fire on
1912
+ },
1913
+ STIM_NO_SIGNING_IDENTITY: {
1914
+ summary: "no single keychain identity resolves; ios.signingIdentitySha1 for two certificates",
1915
+ context: `A simulator build needs no signature, which is why none of these can fire on
1789
1916
  the normal path. A device build carries one, and Stim re-seals any bundle it
1790
1917
  modifies with the identity the bundle already names -- so it checks, before
1791
1918
  spending a build or a bundle, that the check can succeed.`,
1792
- body: () => `STIM_NO_SIGNING_IDENTITY
1919
+ body: () => `STIM_NO_SIGNING_IDENTITY
1793
1920
  No single keychain identity could be resolved to re-seal with. Either
1794
1921
  \`security find-identity -v -p codesigning\` lists nothing, or the identity
1795
1922
  the artifact's own profile names is absent, or two certificates share that
@@ -1797,14 +1924,14 @@ spending a build or a bundle, that the check can succeed.`,
1797
1924
  Open Xcode > Settings > Accounts and download your certificates, or unlock
1798
1925
  the login keychain with \`security unlock-keychain\`. For the two-certificate
1799
1926
  case, set ios.signingIdentitySha1 to the SHA-1 hash beside the one you want.`
1800
- },
1801
- STIM_CODESIGN_FAILED: {
1802
- summary: "codesign failed on the modified copy; the cache entry is untouched and the run builds fresh",
1803
- context: `A simulator build needs no signature, which is why none of these can fire on
1927
+ },
1928
+ STIM_CODESIGN_FAILED: {
1929
+ summary: "codesign failed on the modified copy; the cache entry is untouched and the run builds fresh",
1930
+ context: `A simulator build needs no signature, which is why none of these can fire on
1804
1931
  the normal path. A device build carries one, and Stim re-seals any bundle it
1805
1932
  modifies with the identity the bundle already names -- so it checks, before
1806
1933
  spending a build or a bundle, that the check can succeed.`,
1807
- body: () => `STIM_CODESIGN_FAILED
1934
+ body: () => `STIM_CODESIGN_FAILED
1808
1935
  \`codesign --force --sign\` or \`codesign --verify --strict\` exited non-zero
1809
1936
  on the modified copy. The verbatim codesign stderr is quoted, because it is
1810
1937
  the answer: a locked login keychain reports errSecInternalComponent, an
@@ -1812,14 +1939,14 @@ spending a build or a bundle, that the check can succeed.`,
1812
1939
  and confirm exactly one identity matches the name. The cache entry itself is
1813
1940
  never modified -- the failure is on a temporary copy, and the run builds
1814
1941
  fresh.`
1815
- },
1816
- STIM_NO_LAN_ADDRESS: {
1817
- summary: "the Mac has no non-internal IPv4 interface; a tunnel cannot help a phone",
1818
- separator: "--- iOS DEVICE DEBUG REACHABILITY CODES (`ios --device` in Debug) ---",
1819
- context: `A phone does not share the host's loopback and USB carries no reverse forward,
1942
+ },
1943
+ STIM_NO_LAN_ADDRESS: {
1944
+ summary: "the Mac has no non-internal IPv4 interface; a tunnel cannot help a phone",
1945
+ separator: "--- iOS DEVICE DEBUG REACHABILITY CODES (`ios --device` in Debug) ---",
1946
+ context: `A phone does not share the host's loopback and USB carries no reverse forward,
1820
1947
  so a Debug run on one is wired to a LAN origin instead of localhost. Both codes
1821
1948
  fire BEFORE the build, because a refusal that costs a build is a bad refusal.`,
1822
- body: () => `STIM_NO_LAN_ADDRESS
1949
+ body: () => `STIM_NO_LAN_ADDRESS
1823
1950
  This Mac reports no non-internal IPv4 interface, so there is no address to
1824
1951
  give the phone: it is offline, or on nothing but utun/awdl/bridge. Join a
1825
1952
  Wi-Fi or Ethernet network, or connect this Mac by cable, and run again.
@@ -1828,13 +1955,13 @@ fire BEFORE the build, because a refusal that costs a build is a bad refusal.`,
1828
1955
  ip.txt is read by RCTBundleURLProvider, which prefixes the scheme. A tunnel
1829
1956
  cannot be expressed to a phone, so --device ignores metro.publicUrl,
1830
1957
  metro.tunnel and metro.ngrokUrl and says so when one is set.`
1831
- },
1832
- STIM_LAN_METRO_UNREACHABLE: {
1833
- summary: "the LAN origin did not answer as this workspace's Metro; ios.lanHost on a multi-NIC Mac",
1834
- context: `A phone does not share the host's loopback and USB carries no reverse forward,
1958
+ },
1959
+ STIM_LAN_METRO_UNREACHABLE: {
1960
+ summary: "the LAN origin did not answer as this workspace's Metro; ios.lanHost on a multi-NIC Mac",
1961
+ context: `A phone does not share the host's loopback and USB carries no reverse forward,
1835
1962
  so a Debug run on one is wired to a LAN origin instead of localhost. Both codes
1836
1963
  fire BEFORE the build, because a refusal that costs a build is a bad refusal.`,
1837
- body: () => `STIM_LAN_METRO_UNREACHABLE
1964
+ body: () => `STIM_LAN_METRO_UNREACHABLE
1838
1965
  The chosen LAN origin did not answer as THIS workspace's Metro: no answer, a
1839
1966
  5xx, or a dev server that is not this one -- the message says which.
1840
1967
  \`stim start\` prints the port it reserved. On a Mac with several interfaces
@@ -1844,12 +1971,12 @@ fire BEFORE the build, because a refusal that costs a build is a bad refusal.`,
1844
1971
  routes a host connection to its own address over loopback, so the gate passes
1845
1972
  through a firewall that will block the phone. That evidence only ever arrives
1846
1973
  from the phone's own bundle request, which is what \`launched\` reports.`
1847
- },
1848
- unverified: {
1849
- summary: "launched: \"unverified\" with the Local Network path reason, and the routed recovery",
1850
- context: `A phone does not share the host's loopback and USB carries no reverse forward,
1974
+ },
1975
+ unverified: {
1976
+ summary: "launched: \"unverified\" with the Local Network path reason, and the routed recovery",
1977
+ context: `A phone does not share the host's loopback and USB carries no reverse forward,
1851
1978
  so a Debug run on one is wired to a LAN origin instead of localhost.`,
1852
- body: () => `LAUNCH UNVERIFIED, LOCAL NETWORK NOT GRANTED (not a code -- a routed remedy)
1979
+ body: () => `LAUNCH UNVERIFIED, LOCAL NETWORK NOT GRANTED (not a code -- a routed remedy)
1853
1980
  An app that has not been granted Local Network reaches nothing on the LAN,
1854
1981
  and CFNetwork reports each attempt as NSURLErrorDomain -1009 "The Internet
1855
1982
  connection appears to be offline." with the path reason
@@ -1935,11 +2062,11 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
1935
2062
  General > VPN & Device Management) is refused to automation by the same gate
1936
2063
  that refuses the app, agent-device's own runner included, so its remedy is
1937
2064
  "ask the user" and nothing else. An uninstall clears both.`
1938
- },
1939
- STIM_NO_DEVICE: {
1940
- summary: "no usable phone, or the owned simulator or emulator could not be created or booted",
1941
- separator: "--- DEVICE AND CAPACITY CODES ---",
1942
- body: () => `STIM_NO_DEVICE
2065
+ },
2066
+ STIM_NO_DEVICE: {
2067
+ summary: "no usable phone, or the owned simulator or emulator could not be created or booted",
2068
+ separator: "--- DEVICE AND CAPACITY CODES ---",
2069
+ body: () => `STIM_NO_DEVICE
1943
2070
  With \`--device\`, no physical device answered the selection: none connected,
1944
2071
  a named serial/UDID that is not connected, several connected with none named
1945
2072
  (the refusal lists them), or one that is connected but unusable -- an
@@ -1996,10 +2123,10 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
1996
2123
  not silently boot a different one. Reap it (\`worktree remove\`, or
1997
2124
  \`gc --delete\`) and run \`stim ios\` again to create the requested model.
1998
2125
  That loses the old sim's app state.`
1999
- },
2000
- STIM_DEVICE_BUSY: {
2001
- summary: "another workspace holds the lease on that phone and the wait ran out",
2002
- body: () => `STIM_DEVICE_BUSY
2126
+ },
2127
+ STIM_DEVICE_BUSY: {
2128
+ summary: "another workspace holds the lease on that phone and the wait ran out",
2129
+ body: () => `STIM_DEVICE_BUSY
2003
2130
  Only on a \`--device\` run. Another workspace holds the lease on that phone,
2004
2131
  and the wait ran out: the message names the holder root, the device, and the
2005
2132
  expiry as a clock time and a remaining duration, and \`--json\` adds
@@ -2013,10 +2140,10 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
2013
2140
  no token left in its \`state.json\` (its workspace directory was recreated).
2014
2141
  The remedy for that last one is \`stim device unlock\`, which releases by
2015
2142
  holder rather than by token.`
2016
- },
2017
- STIM_DEVICE_WIRELESS_FAILED: {
2018
- summary: "an iPhone paired over Wi-Fi timed out or dropped during install or launch; use the cable",
2019
- body: () => `STIM_DEVICE_WIRELESS_FAILED
2143
+ },
2144
+ STIM_DEVICE_WIRELESS_FAILED: {
2145
+ summary: "an iPhone paired over Wi-Fi timed out or dropped during install or launch; use the cable",
2146
+ body: () => `STIM_DEVICE_WIRELESS_FAILED
2020
2147
  Only on an \`ios --device\` run whose iPhone devicectl reaches over Wi-Fi
2021
2148
  (transportType localNetwork). The \`devicectl device install app\` ran past
2022
2149
  its 15-minute Wi-Fi bound, the phone did not appear in its own process list
@@ -2028,10 +2155,10 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
2028
2155
  untrusted host, Developer Mode off, full storage, the developer-trust tap --
2029
2156
  keeps STIM_INSTALL_FAILED or STIM_LAUNCH_FAILED and its own remedy, even
2030
2157
  when the step also timed out and even during a signer-conflict reinstall.`
2031
- },
2032
- STIM_DEVICE_LOST: {
2033
- summary: "the lease was gone or re-held at the pre-install check; rerun",
2034
- body: () => `STIM_DEVICE_LOST
2158
+ },
2159
+ STIM_DEVICE_LOST: {
2160
+ summary: "the lease was gone or re-held at the pre-install check; rerun",
2161
+ body: () => `STIM_DEVICE_LOST
2035
2162
  Only on a \`--device\` run. The run held a lease, and the raise before the
2036
2163
  install found it gone or held under another token -- another workspace took
2037
2164
  the device in that window. The message names the new holder and its expiry.
@@ -2039,10 +2166,10 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
2039
2166
  AFTER the install has started this is not a failure: the app is already on
2040
2167
  the phone, so the run prints one warning, continues, and reports
2041
2168
  \`lease: null\` in \`--json\`.`
2042
- },
2043
- STIM_AT_CAPACITY: {
2044
- summary: "concurrency.maxDevices reached; a refusal, not a queue",
2045
- body: () => `STIM_AT_CAPACITY
2169
+ },
2170
+ STIM_AT_CAPACITY: {
2171
+ summary: "concurrency.maxDevices reached; a refusal, not a queue",
2172
+ body: () => `STIM_AT_CAPACITY
2046
2173
  Only when concurrency.maxDevices is set (it is UNSET by default, so this never
2047
2174
  fires unless you opted in). Booting a NEW owned device would exceed the cap:
2048
2175
  the machine already has that many Stim-owned devices booted. It is a refusal, not
@@ -2053,10 +2180,34 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
2053
2180
  \`ios\` on an environment you already have is idempotent. (The build cap
2054
2181
  behaves differently: a compile WAITS for a free slot rather than refusing.
2055
2182
  See \`guide lifecycle concurrency\`.)`
2056
- },
2057
- STIM_BUILD_SLOT_TIMEOUT: {
2058
- summary: "the maxBuilds wait gave up with every slot held by a running process",
2059
- body: () => `STIM_BUILD_SLOT_TIMEOUT
2183
+ },
2184
+ STIM_LOW_DISK: {
2185
+ summary: "free disk stayed below budget.hardFloorDiskGb after reclaiming",
2186
+ body: () => `STIM_LOW_DISK
2187
+ \`start\`, \`ios\` and \`android\` check free disk on the volumes holding
2188
+ the app and $STIM_HOME before they build or boot anything. Below
2189
+ budget.minFreeDiskGb (20 GB by default) they first reclaim what Stim can
2190
+ prove idle (\`guide lifecycle budget\`). This code means a volume was still
2191
+ below budget.hardFloorDiskGb (5 GB by default) afterwards, so the run
2192
+ stopped instead of filling the disk. Nothing was built or booted.
2193
+
2194
+ The message names the free space and the largest uses it could measure:
2195
+ Stim state, the shared build cache, simulators, Xcode DerivedData and
2196
+ Android emulators. The \`--json\` payload lists what was already reclaimed
2197
+ under \`reclaimed\`. To free more:
2198
+
2199
+ stim gc # what else Stim can reclaim
2200
+ stim gc --delete # clear idle workspace outputs, dead entries
2201
+ stim gc --delete --cache all # empty the shared caches
2202
+ stim gc --delete --worktrees # remove clean, idle worktrees
2203
+
2204
+ Ask before deleting anything outside Stim, such as Xcode DerivedData or
2205
+ simulators Stim did not create. Lowering budget.hardFloorDiskGb (0 never
2206
+ refuses) only removes the protection.`
2207
+ },
2208
+ STIM_BUILD_SLOT_TIMEOUT: {
2209
+ summary: "the maxBuilds wait gave up with every slot held by a running process",
2210
+ body: () => `STIM_BUILD_SLOT_TIMEOUT
2060
2211
  Only when concurrency.maxBuilds is set. The build cap does not refuse, it
2061
2212
  WAITS -- this code is that wait giving up: ~90 minutes elapsed and every one
2062
2213
  of the N slots was still held by a running process, or by a holder Stim could
@@ -2070,50 +2221,54 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
2070
2221
  the message names the directory: remove the slot of a builder that is not
2071
2222
  building, or raise concurrency.maxBuilds
2072
2223
  (\`guide lifecycle concurrency\`).`
2073
- },
2074
- STIM_NO_REMOTE_SESSION: {
2075
- summary: "the backend could not use agent-device, or metro.tunnel names an unusable provider",
2076
- separator: "--- REMOTE-DEVICE CODES (`ios --remote <proxy|eas>` / `android --remote <proxy|eas>`) ---",
2077
- body: () => `STIM_NO_REMOTE_SESSION
2224
+ },
2225
+ STIM_NO_REMOTE_SESSION: {
2226
+ summary: "the backend could not use agent-device, or metro.tunnel names an unusable provider",
2227
+ separator: "--- REMOTE-DEVICE CODES (`ios --remote <proxy|eas>` / `android --remote <proxy|eas>`) ---",
2228
+ body: () => `STIM_NO_REMOTE_SESSION
2078
2229
  The selected backend could not use agent-device, or metro.tunnel names a
2079
2230
  provider or mode this workspace cannot use (e.g. "expo" on a bare RN
2080
2231
  project). The remedy line says which. Nothing was created yet.`
2081
- },
2082
- STIM_REMOTE_PROXY_CONFIG: {
2083
- summary: "--remote proxy needs AGENT_DEVICE_DAEMON_BASE_URL and AGENT_DEVICE_DAEMON_AUTH_TOKEN",
2084
- body: () => `STIM_REMOTE_PROXY_CONFIG
2232
+ },
2233
+ STIM_REMOTE_PROXY_CONFIG: {
2234
+ summary: "--remote proxy needs AGENT_DEVICE_DAEMON_BASE_URL and AGENT_DEVICE_DAEMON_AUTH_TOKEN",
2235
+ body: () => `STIM_REMOTE_PROXY_CONFIG
2085
2236
  \`--remote proxy\` requires AGENT_DEVICE_DAEMON_BASE_URL and
2086
2237
  AGENT_DEVICE_DAEMON_AUTH_TOKEN. These variables provide credentials after
2087
2238
  proxy is selected. They never select the backend.`
2088
- },
2089
- STIM_REMOTE_EAS_UNAVAILABLE: {
2090
- summary: "--remote eas needs eas-cli",
2091
- body: () => `STIM_REMOTE_EAS_UNAVAILABLE
2092
- \`--remote eas\` requires eas-cli. Proxy environment variables do not change
2239
+ },
2240
+ STIM_REMOTE_EAS_UNAVAILABLE: {
2241
+ summary: "--remote eas needs eas-cli 21.6.0 or later",
2242
+ body: () => `STIM_REMOTE_EAS_UNAVAILABLE
2243
+ \`--remote eas\` requires eas-cli 21.6.0 or later, the first release with
2244
+ the \`eas simulator:*\` commands Stim runs. Stim reads \`eas --version\` and
2245
+ refuses before any build or session work when eas-cli is missing, older, or
2246
+ reports no version. Upgrade it (\`npm install --global eas-cli@latest\`, or
2247
+ the project's eas-cli dependency). Proxy environment variables do not change
2093
2248
  this selection and are not passed to EAS.`
2094
- },
2095
- STIM_REMOTE_PLATFORM_MISMATCH: {
2096
- summary: "the recorded remote session belongs to the other platform; stop, then rerun",
2097
- body: () => `STIM_REMOTE_PLATFORM_MISMATCH
2249
+ },
2250
+ STIM_REMOTE_PLATFORM_MISMATCH: {
2251
+ summary: "the recorded remote session belongs to the other platform; stop, then rerun",
2252
+ body: () => `STIM_REMOTE_PLATFORM_MISMATCH
2098
2253
  This workspace already has a recorded remote session, and it belongs to the
2099
2254
  OTHER platform ("Session <id> belongs to android, not ios"). A workspace
2100
2255
  holds one remote session, and Stim will not end the recorded one to make
2101
2256
  room -- it may be mid-run for whoever started it. Run \`stim stop\` for this
2102
2257
  workspace, then re-run with the platform you want. Nothing was created here.`
2103
- },
2104
- STIM_REMOTE_SESSION_STATE: {
2105
- summary: "the EAS session was created but its state could not be recorded, so Stim stopped it",
2106
- body: () => `STIM_REMOTE_SESSION_STATE
2258
+ },
2259
+ STIM_REMOTE_SESSION_STATE: {
2260
+ summary: "the EAS session was created but its state could not be recorded, so Stim stopped it",
2261
+ body: () => `STIM_REMOTE_SESSION_STATE
2107
2262
  The EAS session was created and is healthy, but recording it in this
2108
2263
  workspace's state failed (an unwritable STIM_HOME, a full disk). A session
2109
2264
  nothing references is a session nothing will ever stop, so Stim stopped the
2110
2265
  one it had just created and removed its ownership claim before reporting:
2111
2266
  this code means nothing is running and nothing is still billing. Repair the
2112
2267
  state storage the message names, then run the remote command again.`
2113
- },
2114
- STIM_REMOTE_SESSION_CLEANUP: {
2115
- summary: "Stim could not prove an EAS session ended; eas simulator:stop --id",
2116
- body: () => `STIM_REMOTE_SESSION_CLEANUP
2268
+ },
2269
+ STIM_REMOTE_SESSION_CLEANUP: {
2270
+ summary: "Stim could not prove an EAS session ended; eas simulator:stop --id",
2271
+ body: () => `STIM_REMOTE_SESSION_CLEANUP
2117
2272
  Stim tried to end an EAS session and could not PROVE it ended: \`eas
2118
2273
  simulator:stop\` failed, or its output did not confirm the stop, or the
2119
2274
  session stopped but its claim in the machine ledger could not be removed.
@@ -2122,10 +2277,10 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
2122
2277
  \`eas simulator:stop --id <id>\` -- and for a ledger that outlived its
2123
2278
  session, the ledger path to repair. The same code covers a recorded session
2124
2279
  that could not be verified before replacement: inspect it, then \`stim stop\`.`
2125
- },
2126
- STIM_REMOTE_METRO_WRONG: {
2127
- summary: "the tunnel reaches a Metro that is not this workspace's",
2128
- body: () => `STIM_REMOTE_METRO_WRONG
2280
+ },
2281
+ STIM_REMOTE_METRO_WRONG: {
2282
+ summary: "the tunnel reaches a Metro that is not this workspace's",
2283
+ body: () => `STIM_REMOTE_METRO_WRONG
2129
2284
  The gate that proves a tunnel still reaches THIS workspace's Metro failed --
2130
2285
  before a session or a build, whether the tunnel is Expo's own, one Stim
2131
2286
  started (metro.tunnel: cloudflared/ngrok/auto), or a named metro.publicUrl.
@@ -2134,39 +2289,39 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
2134
2289
  port), and it now serves ANOTHER workspace's dev server -- healthy, and
2135
2290
  wrong. Re-run \`stim start\` (it prints the port it reserved) and, for a
2136
2291
  manual tunnel, rebuild it against that port.`
2137
- },
2138
- STIM_REMOTE_METRO_UNREACHABLE: {
2139
- summary: "a remote start could not create its managed tunnel or tell the device where Metro is",
2140
- body: () => `STIM_REMOTE_METRO_UNREACHABLE
2292
+ },
2293
+ STIM_REMOTE_METRO_UNREACHABLE: {
2294
+ summary: "a remote start could not create its managed tunnel or tell the device where Metro is",
2295
+ body: () => `STIM_REMOTE_METRO_UNREACHABLE
2141
2296
  A remote start could not create its selected managed tunnel, or the device
2142
2297
  could not be told where Metro is. Follows the same remedy as
2143
2298
  STIM_NO_REMOTE_SESSION's tunnel guidance -- set metro.tunnel, or use
2144
2299
  metro.publicUrl for an existing endpoint.`
2145
- },
2146
- STIM_RELOAD_AMBIGUOUS: {
2147
- summary: "both owned apps are live; name the platform",
2148
- separator: "--- RELOAD CODES (`stim reload [ios|android]`) ---",
2149
- body: () => `STIM_RELOAD_AMBIGUOUS
2300
+ },
2301
+ STIM_RELOAD_AMBIGUOUS: {
2302
+ summary: "both owned apps are live; name the platform",
2303
+ separator: "--- RELOAD CODES (`stim reload [ios|android]`) ---",
2304
+ body: () => `STIM_RELOAD_AMBIGUOUS
2150
2305
  Both owned apps are live. Name ios or android; Stim never guesses.`
2151
- },
2152
- STIM_RELOAD_RELEASE: {
2153
- summary: "the live app has embedded JS; run a Debug build first",
2154
- body: () => `STIM_RELOAD_RELEASE
2306
+ },
2307
+ STIM_RELOAD_RELEASE: {
2308
+ summary: "the live app has embedded JS; run a Debug build first",
2309
+ body: () => `STIM_RELOAD_RELEASE
2155
2310
  The live app was launched with embedded JavaScript. Run the platform command
2156
2311
  with a Debug configuration or variant first.`
2157
- },
2158
- STIM_RELOAD_STOPPED: {
2159
- summary: "the recorded app is gone, its device is not live and owned, or the process could not be proven",
2160
- aliases: ["STIM_RELOAD_UNOWNED", "STIM_RELOAD_PROBE_FAILED"],
2161
- body: () => `STIM_RELOAD_STOPPED / STIM_RELOAD_UNOWNED / STIM_RELOAD_PROBE_FAILED
2312
+ },
2313
+ STIM_RELOAD_STOPPED: {
2314
+ summary: "the recorded app is gone, its device is not live and owned, or the process could not be proven",
2315
+ aliases: ["STIM_RELOAD_UNOWNED", "STIM_RELOAD_PROBE_FAILED"],
2316
+ body: () => `STIM_RELOAD_STOPPED / STIM_RELOAD_UNOWNED / STIM_RELOAD_PROBE_FAILED
2162
2317
  The recorded app is gone, its exact device is not live and owned by this
2163
2318
  workspace, or simctl/adb could not prove the process exists. No launch or
2164
2319
  device lifecycle action is taken; follow the printed platform-command or
2165
2320
  process-probe remedy.`
2166
- },
2167
- STIM_RELOAD_FAILED: {
2168
- summary: "the reload failed; the remedy differs by shape -- read it before acting",
2169
- body: () => `STIM_RELOAD_FAILED
2321
+ },
2322
+ STIM_RELOAD_FAILED: {
2323
+ summary: "the reload failed; the remedy differs by shape -- read it before acting",
2324
+ body: () => `STIM_RELOAD_FAILED
2170
2325
  The Metro websocket reload did not reach a peer Stim could identify. Two
2171
2326
  shapes reach this code and the remedy differs. Read it rather than assuming.
2172
2327
 
@@ -2192,27 +2347,27 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
2192
2347
  MORE THAN ONE MATCHING PEER IS NOT A FAILURE. A workspace Metro serves one
2193
2348
  app, so several matching peers are that app on several devices. Stim reloads
2194
2349
  every one of them and reports the count in the facts as targets.`
2195
- },
2196
- STIM_WORKTREE_REMOVAL_IN_PROGRESS: {
2197
- summary: "a managed remote start found worktree remove holding the lock; wait, then rerun",
2198
- separator: "--- DEV-SERVER CODES (`stim start`) ---",
2199
- body: () => `STIM_WORKTREE_REMOVAL_IN_PROGRESS
2350
+ },
2351
+ STIM_WORKTREE_REMOVAL_IN_PROGRESS: {
2352
+ summary: "a managed remote start found worktree remove holding the lock; wait, then rerun",
2353
+ separator: "--- DEV-SERVER CODES (`stim start`) ---",
2354
+ body: () => `STIM_WORKTREE_REMOVAL_IN_PROGRESS
2200
2355
  A managed remote start found that \`stim worktree remove\` owns the
2201
2356
  worktree lock. The start did not register the project or create a tunnel.
2202
2357
  Wait for removal to finish, then run \`stim start --remote\` again.`
2203
- },
2204
- STIM_REMOTE_START_REQUIRED: {
2205
- summary: "a running server cannot gain a remote tunnel; stop, then start --remote, or metro.publicUrl",
2206
- body: () => `STIM_REMOTE_START_REQUIRED
2358
+ },
2359
+ STIM_REMOTE_START_REQUIRED: {
2360
+ summary: "a running server cannot gain a remote tunnel; stop, then start --remote, or metro.publicUrl",
2361
+ body: () => `STIM_REMOTE_START_REQUIRED
2207
2362
  A healthy bare or Expo server was started without its required remote
2208
2363
  tunnel. A running server cannot gain that option. For a Stim supervisor,
2209
2364
  run \`stim stop\`, then \`stim start --remote\`. For an external server,
2210
2365
  configure metro.publicUrl or let Stim supervise the server.`
2211
- },
2212
- STIM_BARE_DEPS: {
2213
- summary: "the supervisor cannot host Metro from the project's node_modules; the @stim-cli/metro capture note",
2214
- aliases: ["STIM_BARE_LOAD", "STIM_BARE_API"],
2215
- body: () => `STIM_BARE_DEPS / STIM_BARE_LOAD / STIM_BARE_API (bare RN)
2366
+ },
2367
+ STIM_BARE_DEPS: {
2368
+ summary: "the supervisor cannot host Metro from the project's node_modules; the @stim-cli/metro capture note",
2369
+ aliases: ["STIM_BARE_LOAD", "STIM_BARE_API"],
2370
+ body: () => `STIM_BARE_DEPS / STIM_BARE_LOAD / STIM_BARE_API (bare RN)
2216
2371
  The supervisor hosts Metro out of the PROJECT's node_modules, so metro,
2217
2372
  @react-native/dev-middleware and @react-native-community/cli-server-api must
2218
2373
  be installed there and must match the project's React Native. DEPS = not
@@ -2224,25 +2379,27 @@ captured" (in metro.ndjson, bare RN)
2224
2379
  The dev server is serving; only capture is missing, so \`logs\` would report
2225
2380
  a quiet timeline for a broken build. Install \`@stim-cli/metro\` as a
2226
2381
  devDependency of the project.`
2227
- },
2228
- STIM_EXPO_BIN: {
2229
- summary: "node_modules/.bin/expo is missing; install dependencies",
2230
- body: () => `STIM_EXPO_BIN (Expo)
2382
+ },
2383
+ STIM_EXPO_BIN: {
2384
+ summary: "node_modules/.bin/expo is missing; install dependencies",
2385
+ body: () => `STIM_EXPO_BIN (Expo)
2231
2386
  node_modules/.bin/expo does not exist. Install the project's dependencies.`
2232
- },
2233
- STIM_METRO_TIMEOUT: {
2234
- summary: "the supervisor is alive but Metro or the tunnel was not ready within the wait; --wait 180",
2235
- body: () => `STIM_METRO_TIMEOUT
2387
+ },
2388
+ STIM_METRO_TIMEOUT: {
2389
+ summary: "the supervisor is alive but Metro or the tunnel was not ready within the wait; --wait 180",
2390
+ body: () => `STIM_METRO_TIMEOUT
2236
2391
  "The dev server did not answer on port <n> within <s>s."
2237
2392
  The supervisor is alive, but Metro or its requested Expo tunnel is not ready.
2238
- \`start\` has already
2393
+ A Debug \`ios\` or \`android\` run that starts the dev server reports the
2394
+ same refusal after the default 60s; re-running it waits for the same
2395
+ supervisor again, and \`stim start --wait 180\` waits longer. \`start\` has already
2239
2396
  printed the last lines of the global workspace logs/supervisor.log above this -- read
2240
2397
  them. A cold Metro on a large graph can genuinely need more than the default
2241
2398
  60s: re-run with \`--wait 180\`. Otherwise \`stim stop\`, then \`start\`.`
2242
- },
2243
- STIM_SUPERVISOR_EXITED: {
2244
- summary: "the dev server failed outright; the quoted supervisor.log tail is the real error",
2245
- body: () => `STIM_SUPERVISOR_EXITED
2399
+ },
2400
+ STIM_SUPERVISOR_EXITED: {
2401
+ summary: "the dev server failed outright; the quoted supervisor.log tail is the real error",
2402
+ body: () => `STIM_SUPERVISOR_EXITED
2246
2403
  "The supervisor exited (<code|signal>) before the dev server came up"
2247
2404
  The dev server failed outright, and the quoted evidence is the real error:
2248
2405
  the supervisor.log tail if it wrote one, plus this attempt's error records
@@ -2257,11 +2414,11 @@ captured" (in metro.ndjson, bare RN)
2257
2414
  Stim's processes. For a legacy record, stop the server with the tool that
2258
2415
  started it before retrying. Never reconstruct ownership from a process
2259
2416
  name, port, or wall-clock timestamp.`
2260
- },
2261
- STIM_BAD_ARG: {
2262
- summary: "an argument, setting, directory, flavor, or device name refused before anything starts",
2263
- aliases: ["STIM_NO_PROJECT"],
2264
- body: () => `STIM_BAD_ARG / STIM_NO_PROJECT
2417
+ },
2418
+ STIM_BAD_ARG: {
2419
+ summary: "an argument, setting, directory, flavor, or device name refused before anything starts",
2420
+ aliases: ["STIM_NO_PROJECT"],
2421
+ body: () => `STIM_BAD_ARG / STIM_NO_PROJECT
2265
2422
  The command refused before doing anything: an unusable --wait value, a known
2266
2423
  setting with the wrong type ("Invalid <key> setting <value>. Expected <shape>."
2267
2424
  -- \`guide settings\` names the type each key takes), an invalid
@@ -2301,11 +2458,11 @@ captured" (in metro.ndjson, bare RN)
2301
2458
  (\`simctl list runtimes\`, the SDK's system-images directory) runs only when
2302
2459
  a name was actually given, and a listing that fails is reported as
2303
2460
  STIM_NO_DEVICE naming the tool, never as a crash.`
2304
- },
2305
- STIM_LOCK_REFUSED: {
2306
- summary: "a directory lock is held by a removal, which is never waited out",
2307
- separator: "--- COORDINATION CODES (any command that shares a resource) ---",
2308
- body: () => `STIM_LOCK_REFUSED
2461
+ },
2462
+ STIM_LOCK_REFUSED: {
2463
+ summary: "a directory lock is held by a removal, which is never waited out",
2464
+ separator: "--- COORDINATION CODES (any command that shares a resource) ---",
2465
+ body: () => `STIM_LOCK_REFUSED
2309
2466
  A directory lock that serialises two commands over the same thing -- this
2310
2467
  workspace's managed tunnel, its managed remote worktree, the machine's EAS
2311
2468
  project ledger -- is held by a REMOVAL, and a removal is never waited out:
@@ -2314,10 +2471,10 @@ captured" (in metro.ndjson, bare RN)
2314
2471
  \`workspace removal\` -- both are \`stim worktree remove\`). Let it finish,
2315
2472
  then run the command again. \`start --remote\` reports this same case as
2316
2473
  STIM_WORKTREE_REMOVAL_IN_PROGRESS instead.`
2317
- },
2318
- STIM_LOCK_TIMEOUT: {
2319
- summary: "a lock held past the wait; workspace-process and short directory locks",
2320
- body: () => `STIM_LOCK_TIMEOUT
2474
+ },
2475
+ STIM_LOCK_TIMEOUT: {
2476
+ summary: "a lock held past the wait; workspace-process and short directory locks",
2477
+ body: () => `STIM_LOCK_TIMEOUT
2321
2478
  The same locks, held by an ordinary command that is still running, for
2322
2479
  longer than the wait -- 60s by default, 4 minutes for the remote-session and
2323
2480
  EAS project locks, and ~90 minutes for the \`worktree warm\` lock, which one
@@ -2349,11 +2506,11 @@ captured" (in metro.ndjson, bare RN)
2349
2506
  These paths still time out; verify that no holder is running before removing
2350
2507
  only the named directory. Standalone Metro and Expo cache packages use the
2351
2508
  same core protocol and do not require the Stim CLI.`
2352
- },
2353
- teardown: {
2354
- summary: "an unmanaged port, an unverified supervisor, and a failed device teardown",
2355
- separator: "--- TEARDOWN AND WORKSPACE REFUSALS ---",
2356
- body: () => `"metro refusing to kill port <n>: ... runs from <dir>, outside
2509
+ },
2510
+ teardown: {
2511
+ summary: "an unmanaged port, an unverified supervisor, and a failed device teardown",
2512
+ separator: "--- TEARDOWN AND WORKSPACE REFUSALS ---",
2513
+ body: () => `"metro refusing to kill port <n>: ... runs from <dir>, outside
2357
2514
  <project>" (stop)
2358
2515
  Stim only signals processes it launched and whose saved identity still
2359
2516
  matches. Stop an externally started server with the tool that started it.
@@ -2385,10 +2542,10 @@ the supervisor could not be verified" (stop)
2385
2542
  Stim could not release the owned device and keeps its record for a retry.
2386
2543
  \`worktree remove\` exits 1 without removing the worktree while the device is
2387
2544
  still tracked. Fix the reported cause and re-run.`
2388
- },
2389
- remove: {
2390
- summary: "worktree remove refused a dirty tree: what it restores itself and what --force discards",
2391
- body: () => `"Refusing to remove <path>: uncommitted changes / untracked files / commits
2545
+ },
2546
+ remove: {
2547
+ summary: "worktree remove refused a dirty tree: what it restores itself and what --force discards",
2548
+ body: () => `"Refusing to remove <path>: uncommitted changes / untracked files / commits
2392
2549
  not on any remote" (worktree remove)
2393
2550
  A native build rewrites tracked files, and Stim now RESTORES the one class
2394
2551
  it can prove is not work: when the only dirt left is \`pod install\` churn
@@ -2408,10 +2565,10 @@ not on any remote" (worktree remove)
2408
2565
  refusal actually named.
2409
2566
  Use --force only when you genuinely intend to discard work; it deletes
2410
2567
  uncommitted and untracked files permanently.`
2411
- },
2412
- STIM_MAIN_DIRTY: {
2413
- summary: "warm --refresh will not move a source checkout with local work or an operation in progress",
2414
- body: () => `STIM_MAIN_DIRTY
2568
+ },
2569
+ STIM_MAIN_DIRTY: {
2570
+ summary: "warm --refresh will not move a source checkout with local work or an operation in progress",
2571
+ body: () => `STIM_MAIN_DIRTY
2415
2572
  \`worktree warm --refresh\` writes to the SOURCE CHECKOUT, and it refuses one
2416
2573
  it cannot move: tracked files with uncommitted changes (the refusal names
2417
2574
  them), or a rebase or merge in progress. Untracked files are not a reason to
@@ -2421,28 +2578,28 @@ not on any remote" (worktree remove)
2421
2578
  or \`git -C <source-checkout> rebase --abort\`. Nothing was installed or
2422
2579
  copied. Plain \`stim worktree warm\` does not care: it copies from a dirty
2423
2580
  source checkout exactly as it always has.`
2424
- },
2425
- STIM_MAIN_DETACHED: {
2426
- summary: "warm --refresh needs a branch to fast-forward, not a detached HEAD",
2427
- body: () => `STIM_MAIN_DETACHED
2581
+ },
2582
+ STIM_MAIN_DETACHED: {
2583
+ summary: "warm --refresh needs a branch to fast-forward, not a detached HEAD",
2584
+ body: () => `STIM_MAIN_DETACHED
2428
2585
  The source checkout's HEAD is detached, so there is no branch to fast-forward
2429
2586
  and no upstream to fast-forward it to. Run
2430
2587
  \`git -C <source-checkout> checkout <branch>\` and warm again. \`--refresh\`
2431
2588
  never picks a branch for you; a checkout whose job is to seed worktrees
2432
2589
  should sit on a branch someone chose.`
2433
- },
2434
- STIM_MAIN_DIVERGED: {
2435
- summary: "the source checkout is both ahead of and behind its upstream; warm --refresh will not merge",
2436
- body: () => `STIM_MAIN_DIVERGED
2590
+ },
2591
+ STIM_MAIN_DIVERGED: {
2592
+ summary: "the source checkout is both ahead of and behind its upstream; warm --refresh will not merge",
2593
+ body: () => `STIM_MAIN_DIVERGED
2437
2594
  The source checkout's branch has commits its upstream does not, AND its
2438
2595
  upstream has commits it does not. A fast-forward is impossible, and
2439
2596
  \`--refresh\` will not merge or reset someone else's checkout to make one:
2440
2597
  that decision is yours. Rebase or merge it yourself, then warm again. The
2441
2598
  message reports both counts. Nothing was installed or copied.`
2442
- },
2443
- STIM_DEPS_INCOMPLETE: {
2444
- summary: "the last install recorded for the lockfile on disk now did not finish, so warm will not copy it",
2445
- body: () => `STIM_DEPS_INCOMPLETE
2599
+ },
2600
+ STIM_DEPS_INCOMPLETE: {
2601
+ summary: "the last install recorded for the lockfile on disk now did not finish, so warm will not copy it",
2602
+ body: () => `STIM_DEPS_INCOMPLETE
2446
2603
  \`worktree warm\` refuses to copy dependencies the source checkout never
2447
2604
  finished installing. \`--refresh\` records a COMPLETED install of the lockfile
2448
2605
  it read under ~/.stim/warm-installs; an install that failed, or whose process
@@ -2460,20 +2617,20 @@ not on any remote" (worktree remove)
2460
2617
  repository before its first \`--refresh\` -- copies as it always has.
2461
2618
  In a monorepo the record is keyed on the directory that owns the lockfile,
2462
2619
  which is usually the repository root, so every app of it reads the same one.`
2463
- },
2464
- warm: {
2465
- summary: "worktree warm claim storage refusals and recovery",
2466
- body: () => `Both plain warm and --refresh require an ownership claim.
2620
+ },
2621
+ warm: {
2622
+ summary: "worktree warm claim storage refusals and recovery",
2623
+ body: () => `Both plain warm and --refresh require an ownership claim.
2467
2624
  Permission-denied or read-only claim storage reports STIM_CLAIM_UNAVAILABLE
2468
2625
  and names the path. Restore access to the existing claim store and retry.
2469
2626
  A non-directory claim ancestor reports STIM_CLAIM_REFUSED and names the
2470
2627
  blocking file. Inspect it and use the printed move-aside command to preserve
2471
2628
  its contents before retrying. See guide errors STIM_CLAIM_UNAVAILABLE and
2472
2629
  guide errors STIM_CLAIM_REFUSED for the recovery details.`
2473
- },
2474
- carry: {
2475
- summary: "worktree warm copy results, lockfile mismatches, and remedies",
2476
- body: () => `"carry incomplete: ... ignored entries copied, ... kept, ... failed"
2630
+ },
2631
+ carry: {
2632
+ summary: "worktree warm copy results, lockfile mismatches, and remedies",
2633
+ body: () => `"carry incomplete: ... ignored entries copied, ... kept, ... failed"
2477
2634
  (worktree warm)
2478
2635
  At least one entry could not be copied. The command exits 1 and names each
2479
2636
  failure. Existing entries stay untouched; any files already published remain.
@@ -2492,9 +2649,21 @@ not on any remote" (worktree remove)
2492
2649
  Warm copied Pods but the destination has no Podfile.lock. Follow the printed
2493
2650
  pod-install command before building.
2494
2651
 
2652
+ "carry carried [<dir>/]node_modules was installed from a different <lockfile> than ..."
2653
+ The carried node_modules records the lockfile it was installed from (npm
2654
+ node_modules/.package-lock.json, pnpm node_modules/.pnpm/lock.yaml, Yarn
2655
+ node_modules/.yarn-integrity or node_modules/.yarn-state.yml), and it
2656
+ differs from this worktree's lockfile. This fires even when both checkouts
2657
+ share the lockfile, because the source checkout itself was not reinstalled
2658
+ after it changed. Run the printed install command before building;
2659
+ otherwise pod install and native builds resolve the old package versions.
2660
+ A pnpm source checkout installed with --filter or --prod also reads as a
2661
+ mismatch, because pnpm records only what it installed.
2662
+
2495
2663
  "carry carried dependencies may be stale: they do not match ..."
2496
- The source checkout's lockfile differs from this branch's lockfile. Run the
2497
- printed package-manager command before building. A carry whose lockfile
2664
+ node_modules records no install state Stim can read (Bun, or no record),
2665
+ and the source checkout's lockfile differs from this branch's lockfile. Run
2666
+ the printed package-manager command before building. A carry whose lockfile
2498
2667
  matches is silent; the warning means a real difference.
2499
2668
 
2500
2669
  If the source checkout has no dependencies to copy, use this project's
@@ -2502,11 +2671,11 @@ not on any remote" (worktree remove)
2502
2671
  the app is ready, unless \`--refresh\` installed them in the SOURCE CHECKOUT
2503
2672
  first; even then the copy can still carry a lockfile this branch does not
2504
2673
  have, which is exactly what these carry warnings report.`
2505
- },
2506
- environment: {
2507
- summary: "npx registry E401/E404, the Node floor, no free Metro port, the reservation race",
2508
- separator: "--- ENVIRONMENT ---",
2509
- body: () => `"npm error code E401 / E404" while \`npx\` resolves the stim package
2674
+ },
2675
+ environment: {
2676
+ summary: "npx registry E401/E404, the Node floor, no free Metro port, the reservation race",
2677
+ separator: "--- ENVIRONMENT ---",
2678
+ body: () => `"npm error code E401 / E404" while \`npx\` resolves the stim package
2510
2679
  The repo probably pins a private registry in \`.npmrc\`, so \`npx\` looked for
2511
2680
  the package there instead of on npm. Use the public registry for this command:
2512
2681
 
@@ -2527,10 +2696,10 @@ not on any remote" (worktree remove)
2527
2696
  "Could not reserve a Metro port after 5 attempts"
2528
2697
  Several commands raced for the same ports and each one lost. Nothing is
2529
2698
  wrong; retry.`
2530
- },
2531
- sandbox: {
2532
- summary: "running under a sandboxing harness: EPERM under STIM_HOME, CoreSimulatorService, adb",
2533
- body: () => `RUNNING UNDER A SANDBOX
2699
+ },
2700
+ sandbox: {
2701
+ summary: "running under a sandboxing harness: EPERM under STIM_HOME, CoreSimulatorService, adb",
2702
+ body: () => `RUNNING UNDER A SANDBOX
2534
2703
 
2535
2704
  An agent harness that sandboxes shell commands typically permits writes
2536
2705
  inside the project and blocks the rest. Three things Stim needs sit outside
@@ -2577,10 +2746,10 @@ not on any remote" (worktree remove)
2577
2746
 
2578
2747
  A git credential helper is often blocked too. It prints \`failed to store\`
2579
2748
  on a fetch that otherwise succeeded, and is safe to ignore.`
2580
- },
2581
- STIM_CONFIG_CORRUPT: {
2582
- summary: "~/.stim/config.json is not a valid JSON object and Stim never resets it",
2583
- body: () => `STIM_CONFIG_CORRUPT ("Stim config at <path> is not valid JSON",
2749
+ },
2750
+ STIM_CONFIG_CORRUPT: {
2751
+ summary: "~/.stim/config.json is not a valid JSON object and Stim never resets it",
2752
+ body: () => `STIM_CONFIG_CORRUPT ("Stim config at <path> is not valid JSON",
2584
2753
  "... is not a JSON object",
2585
2754
  "... has a projects that is not an object",
2586
2755
  "... has a projects entry "<key>" that is not an object")
@@ -2591,23 +2760,21 @@ not on any remote" (worktree remove)
2591
2760
  resets it for you -- a silent reset would orphan every simulator it names.
2592
2761
  Repair the file, or move it aside (\`mv <path> <path>.broken\`) and accept
2593
2762
  that the devices it recorded become orphans you delete by hand.`
2594
- },
2595
- STIM_RELATIVE_PATH: {
2596
- summary: "STIM_HOME, STIM_BUILD_CACHE or STIM_METRO_CACHE is set to a relative path",
2597
- body: () => `STIM_RELATIVE_PATH ("<NAME>=<value> is not an absolute path")
2763
+ },
2764
+ STIM_RELATIVE_PATH: {
2765
+ summary: "STIM_HOME, STIM_BUILD_CACHE or STIM_METRO_CACHE is set to a relative path",
2766
+ body: () => `STIM_RELATIVE_PATH ("<NAME>=<value> is not an absolute path")
2598
2767
  Any command refuses before it reads or writes state. A relative value would
2599
2768
  resolve against each process's working directory, so the CLI, Metro and the
2600
2769
  Expo build-cache provider would each use a different store. Set the named
2601
2770
  variable to an absolute path, or unset it to use the default. Metro and the
2602
2771
  cache provider, which cannot refuse, ignore a relative value with a warning.`
2772
+ }
2603
2773
  }
2604
- }
2605
- };
2606
- //#endregion
2607
- //#region src/guide/lifecycle.ts
2608
- const lifecycle = {
2609
- summary: "The full worktree -> start -> ios/android -> logs -> teardown flow, with sections for builds, devices and flags",
2610
- preamble: () => `ENVIRONMENT LIFECYCLE
2774
+ },
2775
+ lifecycle: {
2776
+ summary: "The full worktree -> start -> ios/android -> logs -> teardown flow, with sections for builds, devices and flags",
2777
+ preamble: () => `ENVIRONMENT LIFECYCLE
2611
2778
 
2612
2779
  Two workflows share steps 2 through 6.
2613
2780
 
@@ -2638,8 +2805,9 @@ a seed belongs to this workflow.
2638
2805
  deps source /w/main: pnpm-lock.yaml changed -> pnpm install (41s)
2639
2806
  pods source /w/main/apps/mobile: ios/Podfile.lock unchanged -> skipped
2640
2807
 
2641
- # 2. The dev server, under a detached supervisor. Blocks until it is
2642
- # verifiably THIS project's, then hands your shell back.
2808
+ # 2. Optional: the dev server, under a detached supervisor. Blocks until it
2809
+ # is verifiably THIS project's, then hands your shell back. Step 3 runs
2810
+ # this same start when the dev server is not running.
2643
2811
  stim start
2644
2812
  port 8082 (reserved)
2645
2813
  supervisor pid 41233
@@ -2683,10 +2851,17 @@ a seed belongs to this workflow.
2683
2851
  # unwarmed worktrees with no Stim registry entry. Git-created branches stay.
2684
2852
  stim worktree remove
2685
2853
 
2686
- Steps 2 and 3 are ordered, not interchangeable: \`ios\` and \`android\` never
2687
- start the bundler, and refuse with STIM_NO_METRO when nothing holds the
2688
- reserved port. That refusal costs a second; the alternative costs four minutes
2689
- and produces an app that cannot load a bundle.
2854
+ A Debug \`ios\` or \`android\` run checks the reserved port before the device
2855
+ or the build. When no healthy dev server of this workspace answers there, it
2856
+ runs the same start as step 2: a dev server started outside Stim is reused, a
2857
+ reservation held by a foreign process moves to a free port, and the budgets
2858
+ apply. The run then reports \`devServer: { started: true, reason }\` in its
2859
+ JSON, with reason "not running" or "stopped (idle)". When that start fails,
2860
+ the run refuses with the start's own code (STIM_METRO_TIMEOUT,
2861
+ STIM_SUPERVISOR_EXITED, ...) before any build work. Release builds and
2862
+ \`--no-metro-check\` neither check nor start the dev server. \`--remote\`
2863
+ starts it the way \`stim start --remote\` does. Step 2 remains useful to warm
2864
+ Metro while other work happens.
2690
2865
 
2691
2866
  Repeat step 3 whenever a NATIVE input changes. A JS-only edit needs nothing --
2692
2867
  that is what Fast Refresh over the running dev server is for. \`stim reload\` is
@@ -2771,7 +2946,7 @@ ANDROID EMULATOR RESTARTS
2771
2946
  wait windows, diagnostics, and recovery steps.
2772
2947
 
2773
2948
  DESTRUCTIVE COMMANDS -- ask the user first
2774
- gc --delete deletes orphaned stim-* devices, tens of GB
2949
+ gc --delete deletes orphaned devices Stim created, tens of GB
2775
2950
  gc --delete --cache all empties the shared build caches every project uses
2776
2951
  gc --delete --cache <name>
2777
2952
  empties only the caches that carry <name>
@@ -2808,10 +2983,10 @@ WAITING FOR A CHANGE
2808
2983
  the EAS session ledger, adb device arrivals and departures, and simulator
2809
2984
  state, and recomputes every 30 seconds as a fallback. Ctrl+C, SIGTERM or
2810
2985
  closing its stdout ends it with exit 0. Without --json it reprints the human view on change.`,
2811
- sections: {
2812
- eas: {
2813
- summary: "download a matching EAS development build; explicit profile, costs, cache and miss remedies",
2814
- body: () => `EAS DEVELOPMENT BUILDS
2986
+ sections: {
2987
+ eas: {
2988
+ summary: "download a matching EAS development build; explicit profile, costs, cache and miss remedies",
2989
+ body: () => `EAS DEVELOPMENT BUILDS
2815
2990
 
2816
2991
  stim ios --eas-profile ios-simulator
2817
2992
  stim android --eas-profile development
@@ -2883,11 +3058,14 @@ cloud build. Once it completes, retry the same Stim command. No build-on-miss
2883
3058
  flag exists, and Stim never starts a cloud build or falls back to local
2884
3059
  compilation in this mode. Authentication, network, invalid output or download
2885
3060
  failures produce STIM_EAS_UNAVAILABLE, with the failed EAS command to inspect.
3061
+ An eas-cli older than 18.9.0 cannot run build:download --build-id; Stim reads
3062
+ eas --version first and refuses with STIM_EAS_UNAVAILABLE and the upgrade
3063
+ command before any EAS lookup. stim doctor reports it in a project with eas.json.
2886
3064
  If another run holds the artifact claim, wait for it to finish and retry.`
2887
- },
2888
- readiness: {
2889
- summary: "implement optional pending/ready app logs, deadlines, errors, and platform isolation",
2890
- body: () => `OPTIONAL APP READINESS
3065
+ },
3066
+ readiness: {
3067
+ summary: "implement optional pending/ready app logs, deadlines, errors, and platform isolation",
3068
+ body: () => `OPTIONAL APP READINESS
2891
3069
 
2892
3070
  No package or SDK is needed. This applies to debug ios/android launches with
2893
3071
  Metro verification enabled, not release builds or --no-metro-check.
@@ -2947,10 +3125,10 @@ including when device logs are unavailable, the default check applies.
2947
3125
  The app declares readiness; Stim does not inspect a rendered frame. This does
2948
3126
  not change launched JSON semantics or the need to inspect the expected UI and
2949
3127
  stim logs --errors. A reload request does not run this launch check.`
2950
- },
2951
- verification: {
2952
- summary: "reproduce the affected behavior, verify the change on the reported device, and retain proof",
2953
- body: () => `VERIFY THE CHANGE
3128
+ },
3129
+ verification: {
3130
+ summary: "reproduce the affected behavior, verify the change on the reported device, and retain proof",
3131
+ body: () => `VERIFY THE CHANGE
2954
3132
 
2955
3133
  For a UI change or bug fix, decide what observable result would prove the task
2956
3134
  is complete. A successful build, a live process, or an empty log query does
@@ -2980,10 +3158,10 @@ not prove that result.
2980
3158
 
2981
3159
  For a change without a UI effect, use the relevant runtime output or test
2982
3160
  result as proof instead of requiring an unrelated screenshot.`
2983
- },
2984
- progress: {
2985
- summary: "phase lines, the label set, heartbeats and their ~ estimate, what warm, start, stop and remove print",
2986
- body: () => `PROGRESS ON A LONG RUN
3161
+ },
3162
+ progress: {
3163
+ summary: "phase lines, the label set, heartbeats and their ~ estimate, what warm, start, stop and remove print",
3164
+ body: () => `PROGRESS ON A LONG RUN
2987
3165
  Native build progress goes to stderr. In \`--json\` mode, stdout carries only
2988
3166
  the result payload. Plain \`start\` also prints progress on stdout. Every
2989
3167
  progress line has the same shape -- two spaces, a label padded to eleven
@@ -2994,8 +3172,8 @@ result as proof instead of requiring an unrelated screenshot.`
2994
3172
  The labels are a closed set, and nothing else is ever printed in that
2995
3173
  column:
2996
3174
 
2997
- branch build cache caches carry checkout
2998
- deps device
3175
+ branch budget build cache caches carry
3176
+ checkout deps device
2999
3177
  devices error failed findings fingerprint gems
3000
3178
  install installs ip.txt lan launch lease
3001
3179
  lock log
@@ -3077,6 +3255,23 @@ result as proof instead of requiring an unrelated screenshot.`
3077
3255
  device shut down stim-e2e-2
3078
3256
  port released 8084
3079
3257
 
3258
+ \`stop\` verifies the outcome instead of trusting that the shutdown command
3259
+ ran: for a simulator it waits for the Shutdown state the same way park does
3260
+ (\`guide lifecycle pool\`), and for an emulator it waits for the AVD's
3261
+ process lock to clear. A device that never gets there is reported as
3262
+ FAILED, not shut down, and the run's exit code reflects it; \`--json\`
3263
+ carries the same outcome in \`device.<platform>.status\` ("failed") plus a
3264
+ \`remedy\` field naming the manual command and \`stim gc --delete\` as the
3265
+ fallback:
3266
+
3267
+ device failed to shut down stim-e2e-2: simulator 9C1F.. is still
3268
+ Booted after 2 shutdown attempts and 30s of waiting
3269
+ check it with \`xcrun simctl list devices\`, then \`xcrun
3270
+ simctl shutdown 9C1F..\` yourself -- if it will not respond,
3271
+ \`stim gc --delete\` reclaims it
3272
+
3273
+ \`stop\` still never deletes the device itself, even on this failure path.
3274
+
3080
3275
  \`worktree remove\` reports itself the same way: the branch decision, the
3081
3276
  owned device, any released device lease, and this workspace's own state
3082
3277
  directory, each on its own line. Nothing prints on stdout; even the removed
@@ -3103,29 +3298,32 @@ result as proof instead of requiring an unrelated screenshot.`
3103
3298
  until it returns; the next heartbeat then lands on the grid, which is why an
3104
3299
  elapsed value can jump. Read the phase lines, not the wall clock, before
3105
3300
  killing a run.`
3106
- },
3107
- pool: {
3108
- summary: "parked and adopted simulators: what park and adoption clear or keep, the model and runtime match",
3109
- body: () => ` THE SIMULATOR POOL
3301
+ },
3302
+ pool: {
3303
+ summary: "parked and adopted simulators: what park and adoption clear or keep, the model and runtime match",
3304
+ body: () => ` THE SIMULATOR POOL
3110
3305
  \`worktree remove\` PARKS this workspace's owned simulator instead of
3111
3306
  deleting it, and the next workspace that wants the same model and runtime
3112
- ADOPTS it. A simulator that has booted before boots in about 9s; a freshly
3113
- created one costs about 30s, and \`simctl erase\` puts most of that back, so
3114
- a parked simulator keeps its app installed and is cleaned in pieces:
3307
+ ADOPTS it. Adoption reuses the simulator but not its contents:
3115
3308
 
3116
- at park shut down, the app's data cleared on disk (Documents,
3117
- Library, tmp, SystemData: NSUserDefaults, AsyncStorage,
3118
- SQLite), renamed \`stim-parked (<model> <runtime>) <4 hex>\`
3309
+ at park shut down, \`simctl erase\` (apps, app data, keychain,
3310
+ privacy grants, pasteboard, photos, settings), renamed
3311
+ \`stim-parked (<model> <runtime>) <4 hex>\`
3119
3312
  at adoption renamed for the adopting workspace, then, inside the boot
3120
3313
  the run pays anyway, \`simctl privacy reset all\` and
3121
- \`simctl keychain reset\`; at install, every OTHER app the
3122
- previous workspace left is uninstalled
3314
+ \`simctl keychain reset\`; at install, any other app is
3315
+ uninstalled
3123
3316
 
3124
- A parked simulator KEEPS its system state: pasteboard, Safari data, photos,
3125
- contacts, calendars, installed profiles, Simulator settings, app-group
3126
- containers, and device-level defaults. Isolation covers the app's data, the
3127
- privacy grants, the keychain and the installed apps -- not a clean system
3128
- image. Set the bound to 0 when a project needs one.
3317
+ An erased simulator takes about 18 MB on disk instead of the gigabytes a used
3318
+ one holds. Its first boot after the erase takes about 15s instead of a warm
3319
+ boot's few seconds, and the app is always installed again. A SimSlim
3320
+ profile survives the erase.
3321
+
3322
+ Park erases only a simulator that reports Shutdown. It waits up to 15s for
3323
+ the shutdown to settle, retries the shutdown once, and waits up to 15s more.
3324
+ A simulator that is still booted after that is deleted instead, and
3325
+ \`worktree remove\` prints
3326
+ \`could not park <name>: ... -- deleted it instead\`.
3129
3327
 
3130
3328
  Adoption matches the device type AND the runtime EXACTLY: a ticket that asks
3131
3329
  for an iPad never gets an iPhone, and a request for iOS 18.5 never gets 26.5.
@@ -3133,8 +3331,7 @@ result as proof instead of requiring an unrelated screenshot.`
3133
3331
  parked simulators on the old runtime are never adopted; they leave by
3134
3332
  eviction or \`gc --delete\`.
3135
3333
 
3136
- The pool targets at most \`pool.iosParkedMax\` simulators (default 3, about
3137
- 2.5 GB each). Past that the oldest parked one is deleted:
3334
+ The pool targets at most \`pool.iosParkedMax\` simulators (default 3). Past that the oldest parked one is deleted:
3138
3335
 
3139
3336
  device parked stim-parked (iPhone 17 26.5) 9c1f (9C1F..)
3140
3337
  device deleted stim-parked (iPhone 17 26.5) 4b02 (pool over 3)
@@ -3161,9 +3358,10 @@ result as proof instead of requiring an unrelated screenshot.`
3161
3358
 
3162
3359
  and an adopting run says so where a plain boot would say \`booted\`:
3163
3360
 
3164
- device stim-app-412 (iPhone 17 26.5) (9C1F..) adopted (11s)
3361
+ device stim-app-412 (iPhone 17 26.5) (9C1F..) adopted (17s)
3165
3362
 
3166
- That time includes the two resets, so it runs longer than a plain boot.
3363
+ That time includes the first boot after the erase and the two resets, so it
3364
+ runs longer than a plain boot.
3167
3365
  \`stim status\` prints one line while the pool is not empty:
3168
3366
 
3169
3367
  pool: 2 parked iOS simulators (max 3)
@@ -3171,10 +3369,13 @@ result as proof instead of requiring an unrelated screenshot.`
3171
3369
  \`stim gc\` reports the pool, and \`stim gc --delete\` empties every entry
3172
3370
  it can re-verify:
3173
3371
 
3174
- Parked simulators (2, 5.1 GB):
3175
- ios stim-parked (iPhone 17 26.5) 9c1f (9C1F..) iPhone 17 26.5 parked 3d ago 2.6 GB
3372
+ Parked simulators (2, 36 MB):
3373
+ ios stim-parked (iPhone 17 26.5) 9c1f (9C1F..) iPhone 17 26.5 parked 3d ago 18 MB
3176
3374
  --delete attempts every parked simulator and keeps failures.
3177
3375
 
3376
+ \`gc --older-than <days>\` reports and deletes only the devices parked at
3377
+ least that long; one parked at an unknown time stays.
3378
+
3178
3379
  If simulator listing or deletion fails, \`gc --delete\` reports the failure
3179
3380
  and keeps that entry. It never turns an unverified absence into a dropped
3180
3381
  ownership record.
@@ -3190,12 +3391,15 @@ result as proof instead of requiring an unrelated screenshot.`
3190
3391
  \`pool.androidParkedMax\` (default 3) or STIM_POOL_ANDROID_PARKED_MAX.
3191
3392
  A redirected STIM_HOME disables parking unless that environment override
3192
3393
  is set. Zero disables parking and adoption. \`stop\` keeps the assignment;
3193
- \`worktree remove\` parks eligible AVDs, and \`gc --delete\` empties the pool.
3394
+ \`worktree remove\` parks eligible AVDs, and \`gc --delete\` empties the pool
3395
+ (with --older-than, only the AVDs parked that long).
3194
3396
 
3195
3397
  Adoption matches the system image, data partition size, and the creation
3196
3398
  settings from android.avdConfig / android.avdConfigFile. The AVD keeps its
3197
- original stim-<label> name so its Quick Boot snapshot can survive reuse.
3198
- Normal desktop boots allow Quick Boot; headless Linux disables snapshots.
3399
+ original stim-<label> name. Parking wipes its user data, cache and
3400
+ encryption-key images and its snapshots, the files the emulator recreates
3401
+ on the next boot, so a parked AVD takes a few MB and adoption pays a cold
3402
+ boot (about 20s) and a fresh install.
3199
3403
  Incompatible AVDs stay parked until eviction or GC. AVDs created by older
3200
3404
  versions without a recorded creation configuration are deleted at removal.
3201
3405
 
@@ -3211,9 +3415,10 @@ result as proof instead of requiring an unrelated screenshot.`
3211
3415
  manually clearing a claim, and keep the AVD data while its process state
3212
3416
  cannot be verified.
3213
3417
 
3214
- Android cleanup happens AFTER boot, before install or launch: \`adb shell
3215
- pm clear\` clears the adopting app's data while retaining its APK, and
3216
- other third-party apps are uninstalled. If ADB goes offline or closes the
3418
+ Android cleanup happens AFTER boot, before install or launch, for AVDs
3419
+ parked with their data by older versions: \`adb shell pm clear\` clears
3420
+ the adopting app's data while retaining its APK, and other third-party apps
3421
+ are uninstalled. If ADB goes offline or closes the
3217
3422
  connection, Stim waits for boot readiness and retries cleanup for up to 30
3218
3423
  seconds, verifying the same owned AVD before each destructive command.
3219
3424
  Failed cleanup blocks launch and remains pending for a retry. The installed APK's SHA-256 must match the
@@ -3222,15 +3427,15 @@ result as proof instead of requiring an unrelated screenshot.`
3222
3427
  If the retained APK has a conflicting signer or version, adoption uninstalls
3223
3428
  it and retries installation. Adoption stays pending until installation succeeds.
3224
3429
 
3225
- Parked AVDs retain app data until adoption. System apps, shared storage,
3226
- accounts and device settings also remain: this is not a factory reset.
3227
- Set the Android bound to 0 when a project needs a fresh device. Status lists
3228
- parked Android emulators; GC reports their system image, age and disk size.
3430
+ The wipe resets apps, accounts, device settings and the shared storage
3431
+ inside the data partition; the AVD's creation settings and any separate
3432
+ SD card image remain. Status lists parked Android emulators; GC
3433
+ reports their system image, age and disk size.
3229
3434
  `
3230
- },
3231
- builds: {
3232
- summary: "optional cache warm-up, build optimizations, fingerprints, .fingerprintignore, install unchanged, runtime state",
3233
- body: () => `OPTIONAL CACHE WARM-UP FOR REPEATED NATIVE WORK
3435
+ },
3436
+ builds: {
3437
+ summary: "optional cache warm-up, build optimizations, fingerprints, .fingerprintignore, install unchanged, runtime state",
3438
+ body: () => `OPTIONAL CACHE WARM-UP FOR REPEATED NATIVE WORK
3234
3439
  When several native worktrees are coming, build the source checkout once to
3235
3440
  seed the shared caches before warming the linked worktrees. Skip this extra
3236
3441
  build for one-off or JavaScript-only work. For local simulator or emulator
@@ -3475,6 +3680,25 @@ THE BUILD CACHE HAS THREE LEVELS
3475
3680
  of compiling beside it. No second line means nothing was found there and the
3476
3681
  run compiles.
3477
3682
 
3683
+ A GITIGNORED (CNG) ios/ OR android/ IS NOT IN THE KEY: @expo/fingerprint
3684
+ hashes the app config instead, so the directory must come from that config.
3685
+ Stim records the fingerprint each of its prebuilds produced in the
3686
+ workspace state. On a miss, an existing CNG directory with no matching
3687
+ record -- the app config, a config plugin or a dependency changed since, or
3688
+ someone else generated it -- is regenerated with \`expo prebuild --clean\`
3689
+ before pods and the compile, so a stale directory is never built and stored
3690
+ under the new key:
3691
+
3692
+ prebuild ios/ not generated from this fingerprint -> regenerated with --clean (9s)
3693
+
3694
+ Regeneration discards hand edits under a gitignored ios/ or android/ (for
3695
+ example a signing team set in Xcode); put them in the app config, such as
3696
+ ios.appleTeamId, or a config plugin. A committed native directory is hashed
3697
+ into the key and never regenerated. The record only covers prebuilds Stim
3698
+ ran: after a manual \`expo prebuild\` with a different config, run
3699
+ \`expo prebuild --clean\` yourself or delete the directory so the next build
3700
+ regenerates it.
3701
+
3478
3702
  If the iOS fingerprint after prebuild or pod install is unavailable, Stim
3479
3703
  installs the build but skips local storage and remote uploads. fingerprint
3480
3704
  and cacheKey are null in the result and lastBuild; the old key is not reused.
@@ -3528,10 +3752,10 @@ WHAT MAKES THE CACHE ACTUALLY HIT: .FINGERPRINTIGNORE
3528
3752
  mismatch naming the differing sources. Untracked, non-gitignored files under
3529
3753
  ios/ or android/ count too -- they are hashed like any other source, so a
3530
3754
  stray file there moves the key on your machine and nowhere else.`
3531
- },
3532
- concurrency: {
3533
- summary: "waiting on another workspace's build, --no-build-cache, concurrency.maxBuilds and maxDevices",
3534
- body: () => `ONE COMPILE PER FINGERPRINT, ACROSS EVERY WORKSPACE
3755
+ },
3756
+ concurrency: {
3757
+ summary: "waiting on another workspace's build, --no-build-cache, concurrency.maxBuilds and maxDevices",
3758
+ body: () => `ONE COMPILE PER FINGERPRINT, ACROSS EVERY WORKSPACE
3535
3759
  The cache makes the SECOND workspace on a commit free -- but only once the
3536
3760
  first has finished. Three agents starting within the same minute all miss it,
3537
3761
  and without this all three compile the same app at once, fighting for the
@@ -3610,10 +3834,67 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
3610
3834
  reports stale build locks, and \`gc --delete\` clears them. Set the caps
3611
3835
  with \`stim settings set concurrency.maxBuilds 2\`, by editing
3612
3836
  ~/.stim/config.json, or via the two env vars (see \`guide settings\`).`
3613
- },
3614
- options: {
3615
- summary: "every flag per command, Android variants and flavors, the per-run simulator model, runtime and system image",
3616
- body: () => `THE OPTION SURFACE, IN FULL
3837
+ },
3838
+ budget: {
3839
+ summary: "disk and memory budgets: what start, ios and android reclaim first, and STIM_LOW_DISK",
3840
+ body: () => `DISK AND MEMORY BUDGETS (ON BY DEFAULT)
3841
+ Before \`start\`, \`ios\` and \`android\` build or boot anything, they
3842
+ compare the machine against four MACHINE-level settings:
3843
+
3844
+ budget.minFreeDiskGb free disk on the volumes holding the app and
3845
+ $STIM_HOME. Default 20. Below it, Stim reclaims.
3846
+ budget.hardFloorDiskGb default 5. Still below it after reclaiming,
3847
+ the run refuses with STIM_LOW_DISK.
3848
+ budget.maxCommittedMemoryGb the rough memory of live environments, the
3849
+ figure \`stim status\` prints (a booted
3850
+ simulator 1.5 GB, an emulator 2.5 GB, a dev
3851
+ server 0.7 GB). Default 60% of physical memory.
3852
+ budget.maxLiveWorkspaces workspaces with a booted device or running dev
3853
+ server. Unset by default.
3854
+
3855
+ 0 turns a check off; with budget.minFreeDiskGb at 0, Stim still reclaims
3856
+ below the hard floor before refusing. A \`start\` whose dev server already
3857
+ answers checks nothing; \`start --reset-cache\` checks before it stops the
3858
+ server.
3859
+
3860
+ Over budget, Stim reclaims in this order and stops as soon as it is back
3861
+ under budget. Each step prints a \`budget\` line on stderr, and the \`--json\`
3862
+ payload lists the steps that acted under \`reclaimed\`:
3863
+
3864
+ 1. shut down idle owned devices in other workspaces: booted, no driver,
3865
+ no Stim or agent-device lock, no activity for 10 minutes, and no build
3866
+ in progress (\`gc --idle\` semantics; shut down, never deleted)
3867
+ 2. stop idle dev servers in other workspaces: a Stim supervisor with no
3868
+ bundle request, device log, Stim command or supervisor start for 10
3869
+ minutes, no build in progress, and no device in that workspace that is
3870
+ driven, active, or of unknown activity. Stim rechecks under that
3871
+ workspace's native-run and metro-start locks before it stops the
3872
+ server, and skips the step when simulators cannot be listed. The next
3873
+ \`start\`, \`ios\` or \`android\` there brings it back
3874
+ 3. clear the build outputs of workspaces not in use and idle for 10
3875
+ minutes, least recently used first, re-measuring after each
3876
+ (\`gc --delete\` semantics; the next build installs from the shared
3877
+ cache). A workspace whose last use is unknown is kept
3878
+ 4. trim shared cache entries nothing has used for 14 days
3879
+ (\`gc --delete --older-than 14\` for the caches)
3880
+
3881
+ Steps 3 and 4 run only for disk. Memory and workspace limits never refuse;
3882
+ a run still over them prints one \`budget\` warning and continues. The
3883
+ current workspace is never reclaimed.
3884
+
3885
+ With STIM_HOME set, the budget is off unless its environment variable is
3886
+ set (STIM_BUDGET_MIN_FREE_DISK_GB, STIM_BUDGET_HARD_FLOOR_DISK_GB,
3887
+ STIM_BUDGET_MAX_COMMITTED_MEMORY_GB, STIM_BUDGET_MAX_LIVE_WORKSPACES), and
3888
+ step 1 never runs because devices are machine-global.
3889
+
3890
+ \`stim doctor\` prints a \`budget\` line with the free disk and committed
3891
+ memory against the budget, and a finding with the reclaim plan when over it.
3892
+ Change a limit with \`stim settings set budget.minFreeDiskGb 30\`.
3893
+ See \`guide errors STIM_LOW_DISK\`.`
3894
+ },
3895
+ options: {
3896
+ summary: "every flag per command, Android variants and flavors, the per-run simulator model, runtime and system image",
3897
+ body: () => `THE OPTION SURFACE, IN FULL
3617
3898
  start --json --wait <seconds> --remote --reset-cache
3618
3899
  ios --slot <name> --json --no-metro-check --no-build-cache --scheme <name> --configuration <name> --device-type <name> --runtime <version> --simulator-app <xcode|siniulator|stim-desktop> --device [udid] --wait <seconds> --no-wait --remote <proxy|eas>
3619
3900
  android --slot <name> --json --no-metro-check --no-build-cache --variant <name> --system-image <id> --device [serial] --wait <seconds> --no-wait --remote <proxy|eas>
@@ -3841,10 +4122,10 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
3841
4122
  version it was made with. The --json payload reports what was actually
3842
4123
  used: \`deviceType\` and \`runtime\` on iOS, \`systemImage\` on Android,
3843
4124
  read from the device itself, so a settings-driven run reports them too.`
3844
- },
3845
- devices: {
3846
- summary: "ios --device and android --device on a phone: what the run skips, signing, the LAN wiring, the collector",
3847
- body: () => ` \`android --device [serial]\` installs and launches on a physical device
4125
+ },
4126
+ devices: {
4127
+ summary: "ios --device and android --device on a phone: what the run skips, signing, the LAN wiring, the collector",
4128
+ body: () => ` \`android --device [serial]\` installs and launches on a physical device
3848
4129
  connected to this machine instead of this workspace's owned emulator. With
3849
4130
  no serial it takes the first device it can lease (\`guide lifecycle lease\`).
3850
4131
  It cannot be combined with --remote.
@@ -3968,10 +4249,10 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
3968
4249
  by hashing the installed container; there is no cheap equivalent through
3969
4250
  devicectl, so a device run always installs. It is an upgrade install: the
3970
4251
  app's data, and the Local Network permission the phone granted it, survive.`
3971
- },
3972
- lease: {
3973
- summary: "run-scoped leases, --wait and --no-wait, device lock and unlock, which phone an id-less --device picks",
3974
- body: () => `THE DEVICE LEASE ON A \`--device\` RUN
4252
+ },
4253
+ lease: {
4254
+ summary: "run-scoped leases, --wait and --no-wait, device lock and unlock, which phone an id-less --device picks",
4255
+ body: () => `THE DEVICE LEASE ON A \`--device\` RUN
3975
4256
  A physical device is shared, so a \`--device\` run takes a lease on it. The
3976
4257
  lease step sits AFTER the build (a build touches no device) and before the
3977
4258
  install, and the run releases what it took when the command exits: on
@@ -4067,10 +4348,10 @@ THE POOL: WHICH DEVICE AN ID-LESS \`--device\` PICKS
4067
4348
  The chosen device is on the phase line and in \`--json\` (\`udid\` or
4068
4349
  \`serial\`, plus \`deviceName\`), so an agent can hand the same id to its
4069
4350
  device tool.`
4070
- },
4071
- release: {
4072
- summary: "Release configurations and ...Release variants: Metro skipped, process-proven launch, the JS swap",
4073
- body: () => ` A VARIANT WHOSE NAME ENDS IN "Release" IS A RELEASE BUILD (\`release\`,
4351
+ },
4352
+ release: {
4353
+ summary: "Release configurations and ...Release variants: Metro skipped, process-proven launch, the JS swap",
4354
+ body: () => ` A VARIANT WHOSE NAME ENDS IN "Release" IS A RELEASE BUILD (\`release\`,
4074
4355
  \`productionRelease\`), and that is the whole opt-in -- there is no second
4075
4356
  flag. It is the Android half of \`ios --configuration Release\` and behaves
4076
4357
  the same way: AGP's bundle task embeds the JS, so Metro is skipped ENTIRELY
@@ -4143,10 +4424,10 @@ THE POOL: WHICH DEVICE AN ID-LESS \`--device\` PICKS
4143
4424
  builds the \`iphoneos\` slice for a cabled iPhone and keys its cache
4144
4425
  \`-device\` instead of \`-sim\`, but does not install it yet. Archives,
4145
4426
  \`.ipa\` export, store signing and distribution stay out of scope.`
4146
- },
4147
- simslim: {
4148
- summary: "recommended SimSlim profiles and recovery from host memory pressure",
4149
- body: () => `SIMSLIM FOR PARALLEL IOS WORK
4427
+ },
4428
+ simslim: {
4429
+ summary: "recommended SimSlim profiles and recovery from host memory pressure",
4430
+ body: () => `SIMSLIM FOR PARALLEL IOS WORK
4150
4431
  SimSlim is recommended as an optional way to reduce simulator background
4151
4432
  services and memory use, especially with several workspaces. Review which
4152
4433
  services your app and tests need; a slim profile can disable those features.
@@ -4216,14 +4497,12 @@ HOST MEMORY PRESSURE AND STALLED SIMULATORS
4216
4497
  agents' simulators or heavy apps. Rebooting a simulator under the same pressure
4217
4498
  can repeat the stall. Consider fewer concurrent builds/devices (guide lifecycle
4218
4499
  concurrency) and a reviewed SimSlim profile for future runs.`
4500
+ }
4219
4501
  }
4220
- }
4221
- };
4222
- //#endregion
4223
- //#region src/guide/cleanup.ts
4224
- const cleanup = {
4225
- summary: "Where simulators come from, and how they get reclaimed",
4226
- preamble: () => `DEVICE SLOTS
4502
+ },
4503
+ cleanup: {
4504
+ summary: "Where simulators come from, and how they get reclaimed",
4505
+ preamble: () => `DEVICE SLOTS
4227
4506
 
4228
4507
  Cleanup enumerates every slot. stop --slot <name> keeps the shared server and
4229
4508
  other slots; plain stop and worktree remove handle the whole workspace.
@@ -4238,18 +4517,26 @@ WHAT RECLAIMS AN OWNED DEVICE
4238
4517
  stim worktree remove parks eligible owned simulators and emulators
4239
4518
  (\`guide lifecycle pool\`); deletes them when
4240
4519
  parking is disabled or their setup cannot be verified
4241
- stim gc --delete sweeps stim-* devices no project references, and
4242
- clears verified parked simulators and emulators
4520
+ stim gc --delete sweeps devices Stim created that no project
4521
+ references (\`guide cleanup gc\`), clears
4522
+ verified parked simulators and emulators, and
4523
+ runs \`stim worktree remove\` on every clean,
4524
+ Stim-managed linked worktree whose branch is merged
4243
4525
  stim gc --delete --older-than <days>
4244
4526
  also reaps the device of a workspace no Stim
4245
4527
  command has used in that long, even though the
4246
- project is still on disk
4528
+ project is still on disk, and clears only the
4529
+ devices parked at least that long
4247
4530
  stim gc --delete --worktrees
4248
- runs \`stim worktree remove\` on every clean, idle,
4249
- Stim-managed linked worktree (\`guide cleanup gc\`)
4531
+ also runs \`stim worktree remove\` on every clean,
4532
+ idle, Stim-managed linked worktree
4533
+ (\`guide cleanup gc\`)
4534
+ stim gc --idle <duration>
4535
+ shuts DOWN (never deletes) owned devices with no
4536
+ driver, claim or activity for that long
4250
4537
 
4251
4538
  \`worktree remove\` and \`gc --delete\` are the only two commands that delete;
4252
- \`gc --delete --worktrees\` deletes only through \`worktree remove\`. \`gc
4539
+ \`gc --delete\` deletes worktrees only through \`worktree remove\`. \`gc
4253
4540
  --delete\` also clears workspace build outputs (\`guide cleanup disk\`) and
4254
4541
  orphaned workspace directories, never a checkout. \`stim stop\` shuts a device
4255
4542
  DOWN and leaves it assigned, which is what makes returning to a branch cost a
@@ -4271,10 +4558,10 @@ start the counters over. A file this version cannot read -- unparseable, or
4271
4558
  written by a newer Stim -- costs one dim line on stderr and is otherwise left
4272
4559
  alone; only the next \`ios\` or \`android\` run moves an unparseable one aside
4273
4560
  to stats.json.corrupt-<unix ms> and starts a new one.`,
4274
- sections: {
4275
- gc: {
4276
- summary: "what gc and worktree remove delete, keep and refuse: orphans, stale records, locks, leases, EAS sessions",
4277
- body: () => `LINKED WORKTREES
4561
+ sections: {
4562
+ gc: {
4563
+ summary: "what gc and worktree remove delete, keep and refuse: orphans, stale records, locks, leases, EAS sessions",
4564
+ body: () => `LINKED WORKTREES
4278
4565
  \`stim worktree remove\` works with any linked worktree, warmed or not.
4279
4566
  Git registration identifies the worktree; a Stim registry entry is not
4280
4567
  required. Before it reclaims anything, the command refuses a worktree git
@@ -4302,23 +4589,67 @@ IN USE
4302
4589
  existence cannot be read (a permission error) is never treated as deleted.
4303
4590
 
4304
4591
  SWEEPING FINISHED WORKTREES
4305
- \`gc --worktrees\` is opt-in; no cache or age flag implies it. It looks at
4306
- every registered project root and every workspace.json root, grouped by
4307
- git worktree, and reports each worktree with the reason it is kept:
4308
- source checkout, bare, locked, in use, dirty (untracked files count; pod
4309
- install churn alone does not), unpushed (commits no remote-tracking ref or
4310
- other local branch reaches), initialized submodules, or recently used.
4311
- Idle means no recorded use for --older-than days, 7 without it; a worktree
4312
- whose last use is unknown is kept. --cache with --worktrees is refused with
4313
- STIM_BAD_ARG; run them separately.
4314
- stim gc --worktrees --older-than 3 # report only
4315
- stim gc --delete --worktrees --older-than 3 # remove the clean idle ones
4592
+ Every \`gc\` without --cache looks at every registered project root and
4593
+ every workspace.json root, grouped by git worktree, and reports each linked
4594
+ worktree with the reason it is removed or kept. A worktree is finished when
4595
+ its branch is merged into the default branch. \`--worktrees\` also removes
4596
+ one that is idle: no recorded use for --older-than days, 7 without it; a
4597
+ worktree whose last use is unknown is kept. Both need the same clean state;
4598
+ a worktree is kept when it is the source checkout, bare, locked, in use,
4599
+ dirty (untracked files count; pod install churn alone does not), unpushed
4600
+ (commits no remote-tracking ref or other local branch reaches), or has
4601
+ initialized submodules. Without --worktrees, the report leaves out the
4602
+ source checkout and roots outside git.
4603
+
4604
+ MERGED means, with the default branch taken from origin/HEAD, after a
4605
+ \`git fetch origin <default>\` per repository (30s timeout, no credential
4606
+ prompt; skipped when that checkout fetched in the last 10 minutes):
4607
+ - HEAD is reachable from origin/<default> through a merge commit, and the
4608
+ branch's reflog shows a commit made on it that HEAD contains. A branch
4609
+ with no commit of its own -- fresh, cut from another branch, or reset
4610
+ onto one -- is not merged.
4611
+ - the branch changes the tree, and either it has no merge commits and every
4612
+ commit since the merge base has the same \`git patch-id --verbatim\` as a
4613
+ commit on the default branch (a rebase merge), or its whole diff since the
4614
+ merge base has the same verbatim patch id as a commit there, compared on
4615
+ the files the branch changes (a squash merge).
4616
+ Merge state comes from git alone, not from a hosting service. A squash
4617
+ merge whose content changed during the merge (a conflict resolution, a
4618
+ suggested edit, even whitespace) does not match and is kept, and so is a
4619
+ fast-forwarded branch. When origin/HEAD is not set, the fetch fails, or git
4620
+ cannot answer, the state is unknown and never counts as merged (reason
4621
+ merge-unknown, with the remedy); with --worktrees an idle worktree is still
4622
+ removed. A squash- or rebase-merged branch whose upstream is gone (deleted
4623
+ after the merge and pruned locally) has commits only it reaches; they do
4624
+ not block removal, because their change is on the default branch, and the
4625
+ branch is kept.
4626
+ stim gc # report merged worktrees
4627
+ stim gc --delete # remove them
4628
+ stim gc --delete --worktrees --older-than 3 # also the clean idle ones
4629
+ --cache with --worktrees is refused with STIM_BAD_ARG; run them separately.
4316
4630
  With --delete it runs the \`stim worktree remove\` pipeline, never --force,
4317
4631
  on each removable worktree. That pipeline re-inspects the worktree and
4318
- re-checks use and idleness under the removal locks, then parks devices and
4319
- handles the branch exactly as a manual \`stim worktree remove\`. A worktree
4320
- that became busy or recently used since the report is kept with the reason.
4321
- A worktree that fails is reported, gc exits 1, and the sweep continues.
4632
+ re-checks use under the removal locks, and idleness for an idle worktree
4633
+ or an unchanged HEAD for a merged one, then parks devices and handles the
4634
+ branch exactly as a manual \`stim worktree remove\`, which keeps the
4635
+ checkout when its HEAD moves while devices are reclaimed. A worktree that
4636
+ changed since the report is kept with the reason. A worktree that fails is
4637
+ reported, gc exits 1, and the sweep continues.
4638
+
4639
+ IDLE DEVICES
4640
+ Plain \`gc\` lists booted owned simulators and emulators whose \`status\`
4641
+ activity is idle (\`guide facts status\`), with how long. \`gc --idle
4642
+ <duration>\` (30m, 2h, 1d) shuts down each one idle at least that long
4643
+ through the same teardown as \`stim stop\`: ownership is re-checked, the
4644
+ device and its record stay, and the next \`ios\` or \`android\` run boots
4645
+ it again. It acts without --delete and never deletes. It skips a device
4646
+ that is driven (agent-device, a device lock, a test runner), whose
4647
+ activity is unknown, whose idle time is unknown, or whose workspace has a
4648
+ build in progress, and re-checks each device just before shutting it down.
4649
+ Physical and remote devices are out of scope. --cache with --idle is
4650
+ refused with STIM_BAD_ARG.
4651
+ stim gc # lists idle devices
4652
+ stim gc --idle 2h # shuts down those idle 2 hours or more
4322
4653
 
4323
4654
  ORPHANED WORKSPACE DIRECTORIES
4324
4655
  A worktree deleted with \`git worktree remove\`, \`rm -rf\` or a /tmp wipe
@@ -4358,6 +4689,19 @@ If a delete fails, the device's config record is KEPT and the command reports
4358
4689
  it. A record is what makes the device findable again, so it outlives a failed
4359
4690
  teardown rather than turning it into an orphan.
4360
4691
 
4692
+ WHICH stim-* DEVICES gc DELETES
4693
+ A \`stim-\` prefix alone is not proof that Stim made a device. gc deletes an
4694
+ unreferenced device only when Stim created it: the device is listed in
4695
+ $STIM_HOME/created-devices.json, where Stim records every simulator and AVD
4696
+ it creates, or it predates that ledger and matches Stim's own format
4697
+ exactly -- an iOS name \`stim-<label> (<model> <runtime>)\`, or an AVD
4698
+ named \`stim-<label>\` whose config.ini holds the byte-valued
4699
+ disk.dataPartition.size Stim writes. gc lists any other stim-* device under
4700
+ "Unrecognized stim-* devices" with the command that deletes it
4701
+ (\`xcrun simctl delete <udid>\` or \`avdmanager delete avd -n <name>\`), and
4702
+ never runs it. Boot, shutdown and teardown re-check ownership by the same
4703
+ rule, so a device that stops matching is left alone.
4704
+
4361
4705
  ANDROID DATA WITHOUT A REGISTRATION
4362
4706
  \`gc\` also reports stim-*.avd directories whose .ini registration is
4363
4707
  gone. \`gc --delete\` rechecks the directory, emulator process locks, and
@@ -4430,10 +4774,10 @@ THE ONE CASE GC WILL NOT REAP
4430
4774
  it found, so you can judge. Delete them yourself:
4431
4775
  xcrun simctl delete <udid>
4432
4776
  avdmanager delete avd -n <name>`
4433
- },
4434
- collector: {
4435
- summary: "log collector reaping: an unproven collector pid, and why the app on a phone closed",
4436
- body: () => `WHAT ELSE STOP REAPS
4777
+ },
4778
+ collector: {
4779
+ summary: "log collector reaping: an unproven collector pid, and why the app on a phone closed",
4780
+ body: () => `WHAT ELSE STOP REAPS
4437
4781
  The device-log collectors (\`simctl log stream\` / \`adb logcat\`) that
4438
4782
  \`ios\` / \`android\` attach after launch. They are recorded in
4439
4783
  the global workspace state.json, and nothing outside this workspace can name them,
@@ -4487,10 +4831,10 @@ THE ONE CASE GC WILL NOT REAP
4487
4831
  is gone, and the unrelated process is never signalled. A missing, malformed,
4488
4832
  or unreadable identity leaves the record unverified and kept for a retry.
4489
4833
  Wall-clock timestamps and command names are not ownership proof.`
4490
- },
4491
- disk: {
4492
- summary: "disk usage, workspace build outputs, AVD and build-log sizes, the data partition, trimming the shared caches",
4493
- body: () => `DISK
4834
+ },
4835
+ disk: {
4836
+ summary: "disk usage, workspace build outputs, AVD and build-log sizes, the data partition, trimming the shared caches",
4837
+ body: () => `DISK
4494
4838
  Logs, state, pidfiles and Xcode DerivedData are under the global workspace
4495
4839
  directory, and \`worktree remove\` reclaims them. \`gc --delete\` clears the
4496
4840
  build outputs of workspaces nobody is using (WORKSPACE BUILD OUTPUTS
@@ -4498,16 +4842,17 @@ THE ONE CASE GC WILL NOT REAP
4498
4842
  project build directories while sharing task outputs through its build cache.
4499
4843
 
4500
4844
  Android AVDs normally live under ~/.android/avd, and a booted owned AVD can
4501
- use several GB. \`worktree remove\` deletes the workspace's owned AVD; plain
4502
- \`stop\` only shuts it down for reuse. Stim uses Android's default Quick Boot
4845
+ use several GB. \`worktree remove\` parks the workspace's owned AVD with its
4846
+ user data and snapshots wiped, or deletes it when the pool is off or full;
4847
+ plain \`stop\` only shuts it down for reuse. Stim uses Android's default Quick Boot
4503
4848
  unless displayless Linux requires software rendering, where snapshots are
4504
4849
  disabled. The first boot and a boot after the emulator, system image, or AVD
4505
4850
  settings change are cold, while later supported boots load the one automatic
4506
4851
  snapshot saved on exit. \`stop\` waits for the emulator process and, when
4507
4852
  enabled, the snapshot save to finish.
4508
4853
  New owned AVDs default to an 8 GiB data partition, though project settings can
4509
- change it. When enabled, Quick Boot keeps one automatic snapshot, and \`worktree remove\`
4510
- deletes the whole AVD.
4854
+ change it. When enabled, Quick Boot keeps one automatic snapshot until the AVD is
4855
+ parked or deleted.
4511
4856
  \`gc\` prints the on-disk size beside an orphaned or stale owned Android AVD
4512
4857
  when its content directory can be read.
4513
4858
 
@@ -4579,21 +4924,9 @@ SHARED BUILD CACHES
4579
4924
 
4580
4925
  Trim rather than empty. Emptying costs the next build in every project the
4581
4926
  time the cache was saving.`
4927
+ }
4582
4928
  }
4583
- }
4584
- };
4585
- const SETTINGS_SCHEMA_URL = `https://unpkg.com/stim/dist/settings.schema.json`;
4586
- //#endregion
4587
- //#region src/guide/index.ts
4588
- const TOPICS = {
4589
- agent: agent_default,
4590
- facts,
4591
- metro: metro_default,
4592
- ports: ports_default,
4593
- logs: logs_default,
4594
- errors,
4595
- lifecycle,
4596
- cleanup,
4929
+ },
4597
4930
  settings: {
4598
4931
  summary: "Settings Stim reads, and where they can live",
4599
4932
  body: () => `SETTINGS
@@ -4829,6 +5162,14 @@ ${ANDROID_AVD_CONFIG_HELP.map((line) => ` ${line}`).joi
4829
5162
  validates the shape and platform but cannot discover
4830
5163
  runtime entry-point or dev-menu overrides or auto-fix
4831
5164
  them. See \`guide metro\` for warmup behavior.
5165
+ metro.idleStopMinutes minutes without a bundle request, client log record or
5166
+ Stim command before the supervisor stops the dev
5167
+ server (default 60). 0 never stops it. Never during a
5168
+ build or bundle response, while the workspace holds
5169
+ a device lock, or while a workspace device is
5170
+ driven. Read
5171
+ when \`start\` spawns the supervisor; see
5172
+ \`guide metro\`.
4832
5173
  worktree.exclude ignored-path skip list for worktree warm. Settings
4833
5174
  come from the source checkout's repository-root
4834
5175
  .stim.json. A nonempty .worktreeexclude in the source
@@ -4914,6 +5255,19 @@ Unset, 0, or any non-positive value means NO enforcement -- the default, where
4914
5255
  Stim limits nothing. See \`guide lifecycle concurrency\` for what each cap
4915
5256
  does.
4916
5257
 
5258
+ DISK AND MEMORY BUDGETS ARE MACHINE-LEVEL AND ON BY DEFAULT
5259
+ Top-level \`budget\` in ~/.stim/config.json, or the environment, which
5260
+ overrides the file:
5261
+
5262
+ budget.minFreeDiskGb STIM_BUDGET_MIN_FREE_DISK_GB default 20
5263
+ budget.hardFloorDiskGb STIM_BUDGET_HARD_FLOOR_DISK_GB default 5
5264
+ budget.maxCommittedMemoryGb STIM_BUDGET_MAX_COMMITTED_MEMORY_GB 60% of RAM
5265
+ budget.maxLiveWorkspaces STIM_BUDGET_MAX_LIVE_WORKSPACES unset
5266
+
5267
+ 0 turns a check off. With STIM_HOME set and no environment variable, the
5268
+ budget is off. A value of the wrong shape refuses start, ios and android with
5269
+ STIM_BAD_ARG. See \`guide lifecycle budget\` for what each limit reclaims.
5270
+
4917
5271
  THE IOS SIMULATOR APP IS MACHINE-LEVEL
4918
5272
  Top-level \`iosSimulatorApp\` in ~/.stim/config.json selects the macOS app that
4919
5273
  displays Stim's owned local iOS simulator after boot. It is not a project
@@ -4945,7 +5299,7 @@ window behavior.
4945
5299
  THE DEVICE POOL BOUNDS ARE MACHINE-LEVEL TOO
4946
5300
  \`pool.iosParkedMax\` caps how many parked simulators \`worktree remove\` may
4947
5301
  leave behind for a later workspace to adopt. It is machine-level for the same
4948
- reason: the disk they sit on is the whole machine's, about 2.5 GB each.
5302
+ reason: the disk they sit on is the whole machine's, about 18 MB each once erased.
4949
5303
 
4950
5304
  {
4951
5305
  "pool": { "iosParkedMax": 3 }