stim 1.17.2 → 1.19.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 (162) hide show
  1. package/README.md +50 -1
  2. package/dist/{activity-IRvJY_ru.mjs → activity-BSw0x84R.mjs} +4 -4
  3. package/dist/{agent-device-usage-output-BK41vnzN.mjs → agent-device-usage-output-Bjxu_VmX.mjs} +8 -8
  4. package/dist/{android-B9yaNKwa.mjs → android-BXc6BmEi.mjs} +658 -404
  5. package/dist/{android-B_YNZy61.mjs → android-BdQC6zs1.mjs} +9 -9
  6. package/dist/android-BvV1gGdN.mjs +4 -0
  7. package/dist/{android-DE4v59Mb.mjs → android-DE5g_5gv.mjs} +2 -2
  8. package/dist/{android-cas-ZFpjJBL_.mjs → android-cas-C87geAg-.mjs} +5 -5
  9. package/dist/android-cas-compiler.mjs +2 -2
  10. package/dist/api-DIf_s86s.mjs +150 -0
  11. package/dist/api-run.d.mts +1 -0
  12. package/dist/api-run.mjs +145 -0
  13. package/dist/api.d.mts +218 -0
  14. package/dist/api.mjs +2 -0
  15. package/dist/{app-install-VPkI5WEg.mjs → app-install-CFI4ptUL.mjs} +33 -13
  16. package/dist/{attempt-Bk2ouQVL.mjs → attempt-DW-mQqM7.mjs} +14 -4
  17. package/dist/{web-Dl0KSRHf.mjs → browser-web-CDhqQxhv.mjs} +138 -176
  18. package/dist/{budget-DfcgFdQB.mjs → budget-Dz-kyXLv.mjs} +1391 -58
  19. package/dist/build-facts-B87vo-Im.d.mts +142 -0
  20. package/dist/{cache-manifest-DnUHBJaU.mjs → cache-manifest-DiwKSyv9.mjs} +3 -3
  21. package/dist/cache-manifest.mjs +1 -1
  22. package/dist/{chrome-BrcVWQyD.mjs → chrome-BTUOo8p2.mjs} +1 -1
  23. package/dist/cli-C9Zvgbdc.mjs +68 -0
  24. package/dist/cli.mjs +1 -1
  25. package/dist/{client-SvBdgObD.mjs → client-CuUo8Nvg.mjs} +381 -49
  26. package/dist/collector-run.d.mts +6 -0
  27. package/dist/collector-run.mjs +9 -9
  28. package/dist/{command-output-CDAIFD-8.mjs → command-output-Bt8ygHE7.mjs} +2 -2
  29. package/dist/{config-D0DbpZtM.mjs → config-DC5jEtd1.mjs} +9 -5
  30. package/dist/{created-devices-CpvzBL25.mjs → created-devices-D0s26Hx5.mjs} +2 -2
  31. package/dist/{dependency-state-CLcBHH2M.mjs → dependency-state-DKaS1OM9.mjs} +2 -2
  32. package/dist/{detached-entry-CiTEgMR1.mjs → detached-entry-pB_IPfV4.mjs} +1 -1
  33. package/dist/{dev-client-B-XC4uRH.mjs → dev-client-CDY_zAmD.mjs} +2 -2
  34. package/dist/{device-C8IOoJPv.mjs → device-BHw0WlHl.mjs} +8 -8
  35. package/dist/{device-capacity-ZC4XSZ4R.mjs → device-capacity-BoN1jQkO.mjs} +25 -14
  36. package/dist/device-host-worker.mjs +19 -19
  37. package/dist/{gradle-dJpre3Yn.mjs → device-ios-B2ruohE-.mjs} +661 -107
  38. package/dist/{device-lease-DpoS36_u.mjs → device-lease-Cu7PtQpZ.mjs} +9 -9
  39. package/dist/{device-lease-run-B8YltIH_.mjs → device-lease-run-D38SnZtt.mjs} +4 -4
  40. package/dist/{device-pool-3o7s5lo4.mjs → device-pool-CP4rAsHx.mjs} +3 -3
  41. package/dist/{device-remote-hFSkpfQu.mjs → device-remote-B5GiUifk.mjs} +15 -16
  42. package/dist/{doctor-CFv1fCrF.mjs → doctor-PRJH94v6.mjs} +61 -34
  43. package/dist/doctor-watchman-wZhyTdp4.mjs +259 -0
  44. package/dist/{error-diagnostics-C-NMnhrk.mjs → error-diagnostics--v5HGOU0.mjs} +11 -11
  45. package/dist/{exec-CaErVv7t.mjs → exec-i8GDPwq9.mjs} +93 -6
  46. package/dist/{gc-DEmkg3Xh.mjs → gc-CyHMcKcn.mjs} +25 -471
  47. package/dist/{guide-CSmhMTf8.mjs → guide-dRaQJJk9.mjs} +681 -507
  48. package/dist/{hosted-android-E1Heoqnv.mjs → hosted-android-Bs7fytj7.mjs} +3 -3
  49. package/dist/{hosted-client-Bp4knSwu.mjs → hosted-client-C5G4h_OJ.mjs} +2 -2
  50. package/dist/{hosted-ios-BnbeYOUD.mjs → hosted-ios-Bik035eL.mjs} +1 -1
  51. package/dist/{hosted-logs-OcgIBvCT.mjs → hosted-logs-CI9Az9a4.mjs} +9 -9
  52. package/dist/{hosted-macos-DcxKjtfH.mjs → hosted-macos-DABm3ScO.mjs} +5 -5
  53. package/dist/{hosted-native-1Ebbig4w.mjs → hosted-native-DbV2aUVw.mjs} +13 -9
  54. package/dist/{idle-shutdown-IGyNV8Cr.mjs → idle-shutdown-DYiMBn34.mjs} +7 -8
  55. package/dist/{ios-DSfnPfse.mjs → ios-BWbHFZVz.mjs} +19 -7
  56. package/dist/{ios-Cr1h5gnO.mjs → ios-DCrkgXEM.mjs} +1 -1
  57. package/dist/ios-DYUr9LGc.mjs +8 -0
  58. package/dist/ios-ITYno9rO.mjs +2241 -0
  59. package/dist/{ios-device-pAN4t54-.mjs → ios-device-CJ_ZOGky.mjs} +3 -3
  60. package/dist/{ios-device-Ee7sDXfp.mjs → ios-device-CrW8aMb5.mjs} +1 -1
  61. package/dist/{ios-state-8L3lgum6.mjs → ios-state-By0XuLTQ.mjs} +1 -1
  62. package/dist/{launch-verify-lTnWZikR.mjs → launch-verify-DCveSE6d.mjs} +5 -5
  63. package/dist/{logs-BAaS7gS2.mjs → logs-Bao1HOMy.mjs} +13 -13
  64. package/dist/{macos-BIRQS90f.mjs → macos-ciyBdTY5.mjs} +194 -132
  65. package/dist/macos-nIOATJmR.mjs +2 -0
  66. package/dist/macos-run.mjs +1 -1
  67. package/dist/maintenance-run.mjs +349 -47
  68. package/dist/manifest-CX0d1IkZ.mjs +148 -0
  69. package/dist/{metro-g8NqiJp-.mjs → metro-DI2AjPF_.mjs} +196 -43
  70. package/dist/{metro-gateway-C-ijGI2F.mjs → metro-gateway-BxpJLIwh.mjs} +4 -4
  71. package/dist/{metro-store-CDDZHtPl.mjs → metro-store-BAHFcJm6.mjs} +16 -2
  72. package/dist/{build-plan-BDsSi09V.mjs → miss-reason-CjQCOKqq.mjs} +207 -249
  73. package/dist/{named-ports-C8m8EQs2.mjs → named-ports-B4iWH4E-.mjs} +70 -8
  74. package/dist/{native-runtime-BEK171xQ.mjs → native-runtime-BGxsbfny.mjs} +6 -6
  75. package/dist/{ndjson-PclOIwVY.mjs → ndjson-LGZAMsqa.mjs} +8 -4
  76. package/dist/offload-worker.d.mts +3 -45
  77. package/dist/offload-worker.mjs +11 -11
  78. package/dist/{ownership-DyRQru0q.mjs → ownership-EB0ySdGK.mjs} +3 -3
  79. package/dist/{page-QF_Ke10s.mjs → page-Bv9s2KwY.mjs} +1 -1
  80. package/dist/placement-log-Dxe34rJm.mjs +88 -0
  81. package/dist/plan-placement-BVY59qa8.mjs +290 -0
  82. package/dist/{ports-4nYqxPWC.mjs → ports-ByPc0_97.mjs} +3 -3
  83. package/dist/{prebuild-D7RUMkwN.mjs → prebuild-BKJ3wJI1.mjs} +5 -5
  84. package/dist/preview-BbSottmO.mjs +986 -0
  85. package/dist/{rolldown-runtime-BsOwBuO_.mjs → process-identity-Biv3arCO.mjs} +11 -1
  86. package/dist/project-B5eQ1USs.mjs +612 -0
  87. package/dist/{pull-request-pxnDCQzn.mjs → pull-request-IHNdMnG8.mjs} +1 -1
  88. package/dist/pull-requests.mjs +1 -1
  89. package/dist/react-native-ios-BMh2i25I.mjs +2 -0
  90. package/dist/react-native-ios-CSpIxexZ.mjs +2083 -0
  91. package/dist/{recordings-BNgsD1Tr.mjs → recordings-Bv8McZN4.mjs} +1 -1
  92. package/dist/{reload-DcRxj3qG.mjs → reload-rAPdqeRR.mjs} +14 -15
  93. package/dist/{remote-cache-Ci4DQYYj.mjs → remote-cache-N3hAEaAL.mjs} +5 -5
  94. package/dist/{run-CkE9y0nc.mjs → run-lAkle1-c.mjs} +5 -5
  95. package/dist/{server-bare-BkrSxSsZ.mjs → server-bare-CkIjnegq.mjs} +5 -6
  96. package/dist/server-expo-DGsdUGX2.mjs +2 -0
  97. package/dist/{server-expo-CHTyXD0G.mjs → server-expo-n3tUi3Em.mjs} +5 -6
  98. package/dist/{settings-SR9OmW9h.mjs → settings-2am7TSxp.mjs} +5 -5
  99. package/dist/{settings-Cfd-0r2d.mjs → settings-CxgngUZk.mjs} +116 -103
  100. package/dist/{settings-BBpMcDbD.mjs → settings-Djl-mpjX.mjs} +1 -1
  101. package/dist/settings.schema.json +198 -9
  102. package/dist/{simslim-BWNrc3M3.mjs → simslim-D9LkJxIO.mjs} +7 -7
  103. package/dist/{slot-launch-DmjBmpQV.mjs → slot-launch-HV_KtPIw.mjs} +6 -6
  104. package/dist/{spawn-claims-D0XfSe8u.mjs → spawn-claims-Kqiifsk2.mjs} +1 -1
  105. package/dist/{spawn-entry-jw19m8Nl.mjs → spawn-entry-Dcb7sDUz.mjs} +1 -0
  106. package/dist/{stage-CzaiwFsI.mjs → stage-BgaZJVZY.mjs} +14 -3
  107. package/dist/{start-AlfCPRVU.mjs → start-D8NbxNBb.mjs} +1 -1
  108. package/dist/{start-DUz3mYdb.mjs → start-DtxthMfI.mjs} +34 -27
  109. package/dist/{state-PeQiJuIu.mjs → state-D2KtnALG.mjs} +4 -4
  110. package/dist/{state-DyaNTgfR.mjs → state-Tp4uImDS.mjs} +5 -3
  111. package/dist/{state-B4PQFXMW.d.mts → state-lKh8shBp.d.mts} +2 -3
  112. package/dist/{stats-Bq4fJOdB.mjs → stats-8EJ5ivFT.mjs} +6 -6
  113. package/dist/{status-DlKvVJBE.mjs → status-BEeL6IKz.mjs} +6 -4
  114. package/dist/{status-BA2pehwf.mjs → status-BOyp0CUl.mjs} +31 -34
  115. package/dist/{stim-desktop-Cnz8CPnl.mjs → stim-desktop-C65jk_q2.mjs} +1 -1
  116. package/dist/{stim-installations-QMEI32v1.mjs → stim-installations-DbmUjsLv.mjs} +1 -1
  117. package/dist/stop-D0UIpZuh.mjs +72 -0
  118. package/dist/{stop-zF1y04f2.mjs → stop-DRipAg-e.mjs} +2 -2
  119. package/dist/{stop-DQIasgKp.mjs → stop-zDlknFoz.mjs} +79 -33
  120. package/dist/supervisor-run.d.mts +24 -22
  121. package/dist/supervisor-run.mjs +21 -22
  122. package/dist/{support-CsJNVfOc.mjs → support-CNqYkV0y.mjs} +7 -7
  123. package/dist/{support-WvD__tSV.mjs → support-DsNaFg6D.mjs} +5 -5
  124. package/dist/swiftpm-macos-aJzPAj1t.mjs +119 -0
  125. package/dist/{toolchain-DNyR3NPq.mjs → toolchain-C3xSoyUl.mjs} +475 -269
  126. package/dist/{trigger-Bemu8ytw.mjs → trigger-CAhmG-4s.mjs} +6 -6
  127. package/dist/trigger-CoalsMTV.mjs +2 -0
  128. package/dist/{warm-progress-DF4JRvYI.mjs → warm-progress-pFLB2e6P.mjs} +135 -38
  129. package/dist/{status-CzS4bPO9.mjs → watchman-BL1IdOtq.mjs} +75 -7
  130. package/dist/web-DFpFeZu5.mjs +2 -0
  131. package/dist/web-DbbrPUn0.mjs +127 -0
  132. package/dist/web-run.mjs +10 -10
  133. package/dist/{workspace-state-DqcCbpE7.mjs → workspace-state-CR9X86xJ.mjs} +3 -3
  134. package/dist/{ownership-BMLqyBgJ.mjs → workspaces-Djb2gfi9.mjs} +661 -23
  135. package/dist/{worktree-Be_7NLKy.mjs → worktree-BL4M1rhB.mjs} +252 -50
  136. package/dist/{worktree-j3IcFAI5.mjs → worktree-DV-A0LNL.mjs} +1 -1
  137. package/package.json +10 -5
  138. package/shim/bundle-response.cjs +2 -1
  139. package/shim/bundle-response.d.cts +1 -1
  140. package/shim/expo-metro-config.cjs +1 -1
  141. package/dist/build-progress-D0WkjCPd.mjs +0 -1036
  142. package/dist/build-slots-41v_d2x9.mjs +0 -142
  143. package/dist/cli-CYXb29oE.mjs +0 -51
  144. package/dist/deps-iKw0eD4P.mjs +0 -438
  145. package/dist/device-ios-C2Um-XUR.mjs +0 -412
  146. package/dist/errors-B6W7FkxQ.mjs +0 -15
  147. package/dist/guide-status-Du-ZQ3FD.mjs +0 -125
  148. package/dist/idle-BXG__1BA.mjs +0 -176
  149. package/dist/in-use-CvGOUn-x.mjs +0 -130
  150. package/dist/ios-BJ4XUSe1.mjs +0 -3867
  151. package/dist/native-run-D1Q56rAD.mjs +0 -158
  152. package/dist/preview-B-g9d7kV.mjs +0 -318
  153. package/dist/process-identity-BmRm1qV0.mjs +0 -12
  154. package/dist/project-D47Rh0ly.mjs +0 -276
  155. package/dist/server-expo-CeakhI8P.mjs +0 -2
  156. package/dist/state-BKa8wNN5.mjs +0 -148
  157. package/dist/stop-BIiM49EY.mjs +0 -48
  158. package/dist/stop-cause-rhmGfwvt.mjs +0 -97
  159. package/dist/trigger-CXts6qt1.mjs +0 -2
  160. package/dist/watchman-APYjXSQa.mjs +0 -48
  161. package/dist/workspace-process-lock-2mNzOlaB.mjs +0 -58
  162. package/dist/workspaces-Bcib97-6.mjs +0 -306
