stim 1.8.0 → 1.9.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 (80) hide show
  1. package/README.md +23 -155
  2. package/dist/{android-BRgBYzU0.mjs → android-BPGizezh.mjs} +112 -135
  3. package/dist/{android-DeYdjjBP.mjs → android-DEgo4oCo.mjs} +2 -2
  4. package/dist/{android-DWssqHQ8.mjs → android-_y4Ohi5z.mjs} +54 -15
  5. package/dist/android-cas-compiler.mjs +2 -2
  6. package/dist/{android-cas-SWIjzeuL.mjs → android-cas-eiExNLf7.mjs} +21 -14
  7. package/dist/app-install-Uf3a83Tv.mjs +819 -0
  8. package/dist/{build-lock-CgtsB6TV.mjs → build-lock-DPXPLgTc.mjs} +120 -24
  9. package/dist/build-progress-CVqkfPC6.mjs +188 -0
  10. package/dist/{cache-manifest-CYJi8-Hx.mjs → cache-manifest-Dg7OnMrp.mjs} +4 -4
  11. package/dist/cache-manifest.mjs +1 -1
  12. package/dist/cli.mjs +22 -19
  13. package/dist/collector-run.d.mts +2 -0
  14. package/dist/collector-run.mjs +10 -9
  15. package/dist/{command-output-B3ycpsYv.mjs → command-output-gByVmIjy.mjs} +11 -2
  16. package/dist/{config-B7iyKWkL.mjs → config-Cp9lM3wX.mjs} +187 -57
  17. package/dist/{dependency-state-D0M7oKA7.mjs → dependency-state-Bq5DcLGn.mjs} +2 -2
  18. package/dist/{deps-oCXVNEqQ.mjs → deps-Cip5vUm1.mjs} +8 -5
  19. package/dist/{detached-entry-Bpo6ZY_I.mjs → detached-entry-KEu0_AXL.mjs} +1 -1
  20. package/dist/{device-BUoZyMoR.mjs → dev-client-HyqISM0h.mjs} +1178 -635
  21. package/dist/{device-RdujjHLB.mjs → device-D4U-xLty.mjs} +13 -20
  22. package/dist/{device-lease-Ffz_M5t0.mjs → device-lease-Crs-HkJY.mjs} +16 -9
  23. package/dist/{device-pool-BxRwdufO.mjs → device-pool-CnTAz8X1.mjs} +7 -7
  24. package/dist/{device-remote-ByN8P6CB.mjs → device-remote-AQ5rQPqr.mjs} +482 -35
  25. package/dist/{doctor-BMt156qe.mjs → doctor-B_Ei3bZu.mjs} +95 -107
  26. package/dist/{error-diagnostics-D1Kaf-eT.mjs → error-diagnostics-Vfz83GCF.mjs} +22 -16
  27. package/dist/exec-jIAlgilk.mjs +132 -0
  28. package/dist/{gc-v2YcoJvm.mjs → gc-Ckw8RQHi.mjs} +763 -126
  29. package/dist/{guide-BTAlD1_y.mjs → guide-BiAbhs1J.mjs} +891 -501
  30. package/dist/{guide-status-Craq9z0t.mjs → guide-status-BAMVU0ZA.mjs} +5 -3
  31. package/dist/{ios-BcCiI4TS.mjs → ios-BL2NFckr.mjs} +43 -20
  32. package/dist/{ios-BP04pY5f.mjs → ios-BzUJWk1K.mjs} +141 -175
  33. package/dist/{ios-device-CauFJiYx.mjs → ios-device-D972i9wf.mjs} +149 -52
  34. package/dist/{ios-device-CFXA46ez.mjs → ios-device-DPsSm0v_.mjs} +3 -2
  35. package/dist/launch-verify-Q_OtW1Rf.mjs +365 -0
  36. package/dist/{logs-CakdBTtm.mjs → logs-CI9NQSK7.mjs} +18 -13
  37. package/dist/{logs-query-BGL-Pl12.mjs → logs-query-Cqma3WIu.mjs} +36 -28
  38. package/dist/{metro-C7bcJ0uo.mjs → metro-DrLHaMn2.mjs} +78 -8
  39. package/dist/{named-ports-DzMOlmaX.mjs → named-ports-CmLQSIRj.mjs} +4 -4
  40. package/dist/{ndjson-BZwI1NBv.mjs → ndjson-9uFNRwfe.mjs} +36 -3
  41. package/dist/{ownership-Cp52QAK1.mjs → ownership-D15Op8vN.mjs} +3 -3
  42. package/dist/{ownership-wolE-0Do.mjs → ownership-DCQPuWEc.mjs} +26 -7
  43. package/dist/{ports-BIbJaKNW.mjs → ports-BNKoWVWZ.mjs} +3 -3
  44. package/dist/{process-output-Dl5uKE03.mjs → process-output-Fpl8PYi8.mjs} +1 -1
  45. package/dist/{project-DDZWf_16.mjs → project-DS112DYB.mjs} +8 -3
  46. package/dist/{reload-BdypPbjM.mjs → reload-ZNJUo1pl.mjs} +12 -13
  47. package/dist/{server-bare-BQkyxa_b.mjs → server-bare-DWkkcIxV.mjs} +2 -2
  48. package/dist/{server-expo-BVAA1OqJ.mjs → server-expo-B_lctBR3.mjs} +8 -7
  49. package/dist/server-expo-CeILEu86.mjs +2 -0
  50. package/dist/settings-BKNh5rl4.mjs +376 -0
  51. package/dist/{settings-BXXorVM2.mjs → settings-D030k0t4.mjs} +464 -152
  52. package/dist/settings.schema.json +989 -0
  53. package/dist/{teardown-BJqEzPo_.mjs → sim-pool-KaIsp2Ft.mjs} +14 -419
  54. package/dist/simslim-B_LsGLuJ.mjs +259 -0
  55. package/dist/{slot-launch-ZUUe0TQP.mjs → slot-launch-DU9A-gYw.mjs} +4 -4
  56. package/dist/spawn-claims-Dn3C_N6w.mjs +49 -0
  57. package/dist/{start-CmxtOKjU.mjs → start-DYkkKF0s.mjs} +27 -31
  58. package/dist/{state-DZobzxLW.mjs → state-NcJzMEBF.mjs} +5 -3
  59. package/dist/{stats-CLg5RuDS.mjs → stats-B63-JVAY.mjs} +4 -4
  60. package/dist/{stats-BYcZ6n1h.mjs → stats-DG9e66Rz.mjs} +66 -5
  61. package/dist/status-S6ehL4Ui.mjs +932 -0
  62. package/dist/{stop-C8JW6kKp.mjs → stop-BO28HGqF.mjs} +101 -25
  63. package/dist/{stop-B1kOpYei.mjs → stop-T1R-cZRz.mjs} +2 -2
  64. package/dist/supervisor-run.mjs +52 -34
  65. package/dist/{workspace-process-lock-BYfZz_S-.mjs → workspace-process-lock-DWaiRD0e.mjs} +14 -9
  66. package/dist/{workspace-state-BT5E3SA2.mjs → workspace-state-DF2gCFhx.mjs} +52 -29
  67. package/dist/worktree-DKhSF0UX.mjs +2 -0
  68. package/dist/{worktree-CFLA-6MX.mjs → worktree-LGB-UwgG.mjs} +603 -59
  69. package/dist/{worktree-FG_ZKKdb.mjs → worktree-R_cUlE7s.mjs} +62 -41
  70. package/dist/{xcode-B6YaWeg0.mjs → xcode-BZqyxh3e.mjs} +216 -58
  71. package/package.json +5 -6
  72. package/skill/SKILL.md +2 -0
  73. package/dist/app-install-BDbsSgCl.mjs +0 -1175
  74. package/dist/build-slots-C-oAHRT1.mjs +0 -151
  75. package/dist/dev-client-BmT-ioFI.mjs +0 -797
  76. package/dist/exec-B29XNuhP.mjs +0 -77
  77. package/dist/reclaim-KJ-0uWEF.mjs +0 -361
  78. package/dist/server-expo-YDDG3eP7.mjs +0 -2
  79. package/dist/status-DdkLki47.mjs +0 -426
  80. package/dist/{metro-store-CDDZHtPl.mjs → metro-store-DzeWkfqW.mjs} +1 -1
@@ -1,13 +1,11 @@
1
- import { a as findProjectRoot } from "./project-DDZWf_16.mjs";
2
- import { t as ANDROID_AVD_CONFIG_HELP } from "./settings-BXXorVM2.mjs";
3
- import { t as guideStatus } from "./guide-status-Craq9z0t.mjs";
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";
4
4
  import chalk from "chalk";