@@ -1,8 +1,8 @@
1
- import { t as ANDROID_AVD_CONFIG_HELP } from "./settings-Cfd-0r2d.mjs";
2
- import { s as findProjectRoot$1 } from "./project-D47Rh0ly.mjs";
3
- import { t as RECENT_LAUNCH_MS } from "./status-CzS4bPO9.mjs";
4
- import { t as guideStatus } from "./guide-status-Du-ZQ3FD.mjs";
5
- import { SETTINGS, SETTINGS_SCHEMA_URL } from "@stim-cli/core/state";
1
+ import { r as findProjectRoot } from "./project-B5eQ1USs.mjs";
2
+ import { t as ANDROID_AVD_CONFIG_HELP } from "./settings-CxgngUZk.mjs";
3
+ import { s as RECENT_LAUNCH_MS } from "./watchman-BL1IdOtq.mjs";
4
+ import { o as guideStatus, t as WATCHMAN_NESTED_WORKTREES } from "./doctor-watchman-wZhyTdp4.mjs";
5
+ import { SETTINGS, SETTINGS_SCHEMA_URL, TUTORIAL_VERSION } from "@stim-cli/core/state";
6
6
  import chalk from "chalk";
7
7
  //#region src/guide/agent.ts
8
8
  var agent_default = {
@@ -293,6 +293,11 @@ Ask the user before these actions:
293
293
  nothing else.
294
294
  - gc --delete --worktrees, because it runs worktree remove on every clean,
295
295
  idle linked worktree Stim manages, across projects.
296
+ - settings set maintenance.mode on in a home where it is off or report, because
297
+ it lets Stim itself clear build outputs, trim caches and remove finished
298
+ worktrees later, with no command from you. It is on by default outside CI
299
+ and scoped STIM_HOME homes. maintenance.keep true pins a workspace against
300
+ that; maintenance.mode report or off stops it.
296
301
  - stop when the workspace owns an EAS session, because it irreversibly ends
297
302
  that remote session. For a local device, stop shuts it down but does not
298
303
  delete it. An explicit stop shuts down a Stim-owned simulator even when
@@ -382,6 +387,7 @@ FULL TOPIC LIST
382
387
  stim guide lifecycle options # every flag, Android variants, --device-type, --system-image, --device-profile
383
388
  stim guide lifecycle devices # ios --device and android --device on a physical phone
384
389
  stim guide lifecycle release # Release configurations and ...Release variants
390
+ stim guide api # typed run, stop, diagnostics and cancellation from the main package
385
391
  stim guide facts # the --json payloads
386
392
  stim guide facts devmenu # the Expo dev menu or Tools button over the app
387
393
  stim guide macos # Swift Package Debug apps, logs, local preview, --remote on another Mac
@@ -397,6 +403,64 @@ FULL TOPIC LIST
397
403
  stim guide settings # configuration files and supported keys`
398
404
  };
399
405
  //#endregion
406
+ //#region src/guide/api.ts
407
+ var api_default = {
408
+ summary: "Typed lifecycle API: createStim, run, stop, diagnostics, and cancellation",
409
+ body: () => `PROGRAMMATIC API
410
+
411
+ Install stim as a project dependency: npm install --save-dev stim.
412
+ Import { createStim, StimError } from 'stim'. Node 22.12 or later is required.
413
+
414
+ const stim = createStim({ projectRoot: process.cwd() });
415
+ try {
416
+ const result = await stim.run({ platform: 'ios' });
417
+ } finally {
418
+ try {
419
+ const diagnostics = await stim.diagnostics({ errors: true });
420
+ } finally {
421
+ const cleanup = await stim.stop();
422
+ if (!cleanup.ok) throw new Error(cleanup.summary);
423
+ }
424
+ }
425
+
426
+ createStim takes projectRoot, optional absolute home and buildCache paths,
427
+ and onProgress({ stream, message }). message is an output chunk. Without
428
+ onProgress, operations write nothing to the caller's stdout or stderr. Each
429
+ operation uses a bundled worker that calls the same lifecycle operations as
430
+ the CLI; it never parses CLI arguments or changes the caller's cwd/environment.
431
+
432
+ run takes platform plus its options:
433
+ ios: configuration, scheme, deviceType, runtime
434
+ android: variant, systemImage, deviceProfile
435
+ macos: remoteBuild
436
+ web: headed (default false)
437
+ iOS and Android also accept slot, metroCheck, buildCache, and remoteBuild.
438
+ All methods accept signal. run builds, installs and launches; build-only is
439
+ not available. Web requires a running server, just like stim web.
440
+
441
+ run returns { platform, facts }. iOS facts include udid; Android includes
442
+ serial; both include bundleId, appPath, metroPort, cacheHit and launched.
443
+ macOS facts include bundle, bundleId, executable, pid, build and launched.
444
+ Web facts contain the owned browser's existing launch and page facts.
445
+ Do not interpret 'bundling' or 'unverified' as proven application readiness.
446
+
447
+ stop({ slot? }) returns { ok, outcomes, summary }. It acts on the workspace,
448
+ including resources from earlier runs. Use a dedicated workspace in CI.
449
+ Cancellation waits for the operation worker to exit; stop with a fresh signal
450
+ after cancellation or partial failure. Always inspect cleanup.ok.
451
+
452
+ diagnostics({ tail: 200, errors: false }) returns { directory, records } from
453
+ the local timeline, even before a successful run. tail: 0 returns paths only.
454
+ It does not fetch remote logs or capture new crashes. Stim failures reject with
455
+ StimError carrying code, message, remedy and details (including the log path).
456
+ An exception thrown by onProgress cancels its worker and propagates unchanged.
457
+
458
+ Normal ownership, device creation, cache locks and coordination remain active.
459
+ All concurrent artifact-cache writers must share the same coordinating home.
460
+ Do not point independent Stim homes at a concurrently writable shared cache.
461
+ `
462
+ };
463
+ //#endregion
400
464
  //#region src/guide/facts.ts
401
465
  const facts = {
402
466
  summary: "The --json payloads: `start`, `ios`, `android`, `web`, `ios|android --plan`, `reload`, `stop`, `status`, `doctor`, `device lock`/`unlock`, `gc`, and the error contract",
@@ -945,7 +1009,7 @@ leased until <time>" for each one.`,
945
1009
  reasons as { code, reason } with that code. capacity
946
1010
  is the machine's offer: { running, max, diskFreeBytes,
947
1011
  minDiskFreeBytes, cpus?, loadPerCore?, builds?, maxBuilds?,
948
- maxLoadPerCore?, declined? }; an older stim-server omits the
1012
+ maxLoadPerCore?, memoryUsedBytes?, memoryTotalBytes?, declined? }; an older stim-server omits the
949
1013
  optional fields
950
1014
  findings the diagnostic findings; a lower resolved Stim is a
951
1015
  costs-time finding with a PATH or installation remedy
@@ -1203,7 +1267,7 @@ RULES
1203
1267
  cannot read. A macOS privacy denial (EPERM) names the
1204
1268
  Privacy & Security setting to grant
1205
1269
  maintenance top-level { mode, pressure, actions, blocked,
1206
- skips, note, invalid? }, the next report-only maintenance plan
1270
+ skips, note, invalid? }, the next maintenance plan
1207
1271
  from live pressure and cached sizes. note says when no
1208
1272
  pass has run yet. No du runs for this preview.
1209
1273
  sections one array per report section, in the text order. Every key
@@ -1368,8 +1432,9 @@ RULES
1368
1432
  body: () => ` stim status --json
1369
1433
 
1370
1434
  logs { dir, errorsSinceMarker }, or null without a log directory
1371
- agentDevice { stateDir }: absolute workspace agent-device state path on
1372
- every environment, shared by its slots. Reporting it creates
1435
+ agentDevice { stateDir, installed }: absolute workspace agent-device state
1436
+ path on every environment, shared by its slots, and whether
1437
+ the agent-device executable is on PATH. Reporting it creates
1373
1438
  no directory; agent-device creates it when used.
1374
1439
 
1375
1440
  Each environment carries phase, where the workspace is in its lifecycle:
@@ -1685,14 +1750,18 @@ RULES
1685
1750
  once the app is ready and covers waiting for the
1686
1751
  device: its boot, adoption cleanup, or a physical
1687
1752
  device's lease and connection check. A boot that
1688
- finishes during the build adds no device time. An
1753
+ finishes during the build adds no device time. For a
1754
+ device hosted on another Mac, device covers reserving
1755
+ and preparing it there, install covers delivering the
1756
+ app, and launch starts when the host launches it. An
1689
1757
  --eas-profile run has no cache lookup, so its outcome
1690
1758
  stays the project's most recent one until install.
1691
1759
  A stim macos run enters only prepare, compile (SwiftPM,
1692
1760
  with detail), install (staging the bundle, or fetching
1693
1761
  it from a build machine) and launch. It has no cache
1694
1762
  lookup: outcome is null, plannedPhases is null, and
1695
- builds.macos holds its finished runs and their phases.
1763
+ builds.macos holds its finished runs, their phases and
1764
+ compileSteps.
1696
1765
  startedAt when the run started; phaseStartedAt when its phase did
1697
1766
  outcome "cold" after the local/provider lookups resolve a miss;
1698
1767
  "hit" after a cached artifact is ready to reuse, including
@@ -1877,7 +1946,8 @@ RULES
1877
1946
  disk (environment) { worktreeBytes, nodeModulesBytes, buildBytes,
1878
1947
  measuredAt }
1879
1948
  worktreeBytes the linked worktree, or else the checkout holding the
1880
- workspace, node_modules included
1949
+ workspace, node_modules included; the linked
1950
+ worktrees nested inside a checkout are not counted
1881
1951
  nodeModulesBytes node_modules at that root and at the workspace path;
1882
1952
  part of worktreeBytes
1883
1953
  buildBytes Stim's folder for the workspace: Xcode derived data,
@@ -1887,11 +1957,13 @@ RULES
1887
1957
  disk (device) { bytes, measuredAt }: the simulator's data folder
1888
1958
  under CoreSimulator/Devices, or the AVD's .avd folder
1889
1959
 
1890
- \`status --watch\` runs one du at a time off its refresh path, and measures
1891
- a folder at most every 5 minutes while its environment is active and every
1892
- hour otherwise. It caches each size under $STIM_HOME/disk-usage, which
1893
- one-shot status only reads, so the fields appear once a watcher, such as
1894
- stim-server or Stim Desktop, has measured.
1960
+ \`status --watch\` runs one du at a time on the whole machine, at low
1961
+ priority, off its refresh path, and measures a folder at most every 5
1962
+ minutes while its environment is active and every hour otherwise. A walk
1963
+ that fails or times out is not repeated by any watcher for that period. It
1964
+ caches each size under $STIM_HOME/disk-usage, which one-shot status only
1965
+ reads, so the fields appear once a watcher, such as stim-server or Stim
1966
+ Desktop, has measured.
1895
1967
 
1896
1968
  An environment carries agents when coding-agent sessions work in it, most
1897
1969
  recently active first:
@@ -1957,13 +2029,15 @@ RULES
1957
2029
  and always uses the estimate. What is using CPU and memory now is the
1958
2030
  top-level machine section:
1959
2031
 
1960
- maintenance { mode, lastChecks: { pressure, size }, pressure, sizes,
2032
+ maintenance { mode, lastChecks: { pressure, size, worktree, sweep }, pressure, sizes,
1961
2033
  lastPass, running, recent, plan, invalid?, claim? }
1962
- Report-only observations; every plan action's kind starts with would-.
2034
+ mode is off, report or on. Plan actions start with would-; the records
2035
+ of actions taken drop the prefix (clear-outputs, trim-cache, empty-cache,
2036
+ remove-worktree, remove-orphan, unregister-cache).
1963
2037
  lastChecks are epoch milliseconds or null. running comes only from a
1964
2038
  live maintenance/run.claims owner, never from a check stamp.
1965
- lastPass carries startedAt, durationMs, trigger, mode, freedBytes (0),
1966
- actions, stopped (0), blocked. recent holds the last 20 action, failure
2039
+ lastPass carries startedAt, durationMs, trigger, mode, freedBytes
2040
+ (0 in report mode), actions, stopped (0), blocked. recent holds the last 20 action, failure
1967
2041
  and blocked NDJSON records from maintenance/maintenance.ndjson.
1968
2042
  invalid names invalid settings; invalid cache caps use their defaults,
1969
2043
  while invalid maintenance settings disable passes. claim contains an
@@ -2018,7 +2092,7 @@ RULES
2018
2092
 
2019
2093
  { platform, slot?, fingerprint, cacheKey, cacheHit, provider,
2020
2094
  cacheSkipped, prebuild, outcome, expectedMs, basis, missReason?,
2021
- refusal? }
2095
+ placement?, refusal? }
2022
2096
 
2023
2097
  fingerprint the fingerprint the run would look up first; with
2024
2098
  --eas-profile, the one EAS CLI computes
@@ -2044,6 +2118,9 @@ RULES
2044
2118
  plan does not, so changes compare the fingerprint before that
2045
2119
  prebuild; changeCount 0 then means those inputs match the
2046
2120
  baseline. rekeyedBy is empty.
2121
+ placement with ios.remote or android.remote set to auto or a Mac,
2122
+ where the plan assumes the device runs: "on <machine>" or
2123
+ "this Mac; auto may use <machines>"; absent otherwise
2047
2124
  refusal { code, message, remedy } when the run would refuse:
2048
2125
  STIM_PREBUILD_FAILED for a tracked native dir the fingerprint
2049
2126
  leaves out, STIM_EAS_BUILD_MISSING for an EAS miss
@@ -2182,8 +2259,9 @@ HOW A RUN IS COUNTED (\`stats\`)
2182
2259
 
2183
2260
  DEVICE PLACEMENT (\`ios|android --remote auto\`)
2184
2261
  Auto runs include devicePlacement: { decision, reason, machine? } in the run
2185
- facts, lastBuilds and build history. decision is "local", "hosted" or
2186
- "waited-locally" (the local run actually waited for a device slot). The same
2262
+ facts, lastBuilds and build history. decision is "local", "hosted",
2263
+ "waited-locally" (the local run actually waited for a device slot) or "eas"
2264
+ (remote.easFallback put it on an EAS Simulator, reported like --remote eas). The same
2187
2265
  optional devicePlacement appears on the status device entry for each slot.
2188
2266
  Hosted host facts include selected: "auto" or the named machine, and reason
2189
2267
  for automatic placement. Plain status prints (auto: <reason>) after the host.
@@ -2530,9 +2608,15 @@ then 8081, without probing or reserving; an invalid pin still refuses.
2530
2608
 
2531
2609
  New allocations scan TCP ports 8900-8999. They skip registry reservations
2532
2610
  and existing listeners, announcing occupied ports and upward retries on
2533
- stderr. Listener checks require lsof, or netstat on Windows. All 100 ports
2534
- occupied or reserved is a refusal; stop or release unused allocations in
2535
- their owning workspaces.
2611
+ stderr. All 100 ports occupied or reserved is a refusal; stop or release
2612
+ unused allocations in their owning workspaces.
2613
+
2614
+ Metro allocation (from 8082 up) and named allocation find listeners in the
2615
+ native TCP table: netstat on macOS and Windows, /proc/net on Linux. When that
2616
+ table is denied, empty or unreadable, as in some sandboxes, each candidate is
2617
+ checked with an lsof listener scan and connects to 127.0.0.1 and ::1 instead.
2618
+ Allocation refuses only when none of them can answer: start refuses with
2619
+ STIM_PORT_INSPECTION_FAILED, and ports get prints the same message.
2536
2620
 
2537
2621
  The machine registry, under STIM_HOME, serializes allocation and cleanup.
2538
2622
  The workspace is the nearest package.json directory, resolved through
@@ -2555,6 +2639,7 @@ ports lists named labels and ports, plus Metro marked managed.
2555
2639
  ports stop [label] kills TCP listeners on those named ports and releases
2556
2640
  the allocations. It sends SIGTERM, waits two seconds, then SIGKILL if needed;
2557
2641
  on Windows it terminates the listener's process tree with taskkill.
2642
+ Stopping listeners requires lsof on macOS and Linux, or netstat on Windows.
2558
2643
  It prints the PID and command (the image name on Windows) for each stopped
2559
2644
  process. The listener's cwd can be anywhere: the named reservation is
2560
2645
  permission to stop that listener.
@@ -2646,7 +2731,7 @@ FLAGS
2646
2731
  --slot <name> only this slot's records plus, once it has launched, the
2647
2732
  shared untagged Metro and app client records (not the web
2648
2733
  page's); default also includes untagged legacy records
2649
- --source <s...> metro, client, device, build, agent, maintenance (one or more), or all.
2734
+ --source <s...> metro, client, device, build, agent, maintenance, placement (one or more), or all.
2650
2735
  An unknown value is REJECTED rather than quietly matching
2651
2736
  nothing.
2652
2737
  --level <l> minimum level: debug, info, warn, error, fatal
@@ -2783,6 +2868,12 @@ THE RECORD
2783
2868
  event the producer's own event name (bundle_build_done, client_log, ...)
2784
2869
  stack frames of { file, line, column, fn }, passed through as reported
2785
2870
  marker true on the records that close an error window
2871
+ runId the id of the stim invocation that wrote the record: STIM_RUN_ID
2872
+ when set to a valid id (letters, digits, . _ -, at most 64),
2873
+ else generated per run. Processes a command starts, such as the
2874
+ Metro supervisor and the collectors, keep the id of the command
2875
+ that started them. Debug records and the hello to stim-server
2876
+ carry it too, and stim-server puts it on its log lines.
2786
2877
  deviceTs Android logcat's original epoch milliseconds; ts is aligned to
2787
2878
  host time using a bounded clock query at each collector attachment
2788
2879
  clockOffsetMs the offset added to deviceTs; absent if the query failed.
@@ -2800,6 +2891,63 @@ Add --errors to --source maintenance to show only maintenance_failure events;
2800
2891
  failures before a later launch marker are hidden. Machine passes are reported in
2801
2892
  status and maintenance/maintenance.ndjson; this command reads workspace logs.
2802
2893
 
2894
+ REMOTE REQUEST RECORDS
2895
+ A request to another Mac that fails is a warn record with src: build in the
2896
+ run's build log: remote_connect_failed { host, capability, ms, timeoutMs, msg,
2897
+ code } (could not connect or say hello, e.g. "no reply in time" when the Mac
2898
+ did not answer hello) and remote_request_failed { method, ms, code, msg }.
2899
+ Timings of requests that succeed are debug records (below).
2900
+ stim-server writes to its service log (~/Library/Logs/Stim/<label>.log), with
2901
+ no tokens or tickets: "request failed method=<m> client=<device id> run=<runId>
2902
+ ms=<n> error=<code>" for an error reply (once a minute per client, method and
2903
+ code) and "host_connect ... error=<reason_with_underscores>" when its connection to a hosting
2904
+ Mac fails. With debug.logs on or STIM_DEBUG=1 for the server it logs every
2905
+ request instead: "debug request method=<m> client=<id> run=<runId> ms=<n>
2906
+ slow=true error=<code> whois=<ms> probe=<ms>" (slow means 1000 ms or more;
2907
+ whois and probe are the hello's Tailscale identity lookup and its wait for
2908
+ the host permission probe; a method name that is not a plain dotted lowercase
2909
+ name is logged as unknown)
2910
+ and "debug host_connect host=<mac> ms=<n> connectMs=<n> helloMs=<n>" (reused=true instead of the two
2911
+ timings when an open connection was shared), also as
2912
+ records in STIM_HOME/logs/debug/server.ndjson. Search the run id in both logs.
2913
+
2914
+ DEBUG LOGS
2915
+ With debug.logs on or STIM_DEBUG=1 (stim guide settings), the CLI also appends
2916
+ debug records to STIM_HOME/logs/debug/cli.ndjson, outside any workspace, so
2917
+ stim logs does not show them. Read them with jq:
2918
+ jq -c 'select(.event=="exec" and .ms>1000)' ~/.stim/logs/debug/cli.ndjson
2919
+ Events: run_start, run_end { exit, ms }, exec { program, ms, ok, exit? }, and
2920
+ remote_connect and remote_request { ms, ok, code? } for requests to another
2921
+ Mac. They carry no arguments, tokens or tickets.
2922
+
2923
+ Placement records use src: placement and sit in the run's build log
2924
+ (build-*.ndjson), so a new run replaces the previous run's. A run writes one
2925
+ record per decision it makes (a build that compiles and has a remote Mac to
2926
+ consider, or ios/android --remote auto), with event build_placement,
2927
+ device_placement or placement_fallback. A cache hit, a project with no paired
2928
+ remote Mac and a named --remote target write none:
2929
+ stim logs --source placement
2930
+ stim logs --source placement --json
2931
+ Fields: kind (build or device), platform, settings [{ key, value, from }] with
2932
+ from flag, env, setting or default (remote.build and remote.buildMode for a
2933
+ build, ios.remote or android.remote, plus remote.easFallback when it is on, for
2934
+ a device), candidates [{ machine, code,
2935
+ msg, detail? }], choice { machine, code, msg } with machine local when the run
2936
+ stays on this Mac, and fallback { code, msg, machine? } when a remote Mac that
2937
+ was meant to take the run did not (level warn). The msg is the same text the
2938
+ placement: and build: phase lines print.
2939
+ Candidate codes: accepted, unreachable, busy, disk, load, version-mismatch
2940
+ (detail lists the toolchain parts: xcode, arch, jdk, ...), no-matching-device,
2941
+ declined, memory, no-capacity; with machine eas, why an EAS Simulator was not
2942
+ used: eas-named-slot, eas-local-flags, eas-no-agent-device, eas-no-cli,
2943
+ eas-cli-too-old, eas-session-busy, eas-metro-unreachable, eas-logged-out, eas-not-enabled,
2944
+ eas-unavailable. Choice codes for a build: placed, named,
2945
+ forced, this-mac-busy, this-mac-free, mode-off, local-selected, no-remote-mac,
2946
+ unsupported; fallback codes: no-remote-mac-took-it, offload-failed, fallback.
2947
+ Choice codes for a device: placed, sticky, this-mac-free, no-remote-mac,
2948
+ device-count-unknown, no-host-admits, eas-fallback (machine eas). Machine names appear in these records;
2949
+ they stay in the local logs. These records carry no tokens.
2950
+
2803
2951
  WHAT WRITES WHAT
2804
2952
  maintenance.ndjson workspace maintenance actions, failures and explaining skips
2805
2953
  metro.ndjson the bundler, in both supervisor modes
@@ -3005,6 +3153,9 @@ Branch on the code, never on the message.`,
3005
3153
  an unavailable choice. A session that exists stays recorded even if delivery
3006
3154
  fails: retry stim ios|android --remote <machine>, or run stim stop to reconcile it.
3007
3155
  An unreachable stop keeps the placement; rerun stim stop when the host answers.
3156
+ To see why a host was slow or refused: stim logs --source placement (hosts checked and
3157
+ reason codes), stim logs --source build (remote_connect_failed, remote_request_failed),
3158
+ and the host's stim-server service log, searched for the run id (stim guide logs).
3008
3159
  A failed build handoff uses upload instead. If native log queries are unavailable,
3009
3160
  logs prints a note on stderr and shows copied records. Update an older stim-server
3010
3161
  on the host to enable handoff and native logs (hello features hosted-ios-data
@@ -3308,9 +3459,14 @@ Branch on the code, never on the message.`,
3308
3459
  works. See stim guide macos.`
3309
3460
  },