5
- //#endregion
6
- //#region src/guide/index.ts
7
- const TOPICS = {
8
- agent: {
9
- summary: "The normal coding-agent workflow, safety rules, and topic routing",
10
- body: () => `AGENT WORKFLOW
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
11
9
 
12
10
  Use Stim to run React Native and Expo apps without sharing a Metro port or
13
11
  device with another workspace. Prefer plain output: it streams each phase and
@@ -27,12 +25,6 @@ the source checkout is infrastructure, not a workspace: you edit, build, and run
27
25
  in the worktree, and you keep the seed fit to copy. Every rule below about the
28
26
  source checkout's fitness as a seed belongs to this workflow.
29
27
 
30
- Doctor reports that fitness -- how far behind the seed is, uncommitted tracked
31
- changes, an interrupted rebase or merge, a detached HEAD, a diverged branch, a
32
- branch that is not the default one -- only once the repository has at least
33
- one linked worktree, so read it from inside the worktree. A single-checkout
34
- session is never told that its own branch is a problem.
35
-
36
28
  MULTIPLE DEVICES
37
29
 
38
30
  Use ios/android --slot <name> to retain multiple devices in one workspace,
@@ -67,44 +59,18 @@ If warm fails or reports incomplete, resolve the reported failure first.
67
59
  A warm that cannot record its ownership claim refuses before copying. Follow
68
60
  the printed remedy and keep concurrent runs on the same STIM_HOME.
69
61
 
70
- Before native worktree work, run doctor for the platform in scope. The STATUS
71
- block at the top of this topic says when it is due here: never run in this
72
- checkout, older than seven days, or run under another Stim version. No block
73
- means doctor is current and Stim is up to date; the same block names a newer
74
- Stim release when one exists. Doctor checks the source
75
- checkout from a linked worktree. Fix relevant findings and inspect the
76
- upstream gap; in the single-checkout workflow those seed findings do not
77
- appear. It also prints the running CLI version and the stim installation
78
- resolved from PATH. If that resolved installation is older than another one,
79
- fix PATH or the installation before continuing so commands and guidance match.
80
- Doctor reports cross-volume staging and build-cache copies; read guide settings
81
- for placement overrides and guide lifecycle options for warm behavior.
82
- For iOS Debug architecture findings, review the project's overrides and imported
83
- Podfile helpers using guide lifecycle options. Doctor --fix does not change them.
84
- For parallel iOS work, review the recommended optional SimSlim setup in
85
- stim guide lifecycle simslim. If simulator process startup times out, check
86
- host memory pressure and free memory before retrying. Boot progress reports
87
- current and highest observed pressure; a timeout does not establish OOM.
88
- Avoid repeated reboots under unchanged pressure; do not restart other
89
- workspaces' devices or close their apps without asking. That guide covers
90
- the recovery steps and profile tradeoffs.
91
- An Android boot timeout under observed elevated memory pressure gets one
92
- extra wait while the emulator process this run started is alive. It never
93
- launches a second emulator. For a timeout, read guide errors STIM_NO_DEVICE
94
- and free memory by stopping only unneeded devices in workspaces you own;
95
- ask before closing other apps or devices. Device count alone is not evidence
96
- of memory pressure.
97
- For a linked native library carrying Git metadata, add the printed .git entries to
98
- .fingerprintignore only when the native build does not read Git state.
62
+ Before native work, run doctor for the platform in scope when the STATUS block
63
+ at the top of this topic says it is due. No block means doctor is current and
64
+ Stim is up to date. In the worktree workflow, run it from the linked worktree
65
+ so it also checks the source checkout. Follow each finding's printed remedy,
66
+ and read the routed topic below before acting on one you do not understand. If
67
+ the stim resolved from PATH is older than another installation, fix PATH or the
68
+ installation before continuing so commands and guidance match. Under host
69
+ memory pressure, stop only devices in workspaces you own; ask before closing
70
+ other apps or other workspaces' devices.
99
71
 
100
72
  stim doctor --platform ios # or: --platform android
101
73
 
102
- For stale Android CMake launcher findings, stop native builds and run
103
- stim doctor --fix --platform android in the affected checkout before warming
104
- more worktrees. It removes affected ignored, untracked generated .cxx
105
- configurations, including installed native modules; the next build recreates
106
- them. It preserves source, custom launcher settings, and the shared ccache.
107
-
108
74
  # Skip Git creation if the harness already created this linked worktree.
109
75
  git worktree add -b <branch> <worktree-path> HEAD
110
76
  cd <worktree-path>
@@ -245,8 +211,14 @@ Ask the user before these actions:
245
211
  existing Stim ownership record is deleted only when it has no unique commits.
246
212
  - worktree remove --force, because it also discards uncommitted and untracked
247
213
  files.
248
- - gc --delete, because it deletes orphaned resources. gc --delete --cache all
249
- empties the shared build caches instead; it inspects nothing else.
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
216
+ the user the entries its sections list (guide facts gc). gc --delete
217
+ --cache all empties the shared build caches and those outputs instead, and
218
+ gc --delete --cache workspaces clears only the outputs; both inspect
219
+ nothing else.
220
+ - gc --delete --worktrees, because it runs worktree remove on every clean,
221
+ idle linked worktree Stim manages, across projects.
250
222
  - stop when the workspace owns an EAS session, because it irreversibly ends
251
223
  that remote session. For a local device, stop shuts it down but does not
252
224
  delete it. An explicit stop shuts down a Stim-owned simulator even when
@@ -280,6 +252,12 @@ Read the matching guide before acting in these situations:
280
252
  | Capacity limits | stim guide lifecycle concurrency |
281
253
  | Cache statistics from stim stats | stim guide facts stats |
282
254
  | Worktree carry-over | stim guide lifecycle options |
255
+ | Doctor finding: seed checkout or cross-volume copy | stim guide lifecycle options |
256
+ | Doctor finding: iOS Debug archs, .cxx, fingerprints | stim guide lifecycle builds |
257
+ | Temporary storage placement | stim guide settings |
258
+ | Read or change a setting (stim settings) | stim guide settings |
259
+ | Parallel iOS, simulator stall, or memory pressure | stim guide lifecycle simslim |
260
+ | Android boot timeout | stim guide errors STIM_NO_DEVICE |
283
261
  | gc or orphaned resources | stim guide cleanup gc |
284
262
  | worktree remove refusal or --force | stim guide errors remove |
285
263
  | Cleanup failure or unverified cleanup ownership | stim guide errors teardown |
@@ -316,10 +294,12 @@ FULL TOPIC LIST
316
294
  stim guide cleanup # what reclaims a device, and what deletes
317
295
  stim guide cleanup collector # an unproven collector pid; why the app on a phone closed
318
296
  stim guide settings # configuration files and supported keys`
319
- },
320
- facts: {
321
- summary: "The --json payloads: `start`, `ios`, `android`, `reload`, `stop`, `status`, `doctor`, `device lock`/`unlock`, and the error contract",
322
- preamble: () => `SLOTS
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
323
303
  Named ios/android runs add slot to their JSON facts. Default-run fields remain
324
304
  compatible. status adds a slots array per environment with each named slot's
325
305
  ios/android device facts; top-level ios/android still describe default.
@@ -328,14 +308,37 @@ Named collector, lease-holder, and launch keys use platform:slot internally.
328
308
  FACTS CONTRACT
329
309
 
330
310
  \`start\`, \`ios\`, \`android\`, \`reload\`, \`stop\`, \`status\`, \`stats\`, \`doctor\`,
331
- and \`device lock\`/\`device unlock\` each print exactly ONE line of JSON on
311
+ \`gc\`, and \`device lock\`/\`device unlock\` each print exactly ONE line of JSON on
332
312
  stdout for \`--json\`. Every other line goes to stderr, so it is always safe
333
313
  to pipe. \`logs --json\` is the one exception: it is NDJSON, one record per
334
- line by design (see \`guide logs\`), not this single-payload contract.`,
335
- sections: {
336
- payloads: {
337
- summary: "every field of the start, ios, android and reload payloads, the error contract, the device rules",
338
- body: () => ` stim start --json
314
+ line by design (see \`guide logs\`), not this single-payload contract.
315
+
316
+ status's unprovisionedWorktrees lists this repository's linked worktrees with
317
+ no Stim environment. \`worktree warm\` does not create one; \`start\`, \`ios\`,
318
+ \`android\` and \`doctor\` register it.
319
+
320
+ status's remoteDevices lists each environment's recorded EAS Simulator
321
+ session. The session is billable while it runs, and it makes the environment
322
+ live. status reads only local records; it does not ask EAS whether the session
323
+ is still running.
324
+
325
+ remoteDevices [{ platform, backend, sessionId, state, startedAt,
326
+ webPreviewUrl }], empty when the workspace has none
327
+ backend "eas"
328
+ state "claimed" Stim's ownership ledger holds this workspace's
329
+ claim, so \`stop\` and \`gc --delete\` can end it
330
+ "unclaimed" the ledger has no claim for this workspace;
331
+ \`stop\` still ends the recorded session
332
+ "unknown" the ledger could not be read
333
+ webPreviewUrl the browser page showing the remote screen, recorded when
334
+ Stim created the session, or null
335
+
336
+ Plain status prints one "remote <platform>: EAS session <id> billable" line
337
+ 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
339
342
 
340
343
  port the Metro port RESERVED for this workspace
341
344
  supervisorPid the detached supervisor's pid, or NULL when a dev server was
@@ -355,11 +358,11 @@ line by design (see \`guide logs\`), not this single-payload contract.`,
355
358
  deviceName its name, or null
356
359
  deviceType the owned simulator's MODEL, as
357
360
  \`xcrun simctl list devicetypes\` names it ("iPad Pro 13-inch
358
- (M4)"). Read from the simulator itself, so a run driven by
361
+ (M5)"). Read from the simulator itself, so a run driven by
359
362
  the ios.deviceType setting reports it too, not only a
360
363
  \`--device-type\` run. Null on \`--device\` and on a
361
364
  simulator Stim does not own
362
- runtime that simulator's iOS runtime version ("18.5"), from the same
365
+ runtime that simulator's iOS runtime version ("26.5"), from the same
363
366
  record. Null on the same paths as deviceType
364
367
  fingerprint the @expo/fingerprint hash of the native inputs, AS STORED.
365
368
  A run that had to \`expo prebuild\` or \`pod install\`
@@ -612,10 +615,10 @@ RULES
612
615
  \`android --device\` and
613
616
  \`ios --device\`, which use a connected physical device Stim never
614
617
  creates, boots, or deletes.`
615
- },
616
- devmenu: {
617
- summary: "why the Expo dev menu or Tools button is or is not over the app, per platform and device kind",
618
- body: () => ` EVERY DEV-CLIENT DEEP LINK CARRIES disableOnboarding=1
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
619
622
  INSIDE ITS PROJECT URL
620
623
  (\`...?url=http%3A%2F%2Fhost%3Aport%2F%3FdisableOnboarding%3D1&disableFab=1\`),
621
624
  and expo-dev-launcher finishes its own dev-menu ONBOARDING
@@ -706,10 +709,129 @@ RULES
706
709
  developer trust, has no API at all and is always the user's.
707
710
  \`guide errors unverified\` has the signature and the
708
711
  full commands.`
709
- },
710
- stats: {
711
- summary: "the stats payload, what counts as a run, hit, miss and failed, timeSavedMs, the heartbeat estimate",
712
- body: () => ` stim stats --json
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
717
+
718
+ The report the text prints, as one payload. Show the user its sections
719
+ before you run \`gc --delete\`. Under --delete it is the report that run
720
+ acted on: each entry's outcome is a stderr line, \`failures\` counts the
721
+ entries it could not delete, and a nonzero count exits 1. Run
722
+ \`stim gc --json\` again to see what is left.
723
+
724
+ mode "dry-run" | "delete"
725
+ cacheScope the --cache name, or null. When set, devices, project
726
+ entries and locks were not inspected and their sections
727
+ are empty
728
+ olderThan the --older-than days, or null
729
+ worktreeSweep null without --worktrees; otherwise { olderThan, defaulted }:
730
+ the idle days a linked worktree needs, and whether that is
731
+ the default 7 because --older-than was not given
732
+ 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
735
+ sections one array per report section, in the text order. Every key
736
+ is present, empty when there is nothing to report:
737
+ deadProjects { path }
738
+ invalidProjects { path } the registry key is not absolute
739
+ orphanedPorts { project, label, port }
740
+ orphanedWorkspaces { dir, projectRoot, bytes } --delete removes the
741
+ whole workspace directory
742
+ linkedWorktrees { path, idleDays, willRemove, reason, detail }
743
+ only with --worktrees
744
+ parkedSimulators { udid, name, model, runtime, parkedAt, bytes,
745
+ listed }
746
+ parkedEmulators { name, systemImage, parkedAt, bytes, listed }
747
+ orphanedDevices { kind, id, name, bytes, directory }
748
+ staleDevices { kind, id, name, project, slot, idleDays,
749
+ bytes } only with --older-than
750
+ staleDeviceRecords { kind, id, project, slot } --delete clears
751
+ the record only
752
+ orphanedEasSessions { id, name, platform, status, projectScope }
753
+ staleBuildLocks { path, platform, key, pid, projectRoot }
754
+ staleBuildSlots { path, index, pid, projectRoot }
755
+ unresolvedBuildClaims { kind: "lock" | "slot", path } never touched
756
+ buildsInProgress { path, platform, key, pid, projectRoot }
757
+ never touched
758
+ expiredDeviceLeases { path, platform, id, deviceName, holder,
759
+ expiresAt }
760
+ keptDeviceLeases { name, path, detail } never deleted
761
+ deviceSweepNotices { message }
762
+ easSessionSweepNotices { message }
763
+ skipped { path, detail } not classified as dead
764
+ workspaceBuildOutputs { dir, projectRoot, bytes, idleDays, willClear,
765
+ reason, detail } derived-data, gradle-build,
766
+ android-cas and cache-provider of each
767
+ workspace; willClear marks the ones --delete
768
+ would clear
769
+ caches { name, dir, source, bytes, note, willEmpty,
770
+ emptySkipped } alive, not garbage; willEmpty
771
+ marks the ones --delete would empty
772
+ bytes is null when the size is unknown: not measured, or the measurement
773
+ failed. idleDays is null when the last use is unknown. listed is false for
774
+ a parked device that is no longer on this machine, and null when its
775
+ listing was unavailable.
776
+
777
+ reason is null for an entry --delete acts on, and otherwise a stable code
778
+ to branch on; detail is the text line, for the user. Never parse detail.
779
+ workspaceBuildOutputs unresolved | in-use | last-use-unknown |
780
+ recently-used
781
+ linkedWorktrees not-a-worktree | bare-repository |
782
+ source-checkout-unknown | source-checkout |
783
+ locked | in-use | status-unreadable | dirty |
784
+ unpushed-unchecked | unpushed | submodules |
785
+ last-use-unknown | recently-used
786
+
787
+ These print the error contract instead and exit 1:
788
+
789
+ { "code": "STIM_BAD_ARG", "message": "...", "remedy": "..." }
790
+
791
+ - a --cache name that no shared cache carries; the remedy names the
792
+ 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
800
+ run that holds this workspace's native-run.lock:
801
+
802
+ build { platform, slot, state, phase, startedAt, phaseStartedAt,
803
+ outcome, expectedMs, expectedPhaseMs, basis }
804
+
805
+ state "running" while the run's own native-run claim is live;
806
+ "stale" when that claim was released or its process is
807
+ gone -- the run was killed, and the next run replaces the
808
+ record; "unknown" when the claim set cannot be resolved.
809
+ Never treat a stale record as a build in progress.
810
+ phase prepare | cache-lookup | wait | prebuild | pods |
811
+ compile | install | launch. prepare covers settings,
812
+ Metro and the owned device; wait is waiting on another
813
+ workspace's build of the same fingerprint; install
814
+ includes waiting for the device to boot.
815
+ startedAt when the run started; phaseStartedAt when its phase did
816
+ outcome "cold" once the run reached prebuild, pods or compile,
817
+ "hit" once it reached install without them. Before
818
+ that, the outcome of this project's most recent run.
819
+ expectedMs the median duration of this project's last successful
820
+ runs with that outcome on that platform, or null with
821
+ no history
822
+ expectedPhaseMs the median duration of this phase in those runs, or null
823
+ basis how many runs the medians come from (at most 10)
824
+
825
+ Plain \`stim status\` prints the same as one line per workspace:
826
+
827
+ build: ios compile, 1m10s elapsed -- about 3 min left (median of 4 cold runs)
828
+
829
+ There is no completion fraction: a compile's log volume depends on what
830
+ 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
713
835
 
714
836
  { "version": 1,
715
837
  "project": { "key": "<path>", "ios": <bucket|null>,
@@ -738,8 +860,13 @@ HOW A RUN IS COUNTED (\`stats\`)
738
860
  as a hit and is credited nothing: the compile it skipped was paid for in the
739
861
  wait, and with no cold run recorded for this project and platform there is
740
862
  nothing to compare against, so it credits nothing either. The saved figure
741
- is therefore an ESTIMATE and is printed as one. Nothing per run is stored;
742
- the file is $STIM_HOME/stats.json (see \`guide lifecycle builds\`).
863
+ is therefore an ESTIMATE and is printed as one. The file is
864
+ $STIM_HOME/stats.json (see \`guide lifecycle builds\`). Beside the
865
+ aggregates it keeps the last 10 successful runs per project, platform and
866
+ outcome (hit or cold) that did not wait for another workspace's build: each
867
+ one's duration and per-phase durations. \`stim status\` estimates a running
868
+ build from them (see \`guide facts status\`); \`stats --json\` does not
869
+ print them.
743
870
 
744
871
  A run also keeps the duration of its own two long phases in that bucket:
745
872
  the build phase of a miss that compiled (lastColdBuildMs) and the last
@@ -758,12 +885,14 @@ HOW A RUN IS COUNTED (\`stats\`)
758
885
  and a warm run has no long phase to size. That read takes no lock and
759
886
  ignores what it cannot read, so nothing about statistics can change a
760
887
  run's outcome.`
761
- }
762
888
  }
763
- },
764
- metro: {
765
- summary: "The dev server: `stim start`, the supervisor, and starting your own",
766
- body: () => `THE DEV SERVER
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
767
896
 
768
897
  stim start
769
898
  stim start --remote # prepare Metro for a remote device
@@ -777,7 +906,8 @@ another worktree's bundler.
777
906
  --json one line of facts on stdout, everything else on stderr:
778
907
  { port, supervisorPid, mode, logsDir, alreadyRunning }
779
908
  --wait <seconds> how long to wait for server and remote tunnel readiness
780
- (default 60 for each)
909
+ (default 60 for each). Unlike ios/android/device lock
910
+ --wait, it does not wait for a device lease.
781
911
  --remote expose Metro for a remote device
782
912
 
783
913
  Plain \`stim start\` is local and does not create a public tunnel. Remote intent
@@ -825,7 +955,8 @@ REMOTE DEVICE BACKENDS
825
955
  The EAS backend needs eas-cli and an account with EAS Simulator access. An
826
956
  EAS session is billable. EAS does not inherit the proxy credentials. Always
827
957
  tear the session down: \`stop\`, \`worktree remove\`, and \`gc --delete\`
828
- can end sessions that Stim proves it owns.
958
+ can end sessions that Stim proves it owns. \`stim status\` lists each
959
+ recorded session with its preview URL (see \`guide facts\`).
829
960
 
830
961
  IDEMPOTENT
831
962
  A healthy dev server on the reserved port is a no-op: \`start\` prints the
@@ -904,12 +1035,15 @@ STARTING YOUR OWN BUNDLER STILL WORKS
904
1035
  and finding output is back to redirecting it to a file yourself. Prefer
905
1036
  \`start\`.
906
1037
 
907
- \`stop\` leaves an externally started server running. Stop it with the tool
908
- that started it.`
909
- },
910
- ports: {
911
- summary: "Workspace ports for web and API servers that Stim does not manage",
912
- body: () => `NAMED SERVER PORTS
1038
+ \`stop\` leaves an externally started server running and keeps the port
1039
+ reserved while it answers from this project. Stop it with the tool that
1040
+ 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
913
1047
 
914
1048
  If Stim is not installed globally, replace stim with npx stim.
915
1049
 
@@ -961,10 +1095,12 @@ cleanup: versions without ports do not know about named allocations.
961
1095
 
962
1096
  A shared API does not get a shared reservation. Pass its port by environment
963
1097
  instead of allocating a separate label in every worktree.`
964
- },
965
- logs: {
966
- summary: "Querying the merged NDJSON timeline, and what --errors means",
967
- body: () => `LOGS
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
968
1104
 
969
1105
  stim logs [filters]
970
1106
 
@@ -972,6 +1108,11 @@ Reads every *.ndjson file in the global workspace logs directory, merges them in
972
1108
  ordered by timestamp, prints what matches, and EXITS. The file set is
973
1109
  discovered, not enumerated.
974
1110
 
1111
+ metro.ndjson, client.ndjson and device.ndjson are size-capped. At about 8 MiB a file
1112
+ becomes <name>.1, replacing the previous .1, and a new file starts. Queries read
1113
+ both generations, so the oldest records drop off first. History and --since
1114
+ reach back only as far as those records.
1115
+
975
1116
  EXIT 0 MEANS THE QUERY SUCCEEDED, whether or not records matched. A clean
976
1117
  \`stim logs --errors\` check requires exit code 0 AND no matching errors in
977
1118
  captured logs. A workspace that has never produced a timeline refuses with
@@ -1063,7 +1204,7 @@ FLAGS
1063
1204
  records; use its printed --tail value to include all records before grouping.
1064
1205
  To read the complete timeline without the default error-record limit:
1065
1206
  stim logs --source all
1066
- This includes all sources, levels and history, not just the latest errors;
1207
+ This includes all sources, levels and retained history, not just the latest errors;
1067
1208
  add --since, --level or --grep to narrow it. --source all selects sources,
1068
1209
  not stack depth. For untouched captured records use:
1069
1210
  stim logs --source all --json
@@ -1245,38 +1386,41 @@ WHAT WRITES WHAT
1245
1386
 
1246
1387
  A collector is killed and replaced on the next \`ios\` / \`android\` run for
1247
1388
  that platform, and reaped by \`stop\`.`
1248
- },
1249
- errors: {
1250
- summary: "Every refusal Stim can print: an index of codes, and one section for each",
1251
- sectionHint: "<CODE>",
1252
- preamble: () => `WHAT STIM REFUSES, AND WHY
1253
-
1254
- Every refusal from \`ios\` / \`android\` carries a stable CODE. Branch on the
1255
- code, never on the message.`,
1256
- sections: {
1257
- STIM_EAS_BUILD_MISSING: {
1258
- summary: "no completed EAS development build matches; build only with session authorization",
1259
- body: () => `STIM_EAS_BUILD_MISSING
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
1396
+
1397
+ Every refusal listed here carries a stable CODE, whichever command prints it.
1398
+ 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
1260
1404
  No compatible build matches the selected EAS project, profile, platform and
1261
1405
  native fingerprint. No device was acquired and no local or cloud build was
1262
1406
  started. The remedy prints the exact npx eas-cli build command. Check session
1263
1407
  authorization for its potential cost before running it, then retry Stim.
1264
1408
  See stim guide lifecycle eas.`
1265
- },
1266
- STIM_EAS_UNAVAILABLE: {
1267
- summary: "EAS lookup/download failed, or another run holds the artifact claim",
1268
- body: () => `STIM_EAS_UNAVAILABLE
1409
+ },
1410
+ STIM_EAS_UNAVAILABLE: {
1411
+ summary: "EAS lookup/download failed, or another run holds the artifact claim",
1412
+ body: () => `STIM_EAS_UNAVAILABLE
1269
1413
  EAS CLI is unavailable, a profile/fingerprint/list/download operation failed,
1270
1414
  the response could not be validated, or another run holds the artifact claim.
1271
1415
  Follow the printed remedy: inspect the named EAS command or retry once the
1272
1416
  holder finishes. This is not proof that a build is missing. No native build
1273
1417
  is started. See stim guide lifecycle eas.`
1274
- },
1275
- STIM_WORKSPACE_STATE: {
1276
- summary: "$STIM_HOME/workspaces could not be prepared, or the digest directory belongs to another project",
1277
- aliases: ["STIM_WORKSPACE_COLLISION"],
1278
- separator: "--- BUILD-PATH CODES (`stim ios` / `stim android`) ---",
1279
- body: () => `STIM_WORKSPACE_STATE / STIM_WORKSPACE_COLLISION
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
1280
1424
  Stim could not prepare this project's global workspace directory under
1281
1425
  $STIM_HOME/workspaces. Check that STIM_HOME is writable and has free
1282
1426
  space. An EPERM on a directory the user CAN write is a sandbox, not a
@@ -1284,10 +1428,10 @@ code, never on the message.`,
1284
1428
  readable-name-plus-digest directory already has a workspace.json for a
1285
1429
  different canonical project path; do not overwrite it until you identify
1286
1430
  which workspace owns it.`
1287
- },
1288
- STIM_NO_METRO: {
1289
- summary: "nothing provably this workspace's dev server holds the reserved port (ios, android, reload)",
1290
- body: () => `STIM_NO_METRO
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
1291
1435
  Nothing that could be proven to be THIS workspace's dev server holds the
1292
1436
  reserved port -- or no port is reserved at all. The gate fires in about a
1293
1437
  second, before the device is even booted, rather than after four minutes of
@@ -1302,26 +1446,26 @@ code, never on the message.`,
1302
1446
 
1303
1447
  Reload requires the recorded launch's port to be this workspace's live
1304
1448
  Metro. It refuses a missing, changed, unresponsive, or foreign port.`
1305
- },
1306
- STIM_NO_FINGERPRINT: {
1307
- summary: "@expo/fingerprint produced no hash, so the shared cache cannot be addressed",
1308
- body: () => `STIM_NO_FINGERPRINT
1449
+ },
1450
+ STIM_NO_FINGERPRINT: {
1451
+ summary: "@expo/fingerprint produced no hash, so the shared cache cannot be addressed",
1452
+ body: () => `STIM_NO_FINGERPRINT
1309
1453
  \`@expo/fingerprint\` produced no hash, so the shared build cache cannot be
1310
1454
  addressed. Stim uses its declared @expo/fingerprint dependency directly,
1311
1455
  independently of the target project's package graph. This is a refusal
1312
1456
  rather than a silent full build because an unaddressable cache means every
1313
1457
  workspace on the commit compiles from scratch, forever.`
1314
- },
1315
- STIM_PREBUILD_FAILED: {
1316
- summary: "expo prebuild could not generate the native directory",
1317
- body: () => `STIM_PREBUILD_FAILED
1458
+ },
1459
+ STIM_PREBUILD_FAILED: {
1460
+ summary: "expo prebuild could not generate the native directory",
1461
+ body: () => `STIM_PREBUILD_FAILED
1318
1462
  \`expo prebuild\` could not generate the missing native directory. The
1319
1463
  extracted output is above the code; the transcript is in
1320
1464
  the global workspace logs/build-<platform>.ndjson file.`
1321
- },
1322
- STIM_DEPS_FAILED: {
1323
- summary: "pod install or gradle sync failed; the bundler ladder and BUNDLE_FROZEN",
1324
- body: () => `STIM_DEPS_FAILED
1465
+ },
1466
+ STIM_DEPS_FAILED: {
1467
+ summary: "pod install or gradle sync failed; the bundler ladder and BUNDLE_FROZEN",
1468
+ body: () => `STIM_DEPS_FAILED
1325
1469
  \`pod install\` (iOS) or the gradle dependency sync (Android) failed. On iOS
1326
1470
  this runs only when Podfile.lock and Pods/Manifest.lock disagree, or Pods is
1327
1471
  absent -- which is exactly what a carried worktree produces.
@@ -1349,10 +1493,10 @@ code, never on the message.`,
1349
1493
  \`yarn install\`, \`bun install\`, \`npm ci\`) or that same pod ladder. The
1350
1494
  message names the command, quotes its last lines, and nothing is copied: fix
1351
1495
  the source checkout, then warm again.`
1352
- },
1353
- STIM_BUILD_FAILED: {
1354
- summary: "xcodebuild or gradle failed; the two Android APK refusals; a damaged compilation-cache object",
1355
- body: () => `STIM_BUILD_FAILED
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
1356
1500
  xcodebuild or gradle failed. The EXTRACTED diagnostics are printed (capped),
1357
1501
  not the transcript. Read the log path on the next line for the rest.
1358
1502
  Two Android refusals share this code without gradle itself failing:
@@ -1383,10 +1527,10 @@ code, never on the message.`,
1383
1527
  object, and upgrading the CLI does not clear it. Empty that one cache with
1384
1528
  \`gc --delete --cache "compilation cache"\`, then build again. The next
1385
1529
  build is a cold one.`
1386
- },
1387
- STIM_PATH_TOO_LONG: {
1388
- summary: "Windows only: the project root leaves no room for the NDK object paths ninja must open",
1389
- body: () => `STIM_PATH_TOO_LONG (android, Windows only)
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)
1390
1534
  The NDK's ninja is not long-path aware, so every CMake object path has to
1391
1535
  stay under Windows' MAX_PATH. React Native's codegen objects are named
1392
1536
  after their mangled absolute source path, so each carries the project root
@@ -1402,10 +1546,10 @@ code, never on the message.`,
1402
1546
  preflight does not inspect that configured path and may refuse it too.
1403
1547
  \`doctor --platform android\` reports the same root as a finding for the
1404
1548
  emulator's ABI.`
1405
- },
1406
- fallbacks: {
1407
- summary: "release cache-hit notes that are not codes: swap failure, asset gate, uninstall, device fallbacks",
1408
- body: () => `FALLBACK NOTES THAT ARE NOT CODES (release cache hits)
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)
1409
1553
  On a release cache hit Stim regenerates this workspace's JS bundle into a
1410
1554
  COPY of the cached artifact before installing it -- \`ios --configuration
1411
1555
  Release\` into a copy of the .app, \`android --variant ...Release\` into a
@@ -1471,10 +1615,10 @@ code, never on the message.`,
1471
1615
  The same refusal on a FRESHLY BUILT app is a code (STIM_NO_PROFILE,
1472
1616
  STIM_PROFILE_MISMATCH, STIM_NO_SIGNING_IDENTITY), not a note: building again
1473
1617
  would produce the same app and refuse again.`
1474
- },
1475
- STIM_BUILD_WAIT_TIMEOUT: {
1476
- summary: "waited ~90 minutes for another workspace's build of the same fingerprint",
1477
- body: () => `STIM_BUILD_WAIT_TIMEOUT
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
1478
1622
  This run was waiting for ANOTHER workspace's build of the same fingerprint
1479
1623
  (see \`guide lifecycle concurrency\`), and no artifact arrived within ~90
1480
1624
  minutes.
@@ -1483,10 +1627,10 @@ code, never on the message.`,
1483
1627
  may have failed. The message names the current pid and lock directory:
1484
1628
  check the pid, and if it is not really building, remove that directory and
1485
1629
  run the command again.`
1486
- },
1487
- STIM_CLAIM_REFUSED: {
1488
- summary: "an ownership claim blocks the operation; inspect its holder before removing an unresolved claim",
1489
- body: () => `STIM_CLAIM_REFUSED
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
1490
1634
  A build lock records the holder's process IDENTITY, not just its pid, so a
1491
1635
  recycled pid reads as a gone builder rather than a live one, and a builder
1492
1636
  busy in a long \`simctl\` or gradle call reads as live rather than as stale.
@@ -1524,10 +1668,10 @@ code, never on the message.`,
1524
1668
  Legacy inline \`deletionClaim\` fields in config.json have no process
1525
1669
  identity and require separate manual inspection; \`stim guide lifecycle pool\`
1526
1670
  describes their field-only recovery.`
1527
- },
1528
- STIM_CLAIM_UNAVAILABLE: {
1529
- summary: "a process identity or warm claim store is unavailable, so the protected operation refuses",
1530
- body: () => `STIM_CLAIM_UNAVAILABLE
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
1531
1675
  Every ownership claim records the holder's process identity, captured through
1532
1676
  the \`unique-pid\` native module. This code is that capture failing: no
1533
1677
  prebuilt binary for this platform and architecture, or the OS refusing to
@@ -1547,10 +1691,10 @@ code, never on the message.`,
1547
1691
  access to the existing claim store, checking parent permissions, symlink
1548
1692
  targets, mount access and sandbox rules, then retry. Do not switch STIM_HOME to evade a claim:
1549
1693
  concurrent runs must coordinate through the same store.`
1550
- },
1551
- STIM_INSTALL_FAILED: {
1552
- summary: "simctl, adb, or devicectl refused the artifact; the one signer-conflict retry",
1553
- body: () => `STIM_INSTALL_FAILED
1694
+ },
1695
+ STIM_INSTALL_FAILED: {
1696
+ summary: "simctl, adb, or devicectl refused the artifact; the one signer-conflict retry",
1697
+ body: () => `STIM_INSTALL_FAILED
1554
1698
  The artifact built or came from cache, but \`simctl install\` / \`adb install\` /
1555
1699
  \`devicectl device install app\` refused it. A signature or architecture
1556
1700
  mismatch, or a full device.
@@ -1569,10 +1713,10 @@ code, never on the message.`,
1569
1713
  the flavor's id and not the gradle namespace -- and gives you the
1570
1714
  \`adb -s <serial> uninstall <applicationId>\` that clears it. Re-running after
1571
1715
  that is a cache hit: one install, no build.`
1572
- },
1573
- STIM_LAUNCH_FAILED: {
1574
- summary: "installed but would not start; the developer-trust tap on a phone",
1575
- body: () => `STIM_LAUNCH_FAILED
1716
+ },
1717
+ STIM_LAUNCH_FAILED: {
1718
+ summary: "installed but would not start; the developer-trust tap on a phone",
1719
+ body: () => `STIM_LAUNCH_FAILED
1576
1720
  Installed, but the app would not start. On Android this usually means no
1577
1721
  launchable activity resolved.
1578
1722
  On a local iOS simulator, a timed-out launch can mean the simulator cannot
@@ -1590,41 +1734,41 @@ code, never on the message.`,
1590
1734
  Trust, then run the command again. It is a per-developer-certificate tap, not
1591
1735
  a per-build one -- but an uninstall clears it, including the one Stim's own
1592
1736
  signer-conflict retry performs.`
1593
- },
1594
- STIM_NO_SCHEME: {
1595
- summary: "Xcode schemes unavailable or no unambiguous app scheme in ios/",
1596
- body: () => `STIM_NO_SCHEME
1737
+ },
1738
+ STIM_NO_SCHEME: {
1739
+ summary: "Xcode schemes unavailable or no unambiguous app scheme in ios/",
1740
+ body: () => `STIM_NO_SCHEME
1597
1741
  Stim could not list or select an app scheme in ios/. Share the intended app
1598
1742
  scheme so xcodebuild can see it. Select an available exact name with
1599
1743
  \`stim ios --scheme <name>\`. An unknown explicit name prints available choices.
1600
1744
  Without an explicit selector, a workspace
1601
1745
  name match wins; otherwise Stim accepts a sole non-test scheme, or a listed
1602
1746
  scheme matching app.json. Unmatched ambiguous schemes are refused.`
1603
- },
1604
- STIM_NO_PROFILE: {
1605
- summary: "no or undecodable embedded.mobileprovision; build once from Xcode",
1606
- separator: "--- iOS SIGNING CODES (`ios --device`, and only there) ---",
1607
- context: `A simulator build needs no signature, which is why none of these can fire on
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
1608
1752
  the normal path. A device build carries one, and Stim re-seals any bundle it
1609
1753
  modifies with the identity the bundle already names -- so it checks, before
1610
1754
  spending a build or a bundle, that the check can succeed.`,
1611
- body: () => `STIM_NO_PROFILE
1755
+ body: () => `STIM_NO_PROFILE
1612
1756
  The built or cached .app has no embedded.mobileprovision, or
1613
- \`security cms -D\` could not decode the one it has. The first means the build
1757
+ \`openssl smime\` could not decode the one it has. The first means the build
1614
1758
  produced an unsigned app -- almost always a simulator-sliced artifact.
1615
1759
  Set a team and a Development profile for the target's configuration in
1616
1760
  Xcode > Signing & Capabilities, then BUILD ONCE FROM XCODE to install the
1617
1761
  profile. Stim will not do that step: registering a device or minting a
1618
1762
  profile changes your Apple Developer account, so Stim never passes
1619
1763
  -allowProvisioningUpdates.`
1620
- },
1621
- STIM_PROFILE_MISMATCH: {
1622
- summary: "the profile is expired, has no ProvisionedDevices, or does not name this UDID",
1623
- context: `A simulator build needs no signature, which is why none of these can fire on
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
1624
1768
  the normal path. A device build carries one, and Stim re-seals any bundle it
1625
1769
  modifies with the identity the bundle already names -- so it checks, before
1626
1770
  spending a build or a bundle, that the check can succeed.`,
1627
- body: () => `STIM_PROFILE_MISMATCH
1771
+ body: () => `STIM_PROFILE_MISMATCH
1628
1772
  The profile inside the app cannot admit this phone. Three shapes, and the
1629
1773
  message names which one and the profile type it found:
1630
1774
  - it expired, or carries no ExpirationDate at all;
@@ -1638,14 +1782,14 @@ spending a build or a bundle, that the check can succeed.`,
1638
1782
  With --eas-profile, follow the EAS device:create and build commands in the
1639
1783
  refusal instead. Registration, signing changes and cloud builds need session
1640
1784
  authorization. See stim guide lifecycle eas.`
1641
- },
1642
- STIM_NO_SIGNING_IDENTITY: {
1643
- summary: "no single keychain identity resolves; ios.signingIdentitySha1 for two certificates",
1644
- context: `A simulator build needs no signature, which is why none of these can fire on
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
1645
1789
  the normal path. A device build carries one, and Stim re-seals any bundle it
1646
1790
  modifies with the identity the bundle already names -- so it checks, before
1647
1791
  spending a build or a bundle, that the check can succeed.`,
1648
- body: () => `STIM_NO_SIGNING_IDENTITY
1792
+ body: () => `STIM_NO_SIGNING_IDENTITY
1649
1793
  No single keychain identity could be resolved to re-seal with. Either
1650
1794
  \`security find-identity -v -p codesigning\` lists nothing, or the identity
1651
1795
  the artifact's own profile names is absent, or two certificates share that
@@ -1653,14 +1797,14 @@ spending a build or a bundle, that the check can succeed.`,
1653
1797
  Open Xcode > Settings > Accounts and download your certificates, or unlock
1654
1798
  the login keychain with \`security unlock-keychain\`. For the two-certificate
1655
1799
  case, set ios.signingIdentitySha1 to the SHA-1 hash beside the one you want.`
1656
- },
1657
- STIM_CODESIGN_FAILED: {
1658
- summary: "codesign failed on the modified copy; the cache entry is untouched and the run builds fresh",
1659
- context: `A simulator build needs no signature, which is why none of these can fire on
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
1660
1804
  the normal path. A device build carries one, and Stim re-seals any bundle it
1661
1805
  modifies with the identity the bundle already names -- so it checks, before
1662
1806
  spending a build or a bundle, that the check can succeed.`,
1663
- body: () => `STIM_CODESIGN_FAILED
1807
+ body: () => `STIM_CODESIGN_FAILED
1664
1808
  \`codesign --force --sign\` or \`codesign --verify --strict\` exited non-zero
1665
1809
  on the modified copy. The verbatim codesign stderr is quoted, because it is
1666
1810
  the answer: a locked login keychain reports errSecInternalComponent, an
@@ -1668,14 +1812,14 @@ spending a build or a bundle, that the check can succeed.`,
1668
1812
  and confirm exactly one identity matches the name. The cache entry itself is
1669
1813
  never modified -- the failure is on a temporary copy, and the run builds
1670
1814
  fresh.`
1671
- },
1672
- STIM_NO_LAN_ADDRESS: {
1673
- summary: "the Mac has no non-internal IPv4 interface; a tunnel cannot help a phone",
1674
- separator: "--- iOS DEVICE DEBUG REACHABILITY CODES (`ios --device` in Debug) ---",
1675
- context: `A phone does not share the host's loopback and USB carries no reverse forward,
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,
1676
1820
  so a Debug run on one is wired to a LAN origin instead of localhost. Both codes
1677
1821
  fire BEFORE the build, because a refusal that costs a build is a bad refusal.`,
1678
- body: () => `STIM_NO_LAN_ADDRESS
1822
+ body: () => `STIM_NO_LAN_ADDRESS
1679
1823
  This Mac reports no non-internal IPv4 interface, so there is no address to
1680
1824
  give the phone: it is offline, or on nothing but utun/awdl/bridge. Join a
1681
1825
  Wi-Fi or Ethernet network, or connect this Mac by cable, and run again.
@@ -1684,13 +1828,13 @@ fire BEFORE the build, because a refusal that costs a build is a bad refusal.`,
1684
1828
  ip.txt is read by RCTBundleURLProvider, which prefixes the scheme. A tunnel
1685
1829
  cannot be expressed to a phone, so --device ignores metro.publicUrl,
1686
1830
  metro.tunnel and metro.ngrokUrl and says so when one is set.`
1687
- },
1688
- STIM_LAN_METRO_UNREACHABLE: {
1689
- summary: "the LAN origin did not answer as this workspace's Metro; ios.lanHost on a multi-NIC Mac",
1690
- context: `A phone does not share the host's loopback and USB carries no reverse forward,
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,
1691
1835
  so a Debug run on one is wired to a LAN origin instead of localhost. Both codes
1692
1836
  fire BEFORE the build, because a refusal that costs a build is a bad refusal.`,
1693
- body: () => `STIM_LAN_METRO_UNREACHABLE
1837
+ body: () => `STIM_LAN_METRO_UNREACHABLE
1694
1838
  The chosen LAN origin did not answer as THIS workspace's Metro: no answer, a
1695
1839
  5xx, or a dev server that is not this one -- the message says which.
1696
1840
  \`stim start\` prints the port it reserved. On a Mac with several interfaces
@@ -1700,12 +1844,12 @@ fire BEFORE the build, because a refusal that costs a build is a bad refusal.`,
1700
1844
  routes a host connection to its own address over loopback, so the gate passes
1701
1845
  through a firewall that will block the phone. That evidence only ever arrives
1702
1846
  from the phone's own bundle request, which is what \`launched\` reports.`
1703
- },
1704
- unverified: {
1705
- summary: "launched: \"unverified\" with the Local Network path reason, and the routed recovery",
1706
- context: `A phone does not share the host's loopback and USB carries no reverse forward,
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,
1707
1851
  so a Debug run on one is wired to a LAN origin instead of localhost.`,
1708
- body: () => `LAUNCH UNVERIFIED, LOCAL NETWORK NOT GRANTED (not a code -- a routed remedy)
1852
+ body: () => `LAUNCH UNVERIFIED, LOCAL NETWORK NOT GRANTED (not a code -- a routed remedy)
1709
1853
  An app that has not been granted Local Network reaches nothing on the LAN,
1710
1854
  and CFNetwork reports each attempt as NSURLErrorDomain -1009 "The Internet
1711
1855
  connection appears to be offline." with the path reason
@@ -1791,17 +1935,18 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
1791
1935
  General > VPN & Device Management) is refused to automation by the same gate
1792
1936
  that refuses the app, agent-device's own runner included, so its remedy is
1793
1937
  "ask the user" and nothing else. An uninstall clears both.`
1794
- },
1795
- STIM_NO_DEVICE: {
1796
- summary: "no usable phone, or the owned simulator or emulator could not be created or booted",
1797
- separator: "--- DEVICE AND CAPACITY CODES ---",
1798
- body: () => `STIM_NO_DEVICE
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
1799
1943
  With \`--device\`, no physical device answered the selection: none connected,
1800
1944
  a named serial/UDID that is not connected, several connected with none named
1801
1945
  (the refusal lists them), or one that is connected but unusable -- an
1802
1946
  unauthorized Android device, or an iPhone that is unpaired or has Developer
1803
- Mode off. Hardware is never created or booted, so there is nothing to retry
1804
- into existence: fix the cable, the trust prompt, or Developer Mode.
1947
+ Mode off, over a cable or over Wi-Fi alike. Hardware is never created or
1948
+ booted, so there is nothing to retry into existence: fix the cable, the
1949
+ trust prompt, or Developer Mode.
1805
1950
  Otherwise the owned simulator/emulator could not be created or could not
1806
1951
  reach a booted state. \`stim doctor\` checks the toolchain; \`stim status\` says what
1807
1952
  Stim thinks it owns. Re-running the command creates a fresh owned device
@@ -1851,10 +1996,10 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
1851
1996
  not silently boot a different one. Reap it (\`worktree remove\`, or
1852
1997
  \`gc --delete\`) and run \`stim ios\` again to create the requested model.
1853
1998
  That loses the old sim's app state.`
1854
- },
1855
- STIM_DEVICE_BUSY: {
1856
- summary: "another workspace holds the lease on that phone and the wait ran out",
1857
- body: () => `STIM_DEVICE_BUSY
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
1858
2003
  Only on a \`--device\` run. Another workspace holds the lease on that phone,
1859
2004
  and the wait ran out: the message names the holder root, the device, and the
1860
2005
  expiry as a clock time and a remaining duration, and \`--json\` adds
@@ -1868,10 +2013,25 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
1868
2013
  no token left in its \`state.json\` (its workspace directory was recreated).
1869
2014
  The remedy for that last one is \`stim device unlock\`, which releases by
1870
2015
  holder rather than by token.`
1871
- },
1872
- STIM_DEVICE_LOST: {
1873
- summary: "the lease was gone or re-held at the pre-install check; rerun",
1874
- body: () => `STIM_DEVICE_LOST
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
2020
+ Only on an \`ios --device\` run whose iPhone devicectl reaches over Wi-Fi
2021
+ (transportType localNetwork). The \`devicectl device install app\` ran past
2022
+ its 15-minute Wi-Fi bound, the phone did not appear in its own process list
2023
+ within the 120-second Wi-Fi launch bound, or devicectl reported that the
2024
+ connection dropped in its own ERROR output (an app's log lines never count).
2025
+ The message names the transport and quotes devicectl. Connect the phone
2026
+ with a cable, keep it unlocked, and run the command again: a cabled phone
2027
+ installs over the cable. A cause devicectl names -- a locked phone, an
2028
+ untrusted host, Developer Mode off, full storage, the developer-trust tap --
2029
+ keeps STIM_INSTALL_FAILED or STIM_LAUNCH_FAILED and its own remedy, even
2030
+ 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
1875
2035
  Only on a \`--device\` run. The run held a lease, and the raise before the
1876
2036
  install found it gone or held under another token -- another workspace took
1877
2037
  the device in that window. The message names the new holder and its expiry.
@@ -1879,10 +2039,10 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
1879
2039
  AFTER the install has started this is not a failure: the app is already on
1880
2040
  the phone, so the run prints one warning, continues, and reports
1881
2041
  \`lease: null\` in \`--json\`.`
1882
- },
1883
- STIM_AT_CAPACITY: {
1884
- summary: "concurrency.maxDevices reached; a refusal, not a queue",
1885
- body: () => `STIM_AT_CAPACITY
2042
+ },
2043
+ STIM_AT_CAPACITY: {
2044
+ summary: "concurrency.maxDevices reached; a refusal, not a queue",
2045
+ body: () => `STIM_AT_CAPACITY
1886
2046
  Only when concurrency.maxDevices is set (it is UNSET by default, so this never
1887
2047
  fires unless you opted in). Booting a NEW owned device would exceed the cap:
1888
2048
  the machine already has that many Stim-owned devices booted. It is a refusal, not
@@ -1893,10 +2053,10 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
1893
2053
  \`ios\` on an environment you already have is idempotent. (The build cap
1894
2054
  behaves differently: a compile WAITS for a free slot rather than refusing.
1895
2055
  See \`guide lifecycle concurrency\`.)`
1896
- },
1897
- STIM_BUILD_SLOT_TIMEOUT: {
1898
- summary: "the maxBuilds wait gave up with every slot held by a running process",
1899
- body: () => `STIM_BUILD_SLOT_TIMEOUT
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
1900
2060
  Only when concurrency.maxBuilds is set. The build cap does not refuse, it
1901
2061
  WAITS -- this code is that wait giving up: ~90 minutes elapsed and every one
1902
2062
  of the N slots was still held by a running process, or by a holder Stim could
@@ -1910,50 +2070,50 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
1910
2070
  the message names the directory: remove the slot of a builder that is not
1911
2071
  building, or raise concurrency.maxBuilds
1912
2072
  (\`guide lifecycle concurrency\`).`
1913
- },
1914
- STIM_NO_REMOTE_SESSION: {
1915
- summary: "the backend could not use agent-device, or metro.tunnel names an unusable provider",
1916
- separator: "--- REMOTE-DEVICE CODES (`ios --remote <proxy|eas>` / `android --remote <proxy|eas>`) ---",
1917
- body: () => `STIM_NO_REMOTE_SESSION
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
1918
2078
  The selected backend could not use agent-device, or metro.tunnel names a
1919
2079
  provider or mode this workspace cannot use (e.g. "expo" on a bare RN
1920
2080
  project). The remedy line says which. Nothing was created yet.`
1921
- },
1922
- STIM_REMOTE_PROXY_CONFIG: {
1923
- summary: "--remote proxy needs AGENT_DEVICE_DAEMON_BASE_URL and AGENT_DEVICE_DAEMON_AUTH_TOKEN",
1924
- body: () => `STIM_REMOTE_PROXY_CONFIG
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
1925
2085
  \`--remote proxy\` requires AGENT_DEVICE_DAEMON_BASE_URL and
1926
2086
  AGENT_DEVICE_DAEMON_AUTH_TOKEN. These variables provide credentials after
1927
2087
  proxy is selected. They never select the backend.`
1928
- },
1929
- STIM_REMOTE_EAS_UNAVAILABLE: {
1930
- summary: "--remote eas needs eas-cli",
1931
- body: () => `STIM_REMOTE_EAS_UNAVAILABLE
2088
+ },
2089
+ STIM_REMOTE_EAS_UNAVAILABLE: {
2090
+ summary: "--remote eas needs eas-cli",
2091
+ body: () => `STIM_REMOTE_EAS_UNAVAILABLE
1932
2092
  \`--remote eas\` requires eas-cli. Proxy environment variables do not change
1933
2093
  this selection and are not passed to EAS.`
1934
- },
1935
- STIM_REMOTE_PLATFORM_MISMATCH: {
1936
- summary: "the recorded remote session belongs to the other platform; stop, then rerun",
1937
- body: () => `STIM_REMOTE_PLATFORM_MISMATCH
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
1938
2098
  This workspace already has a recorded remote session, and it belongs to the
1939
2099
  OTHER platform ("Session <id> belongs to android, not ios"). A workspace
1940
2100
  holds one remote session, and Stim will not end the recorded one to make
1941
2101
  room -- it may be mid-run for whoever started it. Run \`stim stop\` for this
1942
2102
  workspace, then re-run with the platform you want. Nothing was created here.`
1943
- },
1944
- STIM_REMOTE_SESSION_STATE: {
1945
- summary: "the EAS session was created but its state could not be recorded, so Stim stopped it",
1946
- body: () => `STIM_REMOTE_SESSION_STATE
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
1947
2107
  The EAS session was created and is healthy, but recording it in this
1948
2108
  workspace's state failed (an unwritable STIM_HOME, a full disk). A session
1949
2109
  nothing references is a session nothing will ever stop, so Stim stopped the
1950
2110
  one it had just created and removed its ownership claim before reporting:
1951
2111
  this code means nothing is running and nothing is still billing. Repair the
1952
2112
  state storage the message names, then run the remote command again.`
1953
- },
1954
- STIM_REMOTE_SESSION_CLEANUP: {
1955
- summary: "Stim could not prove an EAS session ended; eas simulator:stop --id",
1956
- body: () => `STIM_REMOTE_SESSION_CLEANUP
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
1957
2117
  Stim tried to end an EAS session and could not PROVE it ended: \`eas
1958
2118
  simulator:stop\` failed, or its output did not confirm the stop, or the
1959
2119
  session stopped but its claim in the machine ledger could not be removed.
@@ -1962,10 +2122,10 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
1962
2122
  \`eas simulator:stop --id <id>\` -- and for a ledger that outlived its
1963
2123
  session, the ledger path to repair. The same code covers a recorded session
1964
2124
  that could not be verified before replacement: inspect it, then \`stim stop\`.`
1965
- },
1966
- STIM_REMOTE_METRO_WRONG: {
1967
- summary: "the tunnel reaches a Metro that is not this workspace's",
1968
- body: () => `STIM_REMOTE_METRO_WRONG
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
1969
2129
  The gate that proves a tunnel still reaches THIS workspace's Metro failed --
1970
2130
  before a session or a build, whether the tunnel is Expo's own, one Stim
1971
2131
  started (metro.tunnel: cloudflared/ngrok/auto), or a named metro.publicUrl.
@@ -1974,39 +2134,39 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
1974
2134
  port), and it now serves ANOTHER workspace's dev server -- healthy, and
1975
2135
  wrong. Re-run \`stim start\` (it prints the port it reserved) and, for a
1976
2136
  manual tunnel, rebuild it against that port.`
1977
- },
1978
- STIM_REMOTE_METRO_UNREACHABLE: {
1979
- summary: "a remote start could not create its managed tunnel or tell the device where Metro is",
1980
- body: () => `STIM_REMOTE_METRO_UNREACHABLE
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
1981
2141
  A remote start could not create its selected managed tunnel, or the device
1982
2142
  could not be told where Metro is. Follows the same remedy as
1983
2143
  STIM_NO_REMOTE_SESSION's tunnel guidance -- set metro.tunnel, or use
1984
2144
  metro.publicUrl for an existing endpoint.`
1985
- },
1986
- STIM_RELOAD_AMBIGUOUS: {
1987
- summary: "both owned apps are live; name the platform",
1988
- separator: "--- RELOAD CODES (`stim reload [ios|android]`) ---",
1989
- body: () => `STIM_RELOAD_AMBIGUOUS
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
1990
2150
  Both owned apps are live. Name ios or android; Stim never guesses.`
1991
- },
1992
- STIM_RELOAD_RELEASE: {
1993
- summary: "the live app has embedded JS; run a Debug build first",
1994
- body: () => `STIM_RELOAD_RELEASE
2151
+ },
2152
+ STIM_RELOAD_RELEASE: {
2153
+ summary: "the live app has embedded JS; run a Debug build first",
2154
+ body: () => `STIM_RELOAD_RELEASE
1995
2155
  The live app was launched with embedded JavaScript. Run the platform command
1996
2156
  with a Debug configuration or variant first.`
1997
- },
1998
- STIM_RELOAD_STOPPED: {
1999
- summary: "the recorded app is gone, its device is not live and owned, or the process could not be proven",
2000
- aliases: ["STIM_RELOAD_UNOWNED", "STIM_RELOAD_PROBE_FAILED"],
2001
- body: () => `STIM_RELOAD_STOPPED / STIM_RELOAD_UNOWNED / STIM_RELOAD_PROBE_FAILED
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
2002
2162
  The recorded app is gone, its exact device is not live and owned by this
2003
2163
  workspace, or simctl/adb could not prove the process exists. No launch or
2004
2164
  device lifecycle action is taken; follow the printed platform-command or
2005
2165
  process-probe remedy.`
2006
- },
2007
- STIM_RELOAD_FAILED: {
2008
- summary: "the reload failed; the remedy differs by shape -- read it before acting",
2009
- body: () => `STIM_RELOAD_FAILED
2166
+ },
2167
+ STIM_RELOAD_FAILED: {
2168
+ summary: "the reload failed; the remedy differs by shape -- read it before acting",
2169
+ body: () => `STIM_RELOAD_FAILED
2010
2170
  The Metro websocket reload did not reach a peer Stim could identify. Two
2011
2171
  shapes reach this code and the remedy differs. Read it rather than assuming.
2012
2172
 
@@ -2032,27 +2192,27 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
2032
2192
  MORE THAN ONE MATCHING PEER IS NOT A FAILURE. A workspace Metro serves one
2033
2193
  app, so several matching peers are that app on several devices. Stim reloads
2034
2194
  every one of them and reports the count in the facts as targets.`
2035
- },
2036
- STIM_WORKTREE_REMOVAL_IN_PROGRESS: {
2037
- summary: "a managed remote start found worktree remove holding the lock; wait, then rerun",
2038
- separator: "--- DEV-SERVER CODES (`stim start`) ---",
2039
- body: () => `STIM_WORKTREE_REMOVAL_IN_PROGRESS
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
2040
2200
  A managed remote start found that \`stim worktree remove\` owns the
2041
2201
  worktree lock. The start did not register the project or create a tunnel.
2042
2202
  Wait for removal to finish, then run \`stim start --remote\` again.`
2043
- },
2044
- STIM_REMOTE_START_REQUIRED: {
2045
- summary: "a running server cannot gain a remote tunnel; stop, then start --remote, or metro.publicUrl",
2046
- body: () => `STIM_REMOTE_START_REQUIRED
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
2047
2207
  A healthy bare or Expo server was started without its required remote
2048
2208
  tunnel. A running server cannot gain that option. For a Stim supervisor,
2049
2209
  run \`stim stop\`, then \`stim start --remote\`. For an external server,
2050
2210
  configure metro.publicUrl or let Stim supervise the server.`
2051
- },
2052
- STIM_BARE_DEPS: {
2053
- summary: "the supervisor cannot host Metro from the project's node_modules; the @stim-cli/metro capture note",
2054
- aliases: ["STIM_BARE_LOAD", "STIM_BARE_API"],
2055
- body: () => `STIM_BARE_DEPS / STIM_BARE_LOAD / STIM_BARE_API (bare RN)
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)
2056
2216
  The supervisor hosts Metro out of the PROJECT's node_modules, so metro,
2057
2217
  @react-native/dev-middleware and @react-native-community/cli-server-api must
2058
2218
  be installed there and must match the project's React Native. DEPS = not
@@ -2064,25 +2224,25 @@ captured" (in metro.ndjson, bare RN)
2064
2224
  The dev server is serving; only capture is missing, so \`logs\` would report
2065
2225
  a quiet timeline for a broken build. Install \`@stim-cli/metro\` as a
2066
2226
  devDependency of the project.`
2067
- },
2068
- STIM_EXPO_BIN: {
2069
- summary: "node_modules/.bin/expo is missing; install dependencies",
2070
- body: () => `STIM_EXPO_BIN (Expo)
2227
+ },
2228
+ STIM_EXPO_BIN: {
2229
+ summary: "node_modules/.bin/expo is missing; install dependencies",
2230
+ body: () => `STIM_EXPO_BIN (Expo)
2071
2231
  node_modules/.bin/expo does not exist. Install the project's dependencies.`
2072
- },
2073
- STIM_METRO_TIMEOUT: {
2074
- summary: "the supervisor is alive but Metro or the tunnel was not ready within the wait; --wait 180",
2075
- body: () => `STIM_METRO_TIMEOUT
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
2076
2236
  "The dev server did not answer on port <n> within <s>s."
2077
2237
  The supervisor is alive, but Metro or its requested Expo tunnel is not ready.
2078
2238
  \`start\` has already
2079
2239
  printed the last lines of the global workspace logs/supervisor.log above this -- read
2080
2240
  them. A cold Metro on a large graph can genuinely need more than the default
2081
2241
  60s: re-run with \`--wait 180\`. Otherwise \`stim stop\`, then \`start\`.`
2082
- },
2083
- STIM_SUPERVISOR_EXITED: {
2084
- summary: "the dev server failed outright; the quoted supervisor.log tail is the real error",
2085
- body: () => `STIM_SUPERVISOR_EXITED
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
2086
2246
  "The supervisor exited (<code|signal>) before the dev server came up"
2087
2247
  The dev server failed outright, and the quoted evidence is the real error:
2088
2248
  the supervisor.log tail if it wrote one, plus this attempt's error records
@@ -2097,11 +2257,11 @@ captured" (in metro.ndjson, bare RN)
2097
2257
  Stim's processes. For a legacy record, stop the server with the tool that
2098
2258
  started it before retrying. Never reconstruct ownership from a process
2099
2259
  name, port, or wall-clock timestamp.`
2100
- },
2101
- STIM_BAD_ARG: {
2102
- summary: "an argument, setting, directory, flavor, or device name refused before anything starts",
2103
- aliases: ["STIM_NO_PROJECT"],
2104
- body: () => `STIM_BAD_ARG / STIM_NO_PROJECT
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
2105
2265
  The command refused before doing anything: an unusable --wait value, a known
2106
2266
  setting with the wrong type ("Invalid <key> setting <value>. Expected <shape>."
2107
2267
  -- \`guide settings\` names the type each key takes), an invalid
@@ -2127,16 +2287,25 @@ captured" (in metro.ndjson, bare RN)
2127
2287
  or android.systemImage setting is checked the same way, and the check applies
2128
2288
  even when this workspace ALREADY owns a device, so a name that could never
2129
2289
  create anything is caught rather than left to a later run.
2290
+ \`gc --json --cache <name>\` refuses with STIM_BAD_ARG when no shared cache
2291
+ carries the name; the remedy names the caches on this machine. \`gc\`
2292
+ refuses --cache together with --worktrees the same way.
2293
+ A working directory with no package.json above it gets the same
2294
+ STIM_NO_PROJECT refusal from \`start\`, \`ios\`, \`android\`, \`stop\`,
2295
+ \`reload\`, \`logs\`, \`doctor\` and \`device lock|unlock\`. With \`--json\` the { code, message, remedy } object
2296
+ is on stdout, except \`logs --json\`, whose stdout stays empty NDJSON; the
2297
+ refusal is on stderr. \`stop\` outside a project refuses rather than
2298
+ reporting that nothing was running.
2130
2299
  These errors are caught before the port is reserved and before any build or
2131
2300
  device work, so nothing was started. The one listing they need
2132
2301
  (\`simctl list runtimes\`, the SDK's system-images directory) runs only when
2133
2302
  a name was actually given, and a listing that fails is reported as
2134
2303
  STIM_NO_DEVICE naming the tool, never as a crash.`
2135
- },
2136
- STIM_LOCK_REFUSED: {
2137
- summary: "a directory lock is held by a removal, which is never waited out",
2138
- separator: "--- COORDINATION CODES (any command that shares a resource) ---",
2139
- body: () => `STIM_LOCK_REFUSED
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
2140
2309
  A directory lock that serialises two commands over the same thing -- this
2141
2310
  workspace's managed tunnel, its managed remote worktree, the machine's EAS
2142
2311
  project ledger -- is held by a REMOVAL, and a removal is never waited out:
@@ -2145,10 +2314,10 @@ captured" (in metro.ndjson, bare RN)
2145
2314
  \`workspace removal\` -- both are \`stim worktree remove\`). Let it finish,
2146
2315
  then run the command again. \`start --remote\` reports this same case as
2147
2316
  STIM_WORKTREE_REMOVAL_IN_PROGRESS instead.`
2148
- },
2149
- STIM_LOCK_TIMEOUT: {
2150
- summary: "a lock held past the wait; workspace-process and short directory locks",
2151
- body: () => `STIM_LOCK_TIMEOUT
2317
+ },
2318
+ STIM_LOCK_TIMEOUT: {
2319
+ summary: "a lock held past the wait; workspace-process and short directory locks",
2320
+ body: () => `STIM_LOCK_TIMEOUT
2152
2321
  The same locks, held by an ordinary command that is still running, for
2153
2322
  longer than the wait -- 60s by default, 4 minutes for the remote-session and
2154
2323
  EAS project locks, and ~90 minutes for the \`worktree warm\` lock, which one
@@ -2180,11 +2349,11 @@ captured" (in metro.ndjson, bare RN)
2180
2349
  These paths still time out; verify that no holder is running before removing
2181
2350
  only the named directory. Standalone Metro and Expo cache packages use the
2182
2351
  same core protocol and do not require the Stim CLI.`
2183
- },
2184
- teardown: {
2185
- summary: "an unmanaged port, an unverified supervisor, and a failed device teardown",
2186
- separator: "--- TEARDOWN AND WORKSPACE REFUSALS ---",
2187
- body: () => `"metro refusing to kill port <n>: ... runs from <dir>, outside
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
2188
2357
  <project>" (stop)
2189
2358
  Stim only signals processes it launched and whose saved identity still
2190
2359
  matches. Stop an externally started server with the tool that started it.
@@ -2204,14 +2373,22 @@ captured" (in metro.ndjson, bare RN)
2204
2373
  reserved. Re-run \`stop\`, or signal it yourself: kill -9 -<n> (note the
2205
2374
  minus -- it is a process group).
2206
2375
 
2376
+ "metro refusing to signal it: the identity of dev server pid <n> left by
2377
+ the supervisor could not be verified" (stop)
2378
+ "metro dev server pid <n> left by the supervisor did not exit" (stop)
2379
+ The supervisor is gone but the dev server it started may still run. Stim
2380
+ signals it only while its saved identity matches, and on macOS and Linux
2381
+ never escalates to SIGKILL. The record and the port reservation are KEPT for a retry. Check
2382
+ \`ps -p <n>\`, stop it yourself if it is still running, then re-run \`stop\`.
2383
+
2207
2384
  "teardown failed: <reason>"
2208
2385
  Stim could not release the owned device and keeps its record for a retry.
2209
2386
  \`worktree remove\` exits 1 without removing the worktree while the device is
2210
2387
  still tracked. Fix the reported cause and re-run.`
2211
- },
2212
- remove: {
2213
- summary: "worktree remove refused a dirty tree: what it restores itself and what --force discards",
2214
- body: () => `"Refusing to remove <path>: uncommitted changes / untracked files / commits
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
2215
2392
  not on any remote" (worktree remove)
2216
2393
  A native build rewrites tracked files, and Stim now RESTORES the one class
2217
2394
  it can prove is not work: when the only dirt left is \`pod install\` churn
@@ -2231,10 +2408,10 @@ not on any remote" (worktree remove)
2231
2408
  refusal actually named.
2232
2409
  Use --force only when you genuinely intend to discard work; it deletes
2233
2410
  uncommitted and untracked files permanently.`
2234
- },
2235
- STIM_MAIN_DIRTY: {
2236
- summary: "warm --refresh will not move a source checkout with local work or an operation in progress",
2237
- body: () => `STIM_MAIN_DIRTY
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
2238
2415
  \`worktree warm --refresh\` writes to the SOURCE CHECKOUT, and it refuses one
2239
2416
  it cannot move: tracked files with uncommitted changes (the refusal names
2240
2417
  them), or a rebase or merge in progress. Untracked files are not a reason to
@@ -2244,28 +2421,28 @@ not on any remote" (worktree remove)
2244
2421
  or \`git -C <source-checkout> rebase --abort\`. Nothing was installed or
2245
2422
  copied. Plain \`stim worktree warm\` does not care: it copies from a dirty
2246
2423
  source checkout exactly as it always has.`
2247
- },
2248
- STIM_MAIN_DETACHED: {
2249
- summary: "warm --refresh needs a branch to fast-forward, not a detached HEAD",
2250
- body: () => `STIM_MAIN_DETACHED
2424
+ },
2425
+ STIM_MAIN_DETACHED: {
2426
+ summary: "warm --refresh needs a branch to fast-forward, not a detached HEAD",
2427
+ body: () => `STIM_MAIN_DETACHED
2251
2428
  The source checkout's HEAD is detached, so there is no branch to fast-forward
2252
2429
  and no upstream to fast-forward it to. Run
2253
2430
  \`git -C <source-checkout> checkout <branch>\` and warm again. \`--refresh\`
2254
2431
  never picks a branch for you; a checkout whose job is to seed worktrees
2255
2432
  should sit on a branch someone chose.`
2256
- },
2257
- STIM_MAIN_DIVERGED: {
2258
- summary: "the source checkout is both ahead of and behind its upstream; warm --refresh will not merge",
2259
- body: () => `STIM_MAIN_DIVERGED
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
2260
2437
  The source checkout's branch has commits its upstream does not, AND its
2261
2438
  upstream has commits it does not. A fast-forward is impossible, and
2262
2439
  \`--refresh\` will not merge or reset someone else's checkout to make one:
2263
2440
  that decision is yours. Rebase or merge it yourself, then warm again. The
2264
2441
  message reports both counts. Nothing was installed or copied.`
2265
- },
2266
- STIM_DEPS_INCOMPLETE: {
2267
- summary: "the last install recorded for the lockfile on disk now did not finish, so warm will not copy it",
2268
- body: () => `STIM_DEPS_INCOMPLETE
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
2269
2446
  \`worktree warm\` refuses to copy dependencies the source checkout never
2270
2447
  finished installing. \`--refresh\` records a COMPLETED install of the lockfile
2271
2448
  it read under ~/.stim/warm-installs; an install that failed, or whose process
@@ -2283,20 +2460,20 @@ not on any remote" (worktree remove)
2283
2460
  repository before its first \`--refresh\` -- copies as it always has.
2284
2461
  In a monorepo the record is keyed on the directory that owns the lockfile,
2285
2462
  which is usually the repository root, so every app of it reads the same one.`
2286
- },
2287
- warm: {
2288
- summary: "worktree warm claim storage refusals and recovery",
2289
- body: () => `Both plain warm and --refresh require an ownership claim.
2463
+ },
2464
+ warm: {
2465
+ summary: "worktree warm claim storage refusals and recovery",
2466
+ body: () => `Both plain warm and --refresh require an ownership claim.
2290
2467
  Permission-denied or read-only claim storage reports STIM_CLAIM_UNAVAILABLE
2291
2468
  and names the path. Restore access to the existing claim store and retry.
2292
2469
  A non-directory claim ancestor reports STIM_CLAIM_REFUSED and names the
2293
2470
  blocking file. Inspect it and use the printed move-aside command to preserve
2294
2471
  its contents before retrying. See guide errors STIM_CLAIM_UNAVAILABLE and
2295
2472
  guide errors STIM_CLAIM_REFUSED for the recovery details.`
2296
- },
2297
- carry: {
2298
- summary: "worktree warm copy results, lockfile mismatches, and remedies",
2299
- body: () => `"carry incomplete: ... ignored entries copied, ... kept, ... failed"
2473
+ },
2474
+ carry: {
2475
+ summary: "worktree warm copy results, lockfile mismatches, and remedies",
2476
+ body: () => `"carry incomplete: ... ignored entries copied, ... kept, ... failed"
2300
2477
  (worktree warm)
2301
2478
  At least one entry could not be copied. The command exits 1 and names each
2302
2479
  failure. Existing entries stay untouched; any files already published remain.
@@ -2325,11 +2502,11 @@ not on any remote" (worktree remove)
2325
2502
  the app is ready, unless \`--refresh\` installed them in the SOURCE CHECKOUT
2326
2503
  first; even then the copy can still carry a lockfile this branch does not
2327
2504
  have, which is exactly what these carry warnings report.`
2328
- },
2329
- environment: {
2330
- summary: "npx registry E401/E404, the Node floor, no free Metro port, the reservation race",
2331
- separator: "--- ENVIRONMENT ---",
2332
- body: () => `"npm error code E401 / E404" while \`npx\` resolves the stim package
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
2333
2510
  The repo probably pins a private registry in \`.npmrc\`, so \`npx\` looked for
2334
2511
  the package there instead of on npm. Use the public registry for this command:
2335
2512
 
@@ -2350,10 +2527,10 @@ not on any remote" (worktree remove)
2350
2527
  "Could not reserve a Metro port after 5 attempts"
2351
2528
  Several commands raced for the same ports and each one lost. Nothing is
2352
2529
  wrong; retry.`
2353
- },
2354
- sandbox: {
2355
- summary: "running under a sandboxing harness: EPERM under STIM_HOME, CoreSimulatorService, adb",
2356
- body: () => `RUNNING UNDER A SANDBOX
2530
+ },
2531
+ sandbox: {
2532
+ summary: "running under a sandboxing harness: EPERM under STIM_HOME, CoreSimulatorService, adb",
2533
+ body: () => `RUNNING UNDER A SANDBOX
2357
2534
 
2358
2535
  An agent harness that sandboxes shell commands typically permits writes
2359
2536
  inside the project and blocks the rest. Three things Stim needs sit outside
@@ -2400,21 +2577,37 @@ not on any remote" (worktree remove)
2400
2577
 
2401
2578
  A git credential helper is often blocked too. It prints \`failed to store\`
2402
2579
  on a fetch that otherwise succeeded, and is safe to ignore.`
2403
- },
2404
- STIM_CONFIG_CORRUPT: {
2405
- summary: "~/.stim/config.json is not valid JSON and Stim never resets it",
2406
- body: () => `STIM_CONFIG_CORRUPT ("Stim config at <path> is not valid JSON")
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",
2584
+ "... is not a JSON object",
2585
+ "... has a projects that is not an object",
2586
+ "... has a projects entry "<key>" that is not an object")
2407
2587
  Any command can raise it: every command reads ~/.stim/config.json first.
2408
- The file holding every owned-device record will not parse, and Stim never
2588
+ The file holding every owned-device record will not parse into an object,
2589
+ or its projects or repos registry or one of their entries is not an object
2590
+ (a bare [], null or number counts as corrupt, not empty), and Stim never
2409
2591
  resets it for you -- a silent reset would orphan every simulator it names.
2410
2592
  Repair the file, or move it aside (\`mv <path> <path>.broken\`) and accept
2411
2593
  that the devices it recorded become orphans you delete by hand.`
2412
- }
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")
2598
+ Any command refuses before it reads or writes state. A relative value would
2599
+ resolve against each process's working directory, so the CLI, Metro and the
2600
+ Expo build-cache provider would each use a different store. Set the named
2601
+ variable to an absolute path, or unset it to use the default. Metro and the
2602
+ cache provider, which cannot refuse, ignore a relative value with a warning.`
2413
2603
  }
2414
- },
2415
- lifecycle: {
2416
- summary: "The full worktree -> start -> ios/android -> logs -> teardown flow, with sections for builds, devices and flags",
2417
- preamble: () => `ENVIRONMENT LIFECYCLE
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
2418
2611
 
2419
2612
  Two workflows share steps 2 through 6.
2420
2613
 
@@ -2588,8 +2781,10 @@ Permanent local deletion lives in exactly TWO commands: \`worktree remove\`
2588
2781
  (the workspace you name) and \`gc --delete\` (the machine). For a local device,
2589
2782
  \`stop\` shuts it down and never deletes it. For a recorded EAS session,
2590
2783
  \`stop\` irreversibly ends the session. Externally started servers are left
2591
- alone, even when they use the reserved port. There is no \`--delete\` flag on
2592
- \`stop\`.
2784
+ alone, even when they use the reserved port, and the reservation is kept while
2785
+ one answers from the project. A dev server whose supervisor died is stopped
2786
+ only when its recorded process identity still matches. There is no
2787
+ \`--delete\` flag on \`stop\`.
2593
2788
 
2594
2789
  CAPACITY
2595
2790
  A booted iOS sim is roughly 1-2 GB of RAM, an Android emulator 2-3 GB. On a
@@ -2598,14 +2793,25 @@ CAPACITY
2598
2793
  machine, not just this one.
2599
2794
 
2600
2795
  TWO REPORTS, TWO QUESTIONS
2601
- "What is running" is \`stim status\`: live state, right now. "How much the
2796
+ "What is running" is \`stim status\`: live state, right now, including
2797
+ each running ios or android build's phase and time left (see
2798
+ \`guide facts status\`). "How much the
2602
2799
  cache saved" is \`stim stats\`: aggregate counters for this project and for
2603
2800
  the machine, with a hit rate and an estimate of the time saved (see
2604
- \`guide facts stats\`).`,
2605
- sections: {
2606
- eas: {
2607
- summary: "download a matching EAS development build; explicit profile, costs, cache and miss remedies",
2608
- body: () => `EAS DEVELOPMENT BUILDS
2801
+ \`guide facts stats\`).
2802
+
2803
+ WAITING FOR A CHANGE
2804
+ To wait for a device, a build or a dev server instead of polling, run
2805
+ \`stim status --watch --json\`. It keeps running and prints one complete
2806
+ status payload per line: one at once, then one each time the payload
2807
+ changes, never two identical ones in a row. It reacts to Stim state files,
2808
+ the EAS session ledger, adb device arrivals and departures, and simulator
2809
+ state, and recomputes every 30 seconds as a fallback. Ctrl+C, SIGTERM or
2810
+ 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
2609
2815
 
2610
2816
  stim ios --eas-profile ios-simulator
2611
2817
  stim android --eas-profile development
@@ -2678,10 +2884,10 @@ flag exists, and Stim never starts a cloud build or falls back to local
2678
2884
  compilation in this mode. Authentication, network, invalid output or download
2679
2885
  failures produce STIM_EAS_UNAVAILABLE, with the failed EAS command to inspect.
2680
2886
  If another run holds the artifact claim, wait for it to finish and retry.`
2681
- },
2682
- readiness: {
2683
- summary: "implement optional pending/ready app logs, deadlines, errors, and platform isolation",
2684
- body: () => `OPTIONAL APP READINESS
2887
+ },
2888
+ readiness: {
2889
+ summary: "implement optional pending/ready app logs, deadlines, errors, and platform isolation",
2890
+ body: () => `OPTIONAL APP READINESS
2685
2891
 
2686
2892
  No package or SDK is needed. This applies to debug ios/android launches with
2687
2893
  Metro verification enabled, not release builds or --no-metro-check.
@@ -2741,10 +2947,10 @@ including when device logs are unavailable, the default check applies.
2741
2947
  The app declares readiness; Stim does not inspect a rendered frame. This does
2742
2948
  not change launched JSON semantics or the need to inspect the expected UI and
2743
2949
  stim logs --errors. A reload request does not run this launch check.`
2744
- },
2745
- verification: {
2746
- summary: "reproduce the affected behavior, verify the change on the reported device, and retain proof",
2747
- body: () => `VERIFY THE CHANGE
2950
+ },
2951
+ verification: {
2952
+ summary: "reproduce the affected behavior, verify the change on the reported device, and retain proof",
2953
+ body: () => `VERIFY THE CHANGE
2748
2954
 
2749
2955
  For a UI change or bug fix, decide what observable result would prove the task
2750
2956
  is complete. A successful build, a live process, or an empty log query does
@@ -2774,10 +2980,10 @@ not prove that result.
2774
2980
 
2775
2981
  For a change without a UI effect, use the relevant runtime output or test
2776
2982
  result as proof instead of requiring an unrelated screenshot.`
2777
- },
2778
- progress: {
2779
- summary: "phase lines, the label set, heartbeats and their ~ estimate, what warm, start, stop and remove print",
2780
- body: () => `PROGRESS ON A LONG RUN
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
2781
2987
  Native build progress goes to stderr. In \`--json\` mode, stdout carries only
2782
2988
  the result payload. Plain \`start\` also prints progress on stdout. Every
2783
2989
  progress line has the same shape -- two spaces, a label padded to eleven
@@ -2897,10 +3103,10 @@ result as proof instead of requiring an unrelated screenshot.`
2897
3103
  until it returns; the next heartbeat then lands on the grid, which is why an
2898
3104
  elapsed value can jump. Read the phase lines, not the wall clock, before
2899
3105
  killing a run.`
2900
- },
2901
- pool: {
2902
- summary: "parked and adopted simulators: what park and adoption clear or keep, the model and runtime match",
2903
- body: () => ` THE SIMULATOR POOL
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
2904
3110
  \`worktree remove\` PARKS this workspace's owned simulator instead of
2905
3111
  deleting it, and the next workspace that wants the same model and runtime
2906
3112
  ADOPTS it. A simulator that has booted before boots in about 9s; a freshly
@@ -3021,10 +3227,10 @@ result as proof instead of requiring an unrelated screenshot.`
3021
3227
  Set the Android bound to 0 when a project needs a fresh device. Status lists
3022
3228
  parked Android emulators; GC reports their system image, age and disk size.
3023
3229
  `
3024
- },
3025
- builds: {
3026
- summary: "optional cache warm-up, build optimizations, fingerprints, .fingerprintignore, install unchanged, runtime state",
3027
- body: () => `OPTIONAL CACHE WARM-UP FOR REPEATED NATIVE WORK
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
3028
3234
  When several native worktrees are coming, build the source checkout once to
3029
3235
  seed the shared caches before warming the linked worktrees. Skip this extra
3030
3236
  build for one-off or JavaScript-only work. For local simulator or emulator
@@ -3091,7 +3297,9 @@ Runtime state is stored outside the project tree under
3091
3297
  $STIM_HOME/workspaces/<project>--<digest>/ (default ~/.stim/workspaces/).
3092
3298
  The aggregate run counters \`stats\` prints live beside it in
3093
3299
  $STIM_HOME/stats.json, one bucket per project and platform plus a machine-wide
3094
- one; nothing per run is kept there.
3300
+ one, and the last 10 successful runs per project, platform and cache outcome
3301
+ that size \`status\`'s build estimate. The running build itself is the
3302
+ activeBuild key of the workspace's state.json, cleared when the run exits.
3095
3303
  No .gitignore entry is created or required.
3096
3304
  Native preparation can change project files: expo prebuild generates native
3097
3305
  sources, and pod install can update Podfile.lock. Review those changes before
@@ -3320,10 +3528,10 @@ WHAT MAKES THE CACHE ACTUALLY HIT: .FINGERPRINTIGNORE
3320
3528
  mismatch naming the differing sources. Untracked, non-gitignored files under
3321
3529
  ios/ or android/ count too -- they are hashed like any other source, so a
3322
3530
  stray file there moves the key on your machine and nowhere else.`
3323
- },
3324
- concurrency: {
3325
- summary: "waiting on another workspace's build, --no-build-cache, concurrency.maxBuilds and maxDevices",
3326
- body: () => `ONE COMPILE PER FINGERPRINT, ACROSS EVERY WORKSPACE
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
3327
3535
  The cache makes the SECOND workspace on a commit free -- but only once the
3328
3536
  first has finished. Three agents starting within the same minute all miss it,
3329
3537
  and without this all three compile the same app at once, fighting for the
@@ -3347,10 +3555,15 @@ WHAT MAKES THE CACHE ACTUALLY HIT: .FINGERPRINTIGNORE
3347
3555
  Nothing can deadlock on it. The lock records the holder's process IDENTITY,
3348
3556
  so a builder that crashes, is killed, or whose build simply fails frees it:
3349
3557
  the waiters see a released lock with no artifact, and one of them takes over
3350
- and builds. A recycled pid reads as a gone builder, and a builder busy in a
3351
- long synchronous tool call reads as live -- age is never a reason to take a
3352
- lock. The one state that cannot be decided (a truncated claim, an identity
3353
- token that does not decode) is STIM_CLAIM_REFUSED: Stim names the claim and
3558
+ and builds. The xcodebuild, Gradle, pod install or expo prebuild it spawned
3559
+ is recorded on the lock, its build slot and the workspace's native run, so a
3560
+ builder killed while that tool keeps running (SIGTERM to the stim pid alone)
3561
+ frees them only when the tool exits. A recycled builder pid reads as a gone
3562
+ builder, and a builder busy in a long synchronous tool call reads as live --
3563
+ age is never a reason to take a lock. A state that cannot be decided (a
3564
+ truncated claim, an identity token that does not decode, a builder killed
3565
+ between spawning its build tool and recording it, a recorded build tool
3566
+ whose pid was since reused) is STIM_CLAIM_REFUSED: Stim names the claim and
3354
3567
  the command that removes it rather than guessing. A process whose own identity
3355
3568
  cannot be captured takes no lock and no slot, and refuses with
3356
3569
  STIM_CLAIM_UNAVAILABLE rather than building unprotected. The
@@ -3394,26 +3607,28 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
3394
3607
 
3395
3608
  \`stim doctor\` prints one note echoing the caps and the current live count,
3396
3609
  but ONLY when a cap is set. \`stim gc\` reports stale build slots the way it
3397
- reports stale build locks, and \`gc --delete\` clears them. There is no
3398
- \`stim config\` command: set these by editing ~/.stim/config.json or via
3399
- the two env vars (see \`guide settings\`).`
3400
- },
3401
- options: {
3402
- summary: "every flag per command, Android variants and flavors, the per-run simulator model, runtime and system image",
3403
- body: () => `THE OPTION SURFACE, IN FULL
3610
+ reports stale build locks, and \`gc --delete\` clears them. Set the caps
3611
+ with \`stim settings set concurrency.maxBuilds 2\`, by editing
3612
+ ~/.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
3404
3617
  start --json --wait <seconds> --remote --reset-cache
3405
- ios --slot <name> --json --no-metro-check --no-build-cache --scheme <name> --configuration <name> --device-type <name> --runtime <version> --simulator-app <xcode|siniulator> --device [udid] --wait <seconds> --no-wait --remote <proxy|eas>
3618
+ 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>
3406
3619
  android --slot <name> --json --no-metro-check --no-build-cache --variant <name> --system-image <id> --device [serial] --wait <seconds> --no-wait --remote <proxy|eas>
3407
3620
  reload [ios|android] --json
3408
3621
  device lock <ios|android> [id] --slot <name> --for <duration> --wait <seconds> --json;
3409
3622
  unlock [ios|android] --slot <name> --json
3410
3623
  logs --slot <name> --source --level --since --grep --tail --follow --errors --json
3411
3624
  stop --slot <name> --json
3412
- status --json (already machine-wide)
3625
+ status --json --watch (already machine-wide)
3413
3626
  stats --json (this project and machine-wide)
3627
+ settings --json; get <key> --scope <layer> --json;
3628
+ set <key> <value> --scope <layer> --json; unset <key> --scope <layer> --json
3414
3629
  doctor --json --fix --platform <ios|android>
3415
3630
  (--platform keeps shared checks and filters native findings)
3416
- gc --delete --older-than <days> --cache <name|all>
3631
+ gc --delete --older-than <days> --cache <name|all|workspaces> --worktrees --json
3417
3632
  worktree warm --refresh; remove [path] --force
3418
3633
 
3419
3634
  DEVICE SLOTS
@@ -3424,7 +3639,7 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
3424
3639
  the workspace's existing assignment.
3425
3640
 
3426
3641
  stim ios --slot phone
3427
- stim ios --slot tablet --device-type "iPad Pro 13-inch (M4)"
3642
+ stim ios --slot tablet --device-type "iPad Pro 13-inch (M5)"
3428
3643
  stim ios --slot hardware --device <udid>
3429
3644
  stim android --slot second-phone
3430
3645
  stim logs --slot tablet --source device
@@ -3459,6 +3674,12 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
3459
3674
  worktrees of the same Git repository; the source checkout must be available.
3460
3675
  Running it in the source checkout refuses.
3461
3676
 
3677
+ \`stim doctor\` reports the source checkout's fitness as a seed -- how far
3678
+ behind its upstream it is, uncommitted tracked changes, an interrupted rebase
3679
+ or merge, a detached HEAD, a diverged branch, a branch that is not the
3680
+ default one -- only once the repository has at least one linked worktree. A
3681
+ single-checkout session is never told that its own branch is a problem.
3682
+
3462
3683
  \`--refresh\` WRITES TO THE SOURCE CHECKOUT before the copy, which is why it
3463
3684
  is opt-in: it checks the upstream, fetches changes when needed, and fast-forwards
3464
3685
  whatever branch is checked out there to its \`@{upstream}\`, and installs
@@ -3588,8 +3809,8 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
3588
3809
 
3589
3810
  \`ios --device-type <name>\` and \`ios --runtime <version>\` choose the
3590
3811
  MODEL and the iOS version of the simulator this workspace owns --
3591
- \`--device-type "iPad Pro 13-inch (M4)" --runtime 18.5\` is how a ticket that
3592
- says "happens on iPad on iOS 18.5" gets reproduced without writing a
3812
+ \`--device-type "iPad Pro 13-inch (M5)" --runtime 26.5\` is how a ticket that
3813
+ says "happens on iPad on iOS 26.5" gets reproduced without writing a
3593
3814
  \`.stim.json\`. \`android --system-image <id>\` is the Android half, taking
3594
3815
  the sdkmanager package id
3595
3816
  ("system-images;android-36;google_apis;arm64-v8a"). Each overrides its
@@ -3620,10 +3841,10 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
3620
3841
  version it was made with. The --json payload reports what was actually
3621
3842
  used: \`deviceType\` and \`runtime\` on iOS, \`systemImage\` on Android,
3622
3843
  read from the device itself, so a settings-driven run reports them too.`
3623
- },
3624
- devices: {
3625
- summary: "ios --device and android --device on a phone: what the run skips, signing, the LAN wiring, the collector",
3626
- body: () => ` \`android --device [serial]\` installs and launches on a physical device
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
3627
3848
  connected to this machine instead of this workspace's owned emulator. With
3628
3849
  no serial it takes the first device it can lease (\`guide lifecycle lease\`).
3629
3850
  It cannot be combined with --remote.
@@ -3648,6 +3869,24 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
3648
3869
  \`android --device\`, it leases the phone for the run
3649
3870
  (\`guide lifecycle lease\`).
3650
3871
 
3872
+ AN IPHONE PAIRED OVER WI-FI is a device too: devicectl reports its
3873
+ transportType as localNetwork. Only a physical iPhone or iPad counts
3874
+ (devicectl platform iOS, reality physical); an Apple TV, a Vision Pro, a
3875
+ simulator, or a paired phone devicectl reports unavailable is never picked.
3876
+ A UDID that names a Wi-Fi phone is accepted. With no UDID, a healthy
3877
+ cabled iPhone always wins, and a busy one is waited for; a Wi-Fi one is
3878
+ taken only when no cabled one is paired with Developer Mode on, and a busy
3879
+ refusal names any free Wi-Fi phone so \`--device <udid>\` can take it.
3880
+ The transport is read again just before the install, so a cable pulled
3881
+ during the build still gets Wi-Fi treatment, and the run prints one
3882
+ \`device\` line saying the install and launch go over Wi-Fi. Over Wi-Fi
3883
+ each devicectl install step may take 15 minutes instead of 5 and the launch
3884
+ 120 seconds instead of 45; the lease is raised before each step, including
3885
+ the uninstall and reinstall of a signer conflict. A Wi-Fi install or launch
3886
+ that times out or loses the phone fails with STIM_DEVICE_WIRELESS_FAILED,
3887
+ and the remedy is the cable. Metro is unaffected: a Debug app reaches it
3888
+ over the LAN either way.
3889
+
3651
3890
  \`stop\` releases this workspace's leases and stops its log collectors.
3652
3891
  On a physical iPhone that also closes the app, because its collector owns
3653
3892
  the devicectl launch session. \`gc --delete\` removes expired lease files;
@@ -3729,10 +3968,10 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
3729
3968
  by hashing the installed container; there is no cheap equivalent through
3730
3969
  devicectl, so a device run always installs. It is an upgrade install: the
3731
3970
  app's data, and the Local Network permission the phone granted it, survive.`
3732
- },
3733
- lease: {
3734
- summary: "run-scoped leases, --wait and --no-wait, device lock and unlock, which phone an id-less --device picks",
3735
- body: () => `THE DEVICE LEASE ON A \`--device\` RUN
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
3736
3975
  A physical device is shared, so a \`--device\` run takes a lease on it. The
3737
3976
  lease step sits AFTER the build (a build touches no device) and before the
3738
3977
  install, and the run releases what it took when the command exits: on
@@ -3800,13 +4039,15 @@ HOLDING A DEVICE ACROSS RUNS
3800
4039
 
3801
4040
  THE POOL: WHICH DEVICE AN ID-LESS \`--device\` PICKS
3802
4041
  Candidates are the connected devices the resolver already accepts: on iOS,
3803
- wired, paired, with Developer Mode on; on Android, every serial adb reports
3804
- in the \`device\` state that is not an emulator, TCP serials included. Then,
3805
- in order:
4042
+ paired, with Developer Mode on, over a cable or Wi-Fi; on Android, every
4043
+ serial adb reports in the \`device\` state that is not an emulator, TCP
4044
+ serials included. Then, in order:
3806
4045
 
3807
4046
  1. the device this workspace already leases, when it is among them;
3808
4047
  2. otherwise the first one not leased -- or leased and EXPIRED -- in
3809
- case-folded id order.
4048
+ case-folded id order. On iOS a Wi-Fi iPhone is considered only when
4049
+ no cabled candidate is connected: a busy cabled phone is waited for,
4050
+ not skipped for a Wi-Fi one.
3810
4051
 
3811
4052
  Ids are sorted on, never names: adb has no name without one \`getprop\` per
3812
4053
  serial, and models repeat.
@@ -3826,10 +4067,10 @@ THE POOL: WHICH DEVICE AN ID-LESS \`--device\` PICKS
3826
4067
  The chosen device is on the phase line and in \`--json\` (\`udid\` or
3827
4068
  \`serial\`, plus \`deviceName\`), so an agent can hand the same id to its
3828
4069
  device tool.`
3829
- },
3830
- release: {
3831
- summary: "Release configurations and ...Release variants: Metro skipped, process-proven launch, the JS swap",
3832
- body: () => ` A VARIANT WHOSE NAME ENDS IN "Release" IS A RELEASE BUILD (\`release\`,
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\`,
3833
4074
  \`productionRelease\`), and that is the whole opt-in -- there is no second
3834
4075
  flag. It is the Android half of \`ios --configuration Release\` and behaves
3835
4076
  the same way: AGP's bundle task embeds the JS, so Metro is skipped ENTIRELY
@@ -3902,10 +4143,10 @@ THE POOL: WHICH DEVICE AN ID-LESS \`--device\` PICKS
3902
4143
  builds the \`iphoneos\` slice for a cabled iPhone and keys its cache
3903
4144
  \`-device\` instead of \`-sim\`, but does not install it yet. Archives,
3904
4145
  \`.ipa\` export, store signing and distribution stay out of scope.`
3905
- },
3906
- simslim: {
3907
- summary: "recommended SimSlim profiles and recovery from host memory pressure",
3908
- body: () => `SIMSLIM FOR PARALLEL IOS WORK
4146
+ },
4147
+ simslim: {
4148
+ summary: "recommended SimSlim profiles and recovery from host memory pressure",
4149
+ body: () => `SIMSLIM FOR PARALLEL IOS WORK
3909
4150
  SimSlim is recommended as an optional way to reduce simulator background
3910
4151
  services and memory use, especially with several workspaces. Review which
3911
4152
  services your app and tests need; a slim profile can disable those features.
@@ -3952,8 +4193,9 @@ HOST MEMORY PRESSURE AND STALLED SIMULATORS
3952
4193
  a final boot-state query can take 30 seconds. After boot, Stim opens the owned
3953
4194
  simulator in the selected Xcode's Device Hub on Xcode 27, or Simulator on
3954
4195
  older Xcode. Top-level iosSimulatorApp in the machine config can select
3955
- Siniulator instead; see guide settings. Override it for one local run with
3956
- \`stim ios --simulator-app siniulator\` or \`--simulator-app xcode\`. This also
4196
+ Siniulator or Stim Desktop instead; see guide settings. Override it for one
4197
+ local run with \`stim ios --simulator-app siniulator\`,
4198
+ \`--simulator-app stim-desktop\`, or \`--simulator-app xcode\`. This also
3957
4199
  opens an already running owned simulator without rebooting it or saving the
3958
4200
  preference. The flag refuses physical and remote targets, including ios.remote.
3959
4201
  Opening the window is best-effort
@@ -3974,12 +4216,14 @@ HOST MEMORY PRESSURE AND STALLED SIMULATORS
3974
4216
  agents' simulators or heavy apps. Rebooting a simulator under the same pressure
3975
4217
  can repeat the stall. Consider fewer concurrent builds/devices (guide lifecycle
3976
4218
  concurrency) and a reviewed SimSlim profile for future runs.`
3977
- }
3978
4219
  }
3979
- },
3980
- cleanup: {
3981
- summary: "Where simulators come from, and how they get reclaimed",
3982
- preamble: () => `DEVICE SLOTS
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
3983
4227
 
3984
4228
  Cleanup enumerates every slot. stop --slot <name> keeps the shared server and
3985
4229
  other slots; plain stop and worktree remove handle the whole workspace.
@@ -3997,11 +4241,17 @@ WHAT RECLAIMS AN OWNED DEVICE
3997
4241
  stim gc --delete sweeps stim-* devices no project references, and
3998
4242
  clears verified parked simulators and emulators
3999
4243
  stim gc --delete --older-than <days>
4000
- also reaps the device of a project nothing has
4001
- touched in that long, even though the project is
4002
- still on disk
4003
-
4004
- Those are the only two commands that delete. \`stim stop\` shuts a device
4244
+ also reaps the device of a workspace no Stim
4245
+ command has used in that long, even though the
4246
+ project is still on disk
4247
+ stim gc --delete --worktrees
4248
+ runs \`stim worktree remove\` on every clean, idle,
4249
+ Stim-managed linked worktree (\`guide cleanup gc\`)
4250
+
4251
+ \`worktree remove\` and \`gc --delete\` are the only two commands that delete;
4252
+ \`gc --delete --worktrees\` deletes only through \`worktree remove\`. \`gc
4253
+ --delete\` also clears workspace build outputs (\`guide cleanup disk\`) and
4254
+ orphaned workspace directories, never a checkout. \`stim stop\` shuts a device
4005
4255
  DOWN and leaves it assigned, which is what makes returning to a branch cost a
4006
4256
  boot rather than a create, a provision and a reinstall.
4007
4257
 
@@ -4021,17 +4271,65 @@ start the counters over. A file this version cannot read -- unparseable, or
4021
4271
  written by a newer Stim -- costs one dim line on stderr and is otherwise left
4022
4272
  alone; only the next \`ios\` or \`android\` run moves an unparseable one aside
4023
4273
  to stats.json.corrupt-<unix ms> and starts a new one.`,
4024
- sections: {
4025
- gc: {
4026
- summary: "what gc and worktree remove delete, keep and refuse: orphans, stale records, locks, leases, EAS sessions",
4027
- body: () => `LINKED WORKTREES
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
4028
4278
  \`stim worktree remove\` works with any linked worktree, warmed or not.
4029
4279
  Git registration identifies the worktree; a Stim registry entry is not
4030
- required. The command reclaims any owned resources it finds, checks for
4031
- uncommitted and unpushed work, and removes the linked checkout. Git-created
4280
+ required. Before it reclaims anything, the command refuses a worktree git
4281
+ has locked (unlock it first; --force does not override) and, without
4282
+ --force, uncommitted or unpushed work and initialized submodules. It then
4283
+ reclaims any owned resources it finds and removes the linked checkout.
4284
+ When git still refuses the removal, the kept ownership record no longer
4285
+ names the devices that were parked or deleted. A workspace that is in use
4286
+ (see IN USE below) keeps its directory, devices and registry entry, and the
4287
+ command reports why. Git-created
4032
4288
  branches stay. A branch with an existing Stim ownership record is deleted
4033
4289
  only when it has no unique commits.
4034
4290
 
4291
+ IN USE
4292
+ gc and worktree remove never delete a workspace's directory, build outputs
4293
+ or checkout while it is in use: its dev server supervisor is running or
4294
+ cannot be verified, a \`stim ios\`, \`stim android\` or \`stim stop\` run
4295
+ holds its native-run.lock, a live or unresolvable build lock or slot names it,
4296
+ or its managed tunnel or managed remote lock is held. The deletion holds
4297
+ native-run.lock itself, so no native run can start partway through. A lock
4298
+ that names no workspace blocks nothing, because every build also holds its
4299
+ own workspace's native-run.lock; gc lists it with the command that removes
4300
+ it. \`worktree remove\` re-checks uncommitted and unpushed work under its
4301
+ removal locks, just before it reclaims anything. A project root whose
4302
+ existence cannot be read (a permission error) is never treated as deleted.
4303
+
4304
+ 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
4316
+ With --delete it runs the \`stim worktree remove\` pipeline, never --force,
4317
+ 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.
4322
+
4323
+ ORPHANED WORKSPACE DIRECTORIES
4324
+ A worktree deleted with \`git worktree remove\`, \`rm -rf\` or a /tmp wipe
4325
+ leaves its $STIM_HOME/workspaces/<name> directory behind with no registry
4326
+ entry. \`gc\` reads each directory's workspace.json and reports it under
4327
+ "Orphaned workspace directories" when the recorded project root is gone,
4328
+ its volume is mounted, no registry key equals it or sits under it, and the
4329
+ workspace is not in use. \`gc --delete\` re-checks each one, then removes
4330
+ it. A directory with a missing or unparseable workspace.json, or a root on
4331
+ an unmounted volume, is reported under Skipped and never deleted.
4332
+
4035
4333
  NAMED SERVER PORTS
4036
4334
  worktree remove stops TCP listeners on each named allocation and releases
4037
4335
  the ports. gc reports named allocations for missing workspaces; gc --delete
@@ -4095,7 +4393,8 @@ A device leaks when a project is abandoned WITHOUT either delete path -- the
4095
4393
  sim survives with nothing pointing at it. \`stim gc\` (no flag, writes
4096
4394
  nothing, always safe) reports those; \`gc --delete\` reaps them, and in the same
4097
4395
  run drops the dead config ENTRIES those projects left behind and frees their
4098
- Metro ports.
4396
+ Metro ports. \`stim gc --json\` prints the same report as one payload
4397
+ (\`guide facts gc\`); show the user its sections before \`gc --delete\`.
4099
4398
 
4100
4399
  REMOTE EAS SESSIONS
4101
4400
  Plain \`stim gc\` is a dry run. \`gc --delete\` can stop active stim-* EAS
@@ -4131,10 +4430,10 @@ THE ONE CASE GC WILL NOT REAP
4131
4430
  it found, so you can judge. Delete them yourself:
4132
4431
  xcrun simctl delete <udid>
4133
4432
  avdmanager delete avd -n <name>`
4134
- },
4135
- collector: {
4136
- summary: "log collector reaping: an unproven collector pid, and why the app on a phone closed",
4137
- body: () => `WHAT ELSE STOP REAPS
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
4138
4437
  The device-log collectors (\`simctl log stream\` / \`adb logcat\`) that
4139
4438
  \`ios\` / \`android\` attach after launch. They are recorded in
4140
4439
  the global workspace state.json, and nothing outside this workspace can name them,
@@ -4188,12 +4487,14 @@ THE ONE CASE GC WILL NOT REAP
4188
4487
  is gone, and the unrelated process is never signalled. A missing, malformed,
4189
4488
  or unreadable identity leaves the record unverified and kept for a retry.
4190
4489
  Wall-clock timestamps and command names are not ownership proof.`
4191
- },
4192
- disk: {
4193
- summary: "disk usage, AVD and build-log sizes, the data partition, trimming the shared caches",
4194
- body: () => `DISK
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
4195
4494
  Logs, state, pidfiles and Xcode DerivedData are under the global workspace
4196
- directory, and \`worktree remove\` reclaims them. Gradle retains its normal
4495
+ directory, and \`worktree remove\` reclaims them. \`gc --delete\` clears the
4496
+ build outputs of workspaces nobody is using (WORKSPACE BUILD OUTPUTS
4497
+ below) and keeps the rest. Gradle retains its normal
4197
4498
  project build directories while sharing task outputs through its build cache.
4198
4499
 
4199
4500
  Android AVDs normally live under ~/.android/avd, and a booted owned AVD can
@@ -4235,6 +4536,28 @@ project needs another size. Android userdata grows but does not shrink, so the
4235
4536
  setting applies only to a newly created AVD; recreate the environment to adopt
4236
4537
  a changed value.
4237
4538
 
4539
+ WORKSPACE BUILD OUTPUTS
4540
+ Each workspace directory holds derived-data/, gradle-build/, android-cas/
4541
+ and cache-provider/. \`gc\` reports them as one detected cache, "Workspace
4542
+ build outputs", with a per-workspace size, last use and verdict. Plain
4543
+ \`gc --delete\` clears them for every workspace not in use (see \`guide
4544
+ cleanup gc\`), before anything else. \`--older-than <days>\` limits that to
4545
+ workspaces idle at least that long, and keeps one whose last use is
4546
+ unknown. \`--cache workspaces\` acts on them alone; \`--cache all\` includes
4547
+ them. Only those four directories go: workspace.json, state.json, logs/,
4548
+ locks and device records stay, so the workspace keeps its devices and ports.
4549
+ stim gc --delete --cache workspaces --older-than 7
4550
+ Last use is the newest of the lastUsedAt that start, ios, android, reload
4551
+ and worktree warm record in state.json, lastBuild.startedAt,
4552
+ supervisor.startedAt and the mtimes under logs/, so a dev server that keeps
4553
+ logging keeps its workspace in use.
4554
+ The same time decides \`--older-than\` device reaping, and a workspace with
4555
+ no evidence of use keeps its device.
4556
+ The next build of an unchanged app installs from the shared build cache.
4557
+ After a native change the Xcode compilation cache speeds the rebuild, but on
4558
+ React Native 0.86 Swift does not use it (explicit modules are off), so that
4559
+ build recompiles Swift.
4560
+
4238
4561
  SHARED BUILD CACHES
4239
4562
  The caches that make a second workspace fast are alive by design and never
4240
4563
  included in a plain \`gc --delete\`. Every \`gc\` run reports them anyway,
@@ -4253,24 +4576,68 @@ SHARED BUILD CACHES
4253
4576
  The Gradle build cache under GRADLE_USER_HOME (default ~/.gradle) is
4254
4577
  report-only because every Gradle build shares it. Stim reports its size
4255
4578
  but never prunes or empties it, including with --older-than or --cache all.
4579
+
4256
4580
  Trim rather than empty. Emptying costs the next build in every project the
4257
4581
  time the cache was saving.`
4258
- }
4259
4582
  }
4260
- },
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,
4261
4597
  settings: {
4262
4598
  summary: "Settings Stim reads, and where they can live",
4263
4599
  body: () => `SETTINGS
4264
4600
 
4265
- There is no \`stim config\` command. Settings are JSON files, edited by
4266
- hand or committed; command-line selectors override their matching settings.
4601
+ Settings are JSON: machine layers in ~/.stim/config.json and a committed
4602
+ .stim.json. Command-line selectors override their matching settings.
4603
+
4604
+ stim settings --json every setting: effective value, origin
4605
+ (workspace, repo, committed, machine, env,
4606
+ default), and each layer's value
4607
+ stim settings get <key> [--scope <layer>] [--json]
4608
+ stim settings set <key> <value> --scope <layer> [--json]
4609
+ stim settings unset <key> --scope <layer> [--json]
4610
+
4611
+ <layer> is machine, workspace (this project's entry), repo (this repository's
4612
+ git common dir), or committed (.stim.json). Each key accepts only the layers
4613
+ Stim reads it from; --scope may be omitted when a key has one layer. Strings
4614
+ and choices are passed as-is; booleans, numbers, arrays, and objects as JSON
4615
+ (\`'["node_modules"]'\`). A value of the wrong shape, a layer the key is not
4616
+ read from, or an unknown key refuses with STIM_BAD_ARG naming the expected
4617
+ shape, before anything is written. Writes to the machine file take its lock
4618
+ and replace it atomically; a committed write keeps the file's other keys and
4619
+ indentation. Run \`stim settings\` from the app directory: workspace and
4620
+ committed resolve from the nearest package.json, repo from its Git
4621
+ repository. worktree.exclude and worktree.defaultBranch are read only from
4622
+ the repo layer and the repository root's .stim.json.
4623
+
4624
+ android.keystorePassword is sensitive: its value prints as ******** in every
4625
+ output, and a committed .stim.json takes only an \`env:\` or \`file:\`
4626
+ reference, never the password itself.
4627
+
4628
+ The JSON Schema for .stim.json ships in the stim package as
4629
+ dist/settings.schema.json. Point an editor at it with a top-level
4630
+ "$schema": "${SETTINGS_SCHEMA_URL}"; Stim ignores
4631
+ that key. $defs.machine describes the machine settings, and every setting
4632
+ carries its dotted key, layers, and environment override under "x-stim".
4267
4633
 
4268
4634
  Resolution order, first match wins:
4269
- 1. project layer ~/.stim/config.json, under this project's entry
4270
- 2. repo layer ~/.stim/config.json, under this repo's git common dir
4635
+ 1. workspace ~/.stim/config.json, under this project's entry
4636
+ 2. repo ~/.stim/config.json, under this repo's git common dir
4271
4637
  3. committed .stim.json beside the app's package.json
4272
- 4. machine defaults ~/.stim/config.json, top-level optimizations only
4638
+ 4. machine ~/.stim/config.json, top-level optimizations only
4273
4639
  5. Stim default
4640
+ An environment override, where a setting has one, wins over every layer.
4274
4641
 
4275
4642
  The committed file is plain JSON and travels with the app. Each monorepo app
4276
4643
  reads its own file, never an ancestor's runtime settings. A single-app repository
@@ -4291,8 +4658,7 @@ An app's .stim.json can contain:
4291
4658
  "runtime": "26.2",
4292
4659
  "simslimProfile": ".simslim/dev.json"
4293
4660
  },
4294
- "android": { "variant": "productionDebug" },
4295
- "caches": ["~/.myapp-metro-cache"]
4661
+ "android": { "variant": "productionDebug" }
4296
4662
  }
4297
4663
 
4298
4664
  KEYS STIM READS
@@ -4496,16 +4862,21 @@ ${ANDROID_AVD_CONFIG_HELP.map((line) => ` ${line}`).joi
4496
4862
  store Stim injects for you (bare in-process, or the
4497
4863
  Expo config override) stays local-only.
4498
4864
  cache.options free-form object handed to that module's factory. It
4499
- merges key by key across settings layers. Keep secrets
4500
- out of the committed file: read them from the
4501
- environment or the machine layers.
4502
- caches extra shared-cache paths for \`gc\` to report. A JSON
4503
- array; every path is treated as a flat store.
4865
+ merges key by key from the layer that names the
4866
+ provider, higher layers that name none, and lower
4867
+ layers that name the same provider resolved from the
4868
+ same directory. Options in a layer naming another
4869
+ provider, or in a lower layer naming none, are
4870
+ ignored. Keep secrets out of the committed file: read
4871
+ them from the environment or the machine layers.
4504
4872
 
4505
4873
  Each setting takes its documented type: string, array of strings, number,
4506
- boolean, or object. A value of the wrong type is
4507
- refused by name on every command that resolves settings, so a wrong shape never
4508
- falls back to a default silently. \`stim doctor\` reports it as a finding
4874
+ boolean, or object. ios.remote, android.remote, metro.tunnel,
4875
+ optimizations.android.compilerCache and optimizations.android.pch take only
4876
+ their listed choices. A value of the wrong
4877
+ type or outside those choices is refused by name on every command that resolves
4878
+ settings, \`stim ios\` included, so a wrong shape never falls back to a default
4879
+ silently. \`stim doctor\` reports it as a finding
4509
4880
  instead of refusing. The exception is \`optimizations.android.casToolchain\`:
4510
4881
  an invalid value warns and falls back to ccache, or no compiler cache when
4511
4882
  \`compilerCache\` is \`none\`. \`doctor\` also reports the invalid setting.
@@ -4519,19 +4890,22 @@ removed -- check this list rather than assuming it still applies.
4519
4890
  problem. It reports, as notes naming the key, the file it came from, and the
4520
4891
  line that clears it: a setting whose path no longer exists, a setting that needs
4521
4892
  a companion the config does not supply, a key Stim no longer reads, and a
4522
- config file that is not valid JSON. It skips the \`projects\` registry, where an
4523
- entry for a deleted checkout is normal and \`gc\` owns the cleanup.
4893
+ config file that is not a valid JSON object. It skips the \`projects\`
4894
+ registry, where an entry for a deleted checkout is normal and \`gc\` owns the
4895
+ cleanup.
4524
4896
 
4525
4897
  CONCURRENCY LIMITS ARE MACHINE-LEVEL, NOT A PER-PROJECT SETTING
4526
4898
  The caps above are not in the layered settings -- they are not per-project,
4527
4899
  because the resource they share (cores, RAM, booted simulators) is the whole
4528
4900
  machine's. They live under a top-level \`concurrency\` key in
4529
- ~/.stim/config.json, edited by hand:
4901
+ ~/.stim/config.json, set with \`stim settings set\` or by hand:
4530
4902
 
4531
4903
  {
4532
4904
  "concurrency": { "maxBuilds": 2, "maxDevices": 3 }
4533
4905
  }
4534
4906
 
4907
+ The keys are concurrency.maxBuilds and concurrency.maxDevices.
4908
+
4535
4909
  or via the environment, which overrides the file:
4536
4910
 
4537
4911
  STIM_MAX_BUILDS=2 STIM_MAX_DEVICES=3 stim ios
@@ -4548,16 +4922,24 @@ or Simulator on older Xcode. To use an installed Siniulator instead:
4548
4922
 
4549
4923
  { "iosSimulatorApp": "siniulator" }
4550
4924
 
4925
+ To show the booted simulator in Stim Desktop and open no simulator window:
4926
+
4927
+ { "iosSimulatorApp": "stim-desktop" }
4928
+
4929
+ Stim Desktop selects the workspace that owns the simulator and focuses that
4930
+ device. It only displays the simulator; it never boots or shuts it down.
4931
+
4551
4932
  Override the preference for one local launch with
4552
- \`stim ios --simulator-app siniulator\` or \`stim ios --simulator-app xcode\`.
4933
+ \`stim ios --simulator-app siniulator\`, \`stim ios --simulator-app stim-desktop\`,
4934
+ or \`stim ios --simulator-app xcode\`.
4553
4935
  The flag also opens an already running owned simulator without rebooting it.
4554
4936
  It leaves the saved preference unchanged and refuses physical or remote targets.
4555
4937
 
4556
4938
  Stim still creates and boots its owned simulator. An invalid value refuses
4557
- before boot. Opening a window remains best-effort, so install Siniulator before
4558
- selecting it. By default, closing a Siniulator device window shuts down that
4559
- simulator. Enable "Leave simulator running after window is closed" in Siniulator
4560
- Settings if Stim should keep it running. See \`guide lifecycle simslim\` for
4939
+ before boot. Opening a window remains best-effort, so install Siniulator or
4940
+ Stim Desktop before selecting it. By default, closing a Siniulator device
4941
+ window shuts down that simulator. Enable "Leave simulator running after window
4942
+ is closed" in Siniulator Settings if Stim should keep it running. See \`guide lifecycle simslim\` for
4561
4943
  window behavior.
4562
4944
 
4563
4945
  THE DEVICE POOL BOUNDS ARE MACHINE-LEVEL TOO
@@ -4713,7 +5095,8 @@ The example shows the defaults. Full setting names and behavior:
4713
5095
  auto keeps library/project policy, with PCH off by default when Stim supplies
4714
5096
  ccache. on/off overrides Gradle CMAKE_DISABLE_PRECOMPILE_HEADERS arguments;
4715
5097
  CMake target-level overrides still win. on does not fix stock ccache's
4716
- cross-worktree PCH limitations.
5098
+ cross-worktree PCH limitations. A value outside auto|on|off is refused by
5099
+ name.
4717
5100
  optimizations.android.gradleBuildCache
4718
5101
  false passes --no-build-cache to Gradle, overriding org.gradle.caching=true.
4719
5102
  optimizations.android.targetAbiOnly
@@ -4747,9 +5130,10 @@ swaps, and the doctor fingerprint checkout select a writable directory on the
4747
5130
  relevant filesystem. App/APK preparation uses the artifact volume. Worktree
4748
5131
  warm copies directly to the destination and does not use temporary storage.
4749
5132
  A system temporary directory on that
4750
- volume is preferred, then a writable ancestor of the relevant path. Staging
4751
- is private and outside Git working trees, so ignored secrets cannot enter
4752
- Git status or git add. If no safe location exists, the operation refuses.
5133
+ volume is preferred, then a writable ancestor of the relevant path outside
5134
+ the shared build cache. Staging is private and outside Git working trees, so
5135
+ ignored secrets cannot enter Git status or git add. If no safe location
5136
+ exists, the operation refuses.
4753
5137
 
4754
5138
  Set STIM_TMPDIR or top-level tempDir in $STIM_HOME/config.json (default
4755
5139
  ~/.stim/config.json) to override placement. STIM_TMPDIR takes precedence.
@@ -4775,16 +5159,22 @@ cp -c can silently fall back to copying and exit successfully.
4775
5159
  CACHE LOCATIONS ARE MACHINE-LEVEL TOO
4776
5160
  The shared build cache and Metro transform cache default to living under
4777
5161
  ~/.stim. To relocate them (say, to an external disk), set a top-level
4778
- \`caches\` key in ~/.stim/config.json, edited by hand -- absolute paths:
5162
+ \`caches\` key in ~/.stim/config.json (\`stim settings set caches.buildCache
5163
+ <path>\`, or by hand) -- absolute paths:
4779
5164
 
4780
5165
  {
4781
5166
  "caches": { "buildCache": "/Volumes/SSD/stim/build-cache",
4782
5167
  "metroCache": "/Volumes/SSD/stim/metro-cache" }
4783
5168
  }
4784
5169
 
4785
- STIM_BUILD_CACHE / STIM_METRO_CACHE in the environment override the file.
5170
+ caches.buildCache and caches.metroCache are the keys; STIM_BUILD_CACHE /
5171
+ STIM_METRO_CACHE in the environment override the file.
4786
5172
  The CLI and both cache packages resolve these identically, so every process
4787
- finds the same store regardless of shell profile. A relative path is ignored.
5173
+ finds the same store regardless of shell profile. A relative path in the file
5174
+ is ignored. A relative STIM_HOME, STIM_BUILD_CACHE or STIM_METRO_CACHE makes
5175
+ every command refuse with STIM_RELATIVE_PATH; Metro and the Expo build-cache
5176
+ provider, which must not break the bundler, ignore it with a warning and use
5177
+ the next source.
4788
5178
  The Metro value is a PARENT root. The sanitized package name is appended below
4789
5179
  it, so apps remain separately reportable and prunable. Earlier releases used an
4790
5180
  overridden Metro root as one flat store. A new registration replaces that legacy
@@ -4794,7 +5184,7 @@ A marked store that later becomes another override parent remains visible but is
4794
5184
  report-only while its marked child exists. Root-level legacy files remain
4795
5185
  untouched for manual cleanup.
4796
5186
 
4797
- PREFER SELF-REGISTRATION OVER THE 'caches' SETTING
5187
+ REGISTER A CACHE WITH GC
4798
5188
  There is no 'cache' command. A cache registers itself from code instead, once,
4799
5189
  and every 'gc' report shows it from then on, tagged (registered):
4800
5190