3310
3461
  STIM_OFFLOAD_REFUSED: {
3311
- summary: "the selected remote Mac cannot build this app",
3462
+ summary: "the selected remote Mac or automatic build pool cannot build this app",
3312
3463
  body: () => `STIM_OFFLOAD_REFUSED
3313
3464
 
3465
+ Automatic placement also refuses when local is excluded in remote.buildPoolDisabled
3466
+ and no enabled remote can finish this build, or when the build requires a local
3467
+ compiler. Enable local or choose an explicit --remote-build placement. Existing
3468
+ builds are not interrupted and a cache hit needs no compiler.
3469
+
3314
3470
  A named --remote-build selection is strict. The message names the machine
3315
3471
  and why it cannot take or finish the build: not configured or paired, approval
3316
3472
  pending or denied, unreachable or changed pinned identity, incompatible
@@ -3756,7 +3912,9 @@ so a Debug run on one is wired to a LAN origin instead of localhost.`,
3756
3912
  physical, hosted, remote, parked and other homes' devices and never deletes.
3757
3913
  A failed reclaim is logged and skipped; the run keeps waiting.
3758
3914
  Stop an environment (\`stim stop\`), pass a longer \`--wait <seconds>\`,
3759
- or raise concurrency.maxDevices. Waiting prints holder names and elapsed
3915
+ or raise concurrency.maxDevices. With --remote auto, remote.machines and
3916
+ the opt-in remote.easFallback (billed EAS Simulator) are tried before this
3917
+ wait; \`stim logs --source placement\` says why neither took the run. Waiting prints holder names and elapsed
3760
3918
  time; status JSON exposes build.waitingFor independently of phase.
3761
3919
  Stats records capacityWaits for waits and capacityRefusals for this code.
3762
3920
  See \`guide lifecycle concurrency\`.`
@@ -4043,6 +4201,17 @@ captured" (in metro.ndjson, bare RN)
4043
4201
  printed the last lines of the global workspace logs/supervisor.log above this -- read
4044
4202
  them. A cold Metro on a large graph can genuinely need more than the default
4045
4203
  60s: re-run with \`--wait 180\`. Otherwise \`stim stop\`, then \`start\`.`
4204
+ },
4205
+ STIM_PORT_INSPECTION_FAILED: {
4206
+ summary: "no free Metro port could be confirmed: netstat, lsof and loopback connects all failed",
4207
+ body: () => `STIM_PORT_INSPECTION_FAILED
4208
+ Stim reads the native TCP listener table (netstat on macOS and Windows,
4209
+ /proc/net on Linux) to find a free Metro port. When that table is denied,
4210
+ empty or unreadable, it checks each candidate with an lsof listener scan and
4211
+ connects to 127.0.0.1 and ::1. This refusal means none of them could answer,
4212
+ usually because a sandbox denies them. Allow Stim to run netstat or lsof, or
4213
+ to connect to loopback, then retry. A metro.port pin skips the scan for
4214
+ Metro. \`stim ports get\` prints the same message.`
4046
4215
  },
4047
4216
  STIM_SUPERVISOR_EXITED: {
4048
4217
  summary: "the dev server failed outright; the quoted supervisor.log tail is the real error",
@@ -4397,7 +4566,12 @@ not on any remote" (worktree remove)
4397
4566
  Warm copied ignored Pods from the source checkout, but their Manifest.lock
4398
4567
  differs from the tracked Podfile.lock in this worktree. Warm does not change
4399
4568
  tracked files. Run the printed pod-install command before building directly.
4400
- \`stim ios\` detects a mismatch and runs \`pod install\` for you.
4569
+ \`stim ios\` detects a mismatch and runs \`pod install\` for you. A mismatch
4570
+ limited to checksums of podspecs that embed the source checkout's path is
4571
+ resolved by warm itself ("carry moved <dir>/Pods to this checkout's path")
4572
+ and does not print this line. When the locks match but Pods still name the
4573
+ source path and warm cannot move it, it prints "removed <dir>/Pods/Manifest.lock
4574
+ so pod install runs" and \`stim ios\` runs \`pod install\`.
4401
4575
 
4402
4576
  "carry carried <dir>/Pods but there is no <dir>/Podfile.lock"
4403
4577
  Warm copied Pods but the destination has no Podfile.lock. Follow the printed
@@ -4523,6 +4697,22 @@ not on any remote" (worktree remove)
4523
4697
  Expo build-cache provider would each use a different store. Set the named
4524
4698
  variable to an absolute path, or unset it to use the default. Metro and the
4525
4699
  cache provider, which cannot refuse, ignore a relative value with a warning.`
4700
+ },
4701
+ STIM_WORKER_FAILED: {
4702
+ summary: "a programmatic API worker exited without returning an operation result",
4703
+ body: () => `STIM_WORKER_FAILED (programmatic API)
4704
+ The worker process ended without a structured result. StimError.details
4705
+ includes its final stderr output. Inspect that output and call diagnostics()
4706
+ for existing workspace logs, then stop() with a fresh signal to clean up
4707
+ any resources created before the failure. See stim guide api.`
4708
+ },
4709
+ STIM_RUN_FAILED: {
4710
+ summary: "a programmatic operation failed without a more specific Stim code",
4711
+ body: () => `STIM_RUN_FAILED (programmatic API)
4712
+ The underlying operation threw an error without a specific code. Its
4713
+ message and workspace log path are preserved in StimError. Read diagnostics()
4714
+ and call stop() in cleanup, including after a partial run. If the error is
4715
+ unexpected, report its message and logs at github.com/appandflow/stim/issues.`
4526
4716
  },
4527
4717
  STIM_NODE_UNSUPPORTED: {
4528
4718
  summary: "stim or stim-server started on a Node older than 22.12.0, often a project pin",
@@ -4806,6 +4996,46 @@ WAITING FOR A CHANGE
4806
4996
  still running, which it notices at the next change.
4807
4997
  Without --json it reprints the human view on change.`,
4808
4998
  sections: {
4999
+ ci: {
5000
+ summary: "Run an app and tests through @stim-cli/ci with diagnostics and scoped cleanup",
5001
+ body: () => `CONTINUOUS INTEGRATION
5002
+
5003
+ Install app dependencies and platform tools first. Use a dedicated checkout:
5004
+ cleanup stops that workspace, including resources created before a build fails.
5005
+
5006
+ npx --yes --package @stim-cli/ci stim-ci run --platform ios --project ./app --timeout 1800 -- pnpm test:e2e
5007
+
5008
+ Or install npm install --global @stim-cli/ci and use stim-ci directly.
5009
+ Platforms: ios, android, macos, web. Everything after -- is argv, not a shell.
5010
+ --artifacts selects the result directory (a fresh temporary directory by default).
5011
+ An explicit artifacts directory must be empty; use a new directory per run.
5012
+ Stdout is one JSON result; progress and test output go to stderr.
5013
+
5014
+ Tests receive STIM_CI_PLATFORM, STIM_CI_DEVICE_ID, STIM_CI_APP_ID,
5015
+ STIM_CI_METRO_PORT, STIM_CI_ARTIFACTS_DIR and STIM_CI_RUN_RESULT. The last is
5016
+ run.json with the exact public API result. Use that target and check app
5017
+ readiness; launched can still be bundling or unverified.
5018
+
5019
+ result.json preserves the test exit code even if diagnostics or cleanup fail.
5020
+ Passing tests with failed cleanup return 1; timeout returns 124; cancellation
5021
+ returns 130. Leave 70 seconds before the job's hard timeout for diagnostics and
5022
+ stop. SIGKILL and runner loss cannot run cleanup. Stop shuts down owned devices;
5023
+ it never deletes them.
5024
+
5025
+ GitHub-hosted jobs default to $RUNNER_TEMP/stim-ci/home and
5026
+ $RUNNER_TEMP/stim-ci/build-cache, reused between steps in the job. Detection
5027
+ requires GITHUB_ACTIONS=true, RUNNER_ENVIRONMENT=github-hosted and RUNNER_TEMP.
5028
+ An explicit --home or STIM_HOME keeps the selected home and its normal cache
5029
+ configuration; --build-cache or STIM_BUILD_CACHE overrides the cache path.
5030
+ Self-hosted runners and local runs keep normal Stim configuration. Other
5031
+ exclusive disposable providers can set their job-local paths explicitly.
5032
+ CI=true alone does not change defaults, ownership or coordination. Persist only
5033
+ cache artifacts, not state, claims or device ledgers. Independent homes must
5034
+ not share a writable filesystem cache because its claims live in the home.
5035
+
5036
+ The programmatic runner is import { runCI } from '@stim-cli/ci'. See its package
5037
+ README for result types, cancellation, cache policy, and coordination findings.`
5038
+ },
4809
5039
  "hosted-android": {
4810
5040
  summary: "Android on a named approved Mac: strict placement, private Metro, status and stop",
4811
5041
  body: () => `ANDROID ON A HOSTING MAC
@@ -4972,7 +5202,10 @@ there is a free concurrency.maxDevices slot (0 means unlimited), no device
4972
5202
  waiter ahead, normal host memory pressure, no budget shortfall,
4973
5203
  and 5-minute load per core below server.maxLoadPerCore. Without remote.machines,
4974
5204
  auto is local. An unknown local device count also stays local; boot admission
4975
- still decides. The default without a flag or setting stays local.
5205
+ still decides. The default without a flag or setting stays local. remote.devicePoolDisabled excludes
5206
+ members from new automatic placement only; an excluded local member is never a
5207
+ fallback. Existing sessions keep their owner and named placement bypasses the pool.
5208
+ See stim guide settings for separate build/device membership controls.
4976
5209
 
4977
5210
  Otherwise Stim asks every approved remote.machines host in parallel, with a
4978
5211
  3-second probe timeout and this run's model/runtime or image/profile selectors.
@@ -4983,9 +5216,26 @@ Hosts rank by the build preference (flag > STIM_REMOTE_BUILD > remote.build)
4983
5216
  when it names a machine, then lowest load, most free memory and remote.machines order. Automatic build
4984
5217
  offload resolves later, after the device architecture is known.
4985
5218
 
4986
- When no host admits, auto runs here and may wait in the existing FIFO device
5219
+ When no host admits and this Mac is at its concurrency.maxDevices cap (or runs
5220
+ are queued ahead), remote.easFallback (default false; EAS Simulator is billed)
5221
+ runs the device on an EAS Simulator exactly as --remote eas would: the build
5222
+ stays here, nothing starts an EAS cloud build, stop ends the session, and
5223
+ status reports it under remoteDevices. A busy Mac with a free slot never uses
5224
+ it. Before choosing it Stim checks, without starting a session, that eas-cli
5225
+ has simulator commands (and 22.2.0+ for --device-type), eas
5226
+ simulator:availability says this project's account can use it, agent-device
5227
+ is on PATH, the slot is default, no --runtime, --system-image or
5228
+ --device-profile flag is given, no EAS Simulator session of this workspace runs
5229
+ another platform or model, and a Debug run's Metro is reachable (not
5230
+ metro.tunnel off; on an Expo tunnel, run stim start --remote first). A recorded
5231
+ EAS session is not sticky: once this Mac has room, auto runs here and the
5232
+ session bills until stim stop. If any check fails, or the
5233
+ setting is off, auto runs here and may wait in the existing FIFO device
4987
5234
  slot queue; --no-wait and --wait 0 refuse with STIM_AT_CAPACITY. The placement
4988
- line explains the decision and the skipped hosts. JSON progress goes to stderr.
5235
+ line explains the decision, the skipped hosts and why EAS was not used
5236
+ (machine "eas"). JSON progress goes to stderr.
5237
+ The same decision, with a reason code per host, is a src: placement record in
5238
+ stim logs (stim guide logs).
4989
5239
  A recorded hosted session wins over load; a live local owned slot stays here.
4990
5240
  A stopped recorded session places again; unreachable or unknown sessions refuse.
4991
5241
 
@@ -5736,8 +5986,17 @@ PREDICTING THE NEXT BUILD (--plan)
5736
5986
  An Android plan reads the ABI from the emulator the slot records, or from
5737
5987
  the system image a new one would use. A plan refuses --device, --remote,
5738
5988
  --wait, --no-wait, --no-metro-check and --simulator-app with STIM_BAD_ARG.
5739
- Without --eas-profile it also refuses the ios.remote and android.remote
5740
- settings with STIM_BAD_ARG. Without --eas-profile an Android plan also
5989
+ With ios.remote or android.remote set to a Mac name, a plan reads the
5990
+ architecture or ABI from that Mac's device offer, the question a run asks
5991
+ before it reserves anything. With auto it repeats the placement decision a
5992
+ run would make now and reports where in the payload's placement field, for
5993
+ example "this Mac; auto may use janics-mac-mini". When the plan stays on
5994
+ this Mac and a listed Mac would build for another architecture, placement
5995
+ says so, since that Mac's key differs. The plan only predicts the key: a
5996
+ run reads the architecture of the device it actually gets. A Mac that does
5997
+ not answer, ios.remote or android.remote set to eas or proxy, or auto that
5998
+ would use an EAS Simulator now (remote.easFallback), refuses with
5999
+ STIM_BAD_ARG. Without --eas-profile an Android plan also
5741
6000
  refuses the experimental compiler CAS with STIM_BAD_ARG, and refuses with
5742
6001
  STIM_NO_DEVICE when no system image is installed.
5743
6002
 
@@ -6493,6 +6752,24 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
6493
6752
  overlapping a nested destination worktree or below a symlink ancestor.
6494
6753
  Tracked .idea settings come from Git and stay untouched by warm.
6495
6754
 
6755
+ A carried ios/Pods still names the source checkout's path in its generated
6756
+ files and in the checksum of a podspec that embeds that path (the
6757
+ precompiled ExpoModulesCore). When ios/Podfile.lock and the carried
6758
+ ios/Pods/Manifest.lock differ only by those checksums, warm rewrites the
6759
+ source path to the worktree's path in Pods/Target Support Files,
6760
+ Pods.xcodeproj, Local Podspecs and absolute symlinks, then makes
6761
+ Manifest.lock equal Podfile.lock, so the first \`stim ios\` skips
6762
+ \`pod install\`. Any other difference, a missing podspec, or a podspec
6763
+ that does not embed the source path, any other file in Pods that names
6764
+ the source path, or carried node_modules that do not match the worktree's
6765
+ lockfile leaves Pods as copied, and \`pod install\` runs. Warm does not edit Podfile.lock.
6766
+
6767
+ When the two locks already match but the copied Pods still name the source
6768
+ path, warm applies the same rewrite and scan so builds do not read the
6769
+ source checkout's files (entitlements, Podfile.properties.json). If it
6770
+ cannot (stale node_modules, a leftover path, an error), warm deletes
6771
+ ios/Pods/Manifest.lock so \`stim ios\` runs \`pod install\`.
6772
+
6496
6773
  Other generated state stays eligible: .gradle, .cxx, *.tsbuildinfo, build
6497
6774
  directories, and embedded JavaScript need project-specific decisions about
6498
6775
  regeneration. Native intermediates can record the source checkout's paths;
@@ -7042,35 +7319,66 @@ $STIM_HOME/archive by default. See stim guide cleanup archive.
7042
7319
 
7043
7320
  MAINTENANCE
7044
7321
 
7045
- Automatic maintenance is report-only in this release: it measures, plans and
7046
- logs, and never stops or deletes resources. The default mode is report; it is
7047
- off in CI and scoped STIM_HOME homes unless STIM_MAINTENANCE is explicit.
7048
- Commands trigger a detached pass when disk and memory checks (every minute)
7049
- or directory sizes (hourly) are due. guide, settings and help do not trigger
7322
+ Automatic maintenance has three modes. on (the default) runs the disk actions
7323
+ below through gc's own removal code and logs each one. report measures, plans
7324
+ and logs, and never deletes anything. off disables it. It is off in CI and
7325
+ scoped STIM_HOME homes unless STIM_MAINTENANCE is explicit, so a scratch home
7326
+ or CI run never deletes on its own.
7327
+ Commands trigger a detached pass when a check is due: disk and memory pressure
7328
+ (every minute), directory sizes (hourly), finished worktrees (every 15
7329
+ minutes) and the age sweep (daily). guide, settings and help do not trigger
7050
7330
  it; gc --delete also skips the hook. status --watch also triggers checks.
7051
- Size checks defer under high load. Attempts back off for at least one minute.
7052
- Measured directories are workspace build outputs and Stim's shared native,
7053
- Metro, ccache, Swift compilation and registered caches. Pressure checks read
7054
- free disk on the Stim home, projects and worker root volumes. On macOS the
7055
- memory signal is the sysctl pressure level, with no signal if sysctl fails.
7056
- Other platforms use os.freemem(); macOS never falls back to it.
7057
- Memory pressure is recorded only; memory stops are deferred to a later phase.
7331
+ Size, worktree and sweep checks defer under high load. Attempts back off for at
7332
+ least one minute. Measured directories are workspace build outputs and Stim's
7333
+ shared native, Metro, ccache, Swift compilation and registered caches.
7334
+ Pressure checks read free disk on the Stim home, projects and worker root
7335
+ volumes. On macOS the memory signal is the sysctl pressure level, with no
7336
+ signal if sysctl fails. Other platforms use os.freemem(); macOS never falls
7337
+ back to it. Memory pressure is recorded only; stopping devices, dev servers
7338
+ and helpers is not automatic.
7339
+
7340
+ In on mode a pass runs, cheapest to rebuild first:
7341
+ 1. orphaned workspace directories whose project is gone, and registered
7342
+ caches whose directory no longer exists
7343
+ 2. build outputs of idle workspaces, over maintenance.workspaceOutputsMaxGb,
7344
+ below the free-disk floor, or unused for maintenance.olderThanDays
7345
+ 3. build-cache and Metro entries, least recently used first, down to
7346
+ maintenance.capTargetPercent of caches.buildCacheMaxGb or
7347
+ caches.metroCacheMaxGb, and entries unused for olderThanDays
7348
+ 4. the Swift compilation cache, emptied whole, only below
7349
+ budget.hardFloorDiskGb and with no build lock or slot held
7350
+ 5. linked worktrees whose branch or pull request finished (gc's finished
7351
+ worktree rules and gc.worktreeGraceMinutes; maintenance.removeFinishedWorktrees)
7352
+ Disk-driven steps stop once free space is back above the floor. Kept for
7353
+ every pass: a workspace that is in use, holds a lock, was used within
7354
+ maintenance.protectRecentHours, is pinned with maintenance.keep, or is the
7355
+ workspace of the command that started the pass; cache entries used within
7356
+ protectRecentHours, named by a project's last builds, a parked device or a
7357
+ live build lock, or in a Metro store with a running or unverifiable dev
7358
+ server. Anything the pass cannot verify is kept. A pass never shuts down idle devices or dev servers or stops watchman; removing a
7359
+ finished worktree or orphaned directory does tear down that workspace's own
7360
+ dev server, owned devices and Chrome profile, as gc --delete does. ccache evicts by itself under
7361
+ caches.ccacheMaxGb.
7058
7362
 
7059
7363
  stim status last checks, plan and running pass
7060
7364
  stim status --json maintenance observations and recent records
7061
7365
  stim logs --source maintenance this workspace's maintenance records
7062
7366
  stim gc live pressure and cached-size preview
7063
- stim settings set maintenance.mode off
7367
+ stim settings set maintenance.mode off turn automatic cleanup off
7368
+ stim settings set maintenance.mode report plan and log, delete nothing
7064
7369
 
7065
7370
  The machine log is $STIM_HOME/maintenance/maintenance.ndjson, rotated at
7066
7371
  maintenance.logMaxMb with the old generation retained for
7067
7372
  maintenance.logRetentionDays. Child crashes use maintenance/child.log.
7068
- Actions and explaining skips are logged only when newly planned. A pass is
7069
- logged after a size check or a change to actions, skips or blocked reasons.
7373
+ In report mode actions and explaining skips are logged only when newly
7374
+ planned; in on mode every action taken, failure and explaining skip is logged
7375
+ with its bytes, and a pass record carries the bytes freed. Removal of a
7376
+ worktree or orphaned directory is logged to the machine log only.
7070
7377
  maintenance.logChecks enables debug observations; default false.
7071
7378
  status and doctor report invalid settings and unresolved claims with a removal
7072
7379
  command to run only after confirming the holder is gone. Invalid cache caps
7073
7380
  fall back to their own defaults; invalid maintenance settings disable passes.
7381
+ Pin a workspace with \`stim settings set maintenance.keep true\` in its project.
7074
7382
  The run claim serializes passes with gc --delete; gc refuses a held claim
7075
7383
  with the holder and recovery guidance instead of waiting.
7076
7384
 
@@ -7661,7 +7969,23 @@ THE ONE CASE GC WILL NOT REAP
7661
7969
 
7662
7970
  --older-than does not apply to these kinds and is refused with
7663
7971
  STIM_BAD_ARG. With STIM_HOME set, gc skips them: they are machine-global.
7664
- \`stim doctor\` notes a watchman footprint over 2 GiB.`
7972
+ \`stim doctor\` notes a watchman footprint over 2 GiB.
7973
+
7974
+ A checkout that holds its linked worktrees inside it, such as
7975
+ .worktrees/<name>, makes a watchman root there crawl every worktree's files,
7976
+ node_modules and build output. \`stim doctor\` reports each such checkout
7977
+ whose .watchmanconfig ignore_dirs does not exclude them, as
7978
+ ${WATCHMAN_NESTED_WORKTREES}, and says when watchman watches it now.
7979
+ \`stim doctor --fix\` merges the worktrees' shared parent (.worktrees) when
7980
+ git tracks nothing in it, otherwise each worktree path, into that
7981
+ checkout's .watchmanconfig and keeps every other key; it refuses a file
7982
+ that is not a JSON object or whose ignore_dirs is not an array. Watchman
7983
+ matches entries literally: \`.worktrees/\` or \`./.worktrees\` ignores
7984
+ nothing. Commit the file. Watchman reads ignore_dirs only from a root's
7985
+ .watchmanconfig, never from a global config, and only when it adds the
7986
+ root, so an existing root needs \`watchman watch-del <root>\` before the
7987
+ ignore applies. Doctor never runs watch-del or shutdown-server; a watch-del
7988
+ plus a watchman restart frees the memory now.`
7665
7989
  },
7666
7990
  disk: {
7667
7991
  summary: "disk usage, workspace build outputs, logs and device recordings, AVD and build-log sizes, the data partition, trimming the shared caches",
@@ -7918,7 +8242,8 @@ KEYS STIM READS
7918
8242
  ios.remote "proxy", "eas", or a named approved Mac from
7919
8243
  remote.machines, with the same meaning as --remote.
7920
8244
  "auto" places on an approved Mac when this Mac is full or
7921
- busy. Unset runs here. See lifecycle hosted-ios.
8245
+ busy. Unset runs here; "local" runs here even when a
8246
+ lower layer says otherwise. See lifecycle hosted-ios.
7922
8247
  ios.simslimProfile a SimSlim JSON profile under the app directory,
7923
8248
  at most 64 KiB. Install the
7924
8249
  external tool once with
@@ -8060,8 +8385,23 @@ ${ANDROID_AVD_CONFIG_HELP.map((line) => ` ${line}`).joi
8060
8385
  means the debug keystore's fixed "android".
8061
8386
  android.remote "proxy", "eas", or a named approved Mac in
8062
8387
  remote.machines; "auto" places on an approved Mac when
8063
- this Mac is full or busy. Unset runs here.
8388
+ this Mac is full or busy. Unset runs here; "local"
8389
+ runs here even when a lower layer says otherwise.
8064
8390
  See lifecycle hosted-android.
8391
+ remote.easFallback true lets "auto" (ios.remote, android.remote or
8392
+ --remote auto) run the simulator or emulator on a
8393
+ billed EAS Simulator when this Mac is at its
8394
+ concurrency.maxDevices cap (or has runs queued) and no
8395
+ remote.machines Mac takes the run, instead of waiting
8396
+ or refusing with STIM_AT_CAPACITY. Default false. A
8397
+ busy Mac with a free slot never uses it. Stim checks
8398
+ first that the run could use --remote eas (eas-cli
8399
+ with simulator commands, eas simulator:availability,
8400
+ agent-device, the default slot, a reachable Metro);
8401
+ otherwise it waits or refuses as before. Machine or
8402
+ project scope; a committed value opts in everyone who
8403
+ runs the app. Only the user enables it. See lifecycle
8404
+ hosted-ios.
8065
8405
  metro.tunnel selects how a remote device reaches this workspace's
8066
8406
  Metro after remote intent exists. Plain \`start\` stays
8067
8407
  local. For Expo and bare React Native, "auto" (default)
@@ -8267,12 +8607,26 @@ overrides the file:
8267
8607
  budget is off. A value of the wrong shape refuses start, ios and android with
8268
8608
  STIM_BAD_ARG. See \`guide lifecycle budget\` for what each limit reclaims.
8269
8609
 
8610
+ DEBUG LOGS ARE MACHINE-LEVEL AND OFF BY DEFAULT
8611
+ debug.logs STIM_DEBUG default false. STIM_DEBUG=1 (or 0) overrides the setting
8612
+ for one command. While on, the CLI writes debug records to
8613
+ STIM_HOME/logs/debug/cli.ndjson (run_start, run_end, exec with the program name,
8614
+ duration and exit status but never arguments, remote_connect and
8615
+ remote_request with durations and codes); stim-server writes server.ndjson there
8616
+ and logs every request with its timings to its service log. The file rotates at about 8 MiB and keeps one previous
8617
+ generation, and nothing is sent anywhere. Keys named like a secret are redacted.
8618
+ stim settings set debug.logs true --scope machine
8619
+ See \`guide logs\` for reading the files.
8620
+
8270
8621
  AUTOMATIC MAINTENANCE IS MACHINE-LEVEL
8271
- Report-only in this release: it measures and plans, and deletes or stops nothing.
8622
+ maintenance.mode on (the default) removes what gc would under the caps and
8623
+ floors below; report measures and plans and deletes nothing; off disables it.
8272
8624
  STIM_HOME and CI make the mode off unless STIM_MAINTENANCE is set.
8625
+ maintenance.keep is a project setting: pin a workspace so a pass never clears
8626
+ its build outputs or removes its worktree.
8273
8627
  See \`guide cleanup\` for checks, plans and log paths.
8274
8628
 
8275
- ${SETTINGS.filter((setting) => setting.key.startsWith("maintenance.") || /^caches\..*MaxGb$/.test(setting.key)).map((setting) => ` ${setting.key} ${setting.env} ${setting.default === void 0 ? "unset" : `default ${setting.default}`}
8629
+ ${SETTINGS.filter((setting) => setting.key.startsWith("maintenance.") || /^caches\..*MaxGb$/.test(setting.key)).map((setting) => ` ${setting.key} ${setting.env ?? "project setting"} ${setting.default === void 0 ? "unset" : `default ${setting.default}`}
8276
8630
  ${setting.description}`).join("\n")}
8277
8631
 
8278
8632
  Maintenance uses the sysctl pressure level on macOS, with no memory signal
@@ -8296,8 +8650,9 @@ To show the booted simulator in Stim Desktop and open no simulator window:
8296
8650
 
8297
8651
  { "iosSimulatorApp": "stim-desktop" }
8298
8652
 
8299
- Stim Desktop selects the workspace that owns the simulator and focuses that
8300
- device. It only displays the simulator; it never boots or shuts it down.
8653
+ Stim Desktop keeps the current page and shows a launch card. Clicking Show
8654
+ opens the workspace that owns the simulator and focuses that device.
8655
+ It only displays the simulator; it never boots or shuts it down.
8301
8656
  When Stim Desktop is not running, Stim starts it without the command's
8302
8657
  \`STIM_HOME\`, so it reads the same Stim home as when you open it yourself.
8303
8658
  It shows only devices from that home: under another \`STIM_HOME\`, pick
@@ -8326,7 +8681,8 @@ headlessly and show it in Stim Desktop:
8326
8681
  { "androidEmulatorApp": "stim-desktop" }
8327
8682
 
8328
8683
  Stim then starts the emulator with \`-no-window -gpu host\` and opens
8329
- \`stim-desktop://open?serial=<serial>\` in the background. Stim Desktop reads
8684
+ \`stim-desktop://open?serial=<serial>\` in the background. Stim Desktop shows a
8685
+ launch card and opens the emulator only when Show is clicked. It reads
8330
8686
  frames and sends input over the emulator's gRPC endpoint. The setting applies
8331
8687
  only when Stim boots the emulator: one that is already running keeps its
8332
8688
  current display until it next boots, and physical devices are unaffected.
@@ -8409,6 +8765,32 @@ simulator sessions on, by MagicDNS name with an optional serve port (default
8409
8765
  stim settings set remote.machines '["janics-mac-mini"]'
8410
8766
  stim doctor --fix
8411
8767
 
8768
+ Automatic membership is separate from approval. In Desktop Settings > Remote Macs,
8769
+ use Automatic builds and Automatic simulators for this Mac or a configured remote.
8770
+ The equivalent machine settings list the excluded members; both default to []:
8771
+
8772
+ stim settings set remote.buildPoolDisabled '["local"]'
8773
+ stim settings set remote.devicePoolDisabled '["janics-mac-mini"]'
8774
+ stim settings unset remote.buildPoolDisabled
8775
+
8776
+ Use local for this Mac and exact remote.machines entries for remotes, including
8777
+ case and port. Names are not trimmed; unmatched entries exclude nothing. Copy the
8778
+ configured name or use Desktop's switches. Each pool
8779
+ must retain local or at least one configured remote already approved for that role.
8780
+ Offline approved members count as configured members, but placement still requires
8781
+ an available compatible host. Settings refuses removing the last member, including
8782
+ removing it from remote.machines. Disabling does not unpair a machine or stop a run.
8783
+
8784
+ These settings apply only to new automatic work requested by this Mac. They do not
8785
+ change which work other requesters send to a host. Named placement and --remote-build
8786
+ local bypass membership; the default device placement without --remote auto remains
8787
+ local. Existing local and hosted device sessions keep their owner. Cache hits remain
8788
+ usable without compiling. If local is excluded, automatic placement cannot fall back
8789
+ to a local compile or boot; unavailable hosts and unsupported offloads refuse. Excluding
8790
+ local alone does not trigger billed EAS fallback; an existing explicit EAS opt-in still
8791
+ requires the physical device cap or queue condition. Restore membership before retrying or choose
8792
+ an explicit placement. Build and simulator memberships are independent.
8793
+
8412
8794
  A remote Mac is used for a capability only after it grants that approval. Build
8413
8795
  and device-host approvals are separate: a Mac in the list that never granted
8414
8796
  one is not an error, it is not used for that capability, and doctor reports
@@ -8418,7 +8800,7 @@ DEVICE HOSTING
8418
8800
  Only \`doctor --fix\` asks for device-host access. A person on that Mac
8419
8801
  approves the printed id with \`stim-server devices grant <id> --device-host\`.
8420
8802
  A person can also run \`stim-server setup\` on the worker: one node, one ticket,
8421
- one expiry, at most one request per capability, with per-grant y/N in a
8803
+ one expiry, at most one request per capability, with a per-grant Y/n question (Enter approves) in a
8422
8804
  terminal or \`--yes\` otherwise. Agents never run \`stim-server setup\` or
8423
8805
  approve requests. Setup never changes TCC or enables Funnel; an SSH-driven
8424
8806
  run is not offered. Without a terminal or --yes, setup refuses before installing
@@ -8500,7 +8882,7 @@ value is unset. Trimmed auto/local are case-insensitive. Machine names match
8500
8882
  configured names case-insensitively, with port 7443 when omitted; reports use
8501
8883
  the configured entry.
8502
8884
 
8503
- auto follows remote.buildMode and keeps its local fallback behavior
8885
+ auto follows remote.buildMode, considering only enabled automatic pool members
8504
8886
  local builds only on this Mac for this invocation
8505
8887
  name requires a matching entry in remote.machines, already paired and
8506
8888
  approved for builds; ignores remote.buildMode and this Mac's load/slot gating
@@ -8539,7 +8921,7 @@ emulator debug build or a stim macos SwiftPM Debug build compiles:
8539
8921
  Mac's. A Mac too old to report its load counts only while every
8540
8922
  slot here is busy.
8541
8923
  force on a remote Mac whenever one accepts it
8542
- off always here
8924
+ off here when local remains enabled in the automatic build pool
8543
8925
 
8544
8926
  Load per core is the 5-minute load average divided by the CPU count; a Mac's
8545
8927
  native builds are its Stim runs in prebuild, pods or compile on that Mac, not
@@ -8563,10 +8945,12 @@ preferring the one that already holds this repository, then the least loaded,
8563
8945
  and moves to the next one in that order when a machine that offered fails the
8564
8946
  sync or refuses to start the build.
8565
8947
  For iOS the machine's Xcode and simulator SDK must match, and it needs an
8566
- iPhone simulator on the target runtime. Its CocoaPods must match too, unless
8948
+ iPhone simulator on the target runtime. Its project-selected CocoaPods must match too, unless
8567
8949
  the app's Gemfile.lock pins CocoaPods: both Macs then run that version through
8568
8950
  bundler, so the machine needs only Bundler on its stim-server PATH and
8569
- installs the pinned gems itself on the first build. For Android its JDK major
8951
+ installs the pinned gems itself on the first build. The comparison selects
8952
+ the app's .ruby-version when installed, with pod install's UTF-8 locale defaults.
8953
+ For Android its JDK major
8570
8954
  version must match, and its Android SDK must hold the NDK, build-tools and
8571
8955
  compile platform that the project's React Native version names in
8572
8956
  gradle/libs.versions.toml; Gradle and AGP come from the synced project. When
@@ -9157,7 +9541,14 @@ macos.arguments is an array of arguments passed directly to the executable:
9157
9541
 
9158
9542
  The plist must contain CFBundleIdentifier and CFBundleExecutable matching the
9159
9543
  product. Use a development plist without shared URL schemes or an update feed.
9160
- Stim gives the copied bundle a workspace-specific identifier. SwiftPM resource
9544
+ Stim gives the copied bundle a workspace-specific identifier and name: it sets
9545
+ CFBundleDisplayName and CFBundleName on the copy to "<product> \u00b7 <label>",
9546
+ where label is the owned-device label (worktree and app directory names, cleaned
9547
+ to letters, digits, . _ -; at most 24 characters, cut with an ellipsis). The Dock,
9548
+ Cmd-Tab and lsappinfo show it, so several runs tell apart; the process name in
9549
+ System Events stays the executable. The project plist and the app's window titles
9550
+ are untouched. The name is displayName in the launch payload and status. Hosted
9551
+ (--remote) and offloaded builds get the same name. SwiftPM resource
9161
9552
  bundles and frameworks in the reported build directory are copied into it;
9162
9553
  macos.resources maps destinations under Contents/Resources to file or directory
9163
9554
  sources relative to the Swift Package directory, for example:
@@ -9181,26 +9572,48 @@ executables, including Stim Desktop's sim-fold helper, are not built.
9181
9572
 
9182
9573
  stim macos # fixed SwiftPM Debug build, then launch
9183
9574
  stim macos --json # one launch record; progress goes to stderr
9575
+ stim macos --plan --json # validate the next build without running it
9184
9576
  stim status --json # environments[].macos and build state
9185
9577
  stim logs --source build
9186
9578
  stim logs --errors
9187
9579
  stim stop # stop this workspace's owned app and supervisor
9188
9580
 
9189
- Each invocation stops its previous owned app, rebuilds and launches. SwiftPM
9581
+ Each invocation validates the project settings, development plist and resource
9582
+ entries before stopping its previous owned app, rebuilding and launching. SwiftPM
9190
9583
  keeps incremental outputs in the workspace's runtime directory under STIM_HOME.
9584
+ Every process running an app bundle from that directory is the workspace's
9585
+ owned app, including copies opened through LaunchServices (open, agent-device
9586
+ open). stim macos, stim stop, stim worktree remove and stim gc --delete stop all
9587
+ of them: SIGTERM, up to 5 s, then SIGKILL, and success only once each has
9588
+ exited. When an app cannot be verified or does not exit, stop reports
9589
+ STIM_MACOS_OWNER_UNVERIFIED or a failure, and removal keeps the workspace.
9191
9590
  Local stim macos starts the app in the background without activating it or
9192
9591
  changing focus: it sets STIM_BACKGROUND_LAUNCH=1 in the app's environment, which
9193
- Stim Desktop honors. An app that activates itself at launch still takes focus.
9592
+ Stim Desktop honors, including for reopen events from open -g. An app that
9593
+ activates itself at launch or on reopen still takes focus.
9194
9594
  Hosted launches (macos --remote) do not set it.
9195
- macOS artifacts are not cached. This prototype has no --plan, --slot, or reload command.
9595
+ macOS artifacts are not cached. This prototype has no --slot or reload command.
9596
+ --plan validates the Swift Package directory, macos settings, development plist
9597
+ and declared resources without building, signing, staging, stopping or launching
9598
+ an app, writing workspace state or claiming a build slot. It does not execute
9599
+ Package.swift, so it cannot validate the executable product or package dependencies.
9600
+ --remote-build is respected, including named-machine setup refusals; no worker
9601
+ is contacted and live worker availability is unknown. --remote is launch-only
9602
+ and refuses with --plan. The JSON plan has platform "macos", product and
9603
+ buildMachine; fingerprint, cacheKey, provider, prebuild, outcome and expectedMs
9604
+ are null, cacheHit and cacheSkipped are false, and basis is 0. SwiftPM decides
9605
+ incremental compile work when a build runs; the plan predicts no cache outcome
9606
+ or duration. Desktop checks automatically while build details are visible,
9607
+ reusing a completed build or check for 60 seconds and skipping running builds.
9196
9608
  A failed build records its error and compiler output without launching an app.
9197
9609
  While it runs, stim status --json reports it as environments[].build with
9198
9610
  platform "macos": phase prepare, compile, install, then launch, and during
9199
9611
  compile detail.unit "steps" with SwiftPM's [done / total] counts (fetching and
9200
9612
  planning are detail.step "configure"). It has no cache lookup, so outcome and
9201
9613
  plannedPhases are null; finished runs and their phase times are in
9202
- environments[].builds.macos. An offloaded build reports the worker's steps the
9203
- same way.
9614
+ environments[].builds.macos, where phases includes launch once the launch step finishes
9615
+ and compileSteps is SwiftPM's step total. An offloaded build reports the worker's
9616
+ steps the same way.
9204
9617
  Runtime stdout and stderr become client records; build output becomes build
9205
9618
  records, all with platform "macos". Unexpected app exits are error records.
9206
9619
  Stim runs the app with NSUnbufferedIO=YES, so Swift print output arrives per
@@ -9217,7 +9630,10 @@ refuse before stopping the app or changing its build record. See stim guide erro
9217
9630
 
9218
9631
  With remote.build auto, remote.buildMode places these SwiftPM Debug builds:
9219
9632
  auto builds here while this Mac has capacity, force uses an approved build
9220
- machine when one accepts, and off always builds here. Configure remote.machines
9633
+ machine when one accepts, and off builds here when local remains in the automatic
9634
+ build pool. remote.buildPoolDisabled excludes members from automatic placement,
9635
+ including its local fallback. Explicit local or named placement bypasses membership.
9636
+ Configure remote.machines
9221
9637
  and approve build access as described in stim guide settings. The worker needs
9222
9638
  matching Stim, CPU architecture, Xcode and macOS SDK, plus network access to
9223
9639
  fetch package dependencies the first time. It keeps SwiftPM dependencies in a
@@ -9535,295 +9951,153 @@ this workspace's app. Do not change permissions or use custom build scripts."
9535
9951
  };
9536
9952
  //#endregion
9537
9953
  //#region src/guide/tutorial-data.ts
9538
- const TUTORIAL_PINS = {
9539
- createExpoApp: "5.0.0",
9540
- template: "expo-template-blank@58.0.15"
9541
- };
9542
- const TUTORIAL_FILES = {
9543
- "App.js": `import { useEffect, useState } from 'react';
9544
- import { Pressable, StyleSheet, Text, View } from 'react-native';
9545
- import { StatusBar } from 'expo-status-bar';
9546
- import { TITLE_COLOR } from './theme';
9547
-
9548
- const TAG = '[stim:tutorial]';
9549
-
9550
- function Button({ label, onPress }) {
9551
- return (
9552
- <Pressable accessibilityRole="button" onPress={onPress} style={styles.button}>
9553
- <Text style={styles.buttonText}>{label}</Text>
9554
- </Pressable>
9555
- );
9556
- }
9557
-
9558
- export default function App() {
9559
- const [note, setNote] = useState('');
9560
-
9561
- useEffect(() => {
9562
- console.log(\`\${TAG} title color=\${TITLE_COLOR}\`);
9563
- }, [TITLE_COLOR]);
9564
-
9565
- const logError = () => {
9566
- console.error(\`\${TAG} error-button test error\`);
9567
- setNote('Logged an error.');
9568
- };
9569
-
9570
- const crash = () => {
9571
- setTimeout(() => {
9572
- throw new Error(\`\${TAG} crash-button uncaught test error\`);
9573
- }, 0);
9574
- };
9575
-
9576
- const slowRequest = async () => {
9577
- const started = Date.now();
9578
- setNote('Waiting 3 seconds...');
9579
- await new Promise((resolve) => setTimeout(resolve, 3000));
9580
- const elapsed = Date.now() - started;
9581
- console.warn(\`\${TAG} slow-request \${elapsed}ms\`);
9582
- setNote(\`Slow request took \${elapsed}ms.\`);
9583
- };
9584
-
9585
- return (
9586
- <View style={styles.container}>
9587
- <Text style={[styles.title, { color: TITLE_COLOR }]}>Stim Tutorial</Text>
9588
- <Text style={styles.body}>Tap a button, then look at Stim Desktop &gt; Logs.</Text>
9589
- <Button label="Log an error" onPress={logError} />
9590
- <Button label="Crash me" onPress={crash} />
9591
- <Button label="Slow request" onPress={slowRequest} />
9592
- <Text style={styles.note}>{note}</Text>
9593
- <StatusBar style="auto" />
9594
- </View>
9595
- );
9596
- }
9597
-
9598
- const styles = StyleSheet.create({
9599
- container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', padding: 24 },
9600
- title: { fontSize: 32, fontWeight: '700', marginBottom: 8 },
9601
- body: { fontSize: 16, color: '#4b5563', textAlign: 'center', marginBottom: 24 },
9602
- button: { backgroundColor: '#111827', borderRadius: 10, paddingVertical: 14, paddingHorizontal: 28, marginBottom: 12 },
9603
- buttonText: { color: '#fff', fontSize: 17, fontWeight: '600' },
9604
- note: { marginTop: 12, fontSize: 15, color: '#374151' },
9605
- });
9606
- `,
9607
- "theme.js": `export const TITLE_COLOR = '#1f2937';
9608
- `,
9609
- "app.json": `{
9610
- "expo": {
9611
- "name": "Stim Tutorial",
9612
- "slug": "stim-tutorial",
9613
- "version": "1.0.0",
9614
- "orientation": "portrait",
9615
- "icon": "./assets/icon.png",
9616
- "userInterfaceStyle": "light",
9617
- "ios": {
9618
- "supportsTablet": true,
9619
- "bundleIdentifier": "dev.stim.tutorial"
9620
- },
9621
- "android": {
9622
- "adaptiveIcon": {
9623
- "backgroundColor": "#E6F4FE",
9624
- "foregroundImage": "./assets/android-icon-foreground.png",
9625
- "backgroundImage": "./assets/android-icon-background.png",
9626
- "monochromeImage": "./assets/android-icon-monochrome.png"
9627
- },
9628
- "package": "dev.stim.tutorial"
9629
- },
9630
- "web": {
9631
- "favicon": "./assets/favicon.png"
9632
- },
9633
- "extra": {
9634
- "stimTutorial": 1
9635
- }
9636
- }
9637
- }
9638
- `,
9639
- ".gitignore": `/ios
9640
- /android
9641
- /tutorial*.ad
9642
- /tutorial*.png
9643
- `
9644
- };
9645
- const TUTORIAL_PROMPTS = {
9646
- begin: "Run the Stim tutorial.",
9647
- rebuild: "Continue the Stim tutorial: rebuild",
9648
- agent: "Continue the Stim tutorial: agent",
9649
- refresh: "Continue the Stim tutorial: refresh",
9650
- machine: "Continue the Stim tutorial: machine",
9651
- finish: "Continue the Stim tutorial: finish"
9652
- };
9954
+ const TUTORIAL_REPO = "appandflow/stim-tutorial";
9955
+ const TUTORIAL_BUNDLE_ID = "dev.stim.tutorial";
9653
9956
  const TUTORIAL_RESTART_PROMPT = "Restart the Stim tutorial.";
9957
+ /**
9958
+ * The requests Stim Desktop offers to copy, written the way a developer would ask. {base}, {tour}, {worktrees} and
9959
+ * {machine} are filled in by Desktop. Only the first and last name a guide section, which holds the folder and cleanup safety rules.
9960
+ */
9961
+ const TUTORIAL_ASKS = {
9962
+ begin: `Clone ${TUTORIAL_REPO} into {base} and install its dependencies, then run stim doctor for iOS there so Stim registers it. Use a fresh folder: if {base} already exists or is inside another git repository, stop and ask me for another folder, and never git add in my own repo. Follow stim guide tutorial run.`,
9963
+ agent: "Open the app on the iOS simulator, take a screenshot and confirm the title color.",
9964
+ machine: "Build the app for iOS on {machine} with stim instead of on this Mac. Do not approve or pair anything.",
9965
+ finish: "I'm done with these experiments in {base} and don't need the changes. Stop the apps and remove the worktrees {worktrees}, and keep the clone. Follow stim guide tutorial finish.",
9966
+ share: `Open a pull request to ${TUTORIAL_REPO} with my title color change, and include a screenshot of it running in the simulator. See stim guide tutorial share.`,
9967
+ retry: "The first iOS build of the tutorial app in {tour} failed. Find out why and run it on iOS again."
9968
+ };
9654
9969
  const TUTORIAL_STEPS = [
9655
9970
  {
9656
9971
  id: "begin",
9657
- title: "Create the tutorial",
9972
+ title: "Get the Test App",
9658
9973
  who: "agent",
9659
9974
  optional: false,
9660
- prompt: TUTORIAL_PROMPTS.begin,
9975
+ ask: TUTORIAL_ASKS.begin,
9661
9976
  section: "run",
9662
- manual: [
9663
- "base=\"{base}\"",
9664
- "mkdir -p \"${base%/*}\"",
9665
- "if [ ! -e \"$base\" ]; then",
9666
- "cd \"${base%/*}\"",
9667
- "if git -C \"${base%/*}\" rev-parse --is-inside-work-tree >/dev/null 2>&1; then echo \"Stop: inside another repository. Ask for another folder; never git add in the user repository.\"; exit 1; fi",
9668
- `npx --yes create-expo-app@${TUTORIAL_PINS.createExpoApp} stim-tutorial --template ${TUTORIAL_PINS.template} --no-install --no-agents-md --yes`,
9669
- "cd \"$base\"",
9670
- "stim guide tutorial app",
9671
- ...Object.entries(TUTORIAL_FILES).flatMap(([name, content]) => [`cat ${name === ".gitignore" ? ">>" : ">"} ${name} <<'STIM_TUTORIAL_EOF'`].concat(content.trimEnd().split("\n"), "STIM_TUTORIAL_EOF")),
9672
- "npm install --prefer-offline",
9673
- "npm pkg set scripts.ios=\"expo run:ios\" scripts.android=\"expo run:android\"",
9674
- "git init",
9675
- "git add -A",
9676
- "git -c user.name=Stim -c user.email=stim@localhost -c commit.gpgsign=false commit -m \"Stim tutorial\"",
9677
- "else",
9678
- "cd \"$base\"",
9679
- `node -e 'const fs = require("node:fs"); const app = fs.existsSync("app.json") ? JSON.parse(fs.readFileSync("app.json", "utf8")) : null; if (app?.expo?.extra?.stimTutorial !== ${JSON.parse(TUTORIAL_FILES["app.json"]).expo.extra.stimTutorial}) { console.error("Stop: existing non-tutorial folder or another version. Ask for another folder; never overwrite or delete it."); process.exit(1); }'`,
9680
- `git rev-parse --show-toplevel | node -e 'const fs = require("node:fs"); const root = fs.readFileSync(0, "utf8").trim(); if (fs.realpathSync(root) !== fs.realpathSync(process.cwd())) { console.error("Stop: inside another repository. Ask for another folder; never git add in the user repository."); process.exit(1); }'`,
9681
- "fi"
9682
- ]
9683
- },
9684
- {
9685
- id: "sidebar",
9686
- title: "Workspace in sidebar",
9687
- who: "you",
9688
- optional: false,
9689
- prompt: null,
9690
- section: null,
9691
- manual: []
9977
+ commands: []
9692
9978
  },
9693
9979
  {
9694
9980
  id: "build",
9695
- title: "First iOS build",
9981
+ title: "Make a Change",
9696
9982
  who: "you",
9697
9983
  optional: false,
9698
- prompt: null,
9984
+ ask: null,
9699
9985
  section: null,
9700
- manual: [
9701
- "cd \"{base}\"",
9702
- "git worktree add -B stim-tutorial/tour \"{tour}\" HEAD",
9703
- "cd \"{tour}\"",
9704
- "stim worktree warm",
9705
- "stim guide agent",
9706
- "stim doctor --platform ios",
9707
- "stim start",
9708
- "stim ios"
9709
- ]
9986
+ commands: []
9710
9987
  },
9711
9988
  {
9712
- id: "rebuild",
9713
- title: "Rebuild from cache",
9714
- who: "agent",
9715
- optional: false,
9716
- prompt: TUTORIAL_PROMPTS.rebuild,
9717
- section: "rebuild",
9718
- manual: [
9719
- "cd \"{tour}\"",
9720
- "stim ios",
9721
- "stim status --json"
9722
- ]
9723
- },
9724
- {
9725
- id: "device",
9726
- title: "Live view and control",
9989
+ id: "parallel",
9990
+ title: "Change It Again in Parallel",
9727
9991
  who: "you",
9728
9992
  optional: false,
9729
- prompt: null,
9993
+ ask: null,
9730
9994
  section: null,
9731
- manual: ["stim status"]
9995
+ commands: []
9732
9996
  },
9733
9997
  {
9734
- id: "logs",
9735
- title: "App logs",
9998
+ id: "device",
9999
+ title: "Live View and Control",
9736
10000
  who: "you",
9737
- optional: false,
9738
- prompt: null,
10001
+ optional: true,
10002
+ ask: null,
9739
10003
  section: null,
9740
- manual: ["stim logs --errors", "stim logs --grep '\\[stim:tutorial\\]'"]
10004
+ commands: ["stim status"]
9741
10005
  },
9742
10006
  {
9743
10007
  id: "agent",
9744
- title: "Agent actions and replay",
10008
+ title: "Agent Actions and Replay",
9745
10009
  who: "agent",
9746
- optional: false,
9747
- prompt: TUTORIAL_PROMPTS.agent,
9748
- section: "agent",
9749
- manual: [
10010
+ optional: true,
10011
+ ask: TUTORIAL_ASKS.agent,
10012
+ section: null,
10013
+ commands: [
9750
10014
  "cd \"{tour}\"",
9751
10015
  "export AGENT_DEVICE_STATE_DIR=\"{stateDir}\"",
9752
- "stim status --json",
9753
- "read -r iosUdid",
9754
- "agent-device open dev.stim.tutorial --platform ios --udid \"$iosUdid\" --save-script=tutorial.ad",
9755
- "agent-device react-native dismiss-overlay || true",
9756
- `agent-device press 'label="Log an error"' --settle`,
10016
+ `agent-device open ${TUTORIAL_BUNDLE_ID} --platform ios --udid {udid}`,
9757
10017
  "agent-device screenshot tutorial.png",
9758
10018
  "agent-device close",
9759
- "grep -v -e 'target-v1' -e 'dismiss-overlay' tutorial.ad > tutorial-replay.ad",
9760
- "agent-device replay tutorial-replay.ad --platform ios --udid \"$iosUdid\"",
9761
10019
  "stim logs --source agent --tail 10"
9762
10020
  ]
9763
10021
  },
9764
10022
  {
9765
- id: "refresh",
9766
- title: "Fast Refresh",
9767
- who: "agent",
9768
- optional: false,
9769
- prompt: TUTORIAL_PROMPTS.refresh,
9770
- section: "refresh",
9771
- manual: [
9772
- "cd \"{tour}\"",
9773
- "cat > theme.js <<'STIM_TUTORIAL_EOF'",
9774
- "export const TITLE_COLOR = '#7c3aed';",
9775
- "STIM_TUTORIAL_EOF",
9776
- "sleep 6",
9777
- "stim logs --errors",
9778
- "stim logs --grep 'title color'"
9779
- ]
10023
+ id: "logs",
10024
+ title: "App Logs",
10025
+ who: "you",
10026
+ optional: true,
10027
+ ask: null,
10028
+ section: null,
10029
+ commands: ["stim logs --errors", "stim logs --grep stim:tutorial"]
9780
10030
  },
9781
10031
  {
9782
10032
  id: "phone",
9783
- title: "Watch on your phone",
10033
+ title: "Watch on Your Phone",
9784
10034
  who: "you",
9785
10035
  optional: true,
9786
- prompt: null,
10036
+ ask: null,
9787
10037
  section: null,
9788
- manual: []
10038
+ commands: []
9789
10039
  },
9790
10040
  {
9791
10041
  id: "machine",
9792
- title: "Build on another Mac",
10042
+ title: "Build on Another Mac",
9793
10043
  who: "both",
9794
10044
  optional: true,
9795
- prompt: TUTORIAL_PROMPTS.machine,
9796
- section: "machine",
9797
- manual: ["cd \"{tour}\"", "stim ios --remote-build \"{machine}\" --no-build-cache"]
10045
+ ask: TUTORIAL_ASKS.machine,
10046
+ section: null,
10047
+ commands: ["cd \"{tour}\"", "stim ios --remote local --remote-build \"{machine}\" --no-build-cache"]
10048
+ },
10049
+ {
10050
+ id: "share",
10051
+ title: "Share Your Finish",
10052
+ who: "you",
10053
+ optional: true,
10054
+ ask: TUTORIAL_ASKS.share,
10055
+ section: "share",
10056
+ commands: [
10057
+ "cd \"{tour}\"",
10058
+ "git commit -am \"Tutorial change\" -m \"Build <time>, second build cache <hit or miss>\"",
10059
+ "xcrun simctl io {udid} screenshot finish.png",
10060
+ `gh repo fork ${TUTORIAL_REPO} --remote --remote-name fork`,
10061
+ "git push -u fork HEAD",
10062
+ `gh pr create --repo ${TUTORIAL_REPO} --fill --attach "finish.png#The change running in the simulator"`
10063
+ ]
9798
10064
  },
9799
10065
  {
9800
10066
  id: "finish",
9801
- title: "Finish and archive",
10067
+ title: "Finish and Archive",
9802
10068
  who: "agent",
9803
10069
  optional: false,
9804
- prompt: TUTORIAL_PROMPTS.finish,
10070
+ ask: TUTORIAL_ASKS.finish,
9805
10071
  section: "finish",
9806
- manual: [
9807
- "git -C \"{tour}\" checkout -- theme.js",
10072
+ commands: [
9808
10073
  "cd \"{tour}\"",
9809
10074
  "stim stop",
10075
+ "cd \"{second}\"",
10076
+ "stim stop",
9810
10077
  "cd \"{base}\"",
9811
- "stim worktree remove \"{tour}\""
10078
+ "stim worktree remove \"{tour}\"",
10079
+ "stim worktree remove \"{second}\""
9812
10080
  ]
9813
10081
  }
9814
10082
  ];
9815
10083
  //#endregion
9816
10084
  //#region src/guide/tutorial.ts
9817
- const paths = `Use {base} = ~/stim-tutorial and {tour} = ~/stim-tutorial-tour unless the
9818
- user named another parent folder. Expand ~ to the absolute home path when
9819
- substituting inside quotes. Keep the tutorial outside the user's project.`;
10085
+ const paths = `Use {base} = ~/stim-tutorial unless the user named another folder. Expand ~
10086
+ to the absolute home path when substituting inside quotes. Keep the tutorial
10087
+ outside the user's own projects.`;
10088
+ const local = `The tutorial runs on this Mac. Every time you run the tutorial app on iOS
10089
+ from a worktree of {base}, use stim ios --remote local --remote-build local,
10090
+ even when the user's settings would place the device or the build on another
10091
+ Mac. Only an explicit request to build on another Mac changes the build, with
10092
+ stim ios --remote local --remote-build "<machine>"; the device stays here.`;
9820
10093
  function commands(id) {
9821
- return TUTORIAL_STEPS.find((step) => step.id === id).manual.join("\n");
10094
+ return TUTORIAL_STEPS.find((step) => step.id === id).commands.join("\n");
9822
10095
  }
9823
10096
  //#endregion
9824
10097
  //#region src/guide/index.ts
9825
10098
  const TOPICS = {
9826
10099
  agent: agent_default,
10100
+ api: api_default,
9827
10101
  facts,
9828
10102
  metro: metro_default,
9829
10103
  ports: ports_default,
@@ -9835,260 +10109,160 @@ const TOPICS = {
9835
10109
  web: web_default,
9836
10110
  macos: macos_default,
9837
10111
  tutorial: {
9838
- summary: "An iOS tutorial: isolated worktree, builds, devices, logs, agent actions, and cleanup",
10112
+ summary: "A cloned test app: a first build, a second change in parallel with a cache hit, and cleanup",
9839
10113
  sectionHint: "run",
9840
10114
  preamble: () => `STIM TUTORIAL
9841
10115
 
9842
- Create a small Expo app in its own repository and tour worktree. Follow the
9843
- normal Stim flow on iOS, then inspect builds, cache reuse, device control,
9844
- logs, agent actions, Fast Refresh, and cleanup.
10116
+ The tutorial clones ${TUTORIAL_REPO}, a tiny Expo app, into {base}; the clone is
10117
+ never run or removed. The user then asks for a visual change, and for another
10118
+ change while the first builds. Each change runs in its own linked worktree of
10119
+ {base}, so each has its own simulator and Metro port, and the second worktree's
10120
+ first iOS build reuses the first one's native build. Only the clone, the
10121
+ optional share and the cleanup need this guide: the changes are ordinary
10122
+ requests, so follow stim guide agent for them, and check each change on the
10123
+ device.
9845
10124
 
9846
- Run one section per user request. Then end the turn, say what to look at in
9847
- Stim Desktop, and give the next prompt. Pause never means stim stop.
9848
- "${TUTORIAL_PROMPTS.begin}" means stim guide tutorial run.
9849
- "Continue the Stim tutorial: <section>" means stim guide tutorial <section>.
9850
- "${TUTORIAL_RESTART_PROMPT}" means stim guide tutorial restart.
10125
+ ${local}
9851
10126
 
9852
- Without Stim Desktop, use the simulator, stim status for the workspace and
9853
- device, stim logs --errors for errors, and stim stats for build performance.
9854
- Follow stim guide agent for doctor, errors, consent, and cleanup. Never pair
9855
- phones, approve machines, or grant access on the user's behalf.
10127
+ "Follow stim guide tutorial run" means stim guide tutorial run.
10128
+ "Follow stim guide tutorial finish" means stim guide tutorial finish.
10129
+ "${TUTORIAL_RESTART_PROMPT}" means stim guide tutorial restart.
9856
10130
 
9857
10131
  ${paths}
9858
10132
 
9859
- Start with stim guide tutorial run. For commands to type yourself, read
9860
- stim guide tutorial manual. The first run needs network access unless the
9861
- required npm and native dependencies are already cached.`,
10133
+ Never pair phones, approve machines, or grant access on the user's behalf.
10134
+ For commands to type yourself, read stim guide tutorial manual.`,
9862
10135
  sections: {
9863
10136
  run: {
9864
- summary: "Create or reuse the app, warm a tour worktree, and build on iOS",
10137
+ summary: "Clone the test app into a fresh folder and install its dependencies",
9865
10138
  body: () => `RUN THE TUTORIAL
9866
10139
 
9867
10140
  ${paths}
9868
10141
 
9869
- Folder safety: create the base folder only if absent. Reuse an existing
9870
- folder only if app.json's expo.extra.stimTutorial equals this guide's version,
9871
- ${JSON.parse(TUTORIAL_FILES["app.json"]).expo.extra.stimTutorial}; skip creation and file writes when reusing it. For an existing non-tutorial
9872
- folder or another tutorial version, stop and ask the user for another folder.
9873
- Never overwrite or delete it.
10142
+ Folder safety: use a fresh folder. If {base} exists, stop and ask the user for
10143
+ another folder; never overwrite or delete it. First check the parent folder:
10144
+ create it if absent, then run git -C <parent> rev-parse --is-inside-work-tree.
10145
+ If that succeeds, the folder is inside another repository: stop and ask the
10146
+ user for another folder. Never git add in the user's repo.
9874
10147
 
9875
- For a new app, first check the selected parent folder: create it if absent,
9876
- then run git -C <parent> rev-parse --is-inside-work-tree. If it succeeds, the
9877
- folder is inside another repository: stop and ask the user for another folder.
9878
- Never git add in the user's repo. Then, from the parent folder, run:
10148
+ Then, from the parent folder, run:
9879
10149
 
9880
- npx --yes create-expo-app@${TUTORIAL_PINS.createExpoApp} stim-tutorial --template ${TUTORIAL_PINS.template} --no-install --no-agents-md --yes
10150
+ git clone https://github.com/${TUTORIAL_REPO}.git stim-tutorial
9881
10151
 
9882
- Enter the base folder. Run stim guide tutorial app, write its three app files
9883
- verbatim, and append its .gitignore lines. Then run:
10152
+ Check that app.json in the clone has expo.extra.stimTutorial equal to
10153
+ ${TUTORIAL_VERSION}. If it does not, report the mismatch and stop; the user needs a newer Stim.
10154
+ Enter the clone and install its dependencies (npm ci) so the worktrees made for
10155
+ the changes inherit them, then run stim doctor --platform ios there: it registers
10156
+ the clone with Stim so Stim Desktop sees it, and builds or boots nothing. Do not
10157
+ run the app: the clone is only the base for the user's changes and is never removed.
10158
+ Report the findings of stim doctor and do not act on them: no SimSlim install, no
10159
+ --fix, nothing that changes the machine during the tutorial. On npm or network failure, report stderr
10160
+ and stop.
9884
10161
 
9885
- npm install --prefer-offline
9886
- npm pkg set scripts.ios="expo run:ios" scripts.android="expo run:android"
9887
- git init
9888
- git add -A
9889
- git -c user.name=Stim -c user.email=stim@localhost -c commit.gpgsign=false commit -m "Stim tutorial"
10162
+ ${local}
9890
10163
 
9891
- On reuse, run git rev-parse --show-toplevel in the base folder before adding
9892
- a worktree; if it does not equal the base folder, the folder is inside another
9893
- repository: stop and ask the user for another folder.
9894
-
9895
- Set the scripts before the commit because Expo prebuild rewrites them to
9896
- expo run:ios and expo run:android; otherwise the dirty worktree blocks removal.
9897
- On npm or network failure, report stderr and stop.
9898
-
9899
- From the base folder, add the tour worktree and run:
9900
-
9901
- git worktree add -B stim-tutorial/tour "{tour}" HEAD
9902
- cd "{tour}"
9903
- stim worktree warm
9904
- stim guide agent
9905
-
9906
- Apply the guide agent doctor rule before native work: if its STATUS block
9907
- says doctor is due, run stim doctor --platform ios from the tour worktree and
9908
- resolve its findings. No STATUS block means doctor is current. Then run:
9909
-
9910
- stim start
9911
- stim ios
9912
-
9913
- Tell the user the first build can take about four minutes on a cold cache.
9914
- Relay the Open in Stim Desktop link printed by stim ios once.
9915
-
9916
- PAUSE: end the turn. Ask the user to look at the Build section, then the
9917
- device, then Logs in Stim Desktop. Without Desktop, use stim status,
9918
- stim stats, and stim logs --errors. Give the next prompt:
9919
- "${TUTORIAL_PROMPTS.rebuild}". Leave the workspace running.`
9920
- },
9921
- app: {
9922
- summary: "Pinned template, verbatim app files, and .gitignore additions",
9923
- body: () => `TUTORIAL APP
9924
-
9925
- create-expo-app: ${TUTORIAL_PINS.createExpoApp}
9926
- Template: ${TUTORIAL_PINS.template}
9927
- Write the app files verbatim. Append the .gitignore lines to the template's
9928
- existing file. App log lines start with [stim:tutorial]. Crash me raises an
9929
- uncaught JavaScript error, and Slow request times a local three-second timer;
9930
- Stim does not capture native network requests.
9931
-
9932
- ${Object.entries(TUTORIAL_FILES).map(([name, content]) => `${name}\n\n\`\`\`${name.endsWith(".json") ? "json" : name.endsWith(".js") ? "js" : "text"}\n${content}\`\`\``).join("\n\n")}`
9933
- },
9934
- rebuild: {
9935
- summary: "Repeat the iOS build and explain the cache result",
9936
- body: () => `REBUILD
9937
-
9938
- ${paths}
9939
-
9940
- Run the same build again, never with --no-build-cache:
9941
-
9942
- ${commands("rebuild")}
9943
-
9944
- Read this workspace's environments[].lastBuilds.ios.cacheHit from
9945
- stim status --json, and the miss reason printed by stim ios. Explain a local or remote hit, or the actual miss
9946
- reason when cacheHit is false. A repeat run can miss; report the evidence.
9947
-
9948
- PAUSE: end the turn. Point at the Build section and cache badge, or stim stats.
9949
- Ask the user to open the device's live view and tap Log an error; without
9950
- Desktop use the simulator. Then inspect Logs or stim logs --errors. Crash me
9951
- and Slow request are optional: the former shows a red box, the latter prints
9952
- a timing line. Give the next prompt: "${TUTORIAL_PROMPTS.agent}".`
9953
- },
9954
- agent: {
9955
- summary: "Record a tap and screenshot, replay it, and inspect agent logs",
9956
- body: () => `AGENT ACTIONS
9957
-
9958
- ${paths}
9959
-
9960
- Set {stateDir} to this workspace's agentDevice.stateDir from stim ios or
9961
- stim status --json. Replace <ios.udid> with this workspace's ios.udid from
9962
- stim status --json. Use that exact owned simulator, not a guessed one.
9963
- Run from the tour worktree:
9964
-
9965
- ${commands("agent").replace("read -r iosUdid", "iosUdid=\"<ios.udid>\"")}
9966
-
9967
- Use --save-script=tutorial.ad with the equals sign: agent-device treats a
9968
- separate path as a URL and refuses. The dismiss-overlay step clears a red box that would cover the buttons.
9969
- Replay with the same --platform and --udid so its steps reach the agent log.
9970
- Remove the target-v1 evidence and dismiss-overlay lines before replay: replaying
9971
- them can fail with REPLAY_DIVERGENCE on the recorded button identity.
9972
- The scripts and screenshot stay in the tour worktree and are git-ignored.
9973
-
9974
- PAUSE: end the turn. Point at Agent actions, or the agent log records just
9975
- printed. Expect another error-button line after replay. When screen recording
9976
- is enabled, the user can scrub Replay in Desktop. Give the next prompt:
9977
- "${TUTORIAL_PROMPTS.refresh}".`
9978
- },
9979
- refresh: {
9980
- summary: "Change the title to purple and verify Fast Refresh through logs",
9981
- body: () => `FAST REFRESH
9982
-
9983
- ${paths}
9984
-
9985
- Set TITLE_COLOR in theme.js to '#7c3aed' (purple). Wait a few seconds for
9986
- Fast Refresh, without reloading the app:
9987
-
9988
- ${commands("refresh")}
9989
-
9990
- The line [stim:tutorial] title color=#7c3aed is the proof that the edit reached
9991
- the app. The intentional button errors may still be in the error log; check
9992
- whether the edit introduced a new error.
9993
-
9994
- PAUSE: end the turn. Point at the purple title in the device view or simulator
9995
- and the title color log line. Phone viewing and an approved remote Mac are
9996
- optional user steps; skip them if unwanted. Never pair, approve, or grant
9997
- anything. If the user names an approved machine, the next prompt is
9998
- "${TUTORIAL_PROMPTS.machine}". Otherwise give
9999
- "${TUTORIAL_PROMPTS.finish}".`
10000
- },
10001
- machine: {
10002
- summary: "Optionally build using a machine the user names and has approved",
10003
- body: () => `REMOTE MAC (OPTIONAL)
10004
-
10005
- ${paths}
10006
-
10007
- Proceed only when the user names an approved remote Mac. Substitute that
10008
- name for {machine}. Never approve, pair, or grant anything. If none is named,
10009
- ask for the name or let the user skip this step.
10010
-
10011
- ${commands("machine")}
10012
-
10013
- This step bypasses the artifact cache so the build can use the named machine.
10014
- A named machine refuses without a local fallback. If it refuses, report the
10015
- refusal and offer --remote-build auto or local; do not retry silently.
10016
-
10017
- PAUSE: end the turn. Point at the build's machine in Desktop or its report in
10018
- stim status --json. Give the next prompt: "${TUTORIAL_PROMPTS.finish}".`
10164
+ PAUSE: end the turn. Tell the user to ask for a visual change next, such as
10165
+ making the title purple, in their own words. Each change runs in a new linked
10166
+ worktree of {base} (stim guide agent), and its first iOS build takes a few
10167
+ minutes.`
10019
10168
  },
10020
10169
  finish: {
10021
- summary: "Revert the tutorial edit, stop, and remove only the tour worktree",
10170
+ summary: "Stop and remove the two tutorial worktrees, keeping the clone",
10022
10171
  body: () => `FINISH
10023
10172
 
10024
10173
  ${paths}
10025
10174
 
10026
- The user's finish request authorizes removing this tour worktree only.
10027
- Revert the refresh edit before removal; worktree remove refuses dirty trees.
10028
- Stop from the tour path, then remove from the base checkout:
10175
+ The worktrees to remove are the two the tutorial tracked: the linked
10176
+ worktrees of {base} made for the user's changes. The finish request names them
10177
+ as {tour} and {second}. Never touch any other worktree, an earlier tutorial
10178
+ clone, or the clone itself. For each, stop from its path, then remove it from
10179
+ the clone:
10029
10180
 
10030
10181
  ${commands("finish")}
10031
10182
 
10032
- Never use --force. On a refusal, report it and stop. Keep the base folder
10033
- and branch. Print these optional cleanup commands for the user; do not run. Deleting
10034
- the base folder also removes the branch, so they are alternatives:
10183
+ Use a plain remove first. The user's finish request says they do not need the
10184
+ changes, which is the consent stim guide agent asks for before worktree remove
10185
+ --force, for exactly those two paths: if the plain remove refuses one of them
10186
+ only because of uncommitted changes or commits found nowhere else, remove that
10187
+ worktree with --force. Never use --force on the clone or on any other
10188
+ worktree, and on any other refusal report it and stop. Keep the clone. Print
10189
+ these optional cleanup commands for the user; do not run them:
10035
10190
 
10036
10191
  rm -rf "{base}"
10037
- git -C "{base}" branch -D stim-tutorial/tour
10038
10192
 
10039
- If archive is enabled, tell the user the tour appears under Archived in Stim
10040
- Desktop. With archive disabled, report removal without promising an archive.
10041
- Without Desktop, inspect stim status --json for the removed environment and,
10042
- when enabled, its archived entry. End the turn.`
10193
+ If archive is enabled, tell the user the worktrees appear under Archived in
10194
+ Stim Desktop. With archive disabled, report removal without promising an
10195
+ archive. Without Desktop, inspect stim status --json for the removed
10196
+ environments. End the turn.`
10197
+ },
10198
+ share: {
10199
+ summary: "Optionally open a public pull request with a screenshot of the change",
10200
+ body: () => `SHARE YOUR FINISH (OPTIONAL)
10201
+
10202
+ Only on the user's explicit request, which the share prompt is, and before the
10203
+ finish step removes the worktrees. The pull request is public: the user's GitHub
10204
+ name and change appear on ${TUTORIAL_REPO}, and a bot replies and closes it. It
10205
+ needs gh signed in. Never open it unprompted or from any other step.
10206
+
10207
+ Work from the worktree holding the user's change, {tour}. Commit the change
10208
+ there, fork the repository and push the branch to the fork, since the user has
10209
+ no write access:
10210
+
10211
+ gh repo fork ${TUTORIAL_REPO} --remote --remote-name fork
10212
+ git push -u fork HEAD
10213
+
10214
+ Screenshot: take a PNG under 1 MB of the app showing the change, with
10215
+ agent-device screenshot when it is installed, otherwise
10216
+ xcrun simctl io <ios.udid> screenshot finish.png, using the udid of that
10217
+ worktree from stim status --json, never booted. If the app is no longer
10218
+ running, run it again from the branch first.
10219
+
10220
+ Open the pull request with gh pr create --repo ${TUTORIAL_REPO} --fill. Put one
10221
+ short line with the build time and whether the second worktree's first iOS build
10222
+ was a cache hit, when you know them from stim status --json or stim stats, in the
10223
+ commit message body (git commit -m "<title>" -m "<that line>"), so --fill carries it
10224
+ into the pull request body.
10225
+
10226
+ Attach the screenshot with gh: gh pr create --attach "finish.png#The change
10227
+ running in the simulator" uploads the image and appends it to the body. gh
10228
+ 2.99.0 (2026-09-01) has --attach; confirm with gh pr create --help rather than
10229
+ guessing a version. When gh has no --attach, commit the PNG on the PR branch as
10230
+ finish/<github-login>.png before pushing and embed it in the body with a
10231
+ relative link: ![the change](finish/<github-login>.png).`
10043
10232
  },
10044
10233
  restart: {
10045
- summary: "Remove the existing tour safely and repeat from the worktree step",
10234
+ summary: "Start the tutorial again without removing anything",
10046
10235
  body: () => `RESTART
10047
10236
 
10048
10237
  ${paths}
10049
10238
 
10050
- For "${TUTORIAL_RESTART_PROMPT}", if the tour worktree is present, revert
10051
- the refresh edit, stop, and remove it using the finish section's commands:
10052
-
10053
- ${commands("finish")}
10054
-
10055
- Never use --force. On a refusal, report it and stop. Keep the base repository.
10056
- Read stim guide tutorial run, recheck its folder and repository safety rules,
10057
- and redo run from the worktree step. Pause after the build as run instructs.`
10239
+ For "${TUTORIAL_RESTART_PROMPT}", remove nothing: the user may still want the
10240
+ earlier worktrees. Finish only removes the pair named in its request, so leave
10241
+ any earlier worktrees and tell the user they stay until they ask for each by
10242
+ path; never use --force for them. Reuse the clone at {base} only if its
10243
+ stimTutorial marker equals ${TUTORIAL_VERSION}; otherwise follow stim guide
10244
+ tutorial run into a fresh folder. Pause as run instructs. Stim Desktop starts
10245
+ over: only worktrees and builds after the restart count.`
10058
10246
  },
10059
10247
  manual: {
10060
- summary: "Commands for every step, including heredocs for the app files",
10248
+ summary: "The commands behind each step, for typing yourself",
10061
10249
  body: () => `MANUAL TUTORIAL
10062
10250
 
10063
10251
  ${paths}
10064
10252
 
10065
- These commands are for a person typing them, one step at a time,
10066
- as scripts: save a block to a file and run it with sh -e, so it stops on the
10067
- first failure. Read stderr and do not continue to later commands. Follow stim guide tutorial run for folder and repository
10068
- safety. Reuse only this version's tutorial app, never overwrite another folder.
10069
- The creation block skips writes on reuse and checks the repository root.
10070
-
10071
- Replace {base} and {tour} with absolute paths; {base} must end in stim-tutorial. For Agent actions, replace
10072
- {stateDir} with agentDevice.stateDir from stim ios or stim status --json;
10073
- when read -r iosUdid waits, type this workspace's ios.udid from that status.
10074
- For the optional machine step,
10075
- replace {machine} with a machine you have already approved, or skip it.
10076
-
10077
- Look at the sidebar during warm and Build during the first build (about four
10078
- minutes on a cold cache). Compare cacheHit and missReason on rebuild. At Live
10079
- view and control, open the device viewer and tap Log an error; without Desktop
10080
- use the simulator. At App logs, try Crash me or Slow request if wanted. A JS
10081
- crash shows a red box; the slow request is a local timer, not network capture.
10082
- At Watch on your phone, optionally open an already paired Stim phone to see
10083
- the tour workspace; phone setup and machine approval stay with you.
10084
- Use stim status, stim logs --errors, and stim stats without Desktop.
10085
-
10086
- ${TUTORIAL_STEPS.map((step) => `${step.title}${step.optional ? " (optional)" : ""}\n\n${step.manual.length ? `\`\`\`sh\n${step.manual.join("\n")}\n\`\`\`` : "Observe this step in Stim Desktop, or skip it without Desktop."}`).join("\n\n")}
10087
-
10088
- Finish removes only the tour worktree. Never use --force; report a refusal.
10089
- With archive enabled, find the tour under Archived in Desktop or archived[]
10090
- in stim status --json. Keep the base repository and branch. To delete them,
10091
- read stim guide tutorial finish for commands to review and run yourself.`
10253
+ Replace {base}, {tour} and {second} with absolute paths: {base} is the clone,
10254
+ {tour} the first worktree and {second} the second. For Agent Actions, replace
10255
+ {stateDir} with agentDevice.stateDir from stim ios or stim status --json and
10256
+ {udid} with the workspace's ios.udid. For the optional machine step, replace
10257
+ {machine} with a machine you have already approved, or skip it. Clone with git
10258
+ clone https://github.com/${TUTORIAL_REPO}.git into a fresh folder outside any
10259
+ repository. Each change runs in its own worktree of that clone (stim guide
10260
+ agent); the clone itself is never run. Run each worktree on iOS with
10261
+ stim ios --remote local --remote-build local so it stays on this Mac.
10262
+
10263
+ ${TUTORIAL_STEPS.map((step) => `${step.title}${step.optional ? " (optional)" : ""}\n\n${step.commands.length ? `\`\`\`sh\n${step.commands.join("\n")}\n\`\`\`` : "Ask your agent in your own words, or observe this step in Stim Desktop."}`).join("\n\n")}
10264
+
10265
+ Finish removes only the two tutorial worktrees, never the clone; stim guide tutorial finish says when --force is allowed for them.`
10092
10266
  }
10093
10267
  }
10094
10268
  }
@@ -10172,7 +10346,7 @@ function renderIndex(version) {
10172
10346
  return lines.join("\n");
10173
10347
  }
10174
10348
  function guideCommand(program, version, status = (running) => guideStatus({
10175
- projectRoot: findProjectRoot$1(process.cwd()),
10349
+ projectRoot: findProjectRoot(process.cwd()),
10176
10350
  running
10177
10351
  })) {
10178
10352
  program.command("guide [topic] [section]").description("Print reference documentation for THIS version of Stim (topics: " + topicNames().join(", ") + "). A topic with sections prints its section index; name a section to print only it, e.g. `stim guide errors STIM_NO_METRO` or `stim guide lifecycle builds`. Generated by the binary, so it cannot drift from the installed CLI.").action(async (topic, section) => {