Skip to main content

Changelog

Full release history is also on GitHub Releases.

1.27.0​

Stim Desktop and the Stim phone app ship through their own workflows; this release covers the twelve npm packages (@stim-cli/core, @stim-cli/cache, @stim-cli/cache-s3, @stim-cli/agent-driver, @stim-cli/agent-driver-agent-device, @stim-cli/metro, @stim-cli/expo-build-cache, stim, @stim-cli/ci, @stim-cli/transport, @stim-cli/transport-tailscale and @stim-cli/server). No package publishes for the first time in this release. Commits that only changed those apps, tests or CI are not listed: #4341, #4346, #4376, #4423, #4424, #4425, #4426, #4427, #4428 and #4439, and the Stim Desktop parts of #4331 and #4332.

New​

  • gc --delete lightens workspaces that did not run a native build in the last 15 minutes. A workspace's build outputs (derived-data/ and the rest under its Stim workspace, and the project's own android/app/build, android/build, android/.gradle and .build when git ignores every file in them) are now cleared once no stim ios, android or macos run, cache hits included, has happened there for 15 minutes. The time comes from the workspace's build records, not from its logs, so a dev server that runs all day no longer keeps the outputs; only a native run, a build lock or slot, an active build claim or a running macOS app does. ios/build is never cleared, because pod install writes codegen output and podspec facades there that only pod install recreates. Maintenance at disk pressure level 2 now clears build outputs and build directories by the same rule instead of its 10-minute maintenance.pressureProtectMinutes window and full in-use check, and no longer deletes ios/build. The shared build cache, the Xcode compilation cache and the Gradle and Metro caches are not touched. The report says would be LIGHTENED and lists the in-tree directories; gc --json adds inTreeDirs to each workspaceBuildOutputs entry. Fixes #4397 and #4413. (stim, @stim-cli/core) 5d3c12793, d850b937b
  • gc --delete lightens the dependencies of idle linked worktrees, and the next run restores them. node_modules and ios/Pods of a linked worktree that has a Stim workspace are deleted when the worktree has had no activity for 15 minutes and stim worktree warm can put the same versions back: the source checkout has the directory, its installed tree matches its lockfile, the worktree's lockfiles (pnpm-lock.yaml, package-lock.json, yarn.lock, bun.lock, bun.lockb, or ios/Podfile.lock for Pods) equal the source checkout's, and the directory is not excluded from warm. The source checkout, a running dev server, maintenance.keep pins and the worktree gc runs in are kept, and so is a worktree that the installed stim-server service, the stim on PATH, or any live process's command line or working directory points into, or every worktree when the process list cannot be read. Before deleting, gc writes workspaces/<id>/lightened.json; the next stim start, ios or android in that workspace runs stim worktree warm first, and refuses with STIM_DEPS_FAILED when the restore check or warm fails. Maintenance at disk pressure level 3 writes the same record when the same check passes. The report gains a Worktree dependencies section, and gc --json gains sections.worktreeDependencies and the result kind worktreeDependencies. Fixes #4414, refs #4397. (stim, @stim-cli/core) 83fa540ee
  • Finished worktrees with uncommitted files are archived, then removed. A linked worktree that is otherwise removable (merged into the default branch or its pull request merged or closed, pushed, not locked or in use) but has uncommitted or untracked files was kept forever. gc and automatic maintenance now write tracked.patch, untracked.tar, head.bundle (the HEAD commits the default branch lacks) and manifest.json in a new directory under $STIM_HOME/archive/worktrees/<repo>/ once the worktree has been idle for gc.worktreeGraceMinutes (120), verify the archive, check again just before git worktree remove --force that nothing changed, and then remove the worktree. The removal line, the gc log and the maintenance log name the archive and the commands that restore it. Ignored files are not saved. archive.enabled false, for the worktree or a project registered inside it, keeps such worktrees as before; archive.maxAgeDays (30) and archive.maxTotalGb (5, a budget of its own for worktree archives) bound them, and an archive that would not fit or would take free disk under budget.hardFloorDiskGb keeps the worktree. Disk pressure level 3 and stim worktree remove never archive. Clean worktrees with no commits of their own, detached HEADs included, whose HEAD the default branch contains and which have no open pull request, now count as finished once idle for max(gc.worktreeGraceMinutes, maintenance.protectRecentHours), 2 hours by default, instead of only under disk pressure. A worktree that holds the running stim or the stim on PATH is in use, and stim worktree remove refuses it even with --force. stim guide agent now tells agents to remove the worktrees they create and keep scratch files outside them. Fixes #4398 and, on Windows, #4429. (stim, @stim-cli/core) a4cade8ca, e5182d76b
  • STIM_TRACE=<path> writes a Chrome trace of a command run. The file opens in Perfetto and chrome://tracing. It has one span per child process run through the exec wrapper, with its arguments, working directory and outcome; the build phases that stats.json and the build ledger use; worktree warm steps; the fingerprint, a fingerprint memo hit and the wait for Metro. Nested stim commands and the processes a command starts append to the same file. Secret-looking flags and their values, NAME=value assignments with secret names, URL credentials, bearer tokens, Authorization and Cookie headers, curl -u and security -p/-P credentials, and common token shapes (JWT, sk_live_/sk_test_, AIza, glpat-, npm_) are redacted; a trace still holds full paths and arguments, so treat it like a log. Unset, each call costs one environment check. Documented in stim guide logs (TIMING TRACES) and the website settings page. Fixes #4367, refs #4334. (stim, @stim-cli/core) c18b1cd04, c9929dcb5
  • A machine-wide build ledger, read with stim logs --source builds. Every ios, android and macos run, local, offloaded or from a cache, succeeded, failed or cancelled, appends one record to $STIM_HOME/builds/builds.ndjson, which keeps the newest 500. A record has the project, platform, scheme, machine, outcome, failure code, message and remedy, the device name and runtime (never a serial or UDID), and the phase times, including the build machine's. stim logs --source builds [--json] reads it from any directory, like --source gc: alone, and not part of --source all. gc never touches the file. Fields are in stim guide logs (BUILD LEDGER). Stim Desktop 0.2.8 lists it. Fixes #4329. (stim, @stim-cli/core) 0eff201e4
  • One placement vocabulary for builds, simulators, emulators and macOS apps. remote.build, ios.remote, android.remote and the new macos.remote, and their flags, take local, auto (the previous behavior), remote (remote Macs first, this Mac only when none takes it), remote-only (never this Mac) or one machine name; ios.remote and android.remote keep eas and proxy. The order of remote.machines is now the priority for every workload: the first approved, reachable machine with a free slot and a 5-minute load per core under its own server.maxLoadPerCore takes the work, which replaces "least loaded" for devices and "less loaded than this Mac, warmest first" for builds. A machine that already holds the exact completed artifact still goes first. Under remote-only, a device or macOS app waits for a host with room, re-probing every 5 seconds for --wait seconds (600 by default; stim macos always waits 600), then refuses with STIM_AT_CAPACITY, and a host that can never take the run refuses at once with STIM_HOSTING_REFUSED; a build refuses with STIM_OFFLOAD_REFUSED. stim macos --remote accepts the same words, including auto. stim doctor prints the placement order and a priority per machine. device-host.offer now reports the host's resources.maxLoadPerCore. remote.buildMode is removed (see Migration notes). ios --plan and android --plan refuse an invalid remote.build or STIM_REMOTE_BUILD as that setting, and give the refusal a run gives for remote-only with no remote machines; doctor flags an invalid remote.build or STIM_REMOTE_BUILD. Fixes #4321, #4363, #4364 and #4365. (stim, @stim-cli/core, @stim-cli/server) 65cdbfa8d, d7b0b36b6
  • Debug iOS simulator builds skip the dSYM. stim ios passes DEBUG_INFORMATION_FORMAT=dwarf for a Debug build for the simulator, so Xcode no longer runs dsymutil after every link (about 6 s per build on apps/mobile) or writes the dSYM bundle. The built app is byte-identical, so no cache key changes. lldb reads the debug information from the object files in the workspace's DerivedData. Release and device builds keep the project's setting. There is no option. Fixes #4433. 28dd0453f
  • iOS boot records and SpringBoard crash reports per simulator. The workspace device.ndjson gets ios_boot records keyed by udid for each boot phase (boot_requested, bootstatus_done, springboard_started, viewer_opened, simslim_reboot, restart). When SpringBoard is not running after a boot, Stim copies that simulator's recent SpringBoard-*.ips reports from ~/Library/Logs/DiagnosticReports into the workspace logs directory (at most 40 kept) and writes a springboard_crash_reports record. None of these are errors. Documented in stim guide logs (IOS BOOT RECORDS). Refs #4359. 232bd0a50
  • A setup script for Linux build workers. scripts/linux-worker/setup.sh in the repository sets up an Ubuntu 22.04 or 24.04 x86_64 worker for offloaded Android builds: Node 22.12+ with Corepack, JDK 17, the Android SDK packages, Tailscale, @stim-cli/server and its service; --emulator adds KVM and an emulator image. The remote machines page documents the Linux worker. Doctor and the build notes no longer suggest a CocoaPods install for a Linux worker, the ccache note and doctor finding name sudo apt install ccache, and a deps-failed: spawn pnpm|yarn|npm ENOENT refusal names the missing package manager instead of the approval remedy. Fixes #4383. be4745ea6
  • A benchmark harness for Stim releases (repository tooling, not published). scripts/perf/bench.mjs times worktree warm, start, first and cached ios and android runs and stop for an npm version or a local build, and can publish the results to the perf-data branch. scripts/perf/compare.mjs compares a machine's latest run with the one before, and reports a change within the repetition spread as noise and marks runs taken above --max-load (80). Described in docs/perf.md. Fixes #4334 and #4387. 04a63de9d, 2d50b91c9

Fixes​

  • A cached iOS app that dyld cannot bind at launch is removed from the cache. A Debug simulator app stored by 1.26.0 could hold a Debug React.framework and a Release hermesvm.framework, so dyld killed it on every launch and every cache hit installed it again. After a fatal launch of a cache hit, Stim now reads the run's crash reports; when one is a dyld Symbol missing failure between two images inside the app's own bundle, it removes that cache entry and fails with STIM_LAUNCH_FAILED, naming the entry and asking to run again. A missing system library, any other crash and an app built by the same run leave the cache alone. A cache provider's copy cannot be removed; the remedy points at --no-build-cache. Hosted and remote devices are unchanged. Fixes #4412. ef46995f7
  • pod install no longer drops the React prebuilt build configuration markers. CocoaPods cleans every pod directory when it installs any pod and its glob skips dotfiles, so .last_build_configuration vanished from React-Core-prebuilt, hermes-engine and ReactNativeDependencies while their Release binaries stayed, and React Native 0.88 then linked a Debug build against the Release core. Every pod install Stim runs, locally, on a build worker and in worktree warm --refresh, now puts a marker back when its pod directory is the one that existed before, and says so in a pods note. Fixes #4377. 8ec1967ae
  • stim ios restarts an owned simulator once when SpringBoard is not running. A SpringBoard that started and then died passed every readiness check, and simctl launch failed with "The system shell probably crashed". The post-boot check now requires a SpringBoard process, shuts the simulator down through centralized teardown and boots it once more; if SpringBoard is still missing, the boot fails with stim stop then stim ios as the remedy. Fixes #4074. d9f132f71
  • The SimSlim skip works after a SimSlim reboot. The SpringBoard pid was read before the rebooted SpringBoard finished starting, so the record was not written and the next stim ios ran SimSlim again (1.5 to 2.9 s). Stim now waits for SpringBoard before reading it. Fixes #4350. 9ea54a9a1
  • SwiftUI Invalid Configuration faults are warnings. Records with subsystem com.apple.SwiftUI and category Invalid Configuration, such as duplicate ForEach ids in the Expo dev launcher, counted as app errors at launch. They stay in device.ndjson and stim logs at level warn. Fixes #4378. a3f9809ba
  • Ctrl-C during the launch readiness wait or hosted app delivery reports STIM_CANCELLED. The first Ctrl-C exited with only lock cancelled by an interrupt, writing no cancelled build ledger row and leaving a stale build record in stim status. Both waits now drain through the normal failure path, as stim stop does; exit code stays 130, and a second Ctrl-C still exits at once. Fixes #4389 and #4399. e5d1c3f45, a80406a9b
  • Hosted launches write the launch markers that logs --errors and --slot read. A hosted iOS or Android run wrote no launch_attempt marker or outcome record, so stim logs --errors had no launch window and stim logs --slot dropped the shared Metro and client records. Fixes #4273. 0956a0860
  • stim stop on a hosted slot copies the final native logs once, and Ctrl-C interrupts it. The copy ran before and after the host stopped the session, so a stop could take about a minute; a cancellation was reported as a device failure. Fixes #4285. d99124675
  • The refusal for an unreachable remote Mac names the failing layer. stim ios --remote <mac> printed the raw handshake error. It now prints the diagnosis and remedy stim doctor gives, from one tailscale status read after the connection failed. Fixes #4385. 26204321e
  • Device slot wait and reclaim lines tell same-named worktrees apart. Workspaces that share a project name carry the first directory that differs, as mobile (qa-b-p2); a single checkout keeps the plain name. Payloads are unchanged. Fixes #4379. 7588aa64e
  • A busy agent-device daemon is reported once per device teardown. The line names the daemon, says that sessions naming the device stay open, and gives agent-device daemon stop --state-dir <dir>. Fixes #4386. 8e8fb8200
  • Verifying that an owned AVD stopped retries the process table read. One slow ps under heavy load made stim stop and worktree remove refuse although the emulator was gone. The read now retries with a longer timeout and still refuses when both attempts fail. Fixes #4384. ca74f0219
  • worktree warm --refresh keeps patch-package patches applied. From a monorepo root, the refresh now installs the app warm prepares as well as the root, counts a change under patches/ as a dependency change in npm projects, runs the project's own postinstall after npm ci when npm's ignore-scripts is on, and no longer reinstalls on every refresh when npm ls already failed right after the last install. A root-cwd refresh also finds the nested app registered in the worktree, and says when it refreshes only the repository root. Fixes #4067 and #4368. 94b3e03ec, cef7f7f3f
  • worktree warm deletes the copied pnpm store in the background. The cloned node_modules/.pnpm-store (1.8 s to delete in this repository) and the other known copied directories move into the trash for the background delete that worktree remove uses. A trash entry that rm cannot empty keeps its trash.json so gc can finish it. Fixes #4349. 476387308
  • The iOS launch phase does less work. The Metro location write runs alongside the app process probe, and the launch check reads only the bytes appended to the Metro, device and client logs instead of rereading them every 500 ms, with the native crash probe running beside the poll. Fixes #4373. (stim, @stim-cli/core) 051ddcb0e
  • Android runs no longer block on adb reads. The log collector reads the device clock and app pid asynchronously, the launch check samples the clock once per verification instead of before every crash check, and device boot admission and the owned-emulator path list devices asynchronously. 541c793eb
  • stim gc measures sizes four at a time. Shared caches, orphaned workspaces, workspace outputs and unlisted worktrees were measured one du at a time. A cache directory gc cannot read is now reported as one that could not be read or removed. Fixes #4351. 3025d2cca
  • The budget gate skips a repeated empty cache trim. Below budget.minFreeDiskGb, every start, ios and android walked every shared cache and found nothing (11 to 13 s). A trim that removed nothing is recorded in $STIM_HOME/budget-sweep.json, and the next run within 5 minutes skips it when free disk has not fallen by more than 1 GB and the cache set is unchanged. The STIM_LOW_DISK size scans run concurrently, and an unreadable cache directory counts as a failed trim. Fixes #4348. (stim, @stim-cli/core) 9f5ddbb88
  • gc --delete --json counts failed cache trims and empties in failures. It reported "failures": 0 while exiting 1. Fixes #4366. 14e59b052
  • The iOS JS swap works with macOS 15's plutil. It prints the missing-key error on stdout, so every app without RCTUseAssetCatalog fell back to a full build. Fixes #4358. 40a91d04a
  • Paths on Windows. Fingerprint input paths and the static fingerprint's root and git top level use the native realpath, so an 8.3 short name or a differently cased path no longer drops gitignored inputs sent to a worker or puts every input outside the repository; a static digest computed through such a path changes once. A macOS worker's Xcode paths are joined as POSIX paths, so a Windows client prints the right xcode-select command. The dependency lockfile lookup stops at a project path that is not below the root instead of looping at the filesystem root. Fixes #4401, #4437, #4405 and #4415. (stim, @stim-cli/core) 0e49c510a, a8cbb18d4, cb77db2d3, fe5416bb9

Docs​

  • Run pinned npx stim@<version> outside a Stim checkout. Inside this repository, npm can resolve the workspace package and run whichever stim is first on PATH. RELEASE.md, docs/release-recovery.md, docs/perf.md and docs/field-test-protocol.md say so. Fixes #4388. c8da4721a

Migration notes​

  • gc and maintenance clear idle worktrees sooner. stim gc --delete and maintenance at disk pressure level 2 clear the build outputs of a workspace with no ios, android or macos run in the last 15 minutes, even while its dev server runs. gc --delete now also deletes node_modules and ios/Pods of a linked worktree idle for 15 minutes when warm can restore them. Disk pressure level 3 deletes them as before, by its own 10-minute window and whether or not warm can restore them, and records them for restore only when it can. After a deletion that wrote a restore record, the next stim start, ios or android there runs stim worktree warm first. Other commands, such as a test run, find the directories missing until then; run stim worktree warm in the worktree. A build Stim did not run, such as a hand-run gradlew, leaves no record, so gc --delete and level 2 judge its build directory by the last Stim native run alone and can clear it at once.

  • ios/build is never cleared by gc or maintenance; level 2 used to delete it.

  • Dirty finished worktrees are archived, then removed. Their changes go to a new directory under $STIM_HOME/archive/worktrees/<repo>/, named after the branch or directory, a timestamp and a random suffix, and the removal line, stim logs --source gc and the maintenance log print the restore commands. The bundle unbundle line appears only when HEAD has commits the default branch lacks:

    git -C <repository> bundle unbundle <archive>/head.bundle
    git -C <repository> worktree add --detach <path> <HEAD>
    git -C <path> apply <archive>/tracked.patch
    tar -xf <archive>/untracked.tar -C <path>

    Set archive.enabled false to keep such worktrees instead; that also turns off the workspace history kept on removal. Under a custom STIM_HOME, archive.enabled already defaults to false, so such worktrees are kept there. Ignored files are not archived.

  • Clean detached and no-commit worktrees count as finished after 2 hours (max(gc.worktreeGraceMinutes, maintenance.protectRecentHours)) and are removed by gc --delete and automatic maintenance. gc --worktrees --older-than N no longer holds them for N days. Use git worktree lock on one that must stay.

  • Check for a broken cached iOS Debug app from 1.26.0. With React Native 0.88's prebuilt core, 1.26.0's pod install could drop the build configuration markers and store a Debug simulator app that dyld kills at launch. A 1.27.0 build no longer produces one, but a 1.27.0 machine that offloads to a 1.26.0 worker still can, because the worker runs pod install. The first crashing launch of such a cache hit removes the entry; run the command again to rebuild. A provider's copy stays: build once with --no-build-cache. A worktree whose markers already read Debug over Release binaries is not detected; run rm -rf ios/Pods/React-Core-prebuilt ios/Pods/hermes-engine ios/Pods/ReactNativeDependencies and pod install there. Update build workers to 1.27.0 too.

  • remote.buildMode was replaced by remote.build. auto stays auto, force becomes remote and off becomes local. A leftover key is not an error: runs print that it is not read, stim doctor reports it for each file that holds it with the stim settings set remote.build <value> command, and stim settings unset remote.buildMode removes it from every layer. STIM_REMOTE_BUILD_MODE is removed and runs ignore it without a warning; only stim doctor reports it. Use STIM_REMOTE_BUILD instead, for example STIM_REMOTE_BUILD=local where you had STIM_REMOTE_BUILD_MODE=off; otherwise the default auto applies and builds can go to a remote machine. In 1.26.0 remote.buildMode applied only while remote.build was auto, so if remote.build already names a machine, keep it and just unset remote.buildMode.

  • The order of remote.machines is the placement priority for builds, devices and macOS apps; set it with stim settings set remote.machines '["first","second"]'. Builds no longer prefer the less loaded or warmest machine.

  • STIM_BUILD_COMPAT stays 2. A 1.27.0 machine and a 1.26.0 machine offload to each other. Update a worker's service with stim-server service update --release 1.27.0 to get the worker-side fixes, including the pod install marker fix.

  • Debug iOS simulator builds have no dSYM. Tools that need a dSYM for a Debug simulator build get none; Release and device builds are unchanged.

  • New state under $STIM_HOME: builds/builds.ndjson, budget-sweep.json, archive/worktrees/, and workspaces/<id>/lightened.json. Trash entries written by worktree warm carry kind: warm. Workspace logs gain ios_boot and springboard_crash_reports records and copied SpringBoard-*.ips files.

  • Stim Desktop 0.2.8 needs this release for its Builds section and a machine's build list, which read stim logs --source builds, and for the Remote Macs order and the Add Machine wizard's build choices, which write placement words 1.26.0 does not know.

QA​

The maintainer authorized a stable release without the pre-tag QA gate on 2026-10-25, as for 1.16.0 through 1.26.0. This deviates from RELEASE.md section 3, which has no waiver for a stable version; the expedited RC lane is for prereleases only and was not used. The waiver covers only section 3. No section 3 QA round ran on any commit since v1.26.0, so no change in this release has been through QA, including gc and maintenance lightening and the dependency restore through warm, worktree archives, the no-own-commit worktree rule, the placement vocabulary and remote.machines priority, the build ledger, STIM_TRACE, the dyld cache eviction and pod install marker restore, the SpringBoard restart, the Debug dSYM skip, and the Linux worker script.

Omitted before the tag, for this candidate: all eight rows that node scripts/release-qa-matrix.mjs v1.26.0 requires (287 changed paths with the version commit, 130 mapped to a row, 157 with no native effect, 0 unclassified): loop (expo and bare, iOS and Android), logs (iOS, Android), caches (iOS, Android), real-repository, remote-provider, release-build (iOS, Android), android-flavor and launch-evidence (iOS, Android), plus the native suites test/e2e/native/run-native-e2e.mjs and test/e2e/native/run-cache-e2e.mjs. Not run: test:compat, because the stage's agent-device source checkout (STIM_AGENT_DEVICE_SOURCE) and its validation-owned emulator were not prepared.

Run before the version commit, on the bumped candidate at fe5416bb9, all passing: pnpm install --frozen-lockfile, format:check, lint, build, typecheck, knip, release:prep --check (12 packages at 1.27.0, 23 workspace:* ranges), test:runtime, CLI --version (1.27.0), --help and guide agent, licenses:check and format:check in apps/mobile, and pnpm tarball inspection of the twelve packages (README and LICENSE in each, version 1.27.0, all 23 internal dependencies on exactly 1.27.0 with no ^, ~ or workspace: range; file counts equal 1.26.0's for every package). pnpm test at load average 26 to 41: 437 files, 10929 tests passed and 30 skipped. test:e2e: 119 tests, 118 passed and 1 failed, test/e2e/storage-volumes.e2e.js on macOS, as in 1.24.0 through 1.26.0 (#4205).

Run after the version commit landed: exact-commit CI on main (Node 22 and Node 24 jobs and published runtime (node 22.12.0)), and the Release workflow's own gates. The full Windows unit job is recorded, not gated, per #3878.

1.26.0​

Stim Desktop and the Stim phone app ship through their own workflows; this release covers the twelve npm packages (@stim-cli/core, @stim-cli/cache, @stim-cli/cache-s3, @stim-cli/agent-driver, @stim-cli/agent-driver-agent-device, @stim-cli/metro, @stim-cli/expo-build-cache, stim, @stim-cli/ci, @stim-cli/transport, @stim-cli/transport-tailscale and @stim-cli/server). No package publishes for the first time in this release. Commits that only changed those apps, tests, CI or internal structure are not listed.

New​

  • Automatic maintenance shuts down idle devices that have no dev server. The workspace supervisor already shut down an owned simulator or emulator after devices.idleShutdownMinutes (30) without activity, but a device with no supervisor, such as after a Release run or a dev server that exited, stayed booted. A new maintenance check, every maintenance.idleCheckMinutes (new, default 5), shuts those down by the supervisor's own idle rules: booted, no driver, native-run lock, lease, viewer or build, and no activity for the workspace's devices.idleShutdownMinutes. It re-checks each device before acting, shuts it down through centralized teardown and never deletes it, and stim status shows shut down after <N>m idle. It skips the workspace of the command that started the pass and plans nothing under a scoped STIM_HOME. stim gc without --delete lists the planned shutdowns in its maintenance plan. Stim does not see a person clicking around Simulator.app, so a simulator with no app logs can shut down while someone looks at it. Refs #2598. (stim, @stim-cli/core) 6fe868797
  • The maintenance age sweep removes parked devices and, opt-in, idle worktrees. The sweep now deletes parked simulators and emulators unused for maintenance.olderThanDays (7) through centralized teardown, as gc --older-than does; before, maintenance deleted parked devices only under disk pressure. The new machine setting maintenance.removeIdleWorktrees (default false, env STIM_MAINTENANCE_REMOVE_IDLE_WORKTREES) also removes clean, pushed linked worktrees that no Stim command has used for maintenance.olderThanDays, even when their branch never merged, with their git-ignored files; it needs maintenance.removeFinishedWorktrees. gc's rules still apply: never a dirty or unpushed worktree, the source checkout, or one whose last use is unknown. maintenance.olderThanDays 0 is now valid and turns the age removals off; before, 0 was rejected. Refs #2598. (stim, @stim-cli/core) 368c217c1
  • A machine-wide log records what gc, maintenance and the budget gate cleared. Every run that deletes or changes something appends one record per item to $STIM_HOME/gc/gc.ndjson: gc --delete, --idle and --cache (trigger manual, or desktop from Stim Desktop), maintenance actions in on mode (trigger maintenance, with the pass id), and the budget gate's reclaim steps before start, ios and android (trigger budget), including the dev servers it stops. Each record has kind, status (done, kept or failed), label, id, path, bytes and detail. Dry runs and runs that only kept things write nothing. The log rotates with maintenance.logMaxMb and maintenance.logRetentionDays. stim logs --source gc reads it from any directory, with --json, --since, --grep, --tail, --follow and --errors; gc cannot be combined with other sources and is not part of --source all. Fixes #4253. (stim, @stim-cli/core) 41d117234
  • stim doctor recommends app readiness markers. When none of the app's JS or TS sources contain [stim:readiness], doctor adds the note readiness-markers-missing, pointing to stim guide lifecycle readiness. Without markers a Debug launch ends shortly after the app's JavaScript starts, before the first screen is usable. The scan skips node_modules, native and build directories and dot-directories, follows no symlinked directory, and reports nothing past 5000 source files. The guides and website now call the markers recommended instead of optional. Launch verification is unchanged. Fixes #4264. 212721fad
  • Stim names the layer that stops a remote machine. stim doctor reported every unreachable remote Mac with one message, and printed the raw WebSocket error. It now stops at the first failing layer and adds diagnosis { layer, code, title, message, remedy } beside the unchanged state: Tailscale on this Mac (tailscale-stopped, tailscale-logged-out, tailscale-unavailable), the peer (peer-not-found, peer-ambiguous, peer-offline, peer-no-answer), the serve route (route-missing, route-tls, route-http-error) and stim-server (server-not-running, server-not-ready, server-no-answer, server-outdated, connect-failed). tailscale ping runs only after a silent connection. A peer's /health now adds startup: { state, cause } while the server is not ready, with cause home-unreadable or start-failed; the reason text stays loopback-only. Hosted-session errors point at stim doctor. Fixes #4297. (stim, @stim-cli/server, @stim-cli/core) cea2a2cc4
  • Android builds offload to a worker that Gradle can download missing SDK packages for. A worker without the project's NDK, build-tools or compile platform was refused. When the worker's SDK licenses are accepted and the project has not set android.builder.sdkDownload=false in its Gradle root's gradle.properties, the missing package is now a note (doctor finding build-machine-<code> at level note) and the build is offloaded; the Android Gradle Plugin downloads it on the first build, which is not stored as a worker artifact. Otherwise the problem stays a blocker, now with On <machine>, ... and the sdkmanager --licenses and sdkmanager "<package>" commands at the worker's SDK path. A worker that reports no license field keeps the strict rule. stim-server setup accepts any Android SDK directory. The note's doctor finding carries the sdkmanager command that installs the package ahead of the first build. Fixes #4301 and, for the note's command, #4326. (stim, @stim-cli/server, @stim-cli/core) df356d21d, 1f5287541
  • An offloaded Android build runs on an installed JDK of this Mac's major. A worker whose stim-server default JDK had another major was refused with a JAVA_HOME restart as the fix. The worker now lists the majors of every JDK it finds (JAVA_HOME, /Library/Java/JavaVirtualMachines, Homebrew openjdk, Android Studio's JBR, SDKMAN, /usr/lib/jvm, Windows vendor directories; a JRE without javac does not count). A client that sees the new build-jdk server feature sends its major on build.start, and the worker sets JAVA_HOME for that build only. When no installed JDK matches, doctor gives the install command for the worker's OS (brew install --cask zulu@<N>, apt or winget). The exact-major rule stays. A worker without the field keeps the old rule and text. Fixes #4306. (stim, @stim-cli/server, @stim-cli/core) 302252656
  • Doctor diagnoses xcode-select on a remote Mac and gives the exact Xcode and runtime commands. The worker's offer on a Mac gains developerDir and its Xcode apps in /Applications with version and build, and, while no Xcode works, the xcodebuild -license check and -checkFirstLaunchStatus results. A Command Line Tools selection now reads no Xcode there: xcode-select points at Command Line Tools with sudo xcode-select -s <app>/Contents/Developer and the license or first-launch command only when needed; no Xcode app gives xcodes install <version>; a missing runtime gives xcodebuild -downloadPlatform iOS -buildVersion <version>. The simulator SDK and runtime problems that follow from the Xcode problem point to it instead of repeating it. A worker that reports no developerDir keeps the old text. Fixes #4307. (stim, @stim-cli/server, @stim-cli/core) 11ea3cf73
  • stim-server setup says what it waits for. After the approval prompts it prints Waiting for the requesting machine to connect. Stim Desktop sends this automatically., then the existing approved <client> for builds line when the request arrives. Fixes #4303. (@stim-cli/server) 6524a1cab

Fixes​

  • Fingerprints use @expo/fingerprint 0.21's balanced preset. A native package is now hashed by name and version instead of by its files, which cuts fingerprint CPU on apps/mobile from 3.6 s to 1.6 s on iOS and from 5.2 s to about 1.8 s on Android, and lets a same-version pnpm reinstall with different peer suffixes hit the cache. Every cache key moves once (see Migration notes). Patches at the monorepo install root and the patchedDependencies hashes in pnpm-lock.yaml are hashed, so a patch outside the app's patches/ still moves the key. A native package's version bump is reported as one changed source in the miss line. A hand edit to a native file inside node_modules/ no longer moves the key; use a patch, or --no-build-cache --remote-build local. A project's fingerprint.config.js can still choose strict. Refs #4236. 1025d319d
  • A workspace's first stim ios or stim android reuses a full fingerprint recorded for the same tree. Each full fingerprint is recorded under a static digest of the tree in $STIM_HOME/fingerprint-memo/<platform>/, so a fresh worktree of an already built commit or a new agent session skips the config evaluation and autolinking. The static digest covers the tracked native inputs by git blob id, changed and ignored inputs by content, the config's own sources, .env files and the environment variables it reads, every installed package's path, version and native markers, the lockfile and the fingerprint options. A config it cannot read statically, such as one with a dynamic require or fs access, always takes the full fingerprint. After a hit, a low-priority full fingerprint confirms the record in the background while the app installs; a disagreement prints a fingerprint line naming both hashes, writes fingerprint_memo_stale to the build log, drops the record, and leaves the run's artifact in place. The run waits at most 3 s for the check. Only the pre-mutation lookup uses the memo; --no-build-cache skips it. gc prunes the directory with the other caches. Closes #4209, refs #4236. (stim, @stim-cli/core) a2399ee5f
  • A cached stim android proves the installed APK from its signed tail. Instead of hashing the whole installed APK on the device and the local one in Node (7 to 12 s for a 119 MB APK under load), Stim compares the installed size and the SHA-256 from the v2 or v3 signing block through the end of the file with values recorded beside the cached APK after one apksigner verify, in one adb shell call. A different size or tail installs at once. The whole-file hash remains for APKs outside a build cache entry, APKs without a v2 or v3 block, a missing apksigner, and devices that cannot answer the probe. Android verifies the bytes before the signing block at install, so the only case no longer caught is a rooted write to the installed base.apk. Fixes #4210. b4a48303b
  • stim ios mutes a running simulator again after SimSlim reboots it. With ios.simslim on or a changed ios.simslimProfile, the reconcile on an already-running owned simulator rebooted it and left it at volume 60. Stim now compares SpringBoard's startup pid before and after the reconcile and mutes the simulator when it changed; an Already slim run leaves the volume alone, so a Desktop Sound choice survives. When a pid read fails, Stim mutes anyway. The guide and website now say a running simulator is slimmed on the next stim ios. Fixes #4250. a3880edc6
  • stim ios skips the SimSlim reconcile on an unchanged profile and boot. The owned device record now stores simslimApplied: { profile, springBoardPid } after a reconcile, and a rerun with the same profile content and the same SpringBoard pid skips simslim and its applying line, saving about 1 s of the preamble. Any other case reconciles as before. A manual simslim off on a running simulator is noticed only after a reboot or a profile change. Fixes #4289. (stim, @stim-cli/core) 3a9f29156
  • App readiness applies to hosted iOS and Android launches. On a hosted device (--remote <machine> or automatic placement), the launch check never saw the app's device log, so [stim:readiness] markers, the early end at Running "<name>", device-log app errors and hosted Android native crashes were missed. The hosted finishers now copy the slot's log from the host while they verify and use the same verify path as local runs, waiting up to 10 s past each window for a late copy. launched: true still means a proven bundle request through this workspace's Metro bridge. Local runs keep their timing. Fixes #4270. d317e7798
  • A missing or different global CocoaPods on a worker no longer refuses a build sent to a named machine. A named machine (--remote-build <machine>) now takes the build and reports the difference in the placement line; a failed pod install there stops with pod install failed: <reason> and an install command for that machine instead of the pairing remedy. Automatic placement still skips such a worker unless it already holds the artifact, and stim doctor reports it with brew install cocoapods or gem install cocoapods -v <version>, also when this Mac's own probe finds no CocoaPods. A Bundler pin without bundle on the worker is still refused. stim ios now checks a named machine's toolchain before prebuild and refuses an Xcode, SDK, runtime, CPU, Stim build or Bundler difference with the exact fix, so a mismatch no longer costs a prebuild and pod install first. stim-server rediscovers Homebrew, the JDK and the Android SDK for each build offer and build, so a tool installed after the server started is found. Fixes #4322. (stim, @stim-cli/server) 102fa597b
  • Slow Desktop reports no longer fill a connection's request slots on stim-server. A waiting desktop.reports.get held one of the connection's four request slots, and an invalidation left the stale load running, so a gc report could time out and every other request was refused with limit-exceeded. Report waits now use their own pool of 16 per connection, an invalidation aborts the stale load, and behind the new local feature desktop-report-invalidate-target an invalidation can name one report target. The per-connection request limit rises from 4 to 32, and a tailnet device can run at most 16 requests across its connections until its cancelled work drains. Fixes #4298. (@stim-cli/server, @stim-cli/core) a9e18e635
  • The gc and doctor reports Stim Desktop polls are cheaper. gc reads the worktree inventory up to six at a time (one at a time while an operation declares its children on a claim, as gc --delete and maintenance do), and stim-server's gc report reads worktree build-state sizes only from the disk-usage cache instead of running du. The in-process doctor reuses its iOS architecture inspection for 10 minutes while the project files, xcconfigs, Podfile and lock files are unchanged, so a polled doctor does not rerun xcodebuild -showBuildSettings. On the maintainer's home a warm stim gc --json went from 58 s to 42 s. gc --delete decisions are unchanged. Fixes #4300. (stim, @stim-cli/server) 9402c39d6
  • stim stop names every retained claim when several device teardowns fail. Since devices tear down concurrently, two teardowns could each retain an ownership claim, and only the first was reported. stop now throws one error that names every retained claim with the command that removes each; a cleanup error is reported ahead of any other rejection. Fixes #4249. be8cc4e47
  • stim worktree remove reports the trash as deleted when its background rm exits before it is recorded. It printed left 2 ignored directories in <dir>; stim gc --delete deletes them although they were already gone. It now reports deleted when the trash entry is gone and left only when it remains. Fixes #4267. 264e9b5a1

Migration notes​

  • Update the client Mac and its build workers together again. STIM_BUILD_COMPAT goes from 1 to 2 because every fingerprint key moves. A 1.26.0 machine and a 1.25.x machine report different compatibility versions, so they never offload to each other: automatic placement builds locally, and a named build machine refuses with STIM_OFFLOAD_REFUSED. Update a worker's service with stim-server service update --release 1.26.0.
  • Every build cache key moves once. The first stim ios and stim android of each project, platform and variant after the update misses the cache and builds. Entries stored by earlier versions stay until gc ages them out.
  • A hand edit inside node_modules/ no longer changes the cache key. Use a patch, which is hashed, or build with --no-build-cache --remote-build local.
  • Automatic maintenance does more by default. With maintenance.mode on (the default outside a scoped STIM_HOME and CI), owned devices without a dev server shut down after devices.idleShutdownMinutes (30), and parked devices unused for maintenance.olderThanDays (7) are deleted. Set devices.idleShutdownMinutes 0, for one project or the whole machine, to keep devices running. maintenance.olderThanDays 0 keeps parked devices out of the age sweep and also turns off the sweep's other age removals, old build outputs and shared cache entries; disk pressure level 2 still deletes parked devices, and maintenance.deviceIdleDays (3) still deletes the owned devices of an idle workspace, as before. Idle unmerged worktree removal stays off unless maintenance.removeIdleWorktrees is set.
  • New state under $STIM_HOME: gc/gc.ndjson and its rotated generation, and fingerprint-memo/<platform>/. Android build cache entries gain apk-signed-tail.json, written on first use for older entries. The owned iOS device record gains simslimApplied.
  • stim-server request limits changed. A local client can run 32 requests per connection instead of 4; a tailnet device is limited to 16 across its connections. Report waits have their own limit of 16 per connection.
  • Stim Desktop 0.2.7 needs this release for its Automatic Maintenance settings, Cleanup History and the remote machine diagnosis; with an older stim those parts ask to update it.

QA​

The maintainer authorized a stable release without the pre-tag QA gate on 2026-10-22, as for 1.16.0 through 1.25.0. This deviates from RELEASE.md section 3, which has no waiver for a stable version; the expedited RC lane is for prereleases only and was not used. The waiver covers only section 3. No section 3 QA round ran on any commit since v1.25.0, so no change in this release has been through QA, including the idle-device shutdown and parked-device age sweep in automatic maintenance, the gc log, the @expo/fingerprint 0.21 keys and the fingerprint memo, the signed-tail APK proof, the SimSlim mute and skip, hosted readiness, and the offload changes on a second Mac.

Omitted before the tag, for this candidate: all eight rows that node scripts/release-qa-matrix.mjs v1.25.0 requires (246 changed paths with the version commit, 93 mapped to a row, 153 with no native effect, 0 unclassified): loop (expo and bare, iOS and Android), logs (iOS, Android), caches (iOS, Android), real-repository, remote-provider, release-build (iOS, Android), android-flavor and launch-evidence (iOS, Android), plus the native suites test/e2e/native/run-native-e2e.mjs and test/e2e/native/run-cache-e2e.mjs. Not run: test:compat. The test iPhone was connected, but the stage's agent-device v0.21.28 source checkout and its validation-owned emulator were not prepared.

Run before the version commit, on the bumped candidate: format:check, lint, build, typecheck, knip, release:prep --check (12 packages at 1.26.0, 23 workspace:* ranges), CLI --version (1.26.0), --help and guide agent, licenses:check and format:check in apps/mobile, and pnpm tarball inspection of the twelve packages (README and LICENSE in each, version 1.26.0, all 23 internal dependencies on exactly 1.26.0 with no ^, ~ or workspace: range). File counts equal 1.25.0's except stim, 208 instead of 209: only the hashed dist/ chunk files differ, and every file outside dist/ is the same. pnpm test ran 10630 tests on a Mac at load average 100 to 300: 10567 passed, 29 skipped, 34 failed in 12 files, 27 of them by timeout. The 12 files rerun alone, while the load reached 600, failed 26 cases in 7 files, again mostly timeouts. The two that were not timeouts are a vi.waitFor window in packages/server/__tests__/device-host.test.ts (installing where installed was expected) and one in packages/server/__tests__/server.test.ts (503 where 200 was expected). The failing cases differed between the two runs. The local unit result is therefore not a pass; the full unit suite is left to exact-commit CI. The unit suite ran on the candidate before main gained 1f5287541. After that commit was merged into the candidate, these ran on it and passed: every check listed above, the tarball inspection (unchanged results), test:runtime, and the two unit test files that commit changes (173 passed). test:e2e ran only on it: 119 tests, 118 passed and 1 failed, test/e2e/storage-volumes.e2e.js on macOS, as in 1.24.0 and 1.25.0 (#4205).

Run after the version commit landed: exact-commit CI on main (Node 22 and Node 24 jobs and published runtime (node 22.12.0)), and the Release workflow's own gates. The full Windows unit job of a dispatched CI run on the candidate is recorded, not gated, per #3878.

1.25.0​

Stim Desktop and the Stim phone app ship through their own workflows; this release covers the twelve npm packages (@stim-cli/core, @stim-cli/cache, @stim-cli/cache-s3, @stim-cli/agent-driver, @stim-cli/agent-driver-agent-device, @stim-cli/metro, @stim-cli/expo-build-cache, stim, @stim-cli/ci, @stim-cli/transport, @stim-cli/transport-tailscale and @stim-cli/server). No package publishes for the first time in this release. Commits that only changed those apps, tests, CI or internal structure are not listed.

New​

  • Offloaded builds and worker peer artifacts compare a Stim build compatibility version instead of the exact Stim build (stim-server feature build-compat). Before, a client and a build worker had to run byte-identical Stim builds, so an npm release on one Mac and a checkout build on the other, or any two different commits, could not offload to each other. Each stim now carries STIM_BUILD_COMPAT (1 in this release) in every toolchain it reports, and two machines that both report it build for each other when the values are equal. The value changes only in a release that changes what a worker builds, how it builds it, or the build and artifact messages. A client sends compat on build.start, and in place of stimBuild in React Native and SwiftPM peer artifact queries, only to a stim-server that advertises build-compat; against a server without it, peer artifact queries are skipped. A worker update that keeps the same compatibility version keeps its completed artifacts, and worker peer entries stored before this release miss once. A machine that reports no compatibility version still needs the exact same Stim build. Doctor and placement keep the stim-build problem code and its reason text, so Stim Desktop's update path is unchanged. The local build cache key does not change. Fixes #4206. (stim, @stim-cli/server, @stim-cli/core) 91e3b90aa
  • ios.simslim slims every Stim-owned iOS simulator without a profile per app. The new setting takes on or off, default off, at every layer. With stim settings set ios.simslim on --scope machine, stim ios runs simslim on with SimSlim's default categories on each owned simulator it boots, adopts or finds already running, through the same path as a profile. On a simulator it boots, the mute runs after SimSlim's reboot. A project ios.simslimProfile still applies in place of the defaults, and ios.simslim set to off in a workspace, repo or committed layer opts that app out and restores stock services on a simulator Stim slimmed. The first slim reboots the simulator once, and a simulator that was already running stays unmuted after that reboot, as with a profile change (see the SimSlim mute fix below, and #4250). An already slim one is left alone. Without the simslim command, stim ios prints a one-line note and boots unslimmed, and stim doctor reports ios.simslim is on, but SimSlim is not installed. Hosted simulators on a named Mac are not slimmed. SimSlim's defaults disable push, StoreKit, iCloud, Safari sync and the Mail, Calendar and Contacts services among others, so an app that needs one should use a profile that keeps it, or off. Fixes #4242. (stim, @stim-cli/core) 158350087

Fixes​

  • An owned iOS simulator stays muted after SimSlim reboots it. With an ios.simslimProfile, stim ios muted the simulator right after boot and printed simulator muted, then applied the profile with simslim on, which shuts the simulator down and boots it again. SpringBoard sets its default volume of 60 on that boot, so the simulator played sound. simslim off reboots the same way. Stim now mutes after SimSlim has applied or removed its profile, and waits up to 60 seconds for SpringBoard to finish starting first. When the wait fails or times out, the run prints Could not mute this simulator and continues. A simulator that is already running is still not muted, so a profile change on a running simulator leaves it at the volume SimSlim's reboot set. Fixes #4229. f41fc2b98
  • Every package depends on the other Stim packages at the exact release version. The 1.24.0 tarballs named internal dependencies such as @stim-cli/core as ^1.24.0, so installing an older stim or @stim-cli/server could resolve a newer @stim-cli/core and run code from two versions together. From 1.25.0 every stim and @stim-cli/* dependency in the published manifests is the bare version, pnpm run release:prep --check accepts only workspace:* ranges, and the Release workflow refuses a packed manifest with a ^ or ~ internal range. Fixes #4215. (every package with an internal dependency) 097cbc63c
  • Launch verification waits while an Expo dev client's manifest request is pending. An Expo dev client asks the dev server for its manifest before its bundle, and with the fingerprint runtime version policy Expo recomputes the fingerprint for every manifest request, so on a loaded machine a slow launch could end launched: "unverified" after the 20-second window. On Expo SDK 54 and later, where Stim injects its Metro config, Stim's Metro middleware now records each dev-client manifest request in metro.ndjson as manifest_request_started, manifest_request_finished or manifest_request_failed (debug level, with platform and requestId). Verification keeps waiting while one is in flight and for 15 seconds after one was served, up to 60 seconds after the launch. A manifest request is progress, not launch evidence: true and "bundling" still need the app's bundle request, and a failed manifest request extends nothing. Bare React Native, and Expo SDK 53 and older, keep the 20-second window. Fixes #4207. 7c72fc4b9
  • The launch stability window ends once the app's JavaScript is running. After the bundle response, verification waited a fixed 3 seconds before reporting the launch stable. It now ends half a second after the app's first Running "<name>" log for this launch, on iOS and Android, or at the 3-second cap. The verify line then reads app JavaScript running -- the first screen may still be rendering instead of stable for 3s. A fatal record or an exited process before the window ends still fails the launch; an error after it appears in stim logs --errors. Fixes #4225. c26b9cac0
  • The Metro prefetch uses the bundle URL the app last requested. On an Expo app, the prefetch during stim ios and stim android fetched the manifest to learn the bundle URL, which ran one more Expo fingerprint under the fingerprint runtime version policy and delayed the prefetch until it finished. The bundle_response_started record of an app's bundle request in metro.ndjson now carries its url, and the prefetch uses the newest recorded URL for the platform when it passes the same check as metro.warmupUrl.<platform> and its entry file still exists. Order: metro.warmupUrl.*, then the recorded URL, then the Expo manifest or the bare React Native default. A bare React Native app that has served a bundle also prefetches its recorded URL instead of the default index bundle. A workspace that has never served a bundle still fetches the manifest or uses the default once. Fixes #4226. 65605ae9e
  • stim stop shuts down a workspace's owned simulator and emulator concurrently. It waited for each device's teardown before starting the next, so a workspace with both paid the simulator shutdown on top of the emulator's adb emu kill, which waits for the quickboot snapshot. The teardown itself, with its ownership re-checks, is unchanged. Results are still reported in the same order once every device has settled, so the device lines and the stop --json payload keep their order; progress lines can now come before them. When one teardown throws, the others still finish and are reported before the error propagates. Fixes #4243. ec621a003
  • stim ios restarts a running simulator app with one simctl launch --terminate-running-process. It ran a separate simctl terminate first. The restarted running app (was pid N) note and launch verification are unchanged. A dev client opened through a URL still terminates first, and Android is unchanged. Fixes #4235. 03eee3e24
  • stim ios lists simulators, runtimes and device types with one simctl list --json call. A warm run made five simctl list calls before selecting a simulator. Boot, teardown and the ownership re-checks still list live. Fixes #4228. f7fa3baf5
  • A build cache hit reuses the stored bundle id or Android package. A cache entry now stores the app id read from its artifact, so a hit skips the plutil read of the bundle id on iOS and the aapt read of the package on Android. Entries stored before this release keep the read. Refs #4209. 49526c12e
  • stim worktree warm deletes known problem paths from copied directories instead of walking the whole copy. After copying a directory it now checks only .pnpm-store and expo-modules-jsi/apple/.DerivedData inside node_modules (including pnpm's virtual store), and android/build/generated/autolinking. Whole ignored entries named .jj, .pnpm-store, .DS_Store, .idea or .DerivedData are still not copied, but .DS_Store, .idea and other .DerivedData directories nested inside a copied directory now stay in the copy. A delete whose parent resolves outside the copied entry, such as through a copied symlink, is skipped. Fixes #4216. 2bd8c3281

Migration notes​

  • Update the client Mac and its build workers together. A 1.25.0 machine and a 1.24.x machine never offload to each other: the older side reports no build compatibility version, so both fall back to the exact Stim build match, which differs. Doctor reports stim-build. Until both run 1.25.0, automatic placement builds locally, and a named build machine (--remote-build <machine>, STIM_REMOTE_BUILD or remote.build) refuses with STIM_OFFLOAD_REFUSED, as does automatic placement with local disabled in remote.buildPoolDisabled. Update a worker's service with stim-server service update --release 1.25.0. After that, later releases and checkout builds with the same compatibility version offload to each other.
  • Internal dependencies are exact pins. @stim-cli/expo-build-cache or @stim-cli/metro installed beside a different stim version now brings its own copy of @stim-cli/core at its own version instead of sharing one through a ^ range. Nothing fails to resolve; the cost is a second @stim-cli/core on disk.
  • metro.ndjson has new records and fields. manifest_request_* records appear in Expo workspaces' metro logs, and bundle_response_started gains a url field in every workspace. Readers of the log should ignore unknown events and fields.

QA​

The maintainer authorized a stable release without the pre-tag QA gate on 2026-10-21, as for 1.22.0 through 1.24.0. This deviates from RELEASE.md section 3, which has no waiver for a stable version; the expedited RC lane is for prereleases only and was not used. The waiver covers only section 3. No section 3 QA round ran on any commit since v1.24.0, so no change in this release has been through QA, including the build compatibility version on two Macs, the SimSlim mute, the manifest-aware launch window, the early end of the stability window, the recorded prefetch URL, the warm deletes, the concurrent stim stop and ios.simslim.

Omitted before the tag, for this candidate: all eight rows that node scripts/release-qa-matrix.mjs v1.24.0 requires (118 changed paths with the version commit, 58 mapped to a row, 60 with no native effect, 0 unclassified): loop (expo and bare, iOS and Android), logs (iOS, Android), caches (iOS, Android), real-repository, remote-provider, release-build (iOS, Android), android-flavor and launch-evidence (iOS, Android), plus the native suites test/e2e/native/run-native-e2e.mjs and test/e2e/native/run-cache-e2e.mjs. Not run: test:compat, which needs a connected iPhone.

Run before the version commit, on the bumped candidate: format:check, lint, build, typecheck, knip, release:prep --check (12 packages at 1.25.0, 23 workspace:* ranges), test:runtime, CLI --version (1.25.0), --help and guide agent, licenses:check and format:check in apps/mobile, and pnpm tarball inspection of the twelve packages (README and LICENSE in each, version 1.25.0, all 23 internal dependencies on exactly 1.25.0 with no ^, ~ or workspace: range, file counts equal to 1.24.0's). pnpm test ran 10380 tests on a Mac at load average 140 to 230: 10342 passed, 29 skipped, 9 failed in 5 files. Rerun alone, packages/stim-cli/src/__tests__/doctor-cxx.test.ts, packages/server/__tests__/mobile-protocol.test.ts and packages/stim-cli/src/__tests__/hosted-log-discovery.test.ts passed. packages/server/__tests__/device-host.test.ts failed one or two different cases on each of two reruns at load average 180 to 380. packages/server/__tests__/machine-update.test.ts fails on macOS because a darwin-only case still expects a ^ range for a packed internal dependency, which 097cbc63c made exact (#4245); CI's unit jobs run on Linux and skip it. The local unit result is therefore not a pass; the full unit suite is left to exact-commit CI. test:e2e ran 118 tests: 117 passed and 1 failed, test/e2e/storage-volumes.e2e.js on macOS, as in 1.24.0 (#4205). Those runs were on the candidate built from 03eee3e24. After main gained 65605ae9e and the candidate was updated, the build, CLI --version and the three unit test files it changes plus bundle-response.test.ts (1007 passed) were rerun. After main gained 158350087 and it was merged into the candidate, these were rerun on it and passed: format:check, lint, build, typecheck, knip, test:runtime, release:prep --check, CLI --version, --help and guide agent, licenses:check and format:check in apps/mobile, the tarball inspection (unchanged results), and the four unit test files that commit changes (426 passed). The full pnpm test also ran again on it, at load average 120 to 140: 10367 passed, 29 skipped, 2 failed, the machine-update.test.ts case above and one hosted-log-discovery.test.ts case, which passed when its file was rerun alone (5 of 5). test:e2e was not rerun on it. After main gained ec621a003 and it was merged into the candidate, stop.test.ts (103 passed) and the checks listed above, apart from the unit and e2e suites, were rerun on it and passed. After main gained the test-only 6268767ad, which fixes the stale expectation from #4245, and it was merged into the candidate, machine-update.test.ts passed on it (5 of 5).

Run after the version commit landed: exact-commit CI on main (Node 22 and Node 24 jobs and published runtime (node 22.12.0)), and the Release workflow's own gates. The full Windows unit job of a dispatched CI run on the candidate is recorded, not gated, per #3878.

1.24.0​

Stim Desktop and the Stim phone app ship through their own workflows; this release covers the twelve npm packages (@stim-cli/core, @stim-cli/cache, @stim-cli/cache-s3, @stim-cli/agent-driver, @stim-cli/agent-driver-agent-device, @stim-cli/metro, @stim-cli/expo-build-cache, stim, @stim-cli/ci, @stim-cli/transport, @stim-cli/transport-tailscale and @stim-cli/server). No package publishes for the first time in this release. Commits that only changed those apps, tests, CI or internal structure are not listed.

New​

  • device-host.metro.ports (feature hosted-metro-ports): a hosted iOS Expo dev-client run picks a Metro port that is free on both Macs. An Expo dev client loads its bundle from the port that the client's dev server writes into the manifest, so the hosting Mac's session bridge has to listen on the client's Metro port (see the matching Fixes entry). Both Macs allocate Metro ports upward from 8082, so binding the bridge on the client's port would often be refused with STIM_HOSTING_REFUSED at device-host.metro.open, after the build and the simulator boot, because the host already uses that port. Before it starts Metro for an Expo app that has an expo-dev-client dependency on a hosted iOS Debug run (named host or auto placement), stim now asks the host which of a window of 64 candidate ports already accept loopback connections ({ ports } in, { busy } out, at most 256 ports, device-host approval only) and reserves a port that is free on both. A recorded workspace port the host uses is moved with the existing "Reserved port N for this project instead" note. A Metro that is already running, or a metro.port / STIM_METRO_PORT pin, is never moved: if the host uses that port the run refuses with STIM_HOSTING_REFUSED before the build, a pinned Metro is not started, and the remedy says to run stim stop, then stim ios --remote <machine> without stim start, or to pin a port free on both Macs. Ports held by the same client's own dev-client bridges are not reported busy, so a rerun against a resumed session does not conflict with itself. When the host cannot answer, the run notes that it could not check the host and picks a port free on this Mac; when the host does not advertise hosted-metro-ports, it does the same without a note. Either way, the host's own bind check still refuses an occupied port. Fixes #4096. (stim, @stim-cli/server, @stim-cli/core) f075a6acf
  • remote.easMaxDurationMinutes (machine setting, positive integer, default 30) caps each EAS Simulator session Stim creates. Stim passes it to eas sim as --max-duration-minutes for --remote eas and remote.easFallback, so EAS stops the session and stops billing after that many minutes even when nobody ran stim stop (for example when the workspace was deleted by hand). Before, Stim never passed the flag, so a session ran until the EAS account plan's own limit. A session that expired is replaced by the next run; a run that reuses a live session does not check how much of the cap is left; stim stop and gc --delete still end a session sooner. EAS rejects a value above the account plan's limit, and that rejection appears as the eas sim failed reason. An invalid value in the machine config.json refuses with STIM_BAD_ARG instead of creating an uncapped session. There is no flag or environment override. Fixes #3739. (stim, @stim-cli/core) 8e764f689
  • A fingerprint-mismatch refusal from a build worker names the sources that differ and the files the worker rewrote. Before, a React Native or Expo offload that the worker fingerprinted differently failed with only the two truncated hashes (the checkout there fingerprints <12>, here <12>). The reason now also lists, when the worker rewrote synced files, expo prebuild and pod install there rewrote <files> and, when the build Mac's stim-server advertises fingerprint-sources, differing sources: <names> (three names, then and N more). The STIM_OFFLOAD_REFUSED remedy for a named --remote-build <machine> now says to make the named input agree on both Macs (for example by committing the lockfile pod install rewrote there) instead of the generic pairing remedy. The client sends each fingerprint source's id and hash as fingerprintSources on build.start only to servers that advertise the new feature fingerprint-sources. (stim, @stim-cli/server, @stim-cli/core) 50ae9e043

Fixes​

  • A build worker installs an app's dependencies with the lockfile nearest the app, installs the whole pnpm workspace, and reuses a hoisted install. The worker finds the first pnpm-lock.yaml, yarn.lock or package-lock.json in the app's directory or above it in the checkout, and runs the install in the directory that holds it: pnpm install --frozen-lockfile, yarn install --frozen-lockfile or npm ci. Before, it looked for a lockfile only at the repository root, so an app with its own lockfile inside a larger repository failed prebuild on the worker, and a pnpm install was filtered to the app and its dependencies (--filter <package>...), which missed workspace packages that a Gradle task runs (for example pnpm run --filter <package>). The deps progress line adds in <dir> when the install is not at the checkout root. An install in a subdirectory keeps its own marker (dependencies-<hash>), and the worker skips the install when the lockfile, install command and Node major match the marker and node_modules exists in the install directory. Before, it required node_modules in the app's own directory, so apps whose dependencies are hoisted to the workspace root were reinstalled on every build. The client computes the lockfile hash it sends for dependency ranking from the same nearest lockfile. Fixes #4070, #4094 and #4091. (stim, @stim-cli/server, @stim-cli/core) 6000dbc12, f62250932, 29dc5e251
  • An offloaded React Native or Expo build sends the gitignored files its fingerprint reads, so the worker computes the same fingerprint. Besides the tracked and untracked files git lists, the client now also uploads the gitignored files that the native fingerprint hashes (under fingerprint sources inside the repository, skipping node_modules and paths the fingerprint itself ignores) and, when the Expo config is a fingerprint source, the Expo env files that exist (.env, .env.local, .env.<mode>, .env.<mode>.local, unless EXPO_NO_DOTENV is set). Before, an app whose config reads a gitignored .env.local was refused with fingerprint-mismatch. These files can hold secrets and now go to the paired worker; nothing under node_modules and no build output the fingerprint ignores is sent. Stim's own fingerprint (and so the build cache key) now also ignores Xcode's *.xcodeproj/xcuserdata and, for iOS, the react-native bundle output ios/main.jsbundle and ios/assets while a .gitignore pattern matches them (even when committed); stim doctor's fingerprint-parity remedy and the .fingerprintignore guide text say so. Fixes #4092. (stim, @stim-cli/server, @stim-cli/core) 50ae9e043
  • A build worker ignores the same iOS fingerprint paths the client's git ignores or tracks. The worker has only the synced .gitignore files and an empty git index, so it disagreed with the client for paths ignored through .git/info/exclude or a global excludes file, and for tracked top-level entries that a .gitignore pattern matches. For an iOS build the client now sends fingerprintIgnored (the bundle output and, with ios.projectPath set to ., the top-level entries its git ignores) and fingerprintTracked (the top-level entries it tracks although a pattern matches them, ios.projectPath . only) on build.start, only to servers that advertise the new features fingerprint-ignored and fingerprint-tracked, and the worker applies them in its initial, post-prebuild and final fingerprints. Before, both cases ended in fingerprint-mismatch or fingerprint-moved. No extra files are uploaded for these two. Fixes #4110 and #4152. (stim, @stim-cli/server, @stim-cli/core) 5caf777a7, e23e90475
  • pod install and the CocoaPods version check honor a Gemfile pin in ios/. Stim now takes the nearest Gemfile from the directory pod install runs in (ios/) up to the project root, as Bundler does. When its Gemfile.lock resolves cocoapods, pods run through bundle exec pod install on the client, on the worker and in the stim doctor and stim worktree warm commands it prints, and the bundle install remedies tell you to cd to the Gemfile's own directory. Before, only a root Gemfile counted, so an app with its pin in ios/Gemfile ran plain pod install, a different CocoaPods could rewrite Podfile.lock on the worker, and the offload ended in fingerprint-mismatch. For a project without a pin, this Mac's pod --version now runs in the iOS directory; for a pinned one, Stim reports the version the lockfile resolves and requires only Bundler on the worker. A nearer Gemfile that does not pin cocoapods now hides a pin in a parent directory. Fixes #4112. 22b4bb61d
  • A .ruby-version that names an rvm Ruby gets that Ruby's @global gemset and gemset bin directories. pod install and the CocoaPods probe now set GEM_PATH to the Ruby's gemset plus @global, and put the gemset and @global bin directories on PATH, as rvm use does. Before, only the Ruby's own bin was added, so on a Mac whose CocoaPods dependencies (such as mutex_m) live in @global, pod --version failed and a worker reported no CocoaPods, which the client showed as "no CocoaPods there". rbenv, asdf and mise are unchanged. Fixes #4114. 81f027b4e
  • stim macos --remote <machine> --remote-build <machine> no longer requires this Mac's Xcode, macOS SDK or CPU architecture to match the worker's when both name the same Mac. The app is built and run on that Mac, so the client skips those three checks and now requires only that the worker has Xcode and a macOS SDK and runs the same Stim build. The client's own completed-app cache is skipped, a cache hit is validated against the worker's architecture, and the worker's cache identity comes from its own toolchain. "Same Mac" means the build machine's pinned server key equals the host's. Every other combination, including --remote-build auto, keeps the full match. Fixes #4099. 08ae7f7bd
  • A successful hosted Android build is recorded as ok. stim android --remote <machine> wrote the last-build status success, which the status readers rejected, so stim status, Stim Desktop and the phone app showed no Android last build or history entry for it. It now writes ok. Records already on disk with success read as ok (history succeeded) without being rewritten. Fixes #4183. (stim, @stim-cli/core) 7fd32a463
  • A stim ios or stim android run that builds or reuses an app and then fails records a failed build. Failures after the artifact exists (a simulator or emulator that does not boot, STIM_NO_DEVICE, a hosted placement refusal, a device lease refusal, a hosted launch failure, or a thrown error) now leave a failed entry in lastBuilds and builds with the error code and the cache facts (fingerprint, cacheKey, cacheHit, builtOn, missReason), appPath and bundleId. Before, stim stats counted the run as failed but stim status, Desktop and the phone app kept showing the previous build. The record shape is unchanged. The run also writes one error-level build log record with event run_failed, its code and the phase it failed in, so status and logs --errors agree; a run that fails before it has an app writes none. Fixes #4137. 8f57feec9, dbd1266c0
  • A hosted iOS Expo dev client loads its JavaScript. Expo's dev server writes its own port into the manifest's launchAsset, hostUri and debuggerHost, so a dev client launched through the host's session bridge asked 127.0.0.1:<client Metro port> on the hosting Mac, where nothing listened, and stayed at launched: "unverified" with a redbox. For a hosted iOS run of an Expo dev client, the client now sends its Metro port with device-host.metro.open (it already did for Android) and the host binds the bridge on 127.0.0.1:<that port> instead of a random one. The host first probes by connecting to that port on 127.0.0.1 and ::1, because macOS lets a 127.0.0.1 bind succeed beside a wildcard listener and take its traffic. If the port already accepts connections, device-host.metro.open is refused with device-busy, and stim reports STIM_HOSTING_REFUSED with a remedy that names metro.port and STIM_METRO_PORT. Other iOS runs (bare React Native, non-dev-client Expo) send no port and keep a random bridge port. Fixes #4069. Both Macs need 1.24.0; see Migration notes. (stim, @stim-cli/server) 6edb8944d
  • stim reload reaches an app on a physical device or an EAS Simulator session through the workspace's Metro. Stim records launches only on owned simulators and emulators, so for an app launched with ios --device, android --device or --remote eas, reload refused with STIM_RELOAD_STOPPED ("run stim ios first"), and following that only reinstalled the app and refused again. When no owned launch is live, reload now asks this workspace's Metro for its connected apps and reloads the matching ones: iOS apps by their role=ios, and on Android every non-iOS app, because Stim has no package name for these launches. When Metro lists its clients, it sends nothing unless one matches; the bare React Native dev server cannot list them, so there the reload is broadcast to every connected app and reported as strategy: "metro-broadcast". A bare stim reload takes the platform the connected apps report; when Metro cannot name its apps, it takes the recorded EAS session's platform if there is one. It refuses with STIM_RELOAD_AMBIGUOUS when both platforms are connected, or when Metro cannot name its apps and no EAS session is recorded. In --json and the programmatic API's StimReloadResult, appId is null for such a reload, deviceId is metro:<port>, and deviceName is devices connected to Metro or the EAS session. With no connected app it still refuses with STIM_RELOAD_STOPPED; for an EAS session the remedy is stim <platform> --remote eas, which relaunches the app, because an app in an EAS session may not open Metro's websocket through the tunnel. reload does not restore a lost adb reverse on an Android phone. The physical-device launch remedies now say stim reload ios|android instead of agent-device metro reload, which iOS keeps only for a --remote device. An owned launch keeps the existing path. Fixes #4133 and #4201. 750c5901d, 6771f8539
  • stim reload ios no longer fails with STIM_RELOAD_PROBE_FAILED when the process probe is slow. Before it sends the Metro reload, reload checks that the app process exists; on iOS that is simctl spawn <udid> launchctl list with a 2 second timeout, which a loaded Mac regularly exceeded. The iOS probe now waits 5 seconds scaled up with load per CPU, to at most 30 seconds. A probe that still returns unknown (iOS or Android) no longer refuses: the reload is sent, and STIM_RELOAD_PROBE_FAILED is reported only when the Metro reload then fails and a second probe still cannot tell whether the app runs. A process the probe proves gone still returns STIM_RELOAD_STOPPED. Fixes #4165. 1e1404388
  • stim stop and worktree remove remove the Metro adb reverse that android --device added on a physical phone. The reverse outlived the workspace, so once Stim gave that Metro port to another workspace, apps on the phone that opened localhost:<port> reached that other server. Before each android --device launch against Metro, the run records the phone's serial and the Metro port in the workspace state.json. stop (all slots, or only --slot's phone), worktree remove and gc --delete of the workspace list adb reverse on the recorded serial, remove only tcp: reverses whose host side is a recorded port, and drop the record; every other reverse on the phone stays. An unreachable phone is skipped without a message and its record is dropped, so a later stop does not retry it; neither case fails the stop. When stop refuses to signal an unverified supervisor or dev server, the reverse and its record stay. A slot whose --device run moves to a different phone keeps the previous phone's reverse until that phone is unplugged or the adb server restarts. Fixes #4132. 7d81956a5
  • stim stop removes an owned emulator's Metro adb reverse before it stops Metro. After Metro was killed, the app kept reconnecting through the leftover reverse; on Windows the adb server connects each such stream with a blocking loopback connect (about 2 seconds when refused), so the retries stalled every adb call and the teardown's adb devices timed out. stop now removes the reverses that point at the workspace's Metro port on each owned, running emulator (confirmed through emu avd name, including the tcp:8081 fallback reverse) right before it signals the supervisor or a verified leftover dev server, or when it finds nothing listening. An unverified supervisor or dev server is not signalled and its reverses stay. The removal is best effort and bounded; a failure leaves the existing teardown unchanged. A physical phone or a non-owned emulator is not covered by this entry. Fixes #4101. 71a9f97ae
  • stim logs --errors no longer returns every OS log line from a hosted iOS app. Hosted pulls stamped each record event: "hosted_native_log", and the query admitted that event wholesale, so a healthy hosted iOS launch showed about 100 UIKit and Core Animation lines under --errors, and status error counts read the same criteria. A local simulator keeps those lines outside --errors and shows them with --source device; hosted records now follow the same rule, and none are dropped (they appear with --source device or --source all). So that a hosted Android crash still appears, the hosted pull groups logcat per process from each crash header and stamps a group matching FATAL EXCEPTION: or signal N ( as native_crash, using the same crash-header patterns and grouping as the local crash capture, without its app-id check. Hosted iOS produces no native_crash. Each line of a crash run is its own native_crash record, and a crash run split across two pulled pages keeps native_crash only on the page with its header. Fixes #4136. (stim, @stim-cli/core) 48ee630ca
  • stim ios|android --remote eas tells an Expo app with no EAS project to link one. When eas sim reports "EAS project not configured" (no extra.eas.projectId), the run failed as eas sim failed: ... and then STIM_NO_DEVICE with "run stim ios again", which fails the same way. It now returns STIM_REMOTE_EAS_UNAVAILABLE with the reason "The Expo app is not linked to an EAS project, so no EAS Simulator session was created." and a remedy naming eas init --id <project-id> --non-interactive (existing project) or eas init --account <account-name> --non-interactive (new project), then the stim <platform> --remote eas command to rerun. No session is created. Fixes #4135. e7ead70eb
  • Starting Metro retries when another Stim home takes the reserved port. Two Stim homes on one machine keep separate port reservations and can pick the same free port; the second dev server then failed to bind and start ended with STIM_SUPERVISOR_EXITED and listen EADDRINUSE ... :<port>. When the supervisor exits with that bind error on the port Stim reserved, start, and the Metro start inside stim ios|android, now release the reservation, print "Port N was taken by another process while the dev server started." and start again on another free port, up to 3 attempts; after the third it fails with the original code and a remedy naming the port. A pinned metro.port / STIM_METRO_PORT, --remote starts, and any other supervisor exit are not retried. Fixes #4073. 65a6b77c0
  • stim worktree warm clones a carried directory in one step on macOS. For an ignored entry that is a real directory, such as node_modules or ios/Pods, warm now calls clonefile(2) once on the whole directory through a small helper, stim-clonefile, instead of cp -Rc, which cloned one file at a time. The helper is compiled on first use with xcrun clang into $STIM_HOME/helpers, so it needs the Xcode Command Line Tools. On the same APFS volume a directory with 143k files drops from about a minute to seconds. When there are no Command Line Tools, the compile fails, the platform is not macOS, or clonefile refuses the copy (EXDEV across volumes, ENOTSUP), warm removes the partial copy and falls back to cp -Rc and then cp -R as before. What is copied, skipped and swept, the lockfile warnings and the empty stdout are unchanged. One difference: a directory clone keeps the source's symlink modification times, where cp -Rc stamped the copy time. Fixes #4162. a19e29e2f
  • stim worktree warm removes copied exclusions (.DerivedData, .pnpm-store, .idea and the like) with bounded concurrency. The sweep of the copied tree visited directories one at a time; it now reads up to 32 directories at once. On success the set of removed entries is unchanged; after a failure, the first error is thrown once the in-flight directories settle, so up to 31 of them may still finish their removals. Fixes #4163. f71bcbb34
  • stim worktree warm skips Pods files that do not contain the source checkout's path before it runs the relocation regex. On a carried ios/Pods of 19,707 files, detecting the source path used about 10 s of CPU because every file was decoded to a string and matched against a Unicode boundary regex. Stim now checks the file's bytes for the UTF-8 form of the source root first and runs the regex only on files that contain it, measured at about 2 s. Which files are rewritten, what they are rewritten to and when the leftover check refuses are unchanged. Fixes #4168. 57ce656f5
  • stim worktree remove no longer waits for node_modules, ios/Pods and build outputs to be deleted. Before git removes the worktree, Stim renames its ignored directories into $STIM_HOME/trash/remove-*, holding an ownership claim and writing a trash.json (worktree path and time) first, so git removes a light tree. The CLI then deletes the entry in a detached, low-priority rm recorded as the claim's child and prints a trash phase line on stderr (deleting <n> ignored directories in the background from <dir>); disk is freed when that rm finishes. Through the programmatic API (removeWorktree), the entry is deleted before the call resolves, because a bounded report stops its children when it settles. When git worktree remove fails, the directories are moved back; any that cannot be moved back (the place was taken) stay in the entry, and the error names them and the entry. Nothing is renamed on Windows, when $STIM_HOME is on another filesystem than the worktree, or for a directory that does not rename; git deletes those as before. stim worktree remove stdout stays empty. Fixes #4164. (stim, @stim-cli/core) 81d5d8284
  • stim gc reports the worktree trash and deletes only what is safe to delete. gc lists each directory under $STIM_HOME/trash under "Worktree trash" with its size ("deleting" while a removal holds it) and the worktree it came from; the --json payload gains trash: { root, entries: [{ path, worktree, removedAt, bytes, willDelete, kept }] } (null when the trash is empty or under any --cache scope), and --delete records results under the trash category. gc --delete deletes an entry only when it is a remove-* directory with a valid trash.json, its ownership claim is free, and the worktree path in the manifest is gone. It keeps an entry while that worktree path still exists or cannot be shown absent (any lstat error other than ENOENT or ENOTDIR), which is the state after a removal killed between the rename and git, or after a failed move-back. Without that check gc --delete would delete dependency trees from a worktree the user still has. To finish such a removal, run stim worktree remove on that worktree again; gc then deletes the entry. gc never moves directories back and keeps any other directory under trash. Fixes #4194. (stim, @stim-cli/core) 81d5d8284, 837cc6437
  • stim gc stops listing a removed workspace's build outputs as kept. After stim worktree remove deleted workspaces/<id>, the size recorded for it in maintenance/state.json stayed until the next size check. gc counted it toward the workspace-outputs cap and printed kept <home>/workspaces/<id>: workspace outputs cannot be resolved and blocked: workspace-outputs ... eligible targets cannot reach the target. A recorded size is now dropped before planning when workspaces/ exists and the recorded directory, a direct child of it, is absent (ENOENT or ENOTDIR). If workspaces/ is missing or the check fails any other way, the record stays and is still reported as kept. Fixes #4072. 31ab28706
  • A STIM_CLAIM_UNAVAILABLE refusal from stim gc names the child and the real retry command. Under heavy load, a plain gc dry run could print a remedy that began with undefined and suggested stim gc --idle <duration>. The report-child refusal and the 35 second report-cleanup expiry carried no remedy, and gc suggested --idle for every run that was not --delete, although --idle shuts down idle owned devices. The report-child refusal now names the child's pid, with a ps -p <pid> command on macOS and Linux, and asks you to stop it and its descendants if they are still running; the expiry asks you to check that the child processes the report started and their descendants have stopped. Both give the retry command rebuilt from the options given (--delete, --worktrees, --older-than, --cache, --idle, --json). A STIM_CLAIM_UNAVAILABLE error with no remedy, from any command, now asks you to check that no process from the failed command is still running before the retry, instead of printing undefined. The child is still left untouched and the command still exits 1. Fixes #4155. (stim, @stim-cli/core) 791a9ca66
  • Claim cleanup waits for an exiting process group before it refuses, scaled by host load. On a heavily loaded host, stim start refused with STIM_CLAIM_UNAVAILABLE ("Could not confirm cleanup of supervisor pid N") as soon as a supervisor had exited with a helper, such as Expo's xcrun simctl help, still in its process group, and that error hid STIM_SUPERVISOR_EXITED and blocked the retry on EADDRINUSE. Report children (gc and other reports) had one second after SIGKILL to close. Start now polls the supervisor's process group without signalling it for 5 s times the host's load per CPU (at least 5 s, at most 60 s). A report child is still sent SIGKILL, then gets two waits on the same scale, at most 15 s each, one to close and one for its process group, and its close is judged only after one more I/O turn, so a stalled event loop no longer reads as "did not close within the cleanup budget". When cleanup still cannot be confirmed after a start refusal, the error message starts with the refusal's message and its evidence lines, and the code stays STIM_CLAIM_UNAVAILABLE. A descendant that never exits now takes the full wait before it is refused, instead of 0 to 1 s. Fixes #4093. 0d07f076b
  • stim gc lists and deletes SwiftPM's TemporaryDirectory.* leftovers in TMPDIR. swift build and xcodebuild leave directories named TemporaryDirectory. plus six characters in TMPDIR (six per build), which gc did not recognize. They now appear under "TMPDIR leftovers" with kind swiftpm in the --json payload. gc --delete removes one only when nothing in it changed for 24 hours, keeps one that cannot be read, and keeps all of them while a swift, swift-build, swift-frontend, swift-package, swift-run, swift-test or xcodebuild process runs or the process list cannot be read. Automatic maintenance at disk pressure level 2 deletes them under the same checks. Stim's own SwiftPM macOS builds (stim macos, local and on a build worker) keep running swift with the ambient TMPDIR, where SwiftPM keeps the cross-process locks for its shared caches, so their temp directories land there and gc cleans them. Fixes #4157; #4186 was a regression from an earlier commit in this release, also fixed here. 68ebb740a, 0765f8743
  • Expo apps with a dynamic config skip a second app-config evaluation, and Stim's own Expo children no longer start Expo's telemetry flusher. The remote-cache provider lookup (buildCacheProvider) read the app config with a second expo config --json child after the fingerprint had evaluated it. It now reads the fingerprint's expoConfig source from the same pre-mutation lookup, and spawns expo config --json only when that source is missing or, under the eas provider, has no owner. Stim's own expo config, prebuild, start and export:embed children now run with EXPO_NO_TELEMETRY=1 unless the variable is already set, which drops Expo's detached telemetry process from each call. 5f327a5a4
  • Memory-pressure culprits are read off the boot path. Under warning or critical host memory pressure, stim ios and stim android ran /usr/bin/top -l 1 synchronously before preparing the device, which blocked fingerprinting, the boot and the boot monitor for 3 to 4 s per call (a failing iOS run made two). The read is now asynchronous and shared for 30 seconds per process. Boot prints the memory-pressure message at once and the line naming the processes as a second memory phase line when top finishes, and prints nothing extra if none is abnormal or top fails; that second line is lost if the process exits first. Simulator boot-wait errors, androidBootRemedy and the iOS failure advice still wait for the read to build their text. Fixes #4167. 1f6568c9c
  • stim logs --follow reads a replacement log file from its first record. The follower used to decide a log file had been rotated only when its size fell below the saved byte offset. A new client.ndjson already as large as the old offset was treated as an append: the follower resumed in the middle of the new file's first record, dropped that partial record, and never read the unread tail of the rotated .1 file. It now remembers the device and inode of each followed file. A different identity means the file was replaced: the follower reads the rest of <name>.1 (only if .1 has the remembered identity), then the replacement from byte 0. A replacement made between the initial backfill and the start of the follow is also recognized, because the backfill view now reports the identity of each file it measured. The same follower serves the server's logs subscription. Fixes #3899. (stim, @stim-cli/core) 7bd2e8e57
  • stim logs --follow finds a hosted slot placed just as it starts. Node's fs.watch on macOS can drop an event for a change made just after watch() returns (nodejs/node#52601), so a hosted placement written in that moment was found only on the next 10 second refresh. Once the state watcher is armed, the follower now rechecks hosted placements after 100 ms. Fixes #4200. a3a6f7106
  • The local stim-server socket works under a long Stim home. A Unix socket path is limited to 103 UTF-8 bytes on macOS and 107 on Linux, and a hosted session's private home could make $STIM_HOME/server/local.sock longer than that, so Stim Desktop could not connect to its server. When the path is too long, the socket now lives at /tmp/stim-sockets-<uid>/<sha256 of the socket path>.sock instead, so distinct homes keep distinct sockets; shorter paths are unchanged. Stim Desktop finds the moved socket from desktop-v0.2.5. Fixes #4083. (@stim-cli/core, @stim-cli/server README) d2ec2c4f3
  • Human-mode stim logs stays linear on a very long line. errorDiagnostics scanned each record for file:line:col locations with an unanchored global regex whose cost grew with the square of the longest unbroken run of characters, so one 1.5 MB line took over 14 minutes where --json took 2 seconds. The scan now starts only at the beginning of a run, with the same matches. launchErrorPreview also no longer treats a line over 2048 characters as a stack frame: such a line prints as an ordinary line with its full text and ends the stack block above it. Only a frame-shaped line over 2 KiB renders differently. Fixes #4129. 4db89561b
  • An owned iOS simulator boot now waits for SpringBoard to finish starting, within the ten-minute boot deadline. In 1.23.1 the wait was only part of the mute step: after simctl bootstatus -b returned, Stim polled com.apple.springboard.finishedstartup for up to 30 seconds before setting the volume, and the boot itself ended before SpringBoard was up. simctl launch blocks until SpringBoard has started, so on a loaded host the 60 second launch timeout also paid for SpringBoard's startup and stim ios could fail with STIM_LAUNCH_FAILED while the app started on its own later. Now the boot polls the same state after bootstatus under the existing 600 second boot deadline, reports waiting for SpringBoard in its progress lines, and the mute reuses that result with no wait of its own. The time shows up in the booted (Xs) step instead of the launch. If SpringBoard has not started at the deadline, the boot fails with the usual did not finish booting within 600s message and memory diagnostics; before, the failure was a later launch timeout. If the state cannot be read (an error other than a timeout, or unexpected output), the boot completes and the mute prints its existing could-not-mute warning, as in 1.23.1. Hosted iOS simulator boots on a device-host worker go through the same boot function and get the wait. stim guide errors and website/docs/owned-devices.md describe the SpringBoard wait. Refs #4061. 81c303433
  • stim android --plan honors an explicit --system-image on a React Native or Expo project. The plan keyed its build on the owned emulator's recorded system image even when --system-image named a different one, so it could report a cache key and hit for the old ABI where the run builds for another, or report a plan where the run refuses. The plan now applies the run's rule to the flag: an owned emulator whose boot never finished is treated as replaced and the key comes from the requested image; one that has booted returns STIM_BAD_ARG with the run's message and remedy (stim worktree remove or stim gc --delete, or --slot <name>), for release variants too. With android.remote set to auto, the refusal applies when placement resolves to this machine and not when a remote machine is chosen. android.systemImage alone still leaves an existing emulator in place, so the plan keys on the recorded image for the setting. Fixes #3838. 608a0650d
  • Finding a workspace's own Android emulator no longer pays one timeout per foreign emulator. The resolver asked every running emulator for its AVD name (adb -s <serial> emu avd name, 10 second cap) one after another, so an emulator belonging to another workspace and stuck cost up to a full timeout in each place a run resolves its AVD; one warm stim android measured 4.8 s + 10 s + 10 s. It now reads the serial the workspace recorded first (the record's console port, the serial a run already holds, or the deviceId that stim reload has) and asks the others only when that serial does not run the AVD. Within each group (healthy, then unhealthy emulators) the reads run concurrently and the pending ones are cancelled once one reports the requested AVD, so several stuck emulators cost one timeout together. The ownership check on the AVD name runs before any serial is read and the recorded serial must still report the requested name. Fixes #4166. 753cda2fd
  • stim android on Windows refuses an over-long project root before it starts Metro or prepares the emulator. The STIM_PATH_TOO_LONG check ran only inside the Gradle build, after Stim had created or booted the emulator, and a device failure such as a full disk hid it. The run plan now applies the same check, with the same code, message and remedy, once the target and build are known. It applies to a local build for an emulator or a --remote <backend> target, with the ABI the build would use. It is skipped for a physical or hosted device, an EAS profile, and any run with a paired build machine (an explicit --remote-build <machine>, or auto with machines paired), where the check in the build still applies. Because it does not wait for a cache lookup, an over-long root on an emulator run is now refused even when a cached build would have installed. stim doctor --platform android, stim guide errors and website/docs/requirements.md say so. Fixes #3671. d9e5bd35a
  • stim worktree warm reports a git timeout as a timeout, and on Windows a contained child's time limit counts only the child. On Windows, warm's first git probe ran through the PowerShell report-job wrapper, whose startup could use the whole 10 second git budget on a fresh runner, and the timeout read as Not a git repository. On every platform, warm's path probes now fail with Command timed out after 10000ms: git ...; a real non-repository still reports Not a git repository. For bounded calls, the report-job entry now writes a marker file in the OS temp directory (stim-report-<pid>-<uuid>.started and .closed, removed afterwards) when the child starts and closes, and the caller's timeoutMs runs only between them. PowerShell's startup and exit each get their own 30 second limit, and exceeding either fails with The Windows report job did not start within 30000ms: ... or ... did not exit within 30000ms: ..., code ETIMEDOUT. Marker failures never fail the call; a missing marker leaves the 30 second bound in force. The programmatic API guide and website/docs/programmatic-api.md state the rule. Fixes #4102. 325280205
  • On Windows, every contained child call is about 2 seconds faster. The report-job script ended with [System.Environment]::Exit, which took about 2.5 seconds on windows-latest; it now ends with exit $child.ExitCode, which sets the same exit code (checked for 0, 3, 255, 256, -1, 1073741823 and -1073741819) and keeps the Job Object containment. Each contained git call in stim worktree warm went from 2.8-3.0 s to 0.5-0.8 s, and a whole warm of the bare fixture from 44-45 s to 19-21 s. Fixes #4103. fc12df8eb
  • stim-server keeps serving other connections while a run, stop, warm, remove or gc waits for most locks held by another stim process. The server runs run, build, stop, worktree warm|remove and gc in its own process through createStim. Many writes on those paths (device records, the created-devices ledger, the project registry, workspace state, hosted placement records, the EAS session ledger, prebuild fingerprints, device-capacity stats and error-diagnostic snapshots) waited for a contended lock with Atomics.wait, which stopped the server's whole event loop for up to the 12 second lock wait, so every other connection stalled. These waits now run on a timer. The claim protocol, reentrant nesting, bounded wait and STIM_LOCK_TIMEOUT error are unchanged. The CLI itself, a short-lived process, behaves the same; the supervisor, collector, web and macOS supervisors, maintenance and the offload worker keep the synchronous wait on purpose. The build and warm progress recorders, the run-stats recorder and the physical-device lease lock still wait synchronously, among others. Closes #3532. 3774dd83e, 7dd7cde8d, f71fe11e0

Docs​

  • The guide and website now say the worker writes NODE_BINARY on every offloaded iOS build. stim guide settings and remote-machines.md described the .xcode.env.local write as part of the pinned-Node behavior. The worker writes the first node on its PATH (the pinned Node when it put one first) as NODE_BINARY, pinned or not, replacing any export NODE_BINARY= line. No behavior change. Fixes #4085. aa413d304
  • The stim-server README and the remote-machines page state that a local process on the host can forge X-Forwarded-For to lock one named peer, or the shared local bucket, out for the lockout window. Tailscale rewrites the header to the real tailnet source, so remote clients cannot forge it; the header still authenticates nothing and reads nothing, and the exposure needs local code execution on the host. Documentation only; also touches docs/specs/2026-10-13-pluggable-transports-design.md. (@stim-cli/server README) f00e07be4

Migration notes​

  • EAS Simulator sessions now end after 30 minutes by default. Before 1.24.0 Stim passed no maximum duration, so a session ran until the EAS plan's own limit. For longer sessions, raise the cap up to your plan's limit with stim settings set remote.easMaxDurationMinutes <minutes> --scope machine. A run after the cap creates a new session.
  • Update the client Mac and the build or hosting Mac together. The hosted iOS Expo dev-client fix needs 1.24.0 on both Macs: a 1.24.0 client with an older host, or the reverse, keeps the old random bridge port and the JavaScript still does not load. Offloaded builds already require the same Stim build on both Macs. Update a worker's service with stim-server service update --release 1.24.0.
  • Gitignored fingerprint inputs now go to build workers. An offloaded React Native or Expo build uploads the gitignored files its fingerprint reads, including Expo env files such as .env.local, to the paired worker. Pair only workers you trust with those files.
  • Some iOS build-cache keys change once. Stim's fingerprint now ignores *.xcodeproj/xcuserdata and, while a .gitignore pattern matches them, ios/main.jsbundle and ios/assets. A project with those paths misses its existing build-cache entries on the first build after upgrading. Macs still on 1.23.x that share a remote cache compute the old keys for these projects, and Expo CLI builds that use @stim-cli/expo-build-cache stop sharing those entries with Stim unless the paths are listed in .fingerprintignore.
  • A build worker reinstalls a pnpm repository once. The worker no longer filters the pnpm install to the app, so a repository installed with the filter under 1.23.x reinstalls on its first 1.24.0 build.
  • StimReloadResult.appId can be null. The programmatic API's reload() result now types appId as string | null, because a reload through Metro of an app Stim did not launch has no app id. TypeScript callers that treat it as a string need a null check.
  • Hosted log records written before 1.24.0 drop out of logs --errors and the status error counts. This includes hosted Android crash lines, which were never stamped native_crash. The records are not rewritten and remain under --source device and --source all.

QA​

The maintainer authorized a stable release without the pre-tag QA gate on 2026-10-21 ("Lets cut a quick release"), as for 1.22.0, 1.23.0 and 1.23.1. This deviates from RELEASE.md section 3, which has no waiver for a stable version; the expedited RC lane is for prereleases only and was not used. The waiver covers only section 3. No section 3 QA round ran on any commit since v1.23.1, so no change in this release has been through QA, including the hosted iOS Expo dev-client Metro port, the EAS session cap, the worker's install and fingerprint changes, the worktree trash and the iOS boot's SpringBoard wait.

Omitted before the tag, for this candidate: all eight rows that node scripts/release-qa-matrix.mjs v1.23.1 requires (282 changed paths with the version commit, 140 mapped to a row, 141 with no native effect, 1 unclassified): loop (expo and bare, iOS and Android), logs (iOS, Android), caches (iOS, Android), real-repository, remote-provider, release-build (iOS, Android), android-flavor and launch-evidence (iOS, Android), plus the native suites test/e2e/native/run-native-e2e.mjs and test/e2e/native/run-cache-e2e.mjs. The one unclassified path, vitest.global-tmpdir.ts, is unit-test configuration that the mapping does not list yet (#4203); it would on its own require the full matrix, which was omitted in full under the waiver. Not run: test:compat, which needs a connected iPhone.

Run before the version commit, on the bumped candidate: format:check, lint, build, typecheck, knip, release:prep --check (12 packages at 1.24.0, 23 workspace: ranges), test:runtime, CLI --version (1.24.0), --help and guide agent, licenses:check and format:check in apps/mobile, and pnpm tarball inspection of the twelve packages (README and LICENSE in each, version 1.24.0, internal dependencies on ^1.24.0 or 1.24.0, no workspace: range, file counts equal to 1.23.1's except stim, 209 against 208, which adds dist/stim-clonefile.c). pnpm test ran 10316 tests on a Mac at load average 40 to 130: 10286 passed, 28 skipped, 2 failed in 2 files, one of them a timeout. Rerun alone, packages/server/__tests__/device-host.test.ts and packages/stim-cli/src/__tests__/warm-claim.test.ts passed (196 of 196). The local unit result is therefore not a pass; the full unit suite is left to exact-commit CI. test:e2e ran 118 tests: 117 passed and 1 failed. test/e2e/storage-volumes.e2e.js fails on macOS, also when run alone, because it forces the cp -Rc fallback while the stim-clonefile helper from a19e29e2f clones first; the test does not intercept the helper (#4205). CI runs test:e2e only on Linux, where that test is skipped, so no gate covers it. Those runs were on the candidate built from f71fe11e0. After main gained d2ec2c4f3, a3a6f7106, dbd1266c0 and 6771f8539 and the candidate was updated to include them, these were rerun on it and passed: format:check, lint, build, typecheck, knip, test:runtime, release:prep --check, CLI --version, --help and guide agent, the tarball inspection, and the unit test files those four commits touch (1139 passed, 1 skipped). The full pnpm test and test:e2e were not rerun on it.

Run after the version commit landed: exact-commit CI on main (Node 22 and Node 24 jobs and published runtime (node 22.12.0)), and the Release workflow's own gates. The full Windows unit job of a dispatched CI run on the candidate is recorded, not gated, per #3878.

1.23.1​

Stim Desktop and the Stim phone app ship through their own workflows; this release covers the twelve npm packages (@stim-cli/core, @stim-cli/cache, @stim-cli/cache-s3, @stim-cli/agent-driver, @stim-cli/agent-driver-agent-device, @stim-cli/metro, @stim-cli/expo-build-cache, stim, @stim-cli/ci, @stim-cli/transport, @stim-cli/transport-tailscale and @stim-cli/server). No package publishes for the first time in this release. Commits that only changed those apps, tests, CI or internal structure are not listed.

Fixes​

  • An offloaded build runs the project's own Node code on the Node the project pins. When the worker's own node does not fit the Node that .nvmrc, .node-version, .tool-versions or package.json's volta.node pins, in the app's directory or the nearest one above it in the checkout, the build worker puts an installed Node that does first on PATH before the dependency install, prebuild, pods and xcodebuild. It logs the Node it builds on on the deps line. On every offloaded iOS build, pinned or not, it writes the first node on its PATH as NODE_BINARY in the project's .xcode.env.local, which React Native's Xcode script phases read, replacing any export NODE_BINARY= line already there. Before, those steps ran on the worker's own node, so a project pinning another major could fail to install or build on the worker. A warm Gradle daemon keeps running React Native's Node steps on the Node it started with (#4081). The worker's dependency marker names the pinned Node's major, so a change of major reinstalls the dependencies and an install made under another major is not ranked as reusable. When no installed Node fits the pin, the steps run on the worker's node and a deps-failed or prebuild-failed reason names the pin and that Node's version. Fixes #4077. (stim, @stim-cli/server, @stim-cli/core) 3b5309620
  • Pinned Node discovery finds linked Homebrew node and node@NN installs, and falls back within the pinned major. This applies to Metro's Node and to the build worker. When no install has the exact version the pin names, Stim takes the newest installed Node of the pinned major that is not older than the pin, such as Homebrew's node@22 at 22.23.2 for a 22.22.0 pin, and keeps a node on PATH that meets the same rule. The Metro log, the worker log and stim doctor say when a fallback was used, and stim doctor --json adds match ("exact" or "major") to toolchain.projectNode. (stim, @stim-cli/core) 3b5309620
  • An owned iOS simulator stays muted after boot. SpringBoard sets its default volume while it starts, a few seconds after simctl bootstatus -b returns on a warm boot, so the mute Stim applied and confirmed right after boot was undone and the simulator played sound at volume 60. Stim now waits up to 30 seconds for SpringBoard's com.apple.springboard.finishedstartup state before it sets and confirms volume zero. When that state cannot be read or SpringBoard does not finish in time, startup prints the existing could-not-mute warning instead of simulator muted. Fixes #4075. f349af1da

QA​

The maintainer authorized a stable release without the pre-tag QA gate on 2026-10-20, as for 1.22.0 and 1.23.0. This deviates from RELEASE.md section 3, which has no waiver for a stable version; the expedited RC lane is for prereleases only and was not used. The waiver covers only section 3.

Earlier on 2026-10-20, a full section 3 QA round ran against main at c132bb118, with v1.21.0 as the regression baseline: all eight rows of node scripts/release-qa-matrix.mjs v1.21.0, on Expo and bare fixtures, Word-Darts and tlon-apps, with janics-mac-mini as the remote worker. It found no regression from v1.21.0. It filed #4075 (the iOS simulator mute did not hold, a 1.22.0 claim) and pre-existing issues #4069 (HIGH: a hosted iOS Debug Expo dev-client app never loads JS, still open and not addressed by this release), #4070, #4071, #4072, #4073 and #4074. That round did not cover either change in this release: both fixes above landed after c132bb118, and the mute fix for #4075 was not exercised by it.

Omitted before the tag, for this candidate: all eight rows that node scripts/release-qa-matrix.mjs v1.23.0 requires (41 changed paths with the version commit, 11 mapped to a row, 30 with no native effect, 0 unclassified): loop (expo and bare, iOS and Android), logs (iOS, Android), caches (iOS, Android), real-repository, remote-provider, release-build (iOS, Android), android-flavor and launch-evidence (iOS, Android), plus the native suites test/e2e/native/run-native-e2e.mjs and test/e2e/native/run-cache-e2e.mjs. None of the items above was exercised by hand on this candidate before the tag, including the worker's pinned Node and the SpringBoard wait. Not run: test:compat, which needs a connected iPhone.

Run before the version commit, on the bumped candidate: format:check, lint, build, typecheck, knip, release:prep --check (12 packages at 1.23.1, 23 workspace: ranges), test:runtime, test:e2e (116 of 116), CLI --version (1.23.1), --help and guide agent, licenses:check and format:check in apps/mobile, and pnpm tarball inspection of the twelve packages (README and LICENSE in each, version 1.23.1, internal dependencies on ^1.23.1 or 1.23.1, no workspace: range, file counts equal to 1.23.0's). pnpm test ran 10173 tests on a Mac at load average 24 to 30: 10144 passed, 27 skipped, 2 failed in 2 files. Rerun alone, packages/stim-cli/src/__tests__/hosted-ios-client.test.ts passed (81 of 81); scripts/agent-benchmark/macos-processes.test.mjs fails at its sandbox-exec call on this Mac, as in 1.21.0 to 1.23.0. The local unit result is therefore not a pass; the full unit suite is left to exact-commit CI.

Run after the version commit landed: exact-commit CI on main (Node 22 and Node 24 jobs and published runtime (node 22.12.0)), and the Release workflow's own gates. The full Windows unit job of a dispatched CI run on the candidate is recorded, not gated, per #3878.

1.23.0​

Stim Desktop and the Stim phone app ship through their own workflows; this release covers the twelve npm packages (@stim-cli/core, @stim-cli/cache, @stim-cli/cache-s3, @stim-cli/agent-driver, @stim-cli/agent-driver-agent-device, @stim-cli/metro, @stim-cli/expo-build-cache, stim, @stim-cli/ci, @stim-cli/transport, @stim-cli/transport-tailscale and @stim-cli/server). No package publishes for the first time in this release. Commits that only changed those apps, tests, CI or internal structure are not listed.

New​

  • Private Desktop stim-server method desktop.settings.schema (feature desktop-settings-schema). It returns the server's registry-derived settings JSON Schema, takes no parameters, and is available only to the Desktop connection that proves the bundled server's private bootstrap key for the same Stim home; phones, other local clients and remote clients are refused. Stim Desktop's Settings editor reads its field definitions from it instead of from schema files of an external CLI installation. (@stim-cli/server, @stim-cli/core) 8f9ae81a4

Fixes​

  • A fingerprint change no longer deletes a gitignored Expo native directory and its Pods. When ios/ or android/ was not generated from the current fingerprint, Stim ran expo prebuild --clean, which deleted the directory, including ios/Pods, and forced a full pod install. It now updates the directory in place with expo prebuild --no-clean on Expo SDK 57 and later, whose CLI deletes native folders by default, and with plain expo prebuild before SDK 57, whose CLI keeps them and rejects --no-clean. On iOS it then runs pod install against the existing ios/Pods. The build progress line reads not generated from this fingerprint -> updated in place. A build offloaded to another machine still regenerates that machine's copy with --clean. When the in-place prebuild fails, STIM_PREBUILD_FAILED carries a remedy to delete the directory and run again; stim guide errors, stim guide lifecycle and the build-caches page list the changes that need a fresh directory (a removed config plugin, an Expo SDK or React Native upgrade, an app rename, a plugin that is not idempotent) and --no-build-cache for a build already cached under the current fingerprint. 0a0e046d6

Migration notes​

  • An in-place prebuild does not undo what earlier config wrote into a gitignored ios/ or android/. After removing a config plugin or upgrading the Expo SDK or React Native, delete that directory before the next stim ios or stim android.

QA​

The maintainer authorized a stable release without the pre-tag QA gate on 2026-10-19 ("can u do a quick no qa reg release"), as for 1.22.0. This deviates from RELEASE.md section 3, which has no waiver for a stable version; the expedited RC lane is for prereleases only and was not used. The waiver covers only section 3.

Omitted before the tag: all eight rows that node scripts/release-qa-matrix.mjs v1.22.0 requires (40 changed paths, 8 mapped to a row, 32 with no native effect, 0 unclassified): loop (expo and bare, iOS and Android), logs (iOS, Android), caches (iOS, Android), real-repository, remote-provider, release-build (iOS, Android), android-flavor and launch-evidence (iOS, Android), plus the native suites test/e2e/native/run-native-e2e.mjs and test/e2e/native/run-cache-e2e.mjs. None of the items above was exercised by hand on this candidate, including the in-place prebuild and the desktop.settings.schema method. Not run: test:compat, which needs a connected iPhone.

Run before the version commit, on the bumped candidate: format:check, lint, build, typecheck, knip, release:prep --check (12 packages at 1.23.0, 23 workspace: ranges), test:runtime, test:e2e (116 of 116), CLI --version (1.23.0), --help and guide agent, licenses:check and format:check in apps/mobile, and pnpm tarball inspection of the twelve packages (README and LICENSE in each, version 1.23.0, internal dependencies on ^1.23.0 or 1.23.0, no workspace: range). pnpm test ran 10165 tests on a Mac at load average 33 to 40: 10133 passed, 27 skipped, 5 failed in 5 files, four of them timeouts. Rerun alone, packages/stim-cli/src/__tests__/hosted-log-discovery.test.ts, packages/stim-cli/src/__tests__/metro.test.ts and packages/stim-cli/src/__tests__/ports.test.ts passed; scripts/agent-benchmark/metro-listeners.test.mjs timed out again, and scripts/agent-benchmark/macos-processes.test.mjs fails at its sandbox-exec call on this Mac, as in 1.21.0 and 1.22.0. The local unit result is therefore not a pass; the full unit suite is left to exact-commit CI.

Run after the version commit landed: exact-commit CI on main (Node 22 and Node 24 jobs and published runtime (node 22.12.0)), and the Release workflow's own gates. The full Windows unit job of a dispatched CI run on the candidate is recorded, not gated, per #3878.

1.22.0​

Stim Desktop and the Stim phone app ship through their own workflows; this release covers the twelve npm packages (@stim-cli/core, @stim-cli/cache, @stim-cli/cache-s3, @stim-cli/agent-driver, @stim-cli/agent-driver-agent-device, @stim-cli/metro, @stim-cli/expo-build-cache, stim, @stim-cli/ci, @stim-cli/transport, @stim-cli/transport-tailscale and @stim-cli/server). No package publishes for the first time in this release. Commits that only changed those apps, tests, CI or internal structure are not listed.

Removed (breaking)​

  • Custom Expo buildCacheProvider plugin modules are no longer supported. A project whose expo.buildCacheProvider names a custom module now gets an unsupported-provider note ("Custom Expo buildCacheProvider plugins are not supported. Use EAS or a Stim cache provider.") in --plan output and in ios and android build progress (provider not usable: ...), and falls back as if no provider were configured; the module is never loaded or run. expo.buildCacheProvider: "eas" (EAS CLI caching), the local Expo cache adapter and @stim-cli/cache providers, including S3, are unchanged. If you use a custom plugin, switch to EAS or a Stim cache provider. (stim) 94bb19775

New​

  • Completed builds on paired workers are reused instead of rebuilt. After a local and shared-cache miss, stim asks the paired worker for a completed build of the same inputs before placing a build, including when the worker is busy or the caller is idle. A hit skips source upload and compilation; a missing or corrupt archive follows normal placement, and older workers keep their normal build path. Each kind is negotiated by its own server feature and uses the existing authenticated, repository-scoped file cache. (stim, @stim-cli/server, @stim-cli/core)
    • Native Gradle: the worker caches APKs under its own complete inputs (worker toolchain, signing files and environment values) when the build transfers a complete android.artifactInputs declaration, and the caller can fetch a completed APK with feature native-gradle-peer-cache. Policy flows through native-gradle-artifact-cache. The returned fingerprint stays null, and worker APKs never enter the caller-key cache. 0bac453be, 4fcf220e9
    • React Native and Expo development builds: exact iOS Simulator and Android archive lookup with feature react-native-peer-cache, matching the post-mutation native fingerprint, normalized build choices and a compatible toolchain. Release and physical-device builds keep their existing paths. ac95a793a
    • SwiftPM macOS apps: reuse of a worker's completed app with feature swiftpm-peer-cache, for apps that declare complete, repeatable inputs through macos.artifactInputs. d87f39cab
  • Completed SwiftPM macOS apps are reused across worktrees. macos.artifactInputs (project setting, object) with complete: true declares pinned inputs that produce a relocatable app, with ignored, outputs, localFiles and environment for exact ignored, generated, private or environment inputs. A hit comes from the local build cache or an optimizations.buildCache / cache.provider files provider, is verified, and is re-signed with the receiving workspace's identifier and name before placement or build-slot acquisition. Without the declaration, builds compile as before. stim macos --plan reports cacheHit for a verified local completed artifact. See stim guide macos for what the declaration must cover. bb4dfc43a
  • Build placement ranks workers by reusable dependency installs. For React Native and Expo, a worker whose existing install matches the requested project, lockfile and package-manager arguments (modules and install marker present) ranks before load. Checkout and build-folder presence, and the unverified hints of older workers, no longer affect ranking. Clients send dependencies: { project, packageName } only to servers with the new feature build-dependency-cache. Exact completed-artifact hits still come first. (stim, @stim-cli/server, @stim-cli/core) 439c20847
  • Owned simulators and emulators are muted each time Stim boots them. After a successful boot, Stim sets the volume to 0 and reads it back: xcrun devicectl device settings audio --device <udid> --volume 0 for iOS simulators, and for Android emulators the media stream only, through a 1.4 KB helper shipped in the CLI (shim/android-audio.jar) that Stim pushes to /data/local/tmp, runs with app_process, removes, and confirms with cmd media_session volume --stream 3 --get; this works on Android 14, whose shell ignores the plain volume setter, and needs no host JDK. Stim prints a device phase line when the mute is confirmed and a warning, then continues, when it is not. A device that is already running is not touched, so sound turned on with Stim Desktop's per-device toggle lasts until that device next boots. Hosted (device-host) boots are unchanged. 2459e5df3, 289d8701d
  • stim-server service update --no-wait and stim-server setup --no-wait. Service updates wait for offloaded builds and hosted sessions to finish by default. --no-wait restarts with that work active: shutdown interrupts it, and shutdown verification, health checks and rollback still apply. Setup applies the flag to the service update it runs, and its remedy for a separate update carries it. (@stim-cli/server) c180eb0cf
  • In-process APIs on createStim. Each accepts an AbortSignal and drains its owned children and claims on cancellation, reporting STIM_CLAIM_UNAVAILABLE with recovery paths when cleanup cannot be verified:
    • plan({ platform: 'ios' | 'android' | 'macos', slot?, signal? }) returns the next configured native build as --plan --json reports it, without building or launching. (stim, @stim-cli/cache, @stim-cli/cache-s3) 51c5cd60b
    • start({ remote?, resetCache?, waitSeconds?, signal?, beforeStart? }) starts or reuses the managed dev server and returns flat start facts. Starting and stopping from a child folder resolve the same enclosing app. 243841b81
    • collectGarbage({ cache?, olderThan?, worktrees?, delete?, idleMs?, signal? }) runs the CLI's GC selections and actions and returns { report, exitCode }; gc() stays read-only. A deleting or idle sweep takes the home's exclusive maintenance claim and a concurrent one rejects with STIM_CLAIM_REFUSED. 690f2dc8a
    • removeWorktree({ target?, force?, signal?, beforeRemove? }) returns { removed, exitCode } with the CLI's removal checks. 28c3ac257
    • warmWorktree({ signal?, beforeWarm? }) copies missing ignored state into the linked worktree and returns { exitCode }, with a 90 minute execution and 35 second cleanup budget. It does not refresh or install dependencies. A failed copy that created a new entry now removes that partial entry after its child drains. b5ff73bf4
    • requestMachineAccess({ ticket?, signal? }) requests build and device-host access from the configured remote machines without applying project repairs; a person still approves each capability on the worker. It returns the iOS doctor report. 757f90b40
    • run() exposes the existing remote selector for native runs and forwards it for macOS, so a macOS request with remote: '<machine>' no longer takes the local launch path. The macOS result's launched type now includes 'unverified'. dc29525ed
  • New private Desktop stim-server methods and features. Each is available only to the Desktop connection that proves the bundled server's private bootstrap key (for the same Stim home); phones, other local clients and adopted servers are refused. Each subscription streams desktop-*-progress and desktop-*-ended events; desktop.plan.get returns the plan in its reply. Each runs the in-process API above, waits for cleanup on cancellation and never replays on reconnect. (@stim-cli/server, @stim-cli/core)
    • desktop.run.subscribe (feature desktop-run): iOS, Android, macOS and web runs, with a one-hour default deadline and a 1 MiB output limit. c80c8cf89
    • desktop.build.subscribe (desktop-build): artifact-only builds for iOS, Android and macOS. c5393e953
    • desktop.start.subscribe (desktop-start): dev-server startup. 243841b81
    • desktop.plan.get and desktop.plan.cancel (desktop-plan): next-build reports for a selected existing folder. The registered-workspace build.plan method for phones now also runs in process. 89034c015, e5ca4426c
    • desktop.gc.subscribe (desktop-gc): GC selections and actions, preserving a failing exit status on partial failure. ac7b6952e
    • desktop.worktree.remove.subscribe (desktop-worktree-remove) and desktop.worktree.warm.subscribe (desktop-worktree-warm, 90 minute default deadline). 3a4921c14, aa73c5e6f
    • desktop.machine-access.subscribe (desktop-machine-access): { cwd, ticket? }. 757f90b40
  • Lower idle cost and faster reaction in several waits. Waits that polled now wake on file, process or write events, keeping a slower repair check for a lost event:
    • stim logs --source agent --follow and server log subscriptions follow agent state and claim directories instead of re-reading every 500 ms (repair read every ten seconds). f4b41e242
    • stim-server setup notices the requesting machine's approval from registry changes (one-second repair). a1cae8426
    • Windows service startup waits on supervisor.json changes instead of a TaskScheduler query every 250 ms (one-second repair). e170583bc
    • Hosted Metro gateway acknowledgements wake from the state file instead of a 100 ms reread (caller repair is one second after the first 500 ms). 1ab1c5442
    • Browser launch verification (stim web) reads the owned browser log by file events, and browser registration wakes from its supervisor by IPC (Windows keeps 250 ms checks). b2c2acb60, 232a123be
    • Local and hosted macOS launches (stim macos) wake from the supervisor by IPC. 83cd41dc6
    • Binary build-source uploads wake backpressure from write completion instead of a 5 ms check. 4767d52a8
    • Managed tunnel, Metro startup rollback and Windows launcher cleanup finish on child exit instead of a 25 ms delay. d1882ea04
    • @stim-cli/core oversight wakeAt includes physical-device lease expiry and unattended EAS-session age, so callers can schedule those attention changes without polling. 7ae982cde
    • Status retries pending merge verdicts without rescanning unchanged worktrees (37 to 16 git status commands in a real eight-worktree fixture). 6ec3a74ed

Fixes​

  • ios --device and android --device ignore ios.remote and android.remote. With either setting at auto or a machine name, a physical-device run refused with STIM_BAD_ARG. Those settings place owned simulators and emulators, so --device no longer reads them. --device --remote local now works. --device with any other --remote still refuses, and the error now names the value passed. 23bbd7bd4
  • Build workers recognize an available iPhone by CoreSimulator's device-type identifier instead of its name, so simulators that Stim renamed are found for offers and destination selection; an iPad named "iPhone" stays excluded. (stim) 1a747b4c3
  • S3 file uploads reopen the completed file and retry up to three PUT attempts on ECONNRESET, with short cancellable delays and conditional writes preserved. (@stim-cli/cache-s3) 6c318763b
  • The supervisor stays alive and keeps its registrations and hosted gateways when the dev server's shutdown cannot be confirmed, so a later shutdown can retry. Previously a command that survived termination could outlive a supervisor that had cleared its state. 6745ce291
  • A successful browser navigation no longer leaves a timeout that delays a standalone caller's natural exit by about one second. 480879f43
  • stim status --watch on macOS stops without per-workspace delays: it shares one recursive workspace watcher, and watches CoreSimulator device.plist files instead of each device directory. A real-home probe took 17.8 seconds to exit after SIGINT before and 4.18 seconds after; a 15-device fixture's watcher disposal fell from 1.11 s to 140 ms. 1b5c264c8, e60218eb4
  • Doctor's setup command, once approved on the worker, prints the instruction to run stim doctor again on the requesting machine before stim-server setup waits. Ordinary setup and Desktop-generated commands are unchanged. (@stim-cli/server, stim) 6063465bd

Migration notes​

  • Owned simulators and emulators boot muted. Turn sound on per device in Stim Desktop's options (Sound for simulators, Media sound for emulators); it lasts until that device next boots.
  • A project that relied on a custom expo.buildCacheProvider module must move to EAS ("eas") or a @stim-cli/cache provider.

QA​

The maintainer authorized a stable release without the pre-tag QA gate on 2026-10-18 ("Can u do a no qa release regular release, no rc"). This deviates from RELEASE.md section 3, which has no waiver for a stable version; the expedited RC lane is for prereleases only and was not used. The waiver covers only section 3.

Omitted before the tag: all eight rows that node scripts/release-qa-matrix.mjs v1.21.0 requires (415 changed paths, 210 mapped to a row, 0 unclassified): loop (expo and bare, iOS and Android), logs (iOS, Android), caches (iOS, Android), real-repository, remote-provider, release-build (iOS, Android), android-flavor and launch-evidence (iOS, Android), plus the native suites test/e2e/native/run-native-e2e.mjs and test/e2e/native/run-cache-e2e.mjs. None of the items above was exercised by hand on this candidate, including the muted boot, worker artifact reuse and the custom Expo buildCacheProvider removal. Not run: test:compat, which needs a connected iPhone.

Run before the version commit, on the bumped candidate: format:check, lint, build, typecheck, knip, release:prep --check (12 packages at 1.22.0, 23 workspace: ranges), test:runtime, test:e2e (116 of 116), CLI --version (1.22.0), --help and guide agent, licenses:check and format:check in apps/mobile, and pnpm tarball inspection of the twelve packages (README and LICENSE in each, version 1.22.0, internal dependencies on ^1.22.0 or 1.22.0, no workspace: range). pnpm test ran 10162 tests on a Mac at load average 110 to 190: 10100 passed, 27 skipped, 35 failed in 16 files, almost all timeouts or timing assertions. Rerun alone, at load average 187, packages/server/__tests__/device-host.test.ts, packages/stim-cli/src/__tests__/report-runtime.test.ts and packages/stim-cli/src/__tests__/start-context.test.ts passed and 27 tests in the other 13 files failed again; scripts/agent-benchmark/macos-processes.test.mjs fails at its sandbox-exec call on this Mac, as in 1.21.0. The local unit result is therefore not a pass; the full unit suite is left to exact-commit CI.

Run after the version commit landed: exact-commit CI on main (Node 22 and Node 24 jobs and published runtime (node 22.12.0)), and the Release workflow's own gates. The full Windows unit job of a dispatched CI run on the candidate is recorded, not gated, per #3878.

1.21.0​

Stim Desktop and the Stim phone app ship through their own workflows; this release covers the twelve npm packages (@stim-cli/core, @stim-cli/cache, @stim-cli/cache-s3, @stim-cli/agent-driver, @stim-cli/agent-driver-agent-device, @stim-cli/metro, @stim-cli/expo-build-cache, stim, @stim-cli/ci, @stim-cli/transport, @stim-cli/transport-tailscale and @stim-cli/server). @stim-cli/cache-s3, @stim-cli/agent-driver, @stim-cli/agent-driver-agent-device, @stim-cli/transport and @stim-cli/transport-tailscale publish their first release. Commits that only changed those apps, tests or internal structure are not listed.

Breaking: re-pair every device​

This minor release breaks pairing: stim-server speaks protocol 2 only, and no pairing, approval or credential from protocol 1 is carried over. Every network connection to stim-server, loopback TCP included, now runs a Noise handshake before any request: a new client spends a single-use pairing token or setup ticket once, and from then on the server recognizes it by its own public key and the client accepts only the server key it pinned. There is no backwards compatibility in either direction: a protocol 2 stim-server refuses an older phone app, Stim Desktop or stim CLI with protocol-unsupported ("Update this app or Stim, then pair again"), and a protocol 2 client cannot talk to stim-server 1.20.2 or older. Stim Desktop and other clients of the same user on the same Mac no longer pair: they connect to the Unix socket $STIM_HOME/server/local.sock (mode 0600), which needs no handshake. (@stim-cli/server, @stim-cli/core, stim) be8cb61ae, 8be0bc834

What this means on disk: server/devices.json and the *-clients.json approval files from 1.20.2 are not read, and the first protocol 2 write replaces them, so going back to 1.20.2 afterwards loses them too. server/secrets.json stores each pairing token's or setup ticket's derived PSK and lookup, never the token, and server/pairing.json is no longer used. The client keeps its credentials for the machines in remote.machines in the same files, build-machines.json and device-host-machines.json, with a new schema; old entries are ignored, so remote builds and hosted devices on those machines stop until the machine approves this one again. Automatic placement falls back to a local build; a build named with --remote-build <machine> fails with STIM_OFFLOAD_REFUSED, and so does automatic placement when remote.buildPoolDisabled disables local builds.

Update every machine that runs stim-server or stim to 1.21.0 first, then re-pair. Only Stim Desktop 0.2.0 or later and the Stim phone app build released with 1.21.0 speak protocol 2: Stim Desktop 0.1.28 and older bundle a protocol 1 stim-server and make protocol 1 pairing codes and setup commands, so for Desktop users the steps below take effect once Stim Desktop 0.2.0 or later is installed. Stim Desktop 0.2.0 also requires stim and stim-server 1.21.0: once Sparkle installs it, a Mac whose stim CLI is still 1.20.2 cannot run stim from Desktop, local use included, until the CLI is updated (npm install --global stim@1.21.0). Update the CLI before or together with Desktop.

  1. Each Mac or machine running stim-server. Update the server itself, on that machine. Stim Desktop's bundled server updates with Stim Desktop 0.2.0 or later. A service installed with stim-server service install updates with stim-server service update --release 1.21.0 run on that machine (npx --yes --package @stim-cli/server@1.21.0 stim-server service update --release 1.21.0 without a global install). A protocol 2 client cannot update a protocol 1 machine over the tailnet: remote update needs the machine's pinned server key from a protocol 2 approval.

  2. The phone app. Install the Stim phone app build released with 1.21.0. It deletes its protocol 1 pairings on first launch. In Stim Desktop 0.2.0 or later, open the Phones page (if it is missing, turn on the Phone app flag under Settings > Advanced > Feature Flags), turn on Serve to phones, choose Pair a Phone… and scan the QR code. Without Desktop, run stim-server pair --json (npx --yes --package @stim-cli/server@1.21.0 stim-server pair --json without a global install) and enter its endpoint and token on the phone. The QR code carries the server key's fingerprint, which the phone checks before it pairs; manual entry carries no fingerprint. A phone that meets a server older than protocol 2 shows "Update stim-server on this Mac. It speaks an older protocol than this app.", and a QR code from an older server is refused with "This pairing code is from an older stim-server. Update stim-server on this Mac."

  3. Stim Desktop. Install Stim Desktop 0.2.0 or later. Nothing to pair on its own Mac: it talks to its bundled server over the Unix socket. If a stim-server 1.20.2 already answers on Desktop's port, either a service from stim-server service install or the executable set as stim-server executable under Settings > Advanced, Desktop uses it and shows This stim-server <v> is too old for this Stim Desktop. Update it, or clear the stim-server override in Settings. Update it with stim-server service update --release 1.21.0, or clear that field. Then approve each remote machine again (step 4); Desktop's Add Machine wizard generates the command.

  4. Remote machines for builds and hosted devices. Update the remote machine to stim-server 1.21.0 first (step 1). Then, on the machine that sends work, run stim doctor --fix, or use the Add Machine wizard of Stim Desktop 0.2.0 or later. For each machine in remote.machines it makes a new setup ticket and prints the command a person runs on that machine to approve it, in npx form with this stim's own version:

    STIM_SETUP_TICKET=<ticket> npx --yes --package @stim-cli/server@1.21.0 stim-server setup --expires <ISO-time> --build --device-host

    It drops --device-host for a machine that is not a Mac, and on Windows the variable is set the PowerShell way ($env:STIM_SETUP_TICKET=...), as Stim Desktop's Add Machine command does. Setup updates an older service installed with stim-server service install in place, and refuses an older server it does not manage, such as one an older Stim Desktop runs, with an update remedy. stim doctor reports a machine as not-asked before any --fix, as pending, with the command printed again, after --fix until a person runs setup, and tells you when the machine's stim-server is older than protocol 2.

    A stim 1.20.2 client that reaches a 1.21.0 machine shows "This stim-server speaks protocol 2, with its own authentication. Update this app or Stim, then pair again." and builds locally instead.

  5. Scripts. Pin @stim-cli/server@1.21.0, not an older version, in any npx command that pairs or sets up a machine: an older server prints protocol 1 pairing codes and setup that a 1.21.0 client cannot use.

Removed (breaking)​

  • Protocol 1 commands, flags and fields are gone (see Breaking: re-pair every device). stim-server devices grant <id> takes only --control or --read; build and device-host access come only from stim-server setup with a setup ticket, and the pending access request flow and its Allow/Deny are removed. stim-server setup refuses --client: the ticket alone names the requesting machine. stim-server devices --json replaces identity with from (a label only) and keyFingerprint. stim-server pair --json prints { qr: { v: 2, name, endpoint, pairingToken, fingerprint }, expiresAt } (without --json, the bare qr object), and the phone checks the server key's fingerprint. The tailnet health reply is only { "version" }. The hello request drops protocol and auth and gains an optional access (build or device-host), its result drops deviceToken and approval, and the error codes approval-pending and identity-unavailable are replaced by unknown-secret. In stim doctor --json remoteMachines and deviceHosts, the state node-changed is now key-changed. (@stim-cli/server, @stim-cli/core, stim) be8cb61ae
  • stim-server no longer runs a login shell to capture its environment. A variable that only a shell profile sets, such as a STIM_* variable or a private CocoaPods on PATH, reaches the server only through its own environment, --env KEY=VALUE or --path-prepend <dir>. (@stim-cli/server) edaf8b54c
  • stim-server setup reads the setup ticket from the STIM_SETUP_TICKET environment variable and refuses --ticket, because other users on the machine can read command-line arguments. Setup removes the variable from its environment before it starts children. Stim Desktop's Add Machine command sets it, inside a PowerShell script block with $env: on Windows. (@stim-cli/server) aafaa4f05
  • The programmatic API error code STIM_WORKER_FAILED and its stim guide errors entry are removed (see the in-process API item below). 8174f08a3
  • The eas build cache tier runs fixed eas-cli commands (fingerprint:generate, build:download, upload) instead of loading the eas-build-cache-provider SDK into the Stim process. It needs eas-build-cache-provider in the app and eas-cli installed in the app or on PATH; without one the tier reports the EAS build cache needs an installed eas-cli executable, and Stim never installs it during lookup. A Windows .cmd/.bat launcher needs a project eas-cli so cancellation can stop Node directly. Fingerprint and transfer commands share one deadline per phase, and timeout or cancellation kills that phase's process. EAS authentication is cached per operation. 43f8f321f

New​

  • createStim().settings() and createStim().stats() read settings and stats in the calling process. settings() returns the stim settings --json payload: every setting with its effective value, origin and per-layer values. An invalid environment override rejects with StimError STIM_BAD_ARG and its remedy. stats() returns the stim stats --json payload, with the last recorded agent-device and SwiftPM cache usage instead of measuring it again. Both run in the calling process and accept signal; an aborted read kills its child processes and rejects with STIM_CANCELLED. createStim gains an env option (default process.env) that Stim reads its settings overrides and STIM_HOME from and passes to the operations it runs; the client's home is fixed at creation. New exported types StimReadOptions, StimSettings and StimStats. stim-server answers settings.get and stats.get, and reads the stats for machine details, through these calls instead of running stim settings --json or stim stats --json. c00641db0, 50548d4c4
  • createStim().stop() runs in the calling process instead of a worker. An aborted stop() finishes its teardown and then rejects with STIM_CANCELLED. An exception thrown by onProgress stops further progress and propagates unchanged: build and run are cancelled, and stop() or a read finishes its work first. 6d223b9cc
  • stim-server serves view-shaped status subscriptions. A server that offers them lists status-views in its hello features. workspaces.subscribe sends workspaces events with a seq starting at 0: the first carries snapshot ({ workspaces, worktrees, archived }, with each workspace trimmed to what a list needs and the 10 newest archives), and later events carry only { upsert, remove } deltas by path and archived when it changed. A client that misses a seq subscribes again. workspace.subscribe with { workspace } sends that workspace's full environment, or null. machine.subscribe sends capacity, machine, deviceLeases, simctlAvailable, maintenance, usage and ownLeases. builds.history, archived.list (optional before and limit, at most 50, returning next), worktrees.list and maintenance.get return the full data, waiting up to 30 seconds for the server's first status. @stim-cli/core/state exports the view derivation and keyed deltas, and @stim-cli/core the new protocol types. Stim Desktop and the phone app follow these views, and fall back to status.subscribe on a server without status-views. (@stim-cli/server, @stim-cli/core) 319671da7, 593a490fc, 273683d99, e5492ad03
  • stim-server runs the status watch itself, in a worker thread, from the moment it is ready, so a status subscriber gets the latest payload at once. All subscribers, device recording and paired-phone notifications share it. Before, the server started a stim status --watch --json child for its first subscriber and stopped it with the last. (@stim-cli/server) 558ce0ecb
  • stim-server discovers the toolchain without a login shell, and stim doctor reports it. stim-server no longer reads the login shell's environment at start. It fills gaps in the environment it was given: it appends the default Node of nvm, fnm, Volta, mise or asdf, Homebrew, ~/.local/bin, pnpm's and bun's global directories and the Android SDK tools to PATH, sets ANDROID_HOME to ~/Library/Android/sdk when that exists, and sets JAVA_HOME to a Homebrew openjdk (17, then 21, then the newest) or Android Studio's bundled runtime when no JDK is installed under /Library/Java/JavaVirtualMachines. It never replaces a variable it was given. On macOS, stim doctor prints a Toolchain section with where node, ANDROID_HOME, JAVA_HOME, stim, brew and adb come from and how to override each, and doctor --json adds toolchain ({ entries, projectNode }, null off macOS). When the app pins Node in .nvmrc, .node-version, .tool-versions or package.json volta.node and the node on PATH does not satisfy the pin, the expo-child and command-child dev servers run on an installed Node that does, and the metro log records a project_node event. CocoaPods without an installed .ruby-version Ruby uses rvm's or rbenv's default Ruby before falling back to the login shell. @stim-cli/core adds a ./toolchain export. (@stim-cli/server, @stim-cli/core) edaf8b54c
  • stim status --watch reacts to git HEAD, index and ref changes in the worktrees' repositories and to the exit of a recorded supervisor, Metro, build, browser or macOS app process (checked every 2 seconds). A log append updates the log-derived facts within about a second, and device activity at most every 5 seconds; before, a log append waited at least 15 seconds after the previous refresh. The first line does not wait for git, so worktree git summaries can be null in it and the next line carries them. On macOS, simulator boots, shutdowns, creations and deletions are detected from writes to CoreSimulator's device set, with a simctl check every 30 seconds instead of a poll every 5 seconds. 598598e43, 94c5db1a0
  • @stim-cli/cache-s3 shares native build artifacts and Metro values through S3-compatible storage, including Cloudflare R2. New opt-in package that owns the AWS SDK and archive dependencies, so installing stim or @stim-cli/cache does not pull them in. Install it in the app and select it with "cache": { "provider": "@stim-cli/cache-s3", "options": { "bucket", "endpoint", "region", "prefix" } } in .stim.json. Credentials come from the AWS SDK credential chain; cache.options.profile or AWS_PROFILE selects a named profile, and the explicit option wins. Objects live under <prefix>/builds/<platform>/<key> and <prefix>/metro/<cacheName>/<hex-key> (prefix default stim/v1), downloads verify SHA-256 before reuse, conditional writes keep existing build entries unless overwrite is requested, and uploads are a single PUT, so entries over 5 GiB are not supported. The transport is also exported as createS3FileCache(). @stim-cli/cache adds the optional files provider capability (resolve and store of opaque complete files, with an optional stat) and createFilesystemFileCache(directory); a provider that advertises no files, metro or builds capability is refused. (@stim-cli/cache-s3, @stim-cli/cache) af0c87aeb, 9cd6c689e
  • Native Xcode simulator builds reuse an exact completed artifact from a paired build worker. After a local and configured-provider cache miss, Stim asks enabled paired workers for an archive whose fingerprint and full cache key match (source, Xcode, SDK, architecture and target options). A hit is downloaded and verified with no source upload, no compilation and no build slot, even when the worker's slots are full or this machine is idle. A missing or corrupt archive falls back to normal placement. remote.buildMode: off, --remote-build local and workers without the build-artifact-cache feature skip the lookup. React Native, Android, SwiftPM, compiler and dependency caches are unchanged. stim-server adds artifact: { fingerprint, cacheKey } to native Xcode build offers and the build.cached-artifact request, which needs the existing build approval, streams the archive with the same frames as build.artifact, and only serves the requesting client's own repository entries. (@stim-cli/server) a78694933
  • Workspace links are HTTPS links that open on a paired phone as well as in Stim Desktop. worktree warm, start, ios, android and web print Open in Stim: https://stim.appandflow.com/open?v=1&machine=<home-id>&workspace=<workspace-id> (plus platform and slot when set) on stderr and in links.desktop of --json, instead of Open in Stim Desktop: stim-desktop://workspace?path=.... The link carries a persistent Stim-home ID and the workspace ID, never a path, server address or token, and opening it only navigates. Older stim-desktop:// links keep working. stim status --json adds workspaceId on each environment, the stim-server hello reply adds server.id, createStim adds identity(), and @stim-cli/core exports @stim-cli/core/workspace-link (sharedWorkspaceURL, parseWorkspaceLink). (@stim-cli/core, @stim-cli/server) a63428563
  • createStim().build() returns facts.links.workspace, the HTTPS workspace link for the build's workspace, also when the build machine has no Stim Desktop installed. 68724012f
  • createStim adds logs() and followLogs(). logs({ tail, sources, minLevel, slot, since, grep, errorsOnly, signal }) returns the same merged records as stim logs --json, including hosted logs, native crashes, agent actions and Expo error context; tail bounds the result and tail: 0 returns none. followLogs(options, onRecord) emits the backfill, then appended records, and resolves to a stop function; aborting its signal also stops it, and cancelling before setup rejects with STIM_CANCELLED. Both run in the importing process, and diagnostics() now does too instead of in a separate worker. The new type is StimLogsOptions. d560b112d, d1cea73b2
  • server.buildsPaused and server.devicesPaused pause incoming work on a worker. New boolean machine settings, default false: stim settings set server.buildsPaused true declines new builds from other machines, and server.devicesPaused true declines new hosted iOS, Android and macOS sessions, with a remedy naming the setting. The running server reads them without a restart, and a pause also refuses work offered before it was set. Running builds and existing sessions continue, clients can still attach, fetch completed and cached artifacts and stop sessions, approvals are kept, and local CLI builds and devices are unaffected. (@stim-cli/core setting, @stim-cli/server) ff6475505
  • An x86_64 Linux machine can take offloaded Android debug builds and run stim-server as a service. The toolchain comparison no longer requires the same CPU architecture for Android, the worker gzips the artifact in Node so GNU tar works, and the Android SDK defaults to ~/Android/Sdk on Linux (also in the CLI's own SDK lookup and the Gradle ANDROID_HOME remedy). stim-server service install [--serve] installs a systemd user service on Linux (~/.config/systemd/user/dev.stim.server.service, log ~/.local/state/stim/dev.stim.server.log) and prints, never runs, sudo loginctl enable-linger <user>; service update, service rollback and remote update work as on a Mac, and a failed first install stops and removes its unit. stim-server setup on Linux approves --build only, refuses --device-host and skips Stim Host and the permission steps. (@stim-cli/server) 4895e715d, 478b7b5a0
  • stim doctor rates each remote machine per platform. stim-server reports its OS as toolchain.os in build offers; an iOS or macOS target on a machine that is not a Mac gets one platform reason (finding code build-machine-platform) instead of every missing Apple tool, and a platform the machine cannot build at all is left out while it can build another, so a Linux worker is offloadable for Android. machine.details runs stim doctor --json for every platform instead of --platform ios. Doctor names the platform on a CPU mismatch. (@stim-cli/server, @stim-cli/core protocol type) 7543bcd79
  • New @stim-cli/transport and @stim-cli/transport-tailscale packages. @stim-cli/transport is the contract and loadTransport() loader for the transports that carry clients to stim-server; the loader never throws and reports a missing package, a different apiVersion, a failing factory or a load over 10 seconds as unavailable. The Tailscale driver moves out of @stim-cli/server into @stim-cli/transport-tailscale, with no behavior change. stim-server service update --from <dir> takes the two new tarballs, a --from install directory is also keyed by the package files so a dependency-only change installs, and a remote update upload accepts up to 16 packages instead of 8. (@stim-cli/transport, @stim-cli/transport-tailscale, @stim-cli/server) 1039c1699
  • New @stim-cli/agent-driver and @stim-cli/agent-driver-agent-device packages. @stim-cli/agent-driver is the contract stim-server uses to start the tool a client's coding agent drives hosted apps with (apiVersion 1, id, createAgentDriver, optional client and actions), with loaders that never throw and runAgentDriverContract for driver tests. The agent-device adapter moves to @stim-cli/agent-driver-agent-device with no behavior change; stim-server and the CLI resolve hosting.agentDriver from a registry of first-party drivers they depend on, never from a project. A hosted session stops even when hosting.agentDriver names a driver that is not registered. A driver that fails to load gives the client a generic notice and writes the reason, which can name paths on the hosting Mac, to stim-server's stderr, and stim-server loads the drivers' action readers on the first archived log or replay read instead of at startup. (@stim-cli/agent-driver, @stim-cli/agent-driver-agent-device, @stim-cli/server) 6e99b1c1a, 650596750
  • Automatic maintenance escalates through three disk pressure levels. Level 1 is free disk under budget.minFreeDiskGb (20 GB), level 2 under the new maintenance.urgentFreeDiskGb (default 12), and level 3 under the new maintenance.criticalFreeDiskGb (default 6) or budget.hardFloorDiskGb, whichever is higher. 0 turns maintenance.urgentFreeDiskGb or maintenance.criticalFreeDiskGb off; level 3 still applies below budget.hardFloorDiskGb. At levels 2 and 3 the protection window is the new maintenance.pressureProtectMinutes (default 10) instead of maintenance.protectRecentHours. Level 2 clears build outputs of workspaces unused for that window, parked simulators and emulators, and the build directories of idle worktrees (ios/build, android/app/build, android/build, android/.gradle, .build). Level 3 empties the Swift compilation cache, removes finished worktrees with the short window as grace, stops the dev server of otherwise inactive workspaces and deletes their owned devices, and deletes node_modules and ios/Pods of idle linked worktrees, never the source checkout. Each step re-measures and stops once free disk is back above its level's floor. A directory is cleared only when git tracks nothing in it and nothing under it, nor the worktree's index, HEAD or Stim state, changed within the window. stim status shows the level, status --json reports pressure.diskLevel, and every command prints one stderr line while the last check, at most 10 minutes old, measured level 3. Running dev server supervisors now trigger the disk check each minute. (@stim-cli/core settings and maintenance types) f640b423f
  • Owned simulators and emulators of idle and finished workspaces are deleted. Automatic maintenance deletes the owned devices of workspaces idle for maintenance.pressureProtectMinutes at disk pressure level 2, of any workspace idle for the new maintenance.deviceIdleDays (default 3, 0 turns it off) in the daily sweep, and of a finished worktree that is kept, such as a dirty one, once it is idle for gc.worktreeGraceMinutes. stim gc --delete deletes the owned devices of a finished worktree it keeps (dirty, unpushed, locked) when its workspace is idle for that grace. stim worktree remove deletes owned devices instead of parking them at pressure level 2 or 3. maintenance.keep now also protects a workspace's devices. A workspace with no recorded use ages from its workspace.json mtime. gc --json adds finishedDevices (kind, id, name, project, slot, bytes, reason) and ownedDeviceDisk (kind, count, bytes, unmeasured); stim status prints the total as "owned devices" (size category owned-devices). (@stim-cli/core setting) 11b17dfb3
  • stim gc --cache gradle-versions lists, and with --delete removes, Gradle versions no registered project pins. It covers caches/<version> and wrapper/dists/gradle-<version>-* under GRADLE_USER_HOME, with pins read from the wrapper of each project's Android Gradle root and root. Nothing is deleted while a process that may be Gradle on that home runs, while ps fails, while a registered project is missing or its pin cannot be read, or while the layout is unsafe; with --older-than <days> a version modified within that many days is kept. modules-2, build-cache-1, jdks and other shared Gradle caches are never deleted, --cache all never includes this scope, and with STIM_HOME set nothing outside it is deleted. gc --json adds sections.gradleVersions (home, olderThanDays, blocked, gradleRunning, gradleRunningPids, versions[] with willDelete and reason, leftovers). At disk pressure level 3, automatic maintenance stops the idle Gradle and Kotlin daemons that alone block a version, then deletes versions unmodified for the new maintenance.gradleVersionIdleDays (default 7, 0 turns it off). (@stim-cli/core setting) 9a2ef4824
  • Unscoped stim gc reports other tools' machine-wide caches. It lists Gradle version caches and wrapper dists no registered project pins, the Gradle modules-2 cache, iOS simulator runtimes with their simulator count, Xcode device support entries and agent-device runner builds grouped by agent-device and Xcode version, each with its size and the owning tool's remedy. Stim never deletes them. gc --json adds sections.machineCaches (measuredAt, complete, notices, gradle, simRuntimes, deviceSupport, runnerBuilds), null with any --cache scope. Measurements are cached for 10 minutes in $STIM_HOME/machine-cache-usage.json. (@stim-cli/core state reader) 675ae601e
  • stim gc shows each worktree's in-tree build size and lists unlisted .worktrees directories. Each worktree line and its JSON entry carry buildState ({ bytes, dirs: [{ path, bytes }] }): the build and dependency directories maintenance clears plus a root .pnpm-store, largest first. Sizes come from the disk-usage cache while under an hour old; otherwise one du per directory runs, four at once with a 10 second limit, and an unfinished one shows "size unknown" (bytes: null). The new report-only section "Directories under .worktrees that git does not list as worktrees", JSON unlistedWorktreeDirectories (path, repository, bytes, remedy), names each such directory without its own git with its size and an rm -rf command; gc never removes them. stim worktree warm no longer copies a project-local .pnpm-store into the new worktree. 8120c28a4
  • stim gc --worktrees removes idle worktrees that have no commits of their own. A clean, pushed linked worktree whose HEAD the default branch contains and whose branch has no commit made on it is removed when idle, judged by its git and Stim activity when no Stim use is recorded. Plain gc and gc --delete still keep it (reason not-merged, no commits of its own beyond origin/<default>). Automatic maintenance removes it only at disk pressure level 3, once its activity is older than maintenance.pressureProtectMinutes and at least 5 minutes old. Removal re-checks under the removal locks that HEAD did not move and the worktree is still idle. 7cf4e9eab
  • Report-only stim gc runs no git fetch and no gh. Plain gc and gc --json judge merges from the existing origin/<default> ref and take a pull request from the status --watch cache when it records the worktree's current branch and HEAD; both count as fresh for twice maintenance.worktreeCheckMinutes, at least 10 minutes. The maintenance worktree check and gc --delete now write their pull request lookups into that cache. A kept worktree whose verdict fresher state could change says "merge state not refreshed", and its reason not-merged becomes merge-unknown. gc --delete still fetches and asks gh before removing anything. The status cache entry gains containsHead, endedAt and partial. (@stim-cli/core status types) a80d357f0
  • stim-server service and stim-server setup run on Windows. service install registers a per-user scheduled task \Stim\<label> with schtasks, without elevation, that starts at logon and keeps running after sign-out (S4U); a supervisor, stim-service-runner.mjs, restarts the server 30 seconds after it exits and records its state in supervisor.json. Service files, versions and logs live under %LOCALAPPDATA%\Stim\. update, rollback, status and uninstall work as on macOS and Linux, and install warns when the first node.exe on PATH is not the Node running the server (--path-prepend fixes it). setup takes --build only on Windows. (@stim-cli/server) 6b102672d
  • Windows machines take offloaded Android builds. A Windows stim-server keeps each checkout and each client's Gradle home in private short directories, <SystemDrive>\sw\<6 hex>, so the NDK's ninja stays under 260 characters, cancels a build by ending its process tree with taskkill /T /F, and stops the processes running from a client's Gradle home before deleting it. server.workerRoot accepts a Windows absolute path. @stim-cli/transport-tailscale finds tailscale.exe on PATH or under %ProgramFiles%\Tailscale, and @stim-cli/core keeps a Windows Path variable when it extends the child environment. (@stim-cli/server, @stim-cli/core, @stim-cli/transport-tailscale) 9acaf55fb
  • @stim-cli/core/noise exports Noise XX and XXpsk2 handshakes with Stim's transport framing. It is Noise_XX_25519_ChaChaPoly_SHA256 and Noise_XXpsk2_25519_ChaChaPoly_SHA256 on node:crypto, checked against published vectors. HandshakeOptions and AcceptOptions take an optional dh to supply X25519 from the caller, for runtimes such as workerd. (@stim-cli/core) ece91237e, 9d812446b
  • createStim runs build() and run() in the importing process. Before, each used a bundled worker process. Every API operation now runs in process against the client's home and project and leaves the caller's cwd, environment, output, signal handlers and exit listeners as they were. Cancellation drains the operation's own child processes. STIM_WORKER_FAILED is gone, since there is no worker. 8174f08a3
  • stim device provision ios [udid] registers a connected iPhone and creates or refreshes its Development profile. It is a separate, confirmed step: it finds the Xcode project the way ios does, lists the team, the phone and each signed bundle id of the scheme (--scheme, --configuration), and asks [y/N]; without a terminal, or with --json, it refuses with STIM_PROVISION_UNCONFIRMED unless --yes is passed. It then runs xcodebuild -allowProvisioningUpdates -allowProvisioningDeviceRegistration in a throwaway DerivedData, stops it at the first build task, before compilation, and always removes the DerivedData. It authenticates with the Xcode account or an App Store Connect API key from the new machine settings ios.appStoreConnectKeyPath, ios.appStoreConnectKeyId and ios.appStoreConnectIssuerId (STIM_ASC_KEY_PATH, STIM_ASC_KEY_ID, STIM_ASC_ISSUER_ID). Builds still never pass -allowProvisioningUpdates; signing remedies, stim doctor and the guides name the new command. (@stim-cli/core settings) 343425025
  • Native Android apps can cache their APK from declared inputs. The new project setting android.artifactInputs ({ complete: true, ignored, outputs, localFiles, environment }) declares every input of a repeatable native Gradle build; with it, local builds store and reuse the APK, verified by package, signing certificate and ABI, and inputs are checked again after compilation. A configured cache provider with the files capability, such as @stim-cli/cache-s3, shares the APK and its receipt between machines; private input files and environment values stay local. Builds on build workers remain uncached, and without the declaration native Android stays uncached. Native Android --plan checks the same key without running Gradle. (@stim-cli/core setting, @stim-cli/cache) 5f8cf86dd, 366de14e6
  • createStim adds doctor({ platform?, record? }) and gc({ cache?, olderThan?, worktrees? }). They return the stim doctor --json and stim gc --json reports in process, within a 60 second budget and with signal; expiry rejects with STIM_REPORT_TIMEOUT and an abort with STIM_CANCELLED, after owned cleanup, and cleanup that cannot verify a child rejects with STIM_CLAIM_UNAVAILABLE. Doctor never repairs, and records a check only with record: true; GC is always the dry run. followLogs() takes an optional third callback, onBackfill(records), for the initial records, in batches of at most 500 when no tail is given. a9334b97f, 37e4e8ab5, 56322ba6e
  • stim-server serves Stim Desktop's reports, settings, logs and project checks on the local socket, behind the private Desktop bootstrap: desktop.reports.get, desktop.reports.subscribe and desktop.reports.invalidate (shared doctor and GC dry-run reports, 60 seconds per target, with an optional platform), desktop.settings.get, desktop.logs.subscribe (a workspace's full retained logs), desktop.doctor.run, desktop.stop.subscribe and desktop.reload.subscribe (streamed progress, then the CLI-compatible JSON result) and desktop.settings.write (only on a server Desktop started), each advertised by its own feature. Phone and network clients cannot call them. machine.details reads its doctor, GC and stats reports in process, and its doctor read no longer records a doctor run for the workspace. (@stim-cli/server, @stim-cli/core protocol types) 0288e04cd, c8407e9d9, b50d08943, d7f200c1b, 422403665, 677b62ddb, a57001c7f
  • createStim adds reload(), setSetting() and unsetSetting(). reload({ platform?, signal? }) requests a JavaScript reload in the live iOS or Android app or reloads the owned Chrome page and returns the stim reload --json facts, within a 60 second budget; it never retries, and cancellation cannot retract a request already sent. setSetting(key, text, { scope?, signal? }) and unsetSetting(key, { scope?, signal? }) share the CLI's validation and return its JSON write result with sensitive values masked; cancellation prevents a pending write. 677b62ddb, 45b166fdb, a57001c7f
  • stim gc reports and cleans up Stim's leftovers in TMPDIR. Without --cache, gc lists Stim temp directories (stim-claim-*, stim-host-*, stim-fakehome-*, stim-hosted-macos-*, stim-server-*, stim-ws-*), xctrace recordings (instruments*.ktrace) and Jest's cache under "TMPDIR leftovers" with their size and a remedy; gc skips them when STIM_HOME is set, since TMPDIR is shared by every home. gc --delete and maintenance at disk pressure level 2 delete a Stim temp directory or recording only when nothing in it changed for 24 hours, it holds no live claim and it is not inside or around a registered project; recordings are kept while xctrace or Instruments runs. Jest's cache is only reported. A dead project whose state and registry supervisor records disagree is pruned once both recorded processes are gone. (@stim-cli/core maintenance types) 6dbb3ee07

Fixes​

  • stim status --watch --json prints no line when only a bookkeeping timestamp changed: each pullRequest.checkedAt, maintenance.lastChecks, maintenance.lastPass.startedAt and durationMs, and maintenance.running.startedAt. A line that prints carries their current values. 3e91e6e7b
  • A hosted session that adopts a parked device starts with a fresh native log. Before, adoption reused the parked session's home, so its earlier native log and checkpoint were served as the new session's logs. (@stim-cli/server) 3e915d7b0
  • stim worktree remove bounds the git reads after teardown. When the HEAD re-check times out, it refuses and keeps the directory and the ownership record; when listing worktrees before deleting the branch times out, it keeps the branch, prints kept <branch> (git did not list the worktrees in time to check it) with a retry command, and exits 1. 6b5207d94
  • stim doctor's fingerprint parity check compares an app in a repository subdirectory with the same subdirectory of HEAD. Before, it compared it with the repository root. 951a962d2
  • A native Android launch without a variant labels its verify line debug instead of undefined. bdc2e08ba
  • A hosted iOS, Android or macOS session that the host stops before it is ready fails with stopped before it was ready ... Retry when the host can take it; it needs no stim stop. Before, it was reported as being in an unknown state with a stim stop remedy. 09535c548
  • Hosted waits cover the server's worst case: the client waits up to 280 seconds for a hosted stop instead of 180, which also covers parking and the stop after a failed park, and device-host.reserve gets a deadline sized for retiring up to three parked sessions before a new device. a0edec223
  • stim stop or Ctrl-C ends a hosted run that is waiting for the app install or for a busy host to take the app offer. Before, those waits ran to their deadline. da0bbdc5e
  • An Android run whose remote cache work was abandoned returns its result before the process exits, so a createStim().run() caller receives it. Before, the run exited from inside the launch step. c3c1e22d5
  • stim stop cancels an ios or android run through the workspace state, and sends SIGINT only when the run's holder is a CLI process. A run inside a process that embeds Stim, such as stim-server, is asked to stop without a signal; if it does not exit within 60 seconds, STIM_STOP_BLOCKED says it did not exit "of the stop request" and tells you to cancel it from the client that started it instead of kill. Ctrl-C on a run that holds a physical-device lease now cancels through its cleanup like other runs; only a second Ctrl-C, or one with no build tool or SimSlim to stop, exits at once. 87ff383b0, 8721059e6
  • A supervisor serving hosted Metro reacts to writes to its workspace state file instead of reading it every 500 ms, with a 10-second fallback. 173ed3e70
  • A relative XCODE_XCCONFIG_FILE in the native Xcode cache key resolves from the Xcode project's directory instead of the current directory, and the native Xcode cache key, STIM_REMOTE_BUILD, STIM_REMOTE_BUILD_MODE, STIM_ACCESS_TICKET and STIM_TMPDIR are read from each createStim operation's own environment. 87f02e373, 4f6928850, 95dd41798
  • Failed requests to remote machines are written to the workspace log of the operation that made them, so concurrent createStim operations no longer log into each other's stim logs. 965cc5ffb
  • Cancelling a build-only createStim().build() while it copies the app or APK out waits for the copy and its cleanup, removes the export and rejects; cancellation right after the export still refuses the result. Cancelling an artifact-cache copy stops its child, keeps the previous cache entry and removes only its own staging, a cancelled backfill no longer installs the fetched hit, and a cancelled build waiting for a build slot stops waiting. 5c4c600e1, 104faeed7, 0323f5a3e
  • stim status --watch handles SIGINT and SIGTERM from the start, so an interrupt before the first frame exits 0 like a later one. ba0b9c724
  • stim gc and automatic maintenance consider every linked worktree that git worktree list shows for a registered project's repository, whether or not a stim command ever ran in it. The dirty reason now names up to three of the files that keep a worktree. stim worktree remove refuses a worktree that contains another worktree ("Refusing to remove ...: it contains the worktree ..."), and gc keeps it with the new reason nested-worktree. 810778c09
  • No stim gc invocation starts a background maintenance pass. Before, plain gc, a report, could start one when a check was due, and in on mode that pass deletes. Report-only gc still prints the disk pressure level 3 warning and the maintenance plan it computes. c8d7451c4
  • A maintenance pass starts at zero free disk. Each pass keeps a 1 MiB maintenance/disk-reserve file; when taking the run claim fails with ENOSPC or EDQUOT, the pass deletes it, retries once, and writes it again at its end. Before, the claim write failed and the pass exited when space was most needed. ef7eb69ec
  • An iOS build that used the shared compilation cache and failed with the compiler's own corruption errors (LLVM ERROR: OnDiskCAS: corrupt internal reference, error: CAS operation failed: ..., error: CAS-based dependency scan failed: ...) empties that cache and fails with the new code STIM_COMPILATION_CACHE_CORRUPT, asking for a rerun; the emptying, or why Stim left the cache alone, goes to the maintenance log with trigger "ios build". Every iOS build that compiles with the cache holds a shared claim, <cache>.claims beside the cache directory, while xcodebuild runs, and the build, gc --delete --cache "compilation cache" and maintenance empty it only under the exclusive claim. A build worker empties its own corrupt cache; with --remote-build <machine> the build fails with this code and the worker's message, otherwise it falls back to a local build. An Android build lock no longer keeps maintenance from emptying the cache, and at level 3 a kept cache appears in the pass's blocked reasons with its size and the build. 89cb97e11, cf5775ed1, dc2d601bd
  • stim gc measures each shared cache for at most 60 seconds. A cache it could not measure shows "Size unavailable: measurement failed or exceeded 60 seconds." and the totals read "at least". Before, a measurement had no time limit and an unknown size counted as 0. cee79397b
  • A Linux or Windows machine in remote.machines is used for builds only. It is never a device-pool member or hosting candidate when tailscale status reports an OS other than macOS (an unknown OS still counts as able to host), Stim Desktop's build suggestions offer it, and stim doctor --fix never asks or contacts it for device-host access; doctor reports it as not-asked, even with a saved approval. A stim-server off macOS refuses a device-host connection at hello with forbidden ("Hosting simulators needs a Mac. This machine takes build requests only."). (@stim-cli/core, @stim-cli/server) 2d96252ca, ce0fdd4ff, ca919c50d
  • stim worktree remove refuses, with STIM_WORKTREE_SERVICE, a worktree that a Linux stim-server service runs from: a systemd user unit in ~/.config/systemd/user/<label>.service with an [X-Stim] Managed=true section, as it already did for a macOS LaunchAgent. (@stim-cli/core unit parsing) 46e05cb32
  • On Windows, the STIM_PATH_TOO_LONG preflight accounts for react-native-gesture-handler 3, whose shared shadow nodes have the longest known object path: such a project fits a root of at most 24 characters for an x86_64 Debug build and 21 for arm64-v8a, other projects 51 and 48. The CMake profile directory is shortened to 4 hex characters. The remedy is now to move or clone the project to a short real path such as C:\src\<name>; the subst drive remedy is dropped because Node and Expo autolinking resolve it back to the long path. 42b086f50
  • A createStim operation no longer adds a process exit listener for remote build connections; only CLI operations close those connections on exit. d716dda80
  • stim doctor reports a remote machine whose stim-server still speaks protocol 1 as outdated (Remote machine <machine> runs an older stim-server) instead of unreachable, with the remedy to run stim-server service update --release <version> there and then the setup command, or stim doctor --fix; a hosted session to such a machine gets the same remedy. The offload refusal, stim guide and the docs name the setup command stim doctor --fix prints instead of stim-server devices grant --build. 38a9c2bf1
  • stim doctor's check for other stim installations resolves paths and runs stim --version from the calling operation's directory and environment, including its PATH (Windows Path), instead of the process's. fca5e6746
  • When stim-server setup refuses a server too old for setup, the remedy says to update Stim Desktop with Check for Updates when Desktop runs that server, and otherwise to run npm install --global @stim-cli/server@<version> and restart the server. (@stim-cli/server) 9a890b33f
  • stim-server's stop action runs the in-process stop API on the exact registered workspace, including a partial one without an app manifest, instead of starting the CLI. A stop waiting for native work ends its wait when the 120 second action limit passes, while teardown already underway finishes, and the workspace stays busy until that work drains. (@stim-cli/server) e3a31955c
  • Following logs picks up local appends from filesystem events, with a ten-second repair scan, and hosted targets join and leave the follow stream from local state notifications, reconciled about every 10 seconds. A removed hosted slot's log follower stops. 94c665dac, 5ca538572, aad95307e
  • local.properties stays a local input when the Android project path is not canonical, such as a symlinked root or a Windows short temp path; before, it counted as a transferred source. d9243502f
  • The Stim Host Dev build no longer leaves clang output directories in TMPDIR. (@stim-cli/server) 6b6cb388f
  • A phone event that fails validation is described with the schema of its own event name, instead of the first event type tried. (@stim-cli/core) 0ca651da6

Docs​

  • CLI output, stim guide text, setting descriptions and the docs say "remote machine" and "this machine" instead of "Mac" where a Linux worker or a Linux or Windows client can be meant. "Mac" stays for iOS and macOS builds, stim macos, device hosting, stim-server setup and service, permissions and Stim Desktop's own labels. JSON reason codes are unchanged. aed8cd0f6
  • Guides, errors and doctor remedies name Stim Desktop's Remote Machines tab, and say Mac only where only a Mac applies and machine elsewhere, including the build pause settings and the @stim-cli/transport-tailscale README. 2d96252ca, 7dd48ca38

Migration notes​

  • Re-pair every phone and approve every remote machine again, after updating each machine's stim-server to 1.21.0: see Breaking: re-pair every device. Update the machines first; a phone app or stim 1.21.0 cannot pair with stim-server 1.20.2.
  • Do not downgrade a machine to 1.20.2 after 1.21.0 has written its pairings: the protocol 1 records are gone, and it would need pairing again too.
  • A STIM_* variable or tool path that only your shell profile sets no longer reaches stim-server. Pass it with --env KEY=VALUE or --path-prepend <dir>, or set it in the service's environment.

QA​

The maintainer chose this minor version and approved the release without a QA gate on 2026-10-14. Pre-tag QA is omitted, as with 1.18.0 to 1.20.2. This deviates from RELEASE.md section 3, which has no waiver for a stable version, and from docs/release-recovery.md, which asks for the full lane on the first release that contains a new package (@stim-cli/cache-s3, @stim-cli/agent-driver, @stim-cli/agent-driver-agent-device, @stim-cli/transport and @stim-cli/transport-tailscale).

Omitted before the tag, all eight rows of the matrix (node scripts/release-qa-matrix.mjs v1.20.2; 1133 changed paths, 438 mapped to a row, 0 unclassified): loop, logs, caches, real-repository, remote-provider, release-build, android-flavor and launch-evidence, plus the native suites run-native-e2e.mjs and run-cache-e2e.mjs. The protocol 2 re-pairing steps above were not exercised by hand on this candidate.

Run before the version commit: format:check, lint, build, typecheck, knip, release:prep --check, test:runtime, test:e2e (115 of 115), pnpm test (9323 tests: 9296 passed, 25 skipped, 2 failed locally in files this release does not change: packages/stim-cli/src/__tests__/warm-claim.test.ts "the installer process group keeps the claim after the refresh that spawned it is gone" failed in the full run and passes alone, and scripts/agent-benchmark/macos-processes.test.mjs fails at its sandbox-exec call on this Mac), CLI version/help/guide, licenses:check and format:check in apps/mobile, the QA matrix's own tests, and pnpm tarball inspection of the twelve packages. Not run: test:compat, which needs a connected iPhone.

A first v1.21.0 tag failed the Release workflow's full Windows gate and published nothing; it was deleted and the tag moved to a later commit on main. #3868 and #3871 fix or quarantine the Windows failures; the tests quarantined on Windows CI are listed under #3878, the parent issue for the 1.21.1 Windows patch. For this release the Release workflow runs the full Windows suite without gating publication on it (#3906, the maintainer's decision on 2026-10-15). The random Vitest worker crashes in the Windows suite (#3877) remain.

Run after push: exact-commit CI, with the tests quarantined on Windows CI skipped, and the Release workflow's own gates, with its Windows suite reporting but not gating.

1.20.2​

Stim Desktop and the Stim phone app ship through their own workflows; this release covers the seven npm packages (@stim-cli/core, @stim-cli/cache, @stim-cli/metro, @stim-cli/expo-build-cache, stim, @stim-cli/ci and @stim-cli/server). Commits that only changed those apps, tests or internal structure are not listed.

New​

  • workspace.hidden hides a workspace in Stim Desktop and the phone app. New boolean setting, workspace scope only, default false: stim settings set workspace.hidden true from the workspace. stim status --json then reports hidden: true on that environment. The next start, ios, android, web, macos, reload or worktree warm in the workspace clears it, and worktree remove drops it with the workspace. (@stim-cli/core setting and status type) 1186ed44d

Fixes​

  • Security: on a checkout without its own dependencies, such as a fresh worktree or a hoisted monorepo app, stim doctor skips the fingerprint parity check and says why when Expo packages resolve from outside the app, or when the check would need npx react-native config (Android, or a project without an ios directory). Before, that check could run project code that doctor does not install or own. The linked-library check can still run installed Expo packages on the main checkout, as a build does. 26a22d93c
  • Security: a cache registration whose directory is a filesystem root, or is or contains the home directory, the system temporary directory or the Stim config directory, is refused: the CLI's registration throws cannot register this cache: ..., @stim-cli/core registerCache ignores it, and stim gc never prunes or empties such a directory, reporting it as not a directory the cache owns. (@stim-cli/core) a6c9ba3db
  • Security: a remote server.update.start from a build or device-host client refuses a release older than the running stim-server with bad-request, so a client with an older stim can no longer move a remote Mac back to it. stim-server service update run on the Mac itself is unchanged. The README and stim guide settings now say that a build grant is full access to the account stim-server runs as, since project code runs there unsandboxed. (@stim-cli/server) fa167612a
  • stim gc lists stale entries of the machine EAS session ledger (~/.stim/machine/eas/sessions.json) under "Stale EAS ledger entries": an entry whose id is not a UUID and whose absolute workspace root is missing on a mounted volume, such as ids written by a test fixture. A valid id, an existing root, or a root on an unmounted volume keeps the entry. stim gc --delete re-checks each entry and removes it under the ledger lock without contacting EAS. gc --json adds sections.staleEasLedgerEntries with id and workspaceRoot. 2b0e36d5b
  • Every child process Stim starts has a time budget. Git calls default to 10 seconds, read-only ones run with --no-optional-locks, and asynchronous reads run at most six at a time. The git reads that gate stim worktree remove get 60 seconds; when one times out, removal refuses before any teardown with a git safety check timed out instead of treating a registered worktree as a plain folder. stim gc reports a worktree whose unpushed commits or initialized submodules could not be checked as skipped with that reason. 5f56cc4dc, bbb96cb96
  • stim status --json reports a workspace whose macOS app is running or orphaned as stage: running, from the app's build end when no Metro supervisor runs. Stim Desktop and the phone app now show the stage, app presence and git chip that stim reports instead of computing their own. (@stim-cli/core) 14c46bf48
  • stim-server build uploads: build.sync fails with build-busy while the worker root's volume has less than 10 GiB free and with limit-exceeded once one manifest's sizes add up to more than 20 GiB, and a blob that would leave the volume under 10 GiB free closes the connection with 4400. When a client's build ends and no other build of that client runs, the server deletes the client's blobs that no checkout and no open manifest uses; blob pruning stops without deleting anything when a mirror record cannot be read. A revoked build client's directory under the worker root is deleted once its builds end, unless the client has not connected since the server started. (@stim-cli/server) fdae5685c, f87b6115b
  • stim macos waits up to 20 seconds for macOS LaunchServices to list the launched app before it returns, so agent-device open attaches to it instead of starting a second copy without Stim's launch arguments. If the app is still unlisted, it warns and returns; a --remote launch skips the wait. stim guide macos and the website describe the wait instead of the manual lsappinfo loop. 0d5bbf727, a3a4c9fd6
  • Relaunching an Android app whose process state cannot be read launches with am start -S, which force-stops the app as part of the start. Before, Stim launched without stopping it, so a running app could keep its old process and bundle. An app without a launcher activity gets a best-effort am force-stop before monkey, and a known running pid still gets a separate force-stop that refuses the launch when it fails. 313f2c1a0
  • A hosted install waits 355 seconds instead of 300, covering the host's install deadline plus its settle bound; the client's hosted waits now derive from the server's limits in @stim-cli/core. b5c6b9d86
  • The Release workflow refuses to publish a tag whose commit is not on main. This changes the release process, not a package. 2bbf06e69

Docs​

  • stim guide tutorial run has the agent check the change by picking a size (size-s, size-m or size-l) instead of typing a name. e50e7b25b

QA​

The maintainer approved this release on 2026-10-10. Pre-tag QA is omitted, as with 1.18.0 to 1.20.1. This deviates from RELEASE.md section 3, which has no waiver for a stable version.

Omitted before the tag, all eight rows of the matrix (node scripts/release-qa-matrix.mjs v1.20.1; 250 changed paths, 87 mapped to a row, 0 unclassified): loop, logs, caches, real-repository, remote-provider, release-build, android-flavor and launch-evidence, plus the native suites run-native-e2e.mjs and run-cache-e2e.mjs.

Run before the version commit: format:check, lint, build, typecheck, knip, release:prep --check, CLI version/help/guide, licenses:check in apps/mobile, and pnpm tarball inspection of the seven packages. Not run locally, left to CI: pnpm test, test:e2e, test:runtime and test:compat. Run after push: exact-commit CI, including the full Windows job, and the Release workflow's own gates, including its new tag-on-main check.

1.20.1​

Stim Desktop and the Stim phone app ship through their own workflows; this release covers the seven npm packages (@stim-cli/core, @stim-cli/cache, @stim-cli/metro, @stim-cli/expo-build-cache, stim, @stim-cli/ci and @stim-cli/server). Commits that only changed those apps, tests or internal structure are not listed.

Fixes​

  • npm install and npx no longer print the uuid@7 deprecation warning; Xcode project files are read by an in-tree parser. stim drops the xcode runtime dependency that 1.20.0 added and keeps @xmldom/xmldom. e8f149943

Docs​

  • stim guide tutorial run has the agent check each change on the simulator with agent-device after the after screenshot (tap the button, toggle the switch, type a name, take a screenshot), so Stim Desktop records the actions for replay; when agent-device is not installed it checks the build result and stim logs --errors instead and does not install it unasked. The "Change It Again in Parallel" step has a copyable prompt that asks for a new worktree. The stim guide tutorial manual section is removed and now answers "Unknown section"; the website's Desktop and getting-started pages no longer link it. 52eba84c7
  • The ios.scheme description in stim settings and the shipped JSON Schema says the --scheme flag overrides it, and the website's settings tables are generated from the settings registry. (@stim-cli/core) b52e08b6a

QA​

The maintainer approved this release on 2026-10-10. Pre-tag QA is omitted, as with 1.18.0, 1.19.0 and 1.20.0. This deviates from RELEASE.md section 3, which has no waiver for a stable version.

Omitted before the tag, all eight rows of the matrix (node scripts/release-qa-matrix.mjs v1.20.0; 76 changed paths, 7 mapped to a row, 0 unclassified): loop, logs, caches, real-repository, remote-provider, release-build, android-flavor and launch-evidence, plus the native suites run-native-e2e.mjs and run-cache-e2e.mjs.

Run before the version commit: the native Xcode acceptance job (Native E2E, native_ios_only, fixture project) on main at eafc8cc9f, which includes the new parser: run 38083829755, passed. Also format:check, lint, build, typecheck, knip, release:prep --check, CLI version/help/guide, licenses:check in apps/mobile, and pnpm tarball inspection of the seven packages. Not run locally, left to CI: pnpm test, test:e2e, test:runtime and test:compat. Run after push: exact-commit CI, including the full Windows job, and the Release workflow's own gates.

1.20.0​

Stim Desktop and the Stim phone app ship through their own workflows; this release covers the seven npm packages (@stim-cli/core, @stim-cli/cache, @stim-cli/metro, @stim-cli/expo-build-cache, stim, @stim-cli/ci and @stim-cli/server). Commits that only changed those apps, tests or internal structure are not listed.

New​

  • Native Xcode and Gradle apps build through project recipes. The integration registry gains a native Xcode provider and a native Gradle provider, each with its own planning and doctor checks. Xcode selection reads projects, workspaces, schemes and configuration files; Gradle reports application variants through a checked-in model shim. Both use native process readiness, so a live native Debug or custom-configuration app reports launched: true without Metro. Only proven Xcode input closures qualify for whole-artifact reuse; native Gradle is not artifact-cache eligible. stim guide agent routes native apps to the new stim guide lifecycle native-ios and native-android sections. stim gains two runtime dependencies for reading Xcode projects, @xmldom/xmldom and xcode. 82e649b4c
  • Native iOS apps run on hosted simulators. A hosted iOS session negotiates the host capability hosted-ios-process and waits for live process readiness instead of a Metro bundle. A host without it is refused before reservation or upload, with an update-stim-server remedy, and --remote auto skips it. React Native modes keep bundle readiness. (@stim-cli/core protocol, @stim-cli/server) db536eb72
  • Native Android apps run on hosted emulators. The host capability is hosted-android-process. A native Gradle app uses an approved hosted Android emulator without starting Metro, and a host without the capability refuses before admission or APK upload. The delivery reuses verified APK transfer and package, SDK and ABI checks. React Native development still requires Metro. (@stim-cli/core protocol, @stim-cli/server) 8f1bd25d1
  • Native Xcode builds can run on an approved build worker. The client and worker negotiate the capability native-xcode-build. Only the contained, Git-visible inputs of the provider snapshot are transferred, and the worker mirrors them exactly. A required ignored or external input, or an unproven cache identity, refuses and the build falls back to local. The client sends its identity parameters with build.start, and the worker refuses early with identity-mismatch naming the differing parameter (for example DEVELOPER_DIR). LANG and LC_ALL are no longer part of the artifact key. (@stim-cli/server) 85ae2d460
  • Native Gradle builds can run on an approved build worker. The capability is native-gradle-build, and the project declares what to transfer with the new project setting android.offloadInputs: {"complete": true, "ignored": [...], "outputs": [...]}. The worker receives Git-visible source plus the exact ignored files, and every AGP-reported build directory the build creates must be listed in outputs. External inputs, directory links, submodules, a root buildSrc and custom machine properties refuse. An undeclared output directory fails the remote build by name and falls back to a local build. The APK has no Stim artifact cache identity; Gradle's own incremental and configuration caches are kept. See stim guide lifecycle native-android. (@stim-cli/server) d96f6531b
  • ios.scheme names a project's Xcode scheme once. New project setting (workspace and committed layers). --scheme still wins when passed; otherwise ios.scheme is used by stim ios, --plan, the native Xcode doctor and the stim build API, so the plan and the run agree on the cache key. An --eas-profile build reads only the flag. 3d93ba64d
  • ios.projectPath locates a bare React Native app's Xcode project outside ios/. New project setting (workspace and committed layers; default ios), a directory relative to the app. stim ios, ios --plan and stim build refuse with STIM_BAD_ARG a path that escapes the app, is absolute, or holds no .xcworkspace or .xcodeproj, and any value on an Expo app. The path is added to the fingerprint, travels to build workers in the build job, and worktree remove restores Podfile.lock and the app's own <name>.xcodeproj/project.pbxproj churn there. 97ca1663e
  • android.gradleRoot and android.module build a bare React Native app from a Gradle root outside android/. New project settings (workspace and committed layers). android.gradleRoot (default android) is a directory relative to the app that holds gradlew and settings.gradle; it may be outside the app but not the repository. android.module (default :app) is the app's Gradle project path. With either set, Gradle runs from the root with the qualified task (for example :packages:rn-tester:android:app:assembleDebug). Both join the cache key as a hashed gradleProject component (@stim-cli/core buildCacheKey) and travel in the remote build job. stim android, --plan and stim build refuse with STIM_BAD_ARG a root outside the repository or without settings.gradle(.kts), a module directory without a build script, a malformed module path, and either value on an Expo app. (@stim-cli/server accepts the two fields in build.start.) 7480ff5d2
  • metro.command starts a project's dev server with its own command. New project setting (workspace and committed layers): an argv array, run without a shell from the app directory, where {port} is replaced with the workspace's reserved port. The supervisor uses a third server mode, command-child, ahead of ecosystem detection, runs the command in its own process group, and signals the group on every stop path. Output is logged as command_stdout and command_stderr events and counts as dev-server activity for metro.idleStopMinutes. stim start refuses with STIM_BAD_ARG an empty argv, a blank program, a missing {port}, --reset-cache, and Windows. Stim adds no bundle-response reporter to this server, so stim ios and stim android report the launch as unverified; read stim logs --source metro. start and the iOS and Android launch facts now report the supervisor's recorded mode. 7fcfebd09
  • stim.build() in the public API builds an app without launching it. It builds Android APKs, iOS simulator apps and macOS apps with the same provider recipes, cache validation and build ownership as run(). The result is an owned workspace copy kept until stim worktree remove. A build-only result is recorded only in build history and in its own build-artifact-<platform>.ndjson, so a running app keeps its last-build record, device record and build log, and cache maintenance protects the newest succeeded build key per platform so a later run can reuse it. See stim guide api. 7d92c3395
  • stim-ci build and buildCI produce an app artifact without starting a runtime. (@stim-cli/ci) stim-ci build --platform ios|android|macos calls the public build API and exports an Android APK (app.apk) or an Apple app archive (app.tar.gz, with executable modes and symlinks preserved) into --artifacts, with build.json, result.json and diagnostics.json. A later run in the same job can reuse the build from the same Stim cache and reports cacheKey and cacheHit. Cancellation waits for the owned build to finish cleanup and never calls workspace stop(). New error codes: STIM_CI_BUILD_FAILED and STIM_CI_ARTIFACT_FAILED. Importing a downloaded artifact across jobs is not supported. 90d4d10d1
  • Memory pressure names the process that holds the memory. When macOS reports warning or critical pressure, Stim reads top -l 1 -o mem -n 20 and flags a process at or above the larger of 8 GiB and 25% of RAM, or 2 GiB for fseventsd and Watchman. The flagged process is named in the iOS boot warning and failure advice, the Android memory phase line and boot-timeout remedy, stim doctor (finding memory-culprit, skipped when STIM_HOME is set), and the maintenance pressure check, which stores it in maintenance.pressure.memory.culprits in stim status --json (fields pid, name, bytes, command, note) and shows it on the plain status maintenance line. A command is attached only for processes known to be safe to restart: sudo killall fseventsd for fseventsd, stim gc --delete --cache watchman for Watchman, and stim gc --delete --cache gradle-daemons for Gradle and Kotlin daemons. Stim never runs these commands itself. 8549b4ff1
  • stim status shows what the device and launch phases wait on. stim status --json build gains an optional activity: { name, startedAt, percent? }, where name is booting (the simulator or emulator boot this run started), bundling (from the app's bundle request on this workspace's Metro until Metro delivers it; percent is Metro's own progress for that request) or waiting-ready (the app reports its readiness pending). Plain stim status prints, for example, build: ios launch (bundling JS 45%, 12s), 58s elapsed. launched values are unchanged. A boot on an EAS-hosted simulator is not reported as booting. Stim Desktop and the phone app show the same label. ca5b5017b
  • stim status --json reports doctorRuns on an environment: { ios?: { at }, android?: { at } }, the time stim doctor last ran for each platform in that workspace; the field is absent until doctor has run. 465eb1c5b
  • stim guide tutorial: the "Build on Another Mac" step is removed, the begin prompt is shorter, and a new optional last step deletes the test app. The new stim guide tutorial delete section checks the stimTutorial marker in {base}/app.json, removes each linked worktree and then {base} with stim worktree remove (never --force), and only then runs rm -rf "{base}". The tutorial version is unchanged. b92e8bc1c

Migration notes​

No command, flag or setting is removed. These behaviour changes need attention.

  • @stim-cli/server refuses WebSocket upgrades from web pages. An upgrade that carries Sec-Fetch-Site, or an Origin other than the requested host (http://<Host> with a loopback host name on loopback, https://<Host> through tailscale serve), now gets HTTP 403 before the auth-failure limiter, without spending the failure budget. Upgrades without Origin, such as Desktop and the CLI, pass. Known gap listed in the PR: an iOS simulator app paired to ws://[::1]:... sends Origin: http://::1:... and is refused; 127.0.0.1 and localhost work. A pending approval HelloResult no longer carries server.home, which is now optional in the core HelloResult type and the JSON schema. Paired phone names are cleaned (control and format characters removed except U+200D, whitespace collapsed, cut to 64 UTF-16 units, empty becomes Phone). Previously a web page could fill the shared 'local' limiter bucket and lock out loopback clients for 60 s, and a page on any tailnet node could request build access. 6b69ad01a
  • android.keystorePassword no longer reaches apksigner's argv, and Stim state is owner-only. A literal password is passed as --ks-pass env:STIM_KEYSTORE_PASSWORD with the value only in the child's environment; env:, file: and stdin references pass through. Stim replaces those values with *** in a thrown error, so a failed signing step cannot write the password to the workspace build log, which paired devices with read can fetch. $STIM_HOME, workspace state directories and each ndjson log directory are created 0700 (an existing wider directory is narrowed when the filesystem allows), config.json and created-devices.json are written 0600 through a temp file, and new ndjson logs open 0600. @stim-cli/core exports ensurePrivateDir. Existing log files keep their mode. 8eeac50b8
  • stim doctor no longer executes fingerprint.config.js or the project's eas-cli. When fingerprint.config.js or .cjs exists, doctor skips both fingerprint checks and reports Skipped the fingerprint checks: fingerprint.config.js is code. The project eas-cli version is read from node_modules/eas-cli/package.json, and eas whoami runs only with an eas on PATH that is outside the git root by path and realpath, not in a node_modules/.bin above the project, and with a known repository root. The parity check's temporary worktree runs with core.hooksPath pointed at an empty path. findBinUpward stops at the git repository root, which builds also see: a hoisted monorepo that is not a git repository no longer resolves a parent .bin shim for expoBinPath and resolveEasCliBin. Doctor still can run installed packages through @expo/fingerprint and npx react-native config in the parity check; the PR lists this as open. e18fbec7c
  • Reclaim no longer stops processes on a workspace's named ports. stim gc --delete, stim worktree remove and background maintenance release a reserved named port (8900-8999) without looking up or signalling its listener. stim ports stop is unchanged. A server a worktree started with stim ports get keeps running after removal; stop it first with stim ports stop. Before this change, any process that later bound the port, for example Solr on 8983, was killed. 8c6b3816c
  • A package that lists react-native only under peerDependencies now counts as an app. In a monorepo, a shared package such as packages/ui with such a peer dependency is its own workspace, so logs, stop, reload, settings and worktree warm run from it act on that package; run them from the app directory. Detection reads only package.json, so metro.command set at either the committed or workspace scope works in such a package. 680afc64c
  • One-shot stim status reads git only for live environments. Idle environments and unprovisioned worktrees report worktree.git: null and no gitChip; stim status --watch still reads every worktree. 08857a71d
  • stim status --watch --json prints fewer lines. A usage figure keeps its previously printed value until it moves by a step: an owner's cpuPercent by 5 points, an owner's or environment's memoryMb and capacity.committedMb by 16 MB. A line is still a complete payload. In a watch line capacity.committedMb is held separately from the environments' sum. 88ff46f06
  • stim-server service install --env values no longer appear in the server's process arguments. (@stim-cli/server) Each --env KEY=VALUE is written to the LaunchAgent plist's EnvironmentVariables, and ProgramArguments carries only --env KEY, so a value such as EXPO_TOKEN is no longer readable with ps. The values stay in plain text in the plist, and launchd now also passes them to the server's own Node process, so a variable such as NODE_OPTIONS given with --env affects the server too. service install and setup refuse --env for SHELL as well as STIM_HOME. A plist written by an earlier release keeps its values in the arguments until the next service install; service update and rollback only replace the script path. 9e9f6dc4a
  • A committed android.keystore must resolve inside the repository. A value in the committed .stim.json that is an absolute path outside the repository, or a relative path or symlink that escapes it, is no longer used: the release APK swap is skipped with a swap note and the run builds fresh. Set the path at workspace or repo scope to keep a key outside the repository. The Android package id read from the Expo config or build.gradle must now look like com.example.app; one that does not, such as applicationId "${appId}", counts as not detected, and stim android refuses before any adb call when neither the APK nor the project names a valid package. A __proto__ key in a committed .stim.json or in config.json project entries is ignored. a6546a431
  • On Windows, Stim refuses to run a .bat or .cmd tool whose arguments contain a double quote, carriage return or line feed, so an argument cannot break out of the batch file's argument re-expansion. android.deviceProfile must now be an avdmanager id matching ^[A-Za-z0-9 ._()-]*$ (an empty value still counts as unset; real ids such as pixel_6, Nexus 5X or 10.1in WXGA (Tablet) pass); stim settings set and settings validation refuse anything else. The remaining df, simctl, avdmanager, adb, emulator and lsof calls pass their arguments as argv instead of a shell string. 8d2ab2f91

Fixes​

  • stim macos refuses a macos.infoPlist that is a symbolic link or is not a regular file inside the directory it is relative to, and an offloaded build refuses an artifact that the worker's archive turns into a symlink (or a symlinked Contents) or places outside the staging directory; the error names the worker. 99e421b75
  • CocoaPods runs with the login shell's Ruby environment when no project .ruby-version Ruby is installed. Stim takes GEM_HOME, GEM_PATH and the Ruby-related PATH entries from $SHELL -lic (10 s timeout, cached per shell), so the pod --version probe, pod install and the Bundler environment agree, and remote build placement no longer compares a caller's bundled CocoaPods 1.16.2 against a remote Mac's 1.17.0 and falls back to a local build. An installed .ruby-version Ruby, an existing GEM_HOME, and Bundler-pinned projects are unchanged. 55f102ca9
  • Relaunching an iOS app on a simulator whose process state cannot be read (the launchctl list probe times out) launches with simctl launch --terminate-running-process instead of a separate simctl terminate, so a just-booted simulator no longer fails with STIM_LAUNCH_FAILED when terminate times out. A dev client in that state launches through --initialUrl rather than simctl openurl. A known pid still gets a separate terminate. Emulators still launch without stopping anything in that state. 93fbdbf37, 076d8b2dc
  • Owned-AVD teardown bounds its emulator -list-avds, adb devices and emu avd name inventory queries and kills a timed-out query child. The budget is 15 s per resolver; a timeout fails the teardown and keeps the device record. d7a4f3355, 63170af22
  • Temporary Android build artifacts (the APK copy) are released on every exit, including cancellation and device boot or install failure, in the run, build-only and hosted paths. Stable exports and cached APKs are kept. 723801fcb
  • Hosted preparation waits 355 seconds before it is refused, where it gave up after 180 seconds although the host allows its preparation worker 300. A resumed session that starts stopping keeps its separate 180-second stop budget. c194a510f
  • Hosted Android boot failures include the session device's boot-property and package-manager observations in the preparation and adoption failure notices, without host-wide device inventory. 1a058eb73
  • Native Xcode worker offers collect only the Xcode, simulator SDK, CPU and Stim matching data through the negotiated capability native-xcode-toolchain, instead of CocoaPods, Bundler and other platforms' SDKs. Older native-build-capable hosts still return the complete report. (@stim-cli/server) 48f161a6a
  • Hosted Debug runs that start Metro no longer refuse with metro.ngrokUrl requires metro.tunnel to be "ngrok" (STIM_BAD_ARG) when no layer sets metro.ngrokUrl; an undefined ngrokUrl left by the hosted overlay counts as unset. 227bddc30
  • Structured Expo bundle-response records (those with a string requestId) skip the one-second log dedup, so a dropped bundle_response_finished no longer leaves a request open forever and blocks metro.idleStopMinutes from stopping that dev server. b111e54d7
  • stim status runs at most six git reads at once (the 3 s timeout starts when a read starts), and tailscale status --json for a hosted session is read asynchronously instead of with spawnSync on the main thread. Before, a one-shot status --json with 147 worktrees and environments spawned 147 child processes at once. 08857a71d
  • stim doctor starts its Watchman, device-host, watched-checkout and build-machine probes together, and the remote Mac toolchain comparison (Xcode, simulator SDK, login-shell Ruby, pod --version, simulator runtimes) no longer blocks the event loop. Findings, JSON fields and their order are unchanged. 69044fb11
  • @stim-cli/server usage history keeps the 15-second slots filled between status lines, so the phone's 10-minute usage chart has no gaps while status --watch prints nothing for a steady workspace. 88ff46f06
  • Two kill sites now check the recorded process identity before signalling: stim start's Windows supervisor handoff signals the recorded supervisor only while it is still the same process, and otherwise reports the remedy with stim stop and the taskkill or kill command to run; Chrome cleanup for stim web treats a leftover process group as lingering only when a member still runs the workspace profile, so an unrelated process that reused the group id is not signalled. 09bc84d51, 4d285909e
  • Windows: the "ccache off" note now prints the module build script path with consistent separators. c2e077139
  • server.update.status (@stim-cli/server) reports how an update this server started ended, including one the update lock refused, with the last line of its log, when that is newer than the recorded last update. fcaddb553

Docs​

  • The website, stim guide and the package READMEs cover ios.scheme, ios.projectPath, android.gradleRoot, android.module, android.offloadInputs, metro.command, native Xcode and Gradle apps (stim guide lifecycle native-ios and native-android), hosted native apps and native build offload, stim.build(), stim-ci build, memory-culprit reporting, the status git and watch behaviour, named ports on reclaim, what stim doctor does not run, and the CocoaPods environment used for placement. 82e649b4c, 90d4d10d1, e18fbec7c, 8c6b3816c, 55f102ca9
  • stim guide tutorial run has the agent take before and after screenshots of each change into .expo/screenshots/, and stim guide tutorial share builds the pull request body from the tutorial repository's template with those screenshots attached. 14427a8a4
  • stim guide tutorial run reuses an existing tutorial folder when it is a clean clone of the tutorial repository at the current tutorial version, instead of always asking for a new folder; any other existing folder still stops and asks. 0c2d42489

QA​

The maintainer approved this release on 2026-10-10. Pre-tag QA is omitted, as with 1.18.0 and 1.19.0. This deviates from RELEASE.md section 3, which has no waiver for a stable version.

Omitted before the tag, all eight rows of the matrix (node scripts/release-qa-matrix.mjs v1.19.0; 366 changed paths, 162 mapped to a row, 0 unclassified after this release adds a rule for vitest.reap-children.ts): loop, logs, caches, real-repository, remote-provider, release-build, android-flavor and launch-evidence, plus the native suites run-native-e2e.mjs and run-cache-e2e.mjs.

Run before the version commit: format:check, lint, build, typecheck, knip, release:prep --check, CLI version/help/guide, licenses:check in apps/mobile, and pnpm tarball inspection of the seven packages. Not run locally, left to CI: pnpm test, test:e2e, test:runtime and test:compat. Run after push: exact-commit CI, including the full Windows job, and the Release workflow's own gates.

1.19.0​

Stim Desktop and the Stim phone app ship through their own workflows; this release covers the seven npm packages, including the new @stim-cli/ci. Commits that only changed those apps are not listed.

New​

  • Auto placement can fall back to an EAS Simulator. New setting remote.easFallback (default false, machine or project scope). When ios.remote or android.remote is auto, this Mac is at its concurrency.maxDevices cap and no Mac in remote.machines takes the run, the simulator or emulator runs on a billed EAS Simulator through the existing --remote eas path instead of waiting or refusing with STIM_AT_CAPACITY. A Mac that is only busy with a free slot never bills. A read-only check runs first (eas-cli and agent-device present, no conflicting flags or EAS session, a reachable Metro origin, eas simulator:availability); when it fails, placement behaves as before. The placement record says why EAS was or was not used, and devicePlacement.decision can be eas. An EAS placement keeps billing until stim stop. --plan with auto refuses with STIM_BAD_ARG when it would use EAS. abfca8b29
  • local is a reserved value for ios.remote, android.remote and --remote. It runs the device on this Mac and overrides an auto set at a lower layer. A tailnet Mac named local can no longer be targeted by --remote. 10ff88a79
  • A rewritten stim guide tutorial (tutorial version 2), which Stim Desktop's tutorial follows. It clones appandflow/stim-tutorial, has the agent make a change and a parallel change in two worktrees, then covers live view, agent actions, logs, building on another Mac, an optional pull request to the tutorial repository, and removing the two worktrees. Every tutorial iOS run uses stim ios --remote local --remote-build local, so its builds and devices stay on this Mac; the optional machine step builds on the named Mac with --remote-build. 2c9b5a259, 10ff88a79
  • New package @stim-cli/ci with a stim-ci executable. stim-ci run --platform ios|android|macos|web -- <test command> builds and launches the app through the public stim API, runs the test command, keeps its exit status, stops the workspace, and keeps structured results and diagnostics in --artifacts. A managed GitHub Action in this repository (.github/actions/stim-ci) wraps it and uploads those results as a workflow artifact. On GitHub-hosted runners with no explicit home it uses a job-local Stim home and build cache under $RUNNER_TEMP/stim-ci. New error codes: STIM_CI_SETUP_FAILED, STIM_CI_TEST_FAILED, STIM_CI_TIMEOUT, STIM_CI_CANCELLED, STIM_CI_CLEANUP_FAILED, STIM_CI_DIAGNOSTICS_FAILED and STIM_CI_REPORT_FAILED. See the package README and the website's CI page. 1cf1c7403, 808af7c4f, 56317a90d
  • Automatic pool members can be turned off. New machine settings remote.buildPoolDisabled and remote.devicePoolDisabled list members (local or exact remote.machines entries) that this Mac's automatic build or device placement skips. Explicit placement and running builds or sessions are unaffected, and each pool must keep one enabled member; when every enabled member is unavailable, Stim refuses instead of using a disabled one. d6f652965
  • stim macos --plan --json checks the declared packaging inputs without building, launching or contacting a worker, and prints one typed JSON payload, including for refusals. f6f18f958
  • stim doctor reports worktrees nested in a checkout that Watchman would crawl (finding watchman-nested-worktrees). stim doctor --fix adds the worktrees' parent folder, or each worktree, to the checkout's .watchmanconfig ignore_dirs. Doctor never runs watchman watch-del; it prints the command when Watchman already watches the checkout. 8ebe432bd
  • stim status --json reports agentDevice.installed, whether agent-device is on PATH. 2c9b5a259

Fixes​

  • Faster port allocation. Metro and named-port allocation read the OS TCP listener table once (netstat on macOS and Windows, /proc/net on Linux) instead of probing each candidate. A named port is allocated in about 5 ms where 1.18.0 took 90 ms, or 800 ms with IPv6 listeners. Metro allocation now also detects IPv6-only and LAN listeners, which 1.18.0 missed and could pick an occupied port. When the listener table cannot be read, allocation fails with the new error code STIM_PORT_INSPECTION_FAILED rather than guessing, and on Windows stim ports stop keeps reservations instead of removing them. a30008561
  • stim status no longer piles up du processes. A checkout is measured without the worktrees nested in it, which have their own figures, so worktreeBytes for such a checkout is smaller. Only one du runs at a time on the machine, at nice -n 19, and a failed or timed-out walk is cached for the same period as a success. This removes the load spikes when several stim status --watch instances ran at once. 0e25072d2
  • stim stop stops every running copy of a stim macos app, not only the one it recorded, including copies opened from Finder or the Dock. Relaunch and workspace removal do the same, and gc --delete refuses while an unrecorded copy runs. A copy whose executable cannot be confirmed refuses the stop with the new error code STIM_MACOS_OWNER_UNVERIFIED. The stop waits up to 12 seconds for the supervisor, so a slow-exiting app is no longer left running. 79c7cc590
  • stim worktree warm run from a worktree root records the agent on the app workspace it prepares, so Desktop attributes the agent's actions to that workspace. b04479cfc
  • Hosted iOS and Android devices report the device, install and launch build phases while they are placed on another Mac, so stim status, stim stats and Desktop show the step in progress. de7e8a659
  • stim stop cancels an offloaded build in progress and reports STIM_CANCELLED instead of starting another build, reconnect or artifact fetch. 123ed658c
  • When --remote, ios.remote or android.remote names a Mac, Stim waits up to 45 seconds for that Mac's device offer, where it waited 3 seconds and could refuse a healthy host whose device inventory probe was still running. Automatic placement keeps its 3-second probe. 2b609bb42
  • A device wait cancelled by stim stop no longer starts a reclamation afterwards. 48d99c91d
  • iOS bundle requests wait for the client lookup before Metro serves them, so a launch is attributed to its workspace instead of racing it. f741720db
  • Agent log attribution compares exact device and inode IDs of the log file, read as bigint stats, so a replaced log file is not mistaken for the original. d50226af5 (@stim-cli/core)
  • The programmatic API infers a run's result type from the requested platform. d928510ff
  • Debug logs (debug.logs) record iOS app process inspection outcomes, in-flight native stop phases, and hosted worker cancellation paths (@stim-cli/server), without arguments or environment contents. abf9826d7, 4679d436f, 463cd44e0

Docs​

  • The website and stim guide cover remote.easFallback, the local value, the automatic pool settings, stim macos --plan, the Watchman finding, hosted build phases, port allocation, the macOS app stop, the tutorial, and @stim-cli/ci.
  • The guides and website say that Desktop shows a launch card before opening a device, and that a reopen event does not take focus from a stim macos copy started in the background. f254e20ce, ac4066449

QA​

The maintainer approved this release on 2026-10-09 ("npm 1.19.0 + Desktop 0.1.19"), to ship origin/main as it stood. Pre-tag QA is omitted, as with 1.18.0. This deviates from RELEASE.md section 3, which has no waiver for a stable version, and from docs/release-recovery.md, which asks for the full lane on the first release that contains a new package (@stim-cli/ci).

Omitted before the tag, all eight rows of the matrix (node scripts/release-qa-matrix.mjs v1.18.0; 314 changed paths, 106 mapped to a row, 0 unclassified after this release adds rules for packages/ci, .gitattributes and .watchmanconfig): loop, logs, caches, real-repository, remote-provider, release-build, android-flavor and launch-evidence, plus the native suites run-native-e2e.mjs and run-cache-e2e.mjs.

Run before the version commit: format:check, lint, build, typecheck, knip, release:prep --check, CLI version/help/guide, licenses:check in apps/mobile, and pnpm tarball inspection of the seven packages. Not run locally, left to CI: pnpm test, test:e2e, test:runtime and test:compat. Run after push: exact-commit CI, including the full Windows job, and the Release workflow's own gates.

1.18.0​

Stim Desktop and the Stim phone app ship through their own workflows; this release covers the six npm packages. Commits that only changed those apps are not listed.

Behaviour changes​

  • Automatic maintenance is on by default. maintenance.mode defaults to on where it was report. A background pass, started by CLI commands and stim status --watch when a check is due, now removes what stim gc --delete removes: orphaned workspace directories and stale cache registrations, build outputs of idle workspaces, least recently used build-cache and Metro entries over their caps, the age sweep, the Swift compilation cache (emptied whole, only below the hard free-disk floor), and linked worktrees whose branch is merged or whose pull request was merged or closed. Worktree removal follows gc's finished-worktree rules, including the gc.worktreeGraceMinutes grace period (default 120 minutes), so a worktree that changed recently is kept; gitignored files do not count as changes, and a worktree with uncommitted or untracked files, unpushed commits, a running dev server, a booted owned device or a maintenance.keep true pin is kept. Every action is logged (stim logs --source maintenance, stim status --json). Turn it off with stim settings set maintenance.mode off, or keep only the plan and the log with stim settings set maintenance.mode report; stim settings set maintenance.removeFinishedWorktrees false turns off only worktree removal. A scratch or test home (STIM_HOME set) and CI force it off unless STIM_MAINTENANCE is set, even over a configured maintenance.mode. 888d4f45b, b925c4639
  • stim-server setup approval prompts default to Yes. Each per-grant question reads [Y/n]. Enter, y and yes approve; n, no and any other answer decline. Keys typed ahead before the prompt appears are discarded, so an Enter pressed earlier does not approve a grant. --yes and the no-terminal refusal are unchanged. 422f5ba35

New​

  • stim exports a programmatic API. import { createStim } from 'stim' gives a client with run, stop and diagnostics that call the same lifecycle operations as stim ios|android|macos|web, stim stop and stim logs, in a bundled worker, with typed results, StimError failures and AbortSignal cancellation. See the website's Programmatic API page. d90965a5b
  • Automatic maintenance acts under maintenance.mode on. New settings: maintenance.worktreeCheckMinutes (15), maintenance.sweepHours (24), maintenance.olderThanDays (7), maintenance.protectRecentHours (2), maintenance.removeFinishedWorktrees (true), the project setting maintenance.keep (pins a workspace so a pass never clears its outputs or removes its worktree) and caches.ccacheMaxGb (5). stim status --json maintenance data gains worktree and sweep entries in lastChecks, and plan actions carry dir, olderThanDays and check; lastPass.mode can be on with the bytes freed. stim status and stim gc print an "Auto maintenance (on)" line with the action count and space freed. Disk-driven steps stop once free space is back above the floor. b925c4639
  • --plan works with ios.remote or android.remote set to auto or a Mac name. Instead of refusing with STIM_BAD_ARG, the plan asks that Mac for its device (a read-only probe, nothing reserved) and reads the architecture or ABI from it. With auto it repeats the placement decision a run would make now. --json gains placement, for example on janics-mac-mini or this Mac; auto may use janics-mac-mini, and the plain output prints a placement line. eas, proxy and a Mac that does not answer still refuse with STIM_BAD_ARG. e0edecf04
  • build.offer reports memory (@stim-cli/server, @stim-cli/core). The capacity block gains memoryUsedBytes (Activity Monitor's Memory Used) and memoryTotalBytes; they are omitted when the worker cannot read its memory, and absent from an older stim-server. stim doctor --json carries them for each remote Mac. 2a6af4023
  • stim macos builds record their launch time and compile step count. environments[].builds.macos in stim status --json includes launch in phases once the launch step finishes, and compileSteps, SwiftPM's step total. 494cd2d54
  • stim doctor --json gives the finding for a directory that is not a React Native or Expo app the code not-an-app. c9b8f12ce

Fixes​

  • stim worktree warm moves a carried ios/Pods to the worktree's path, so the first stim ios skips pod install. When Podfile.lock and the carried Pods/Manifest.lock differ only by checksums of podspecs that embed the source checkout's path (the precompiled ExpoModulesCore), warm rewrites the source path in Target Support Files, Pods.xcodeproj, Local Podspecs and absolute symlinks and makes Manifest.lock match. When the locks already match but Pods still name the source path, warm applies the same rewrite. If carried node_modules do not match the worktree's lockfile, another file still names the source path, or the move fails, stim ios still runs pod install: when the locks matched, warm deletes Pods/Manifest.lock to force it, and when they differed, the differing Manifest.lock stays. Warm never edits Podfile.lock. 82ee9c46c, 33797014c
  • stim ios lists simulators once for the budget gate, the capacity check and device setup, and lists them again only after a budget reclaim, instead of once for each. a76467c6e
  • The CocoaPods version that stim doctor, a build offload and build.offer compare is read in the project's Ruby environment (its .ruby-version, with a UTF-8 locale), the one pod install uses, instead of the machine's default Ruby. build.offer takes an optional rubyVersion, and the worker caches its toolchain report for 60 seconds per Ruby version. 05ebc115d
  • stim-server (@stim-cli/server) answers a client's hello with its host health probe running alongside the identity check, not after it, and reads tailscale status without blocking the event loop in serve, pair and service. 93a418c2d
  • The hosted relay waits 20 seconds, where it waited 10, for a hosted Mac to answer a request. When it does not, the error reads "the host was busy or slow: it did not answer METHOD within 20 seconds", where METHOD is the request's method name. 93a418c2d
  • Phone push notifications are quieter. stim-server notifies "Agent stopped" once per workspace per run, where a run ends when the workspace goes idle, and notifies at most one needs-attention item per workspace per run, which starts over when the workspace stops being idle or has no item left. A pull request that became ready or merged keeps its own notification. In the phone app, Work started defaults to Off. 30195c49e

Docs​

  • The website and stim guide cover maintenance.mode, the programmatic API, the --plan placement field, build.offer memory fields, the stim macos status fields, and warm's Pods move.
  • stim guide tutorial step titles and the website use title case for Desktop and phone screen names (Agent Actions, App Logs, Live View and Control). 1cd98eaae

QA​

Pre-tag QA is omitted by the maintainer's explicit decision on 2026-10-08 ("release now"). The maintainer QAs right after the release, as with 1.17.2. This deviates from RELEASE.md section 3, which has no waiver for a stable version.

Omitted before the tag, all eight rows of the matrix (node scripts/release-qa-matrix.mjs v1.17.2 --until 502eb0f7b; 338 changed paths, 75 mapped to a row, 0 unclassified): loop, logs, caches, real-repository, remote-provider, release-build, android-flavor and launch-evidence, plus the native suites run-native-e2e.mjs and run-cache-e2e.mjs.

Run before the version commit: format:check, lint, build, typecheck, knip, release:prep --check, CLI version/help/guide, licenses:check in apps/mobile, and pnpm tarball inspection of the six packages. Not run locally, left to CI by the maintainer's decision of 2026-10-08: pnpm test, test:e2e, test:runtime and test:compat. Run after push: exact-commit CI, including the full Windows job, and the Release workflow's own gates.

1.17.2​

Stim Desktop and the Stim phone app ship through their own workflows; this release covers the six npm packages. Commits that only changed those apps are not listed.

New​

  • stim-server --loopback-only listens on 127.0.0.1 only and refuses any request that names a remote peer (X-Forwarded-For, as tailscale serve adds), so phones and other Macs cannot connect. /health reports loopbackOnly: true, and the flag applies only to serving, not to subcommands. The next Stim Desktop release starts its own server with it and requires stim-server 1.17.2. 5dbd21e9c
  • stim doctor --json reports a lapsed state and expiresAt for build and device-host requests to remote Macs. A pending entry carries expiresAt, when the request lapses; lapsed means the request expired before approval, while revoked means the remote Mac refused or revoked this Mac. stim doctor --fix asks again on a lapsed request. 55a59ddce

Fixes​

  • stim-server setup tells apart Tailscale not installed (a download link), the Tailscale app turned off (turn it on from the menu bar icon), and a CLI-only install that is logged out (tailscale up), where it used to say only "Start Tailscale on this Mac". 2db7176c9
  • stim-server service update --from <dir> installs the .tgz packages of a client's build. It no longer asks the registry to vouch for Stim's own packages, so an unpublished version installs, and it still checks the registry signature of every dependency they bring from npm. 55a59ddce
  • When npm audit signatures fails, the error includes npm's own text: its JSON error summary, or its stderr after "did not print a JSON report:". 55a59ddce
  • stim doctor names a tool that is missing on one side of a toolchain comparison, such as "no CocoaPods there, CocoaPods 1.16.2 here", where it printed "CocoaPods null there". 55a59ddce
  • The hosted iOS and Android device status validators in @stim-cli/core accept a status without a placement reason. 27b518a23

Docs​

  • CLI text, stim guide topics and the website call a running workspace "active" where they said "live", including the stim status memory line. 116af6ee8
  • The website's remote Macs and settings pages and stim guide cover the lapsed state, expiresAt and service update --from. 55a59ddce
  • The website's remote Macs page describes the three Tailscale answers of stim-server setup. 2db7176c9
  • stim guide settings names the Desktop path "Remote Macs > Running here" where it said "Phones > Hosted here". 20333b70e

QA​

Pre-tag QA is omitted by the maintainer's explicit decision on 2026-10-08 ("Lets release before qa actually"). The maintainer QAs the Remote Mac pairing flow right after the release. This deviates from RELEASE.md section 3, which has no waiver for a stable version.

Omitted before the tag, all eight rows of the matrix (node scripts/release-qa-matrix.mjs v1.17.1 --until <head>; 149 changed paths, 18 mapped to a row, 0 unclassified): loop, logs, caches, real-repository, remote-provider, release-build, android-flavor and launch-evidence, plus the native suites run-native-e2e.mjs and run-cache-e2e.mjs.

Run before the version commit: format:check, lint, build, typecheck, knip, pnpm test, test:e2e (99 passed), test:runtime, pnpm run test:compat install-from.compat.test.ts (passed; covers service update --from), CLI version/help/guide, and pnpm tarball inspection of the six packages. The rest of test:compat (iOS device and other native tool calls) was not run. Run after push: exact-commit CI, including the full Windows job, and the Release workflow's own gates.

1.17.1​

Stim Desktop and the Stim phone app ship through their own workflows; this release covers the six npm packages. Commits that only changed those apps are not listed.

Removed (breaking)​

Build machines and hosting machines are now remote Macs. Settings, flags and environment variables are renamed with no migration and no aliases: the old names are no longer read. 59f8172db, adca393d4

BeforeNow
offload.machines and hosting.machinesremote.machines (one list for build and device-host approvals)
offload.machineremote.build
--build-machine (ios, android, macos)--remote-build
STIM_OFFLOAD_MACHINESTIM_REMOTE_BUILD
offload.moderemote.buildMode
STIM_OFFLOAD_MODESTIM_REMOTE_BUILD_MODE
offload.workerRootserver.workerRoot
offload.gradleDaemonIdleMinutesserver.gradleDaemonIdleMinutes
offload.maxLoadPerCoreserver.maxLoadPerCore
stim doctor --json buildMachinesremoteMachines

doctor --fix now asks each machine in remote.machines for build and device-host approval, and drops a device-host finding whose title a build finding already reports.

The plain-text build summary of stim ios and stim android now reads "build placement" where it read "build machine".

Behavior changes​

  • ios.remote and android.remote are allowed at machine scope, so one setting on this Mac applies to every project. bff943280

New​

  • stim macos builds report phases and progress. status --json shows the build as environments[].build with platform macos: phases prepare, compile, install and launch, and during compile SwiftPM's [done / total] as detail.unit: "steps". Finished runs and their phase times are in environments[].builds.macos, and macOS builds are labeled in machine usage. 8c36483a0
  • stim-server setup prints concise output. It shows one running line per step and the result, and --verbose prints the full step detail. The running line is cleared before an error is printed. 6b21927d7
  • stim gc --delete --cache archived-logs:<id>, archived-recordings:<id> and archived-agent:<id> clear one kind of data from one archive and keep the archive's record. 449c76fa4

Fixes​

  • stim-server setup reads the host's Screen Recording and Accessibility permissions from the running service, so it no longer asks again for a permission that is already granted. 814a621f3
  • stim-server setup captures the login shell's environment in its own session, so an interactive shell no longer takes over the terminal's foreground process group and stops the command with SIGTTOU. 6b21927d7
  • The hosted agent driver's error includes the agent-device proxy's stderr when the proxy exits before it is ready. 1e0ba5276
  • The full Windows unit suite stays green: server and worktree-refresh regression tests are synchronized. Test-only change. 71a41d613

Docs​

  • stim guide topics, packages/server/README.md and the website pages follow the remote Mac rename, --verbose and the macOS build phases. 59f8172db, 6b21927d7, 8c36483a0

Migration notes​

  • Rename every setting, flag and environment variable in the table above in your project config, machine config, scripts and CI. Nothing reads the old names.
  • Merge hosting.machines into remote.machines. Run stim doctor --fix to request build and device-host approval from each listed Mac.
  • Move offload.workerRoot, offload.gradleDaemonIdleMinutes and offload.maxLoadPerCore to server.* on the Mac that runs stim-server.
  • Tools that read stim doctor --json must read remoteMachines instead of buildMachines.

QA​

Pre-tag QA waived by the maintainer's coordinator on 2026-10-07. Not run before the version commit: the section 3 native and manual QA matrix, and test:compat (native compatibility). Run before the version commit: the section 2 pre-flight apart from test:compat, and pnpm tarball inspection. Run after push: exact-release-commit CI, including the full Windows job dispatched on the release branch, and the Release workflow's own gates.

1.17.0​

Stim Desktop and the Stim phone app ship through their own workflows; this release covers the six npm packages. Commits that only changed those apps are not listed.

Behavior changes​

  • devices.idleShutdownMinutes now defaults to 30 (it was 0, never). The workspace supervisor shuts down an owned simulator or emulator that has been idle for 30 minutes. Set it to 0 to keep the old behavior. a783d296c
  • New devices.reclaimIdleMinutes setting, default 10. A run that is waiting for a device slot reclaims the slot of an owned device that has been idle that long, shutting that device down. 0 never reclaims. ff7831db6
  • concurrency.maxDevices queues instead of refusing. At the cap, stim ios and stim android on owned devices now wait for a slot in first-come order across the Stim home (600 seconds by default). --wait <seconds> bounds the wait, and --no-wait (or --wait 0) refuses at once as before. A workspace's own live device skips the queue, remote targets do not join it, and stim stop cancels a queued run. 947ffea70
  • --remote auto is no longer refused. stim ios --remote auto and stim android --remote auto (and ios.remote / android.remote set to auto) run on an approved hosting Mac when this Mac is full or busy, and locally otherwise. offload.saturationLoad is also the busy threshold for this placement. 4e298d801
  • worktree remove guardrails. It refuses, even with --force, a folder a stim-server service runs from (STIM_WORKTREE_SERVICE); reinstall or uninstall the service first. Run from a subfolder of a source checkout, it reclaims only the project at or above that folder and the projects nested under it, and refuses when none is registered there. e6a057d20, a3baff816
  • Hosted devices are parked. A hosting Mac parks a client's iOS simulator or Android emulator on stop, within pool.iosParkedMax and pool.androidParkedMax, and the same client adopts it on its next run. Another client never adopts it. Before this release a stopped hosted device was deleted. 96244088f
  • android.remote accepts the same values as ios.remote: eas, proxy, auto or a tailnet machine name. 4e298d801

New​

Hosted Android

  • stim android --remote <machine> runs an owned Android emulator on an approved hosting Mac, as the iOS and macOS hosting paths do. ad373a6d9
  • Hosted Android uploads only the files that changed, hands over an Android build the same Mac ran for the client, and pulls logcat back for stim logs. 1936627c3
  • Agents drive a hosted Android emulator with agent-device, and Desktop and the phone app view and control it through stim-server's relay. ecc07f158, e51dccf1f
  • A host counts its own devices in hosted admission. d3099010f

Hosted parking and the hosting Mac

  • Hosted simulators and emulators are parked on stop and adopted by the same client; a parked device keeps its upload store, so adoption uploads only changed files. stim gc lists and deletes parked hosted devices. 96244088f, 43395b64a, ef79fce38
  • stim-server lists the hosted sessions on the hosting Mac and lets that Mac's person stop them. f3bfff5e2

Device slots and capacity

  • status --json carries build.waitingFor for build and device slot waits, and stats records capacityWaits and deviceSlotWaitMs. 947ffea70
  • stats and status record device capacity refusals, so Desktop can suggest a hosted simulator when the device limit is hit. d9718cfc6
  • Concurrent boots hold concurrency.maxDevices: the count and the boot record share one lock, so parallel worktrees no longer exceed the cap. de96761b5

Setup, guides and status

  • stim guide tutorial teaches the first run in a sample app, and status reports the tutorial marker. b4f24c6ae, e661190d4
  • stim-server setup (shipped in 1.16.0) is unchanged in the packages. Desktop's Add build machine wizard and Discovery prompts, which ship with Stim Desktop, build on it, and stim guide settings tells agents they never run stim-server setup or edit offload.* themselves.
  • Native macOS agent actions show in stim logs. a6958a216

Archives

  • stim-server serves an archived workspace's build history and recordings. 2ee0d97e2

Fixes​

  • Hosted Android logs stop following exited app PIDs. 7254a46cf
  • A skipped cache lookup no longer reports an outcome in status. 32700c487
  • Android CAS wrappers run under the Node Stim runs on. 13ab59ac6
  • Archives of gone worktrees keep their branch and pull request, and a source-checkout or non-git reclaim archives the workspaces it removes. 699de22f5, b0c933929
  • Teardown reports agent-device's own refusal when it cannot list sessions. 80a69c19f
  • Compilation-cache prefix mappings are quoted for paths with spaces. 93b44e092
  • Simulators are listed once between device setup and the boot check, which shortens stim ios. 40c236f04
  • The full Windows unit suite passes again. The Windows Xcode mapping expectations are corrected, the kept-agent fixture waits for its child to exit, and hosted Android and process-group tests that Windows cannot run (POSIX fixtures, process-group signaling) are skipped there. Test-only changes. c5ca280b3, 53a6045a8, c7b464d3f

Docs​

  • website/docs/commands.md, settings.md, owned-devices.md, remote-machines.md, worktrees.md and the phone app pages follow the changes above. 947ffea70, 84b6b8719, 49e9e5ea5

Migration notes​

  • If you rely on owned devices staying booted for more than 30 idle minutes, set devices.idleShutdownMinutes to a larger value, or 0.
  • A run that waits for a device slot may now shut down another workspace's owned device that has been idle for devices.reclaimIdleMinutes. Set it to 0 to disable.
  • Scripts that expected stim ios or stim android to fail immediately at the concurrency.maxDevices cap should pass --no-wait.

QA​

Pre-tag QA waived by the maintainer on 2026-10-07 (asked whether to release with no QA beyond CI, as for 1.16.0, the maintainer answered "Lets do it"). Not run before the version commit: the section 3 native and manual QA matrix, and test:compat (native compatibility). Run before the version commit: the full section 2 pre-flight apart from test:compat (format:check, lint, build, typecheck, knip, unit tests, test:e2e, test:runtime, CLI help, guide and version), and pnpm tarball inspection. Run after push: exact-release-commit CI, including the full Windows job dispatched on the release branch, and the Release workflow's own gates.

1.16.0​

Stim Desktop and the Stim phone app ship through their own workflows; this release covers the six npm packages. Commits that only changed those apps are not listed.

New​

Remote device hosting

  • stim ios --remote <machine> runs an owned iOS simulator on an approved hosting Mac, as stim macos --remote and the Android hosting path already do. It uploads only the app files that changed, hands over an iOS build the same Mac ran for the client, and pulls the hosted app's native logs back for stim logs. 83ad52f, 785a19f
  • Desktop and the phone app view and control a hosted iOS simulator through stim-server's relay. fae6cd2
  • Agents drive a hosted iOS simulator with agent-device. 14f1715

Setting up stim-server

  • stim-server setup walks a Mac through becoming a stim-server host. stim-server issues setup tickets on access requests and serves a node-bound setup journal route; stim doctor --fix sends STIM_ACCESS_TICKET with its access requests, accepts only server-format tickets, and reports the host's permissions. e958eea, cf587dd, 034a6da

Workspaces, status and maintenance

  • stim worktree remove keeps a removed workspace as an archived workspace, and stim-server serves its archived logs, agent actions and replays. gc has archived scopes. ddd8543, 9c05416
  • A report-only automatic maintenance engine in the CLI. b4c9926
  • stats, gc and Desktop Storage report agent-device and SwiftPM cache disk use. daeafa5
  • status reports agentDevice.stateDir, and agent-device state lives in the workspace. 44e7875
  • status --json carries completed step durations and cache hit or miss for a build. af5f3de
  • Run placements record slotWaitMs, and doctor adds a note on macOS when a Mac is a candidate for offloading builds. aa848119

Fixes​

  • stim ios and stim android approve every URL scheme the app declares on owned simulators. ac39538
  • stim-server service install waits for the old stim-server to release its port and fails when the new job exits. 3fa960b
  • Hosted devices are deleted when a hosted session stops, and stopped sessions are retired at server start. 85612bb
  • Old compiled helper builds are pruned. 5bd3bdc
  • @stim-cli/core accepts Windows absolute paths in a hosted macOS remoteConfig; it rejected them before. The full Windows unit suite passes again. f87580a
  • status shows every platform an app supports. 9244fe5

Docs​

  • website/docs/commands.md, settings.md and owned-devices.md follow the changes above. af5f3de, 3fa960b, 85612bb

QA​

Pre-tag QA waived by the maintainer on 2026-10-06 ("Publish it, no qa needed can do regular release"). Not run before the version commit: the section 3 native and manual QA matrix, and test:compat (native compatibility). Run before the version commit: the full section 2 pre-flight (format:check, lint, build, typecheck, knip, unit tests, test:e2e, test:runtime, CLI help, guide and version), and pnpm tarball inspection. Run after push: exact-release-commit CI and the Release workflow's own gates.

1.15.0​

Stim Desktop and the Stim phone app ship through their own workflows; this release covers the six npm packages. Commits that only changed those apps are not listed.

New​

Builds on another Mac

  • Offload builds to a paired build machine. stim ios places simulator Debug builds, stim android emulator debug builds and stim macos SwiftPM Debug builds on a Mac that has granted this one build access, and installs the returned artifact under the post-mutation cache key. Any failure builds locally. offload.mode is auto (build here while a build slot is free and the load is under offload.maxLoadPerCore, otherwise offload), force or off. e433902, c5f3514, 4c6542d
  • stim-server has a build capability, granted only on the worker, per client, by approving a build request with stim-server devices grant <id> --build. A client names its build machines in offload.machines, doctor --fix requests access and pins the worker's tailnet node, and the token goes only to that node. A build client never gets read or control, and every method except hello now needs read. eeadd14
  • Placement is load-aware (offload.maxLoadPerCore), reports the machine's capacity in doctor, and records where each compiling build ran and why in status, build history and stats. offload.machine, or --build-machine on ios, android and macos, names the one machine to require, with no local fallback, or local to always build here. A worker holds one of its own build slots for the whole offloaded build, and a client moves to the next machine when one refuses or fails to sync. d62bc69, 0522c97, eb41678, fc27bae
  • An offloaded build survives a dropped connection: the worker keeps the job for 5 minutes and the client reconnects for up to 3 minutes to reattach before building locally. The Gradle daemon stays warm on the worker for offload.gradleDaemonIdleMinutes (default 30). offload.workerRoot moves the worker's checkouts. 4ad0f8a, ad307fc
  • stim-server service install|status|uninstall manages a per-user LaunchAgent for stim-server, service update --release <version>|--from <dir> and service rollback switch builds, and an approved Mac can update the service over the tailnet. On a hosting Mac the service runs under a signed and notarized Stim Host app that owns its macOS permissions; install downloads the pinned release, checks its digest and team signature, and installs ~/Applications/Stim Host.app. a6a8746, 8710cf9, 1bf3791, 58ae393, 387b202, 28b3566

Native macOS apps

  • stim macos builds an owned Swift Package executable in Debug, launches an isolated development bundle signed ad hoc, and shows its window in Stim Desktop and the phone app. macos.product and macos.infoPlist select the product and development plist, macos.arguments passes launch arguments, and macos.resources and macos.assetCatalog stage resources and a compiled asset catalog before signing. It refuses with STIM_BAD_ARG when macos.product is unset. 038a8ed, 3fd1587, 9f9b4d0, 1bf6e18
  • stim macos --remote <machine> runs the app on an approved hosting Mac named in hosting.machines, streams and controls it from Desktop and the phone, copies its logs to the client for stim logs, and reports a session the host stopped in stim status. Viewing and Control follow the app's front window or a pinned window. An offloaded macOS build is handed to the hosted session on the same Mac. 9a2b135, 3db30ee, 316ee42, 8341d3d, 422a3a1, 3c2ff56, 6e4e82c
  • Agents drive a hosted macOS app through agent-device macos-app leases; the adapter starts only when the daemon advertises that backend, and STIM_AGENT_DEVICE_BIN names the agent-device binary. 38da60b
  • Workspace GC reclaims owned macOS Swift build outputs. 5465786

Remote device hosting

  • A Mac can host owned iOS simulator and Android emulator sessions for another approved Mac. Hosting is approved separately from build access, configured with hosting.machines, and reserves, installs and launches the app, bridges a development app to the client's private Metro, and streams and controls the session. f402f4e, c3ea73b, 116cc4d, 52b1fa1, d1fa435, 1a17b08, be09108
  • metro.tunnel accepts a tailnet-only tailscale option. da7eaf4

Watching and recording devices

  • stim-server records device screens for replay and serves range, markers, seek and live replay to clients. recording.enabled (default true) turns it off and deletes the footage, and gc cleans it. 74cfc23, a752880
  • stim-server streams a USB-cabled physical iPhone (view only), and streams and controls a leased physical Android phone over adb with a vendored scrcpy server, and the owned Chrome page from stim web as platform web. 2d794fd, eddd653, cd0fba9, 1166314
  • stim web records an agent's clicks, typing, key presses and scrolls on the owned page as agent_action records and tracks in-app route changes. 1abc436
  • stim-server serves read-only workspace diffs and machine.details (the gc dry run, stats and build machine state) to paired phones. dba7f7c, d0e46c4

Status and notifications

  • stim status --json reports more: stage, appPresence, gitChip and the cause of a failed run; disk use, metro.bundle, build.detail and build.missReason for a running build, each worktree's pull request, and 10-minute CPU and memory history; warming and ready phases; each workspace's measured physical-footprint memory; the coding-agent sessions working in it (ended sessions stay for 3 days); leased physical devices; the owned page's target and DevTools drivers; and activity.recent. Most fields are optional. 666651e, a268816, 9aac0cc, 6e6d663, 9c845b7, 62868fe, 09133a1, 01a80d5
  • stim-server pushes what a person overseeing agents needs: started, stuck, looping, finished, machine and control notifications, each at the level the phone chose (Alert, Silent or Off), with a notification history served to phones. A build that waits names the workspace it waits on. 0d87710, 786397a, 0584287, 8105f25, add11ae
  • worktree warm, start, ios, android and web print a Stim Desktop link to the workspace on stderr when Desktop is installed, and add it to --json as links.desktop. e00c0fa

Devices, settings and cleanup

  • metro.port pins a workspace's Metro port. 7a41544
  • devices.idleShutdownMinutes (default 0, off) shuts down idle owned simulators and emulators. 27dc326
  • A parked simulator or emulator keeps its apps and data. Adoption removes other third-party apps and clears the adopting app's data, and skips the install after verifying the installed bytes. gc --cache parked --delete erases parked devices. 057d906
  • ios.deviceType, ios.runtime and android.systemImage read the machine settings layer. An uninstalled model or runtime names the layer it came from. ef88892
  • stim ios --device-type works on the EAS remote backend. 6e345dc
  • gc reports and reclaims idle watchman, Gradle and Kotlin daemon memory, reports which caches a scoped gc --delete would empty, and tells caches that share a name apart. 8224491, 8cdf499, 87f7788
  • status detects automation tools from one registry and adds Argent. 1d8be14

Fixes​

  • stim and stim-server refuse to start on a Node older than the engines floor, exiting 1 with STIM_NODE_UNSUPPORTED, instead of crashing mid-command when a version manager picks a project's older Node. d8a77e5
  • Owned AVDs are created and deleted in the root the emulator reads. avdmanager ignored ANDROID_AVD_HOME when the directory was missing, and deleted from XDG_CONFIG_HOME. 3549cea
  • An owned AVD's console queries get at least 1 second each, so a slow earlier call no longer reports a running emulator as not detected. An adopted emulator gets the 60 second bundle window of a new one. da5dddb, 67e2992
  • A Release JS swap refreshes the expo-updates embedded manifest id and commit time on iOS and Android, and recompiles the React Native asset catalog on iOS. Before, an update downloaded after a swapped build launched instead of the injected bundle. 60c5082, faabad4
  • stim ios --remote builds one simulator arch. 9c93b98
  • stim android restores a lost Metro adb reverse on reload and reports it in status. faf34ba
  • stim reload on web reports the page it reached, not the launch URL. fd7a59c
  • A slot's Metro errors stay visible in logs --errors after another slot relaunches. ANSI escape codes are stripped from captured Metro messages. 0b2ef4e, 3fe8690
  • logs --follow ends when its reader is gone. 7741824
  • On iOS, adoption keeps the Expo dev menu off after it clears a dev client's data, skips redundant dev-client simulator writes, and carries scheme approvals through park and adoption, which saves about 4 seconds per adoption. e0bd29b, f71f89d, 644a166, aa86906
  • worktree warm skips .jj, and worktree remove ignores watchman cookies when it checks for dirty work. 60c4505, 5a4edb6
  • gc no longer fails with E2BIG computing merge state on a branch thousands of commits ahead, prunes status cache entries of removed worktrees, and records a stopped Metro's watchman root removal. a4f8486, c34ecf0, ab70e90
  • status accepts a pull request opened from the checkout's own fork and reports self-healing device states and a port another app holds on an idle workspace as info notes. fab89dc, 4312601, 9c71372
  • stim android --system-image and --device-profile refuse with STIM_BAD_ARG on a remote run, where the remote backend picks its own device. cd798b5
  • A build that waits for a device reports the wait as its own phase, and build estimates count one run once. c29950a, d6c31e0
  • stim-server keeps re-checking Tailscale after start, stays up when pruning footage fails, and listens before it reads the Stim home. ee9e595, cfdba80, 14028db

Docs​

  • stim guide agent makes the worktree workflow the default and tells the agent to share the Desktop link once. New website pages cover native macOS apps, and the commands, settings, owned devices and Desktop pages describe the features above. 81a652a, 038a8ed

Migration notes​

  • stim-server pushes changed. The failed-build, log-error, low-disk, stopped-app and slow-build pushes of 1.14.0 are replaced by started, stuck, looping, finished, machine and control notifications, and every category starts Silent, so a paired phone gets no alert until it opts in.
  • A stim-server client that holds only build can no longer read status: every method except hello needs read, and a build client never gets read or control.
  • stim and stim-server exit 1 with STIM_NODE_UNSUPPORTED on a Node below 22.12.0. Run them under a supported Node, for example with nvm exec.
  • A single-arch iOS simulator build is cached under a key that ends in its arch, in the CLI and in @stim-cli/expo-build-cache. The first iOS simulator build after upgrading misses an entry stored by 1.14.0.
  • A parked simulator or emulator keeps its apps, and adoption removes other third-party apps and clears the adopting app's data. Before, parking erased the device.

QA​

The maintainer waived the pre-tag native and manual QA matrix for 1.15.0 on 2026-10-05. This release uses the full local pre-flight and no matrix rows.

Passed on this candidate (the six package.json files at 1.15.0, on be2bc07):

  • pnpm install --frozen-lockfile, format:check, lint, build, typecheck and knip.
  • pnpm test: 237 files, 6740 tests passed, 18 skipped.
  • pnpm run test:e2e: 99 tests passed.
  • pnpm run test:runtime, run under Node 22.22.2. The Node 22.12.0 floor job runs in CI.
  • pnpm run test:compat: 5 files, 26 tests passed with the Old iPhone connected.
  • release:prep --check, stim --help, stim guide agent, stim --version and stim-server --version at 1.15.0.
  • Tarball inspection of all six packages: each carries its README and LICENSE, every version is 1.15.0, and no workspace: range survives the pnpm pack.

Rows of node scripts/release-qa-matrix.mjs v1.14.0 that were not run (all eight were required):

  • loop (expo and bare on ios and android): one variant was started, expo-ios. It built, installed and launched the app and verified a bundle request from its Metro, then failed during later assertions with could not inspect ps: spawnSync ps ETIMEDOUT while the host load average was above 140. It was not retried. The other three variants were not run.
  • caches (ios and android, both Metro modes): not run.
  • logs, release-build and launch-evidence (ios and android): not run.
  • real-repository, remote-provider and android-flavor: not run.

The mapping also reports five paths it does not classify: .worktreeexclude and four files under packaging/store/.

Merged after the version commit and covered by the exact-release-commit CI only: the full Windows unit suite fixes with path-containment hardening (#2586), the stim macos --host to --remote rename (#2594), and two Stim Desktop fixes (#2588, #2596).

Retained after push: exact-release-commit CI, including the full Windows job.

1.14.0​

New​

  • @stim-cli/server pushes attention notifications to paired phones through the Expo push service: a failed build, new log errors, low disk, a stopped app and a slow build. A phone registers an Expo push token with push.register and removes it with push.unregister or by revoking its pairing. Pushes follow the phone's attention rules: once per occurrence, log errors after the count holds, at most 20 an hour per phone, and more than three at once as one summary. What is already wrong when the server starts or a phone registers does not push. Each push carries the workspace title or Mac name, a short reason and the workspace path, never logs. The Stim phone app, shipped separately, registers for them. 5ff39e2, acb19bd
  • A Metro stop records its cause. The supervisor_stopped log line carries a stop field and status reports metro.lastStop: the Stim command that stopped it (stim stop, worktree remove, gc, a budget reclaim naming the reclaiming workspace, start --reset-cache), a signal no Stim command requested, or the dev server's own exit. A supervisor that vanished without recording anything, usually after SIGKILL, a crash or a restart, is logged as supervisor_vanished by the next supervisor or stim stop. 3e69cf4
  • stim stop --slot web closes only the workspace's owned Chrome from stim web and keeps its profile, Metro, devices and port. 2fed395

Fixes​

  • Launch verification works in a workspace with named device slots. A Metro bundle delivery now counts for the slot it can be attributed to: an iOS simulator's request is matched to the launched process through lsof (clientPid on the bundle_response_* records), and an Android or physical-device delivery counts while no other slot of the same platform is running. Before, every delivery was ignored once any slot existed, so Android reported UNVERIFIED and an iOS slot bundling after its bundle loaded. When a running sibling slot makes a delivery unattributable, the launch stays unverified and names the device and stim logs --slot <slot> --source device. 76f2bd5
  • stim ios and stim android restart an app that is already running, like Xcode's Run, with simctl terminate on a simulator and am force-stop on an emulator or --device phone. A repeat run with an unchanged install no longer ends launch UNVERIFIED because the old process kept running and requested no bundle. The launch line reads restarted running app (was pid N). 175fbed
  • status --watch --json exits when its reader is gone, instead of running on, with its watchers and adb track-devices child, on a quiet workspace that never wrote again. 6cf7843
  • stim stop --slot <name> refuses with STIM_BAD_ARG and lists the workspace's slots when it has no such slot, instead of printing Stopped: port kept and exiting 0. 2fed395
  • With web.viewport at desktop, the stim web page is 1280x800 as documented. Chrome's own UI had made it 1280x713. e24bf01
  • The --device-type and --runtime mismatch refusals carry their real remedy in --json: reap the simulator with stim worktree remove or stim gc --delete, or pass --slot <name> to create the requested one beside it. The remedy said to run stim doctor. d70788f
  • stim ios --device-type and --runtime refuse with STIM_BAD_ARG on a remote run (--remote or ios.remote), where the remote backend picks its own device, instead of being silently ignored. The ios.deviceType and ios.runtime settings do not refuse there. 07d41fb
  • stim ports get and stim web no longer print Port N already in use, trying next... for each busy port they skip. 67bdb61

Migration notes​

  • web is refused as a device slot name by ios, android, device lock|unlock and logs, because stop --slot web now means the owned Chrome. A device slot already recorded as web still loads, but only plain stim stop or worktree remove reach it.
  • stim stop --slot <name> with a slot the workspace has not recorded now fails with STIM_BAD_ARG and stops nothing.
  • stim ios and stim android restart an app that is already running, so in-app state from the previous session does not survive a run. If stopping the running app fails, the launch is refused.
  • status --watch --json exits when the reader of its stdout goes away. On Linux, a detached watch whose stdout is a pipe or socket, such as stim status --watch --json | cat &, also exits when the shell that started it exits.
  • stim ios --remote with --device-type or --runtime now refuses. Drop the flags for a remote run.
  • The --device-type mismatch refusal now also offers --slot <name>, and the remedy text is on the remedy line instead of in the message.
  • While a phone is registered for push, stim-server keeps its stim status --watch --json child running with no client connected and reads the disks every minute.

QA​

No-QA stable release explicitly authorized by the maintainer on 2026-09-26, following the 1.10.1 through 1.13.0 precedent. Omitted locally before the version commit: format:check, lint, typecheck, knip, unit tests, test:e2e, test:runtime, test:compat, and the pre-tag native/manual matrix. Retained before the version commit: candidate build, CLI and server version, CLI help, guide agent, web --help, and tarball inspection of all six packages. Retained after push: exact-release-commit CI.

Each included change merged with passing CI. Pull requests record their own real-tool checks.

1.13.0​

New​

  • stim web opens the workspace's web app in a Stim-owned headless Chrome with its own profile. The page's console, failed requests and browser errors go to the workspace logs, launched reports whether the page loaded, status lists the page, and stim reload web, stop, gc and worktree remove reload or tear it down. The web.url, web.ignoreCertificateErrors and web.viewport settings choose the page, accept a dev server's self-signed certificate in the owned profile, and pick a desktop or phone viewport. stim web never starts a dev server: start Metro with stim start, or your own server on a port from stim ports get, first. With nothing serving the page it reports launched: "unverified" and prints the command that starts one. stim guide web documents it. 126da43, b101f65, 881e34b, 3b99318, a938f5a
    • In a monorepo, a web package that depends on neither react-native nor expo, such as a Vite app in apps/web, resolves to the one Stim app registered in the same Git worktree. ports, web, settings, logs, reload, stop and status run from that package act on the app's workspace and name it on stderr. 2926912, 78dbd40
    • A failed page load names its cause: a self-signed certificate, an https:// URL on a plain HTTP server or the reverse, with the stim settings command that fixes it, or a page that did not finish (20 seconds with no answer, 60 once a server other than Metro answered). Page failures appear in logs --errors. cead9a9
    • logs --errors and the status error count report the page's errors from its latest load only: stim web, stim reload, or a reload or navigation the page makes itself. A page load does not hide the native app's errors, and an iOS or Android launch does not hide the page's. 0a510f0
  • gc removes linked worktrees whose pull request was merged or closed, checked with gh pr list, in addition to worktrees whose branch is merged. stim worktree remove accepts a merged pull request whose head is or contains HEAD. gc --json reports pullRequest and pullRequestUnknown for each linked worktree. 2c552c1
  • status --json lists each platform's last 10 runs in builds.<platform>, with their result, slot, configuration, cache key, cache outcome and phase timings. 325bf8d
  • A failed build whose compiler reported errors records up to five diagnostics (file, line, column, message) in lastBuilds and the run result. 1b7f43f
  • status reports whether the workspace's app process runs on each owned booted simulator or emulator, as app: { id, state } in --json and <id> running or <id> not running on the device line. f19e135
  • status --json has a top-level machine section that attributes current CPU and resident memory to one owner per process: each booted simulator and emulator, each workspace's Metro, build and stim web Chrome, stim-server, and shared machine-wide services. status --watch --json rereads it every 15 seconds while something runs. An environment's memoryMb stays a fixed estimate. 227d6af
  • gc --json lists what --delete and --idle did to each entry in results, and a dry run lists every simulator, AVD, iOS runtime and Android system image on the machine, with its owner and the command that removes it, in inventory. Stim never runs those commands. e5c2f9e, 216e014, 762c0a2
  • gc reports each workspace's log size, and gc --delete trims a Metro, client or device log over 16 MiB, written before the rotation cap, to its newest 8 MiB in workspaces not in use. 68507ca
  • gc lists created-devices.json simulators that simctl no longer shows and AVD names with no registration or data in any AVD root, and gc --delete forgets them after checking again under the ledger lock. It also sizes orphaned and stale simulators. d46f51b, d664739, d5b05d0
  • stim doctor --platform android reports a missing Android SDK. When the SDK is found at ANDROID_HOME, ANDROID_SDK_ROOT or the default location and neither variable is set, Stim passes its path to Gradle as ANDROID_HOME. fbb30af
  • @stim-cli/server:
    • The iPhone Duo streams as H.264 through the frames helper, following its lit panel, with the posture in each video packet. 36773c9

Performance​

  • status and status --watch read each state file once per computation, reuse a worktree's git summary until its git files change, reuse the simulator poller's listing (polled every 5 seconds), and refresh only the log facts on a log append. cbef9ab, ccf1ccb, aec1017, 020d18d
  • gc lists devices once and asks GitHub once per repository. beec18a
  • @stim-cli/server serializes each status push once for every phone, and keeps a frame source alive briefly after its last listener leaves. c4a43f4, ebb874f

Fixes​

  • gc deletes only devices this Stim home created. A stim- name or an AVD config.ini no longer proves ownership, because every STIM_HOME writes the same ones; gc lists another home's devices with the command that deletes them. 3e04bf9
  • A build is never stored under a cache key its inputs no longer match. Both platforms fingerprint again after the compile; when the app config, a config plugin or another source changed while the build ran, Stim installs the build but stores nothing locally, in the cache provider or remotely. 4e51a7d
  • ios --runtime refuses when the slot's simulator runs another iOS version, instead of booting it. 48f1195
  • A launch whose app process is gone when the bundle wait ends fails as FATAL, "the app process exited", with the device log's errors, instead of reporting bundling or unverified. 66ac203
  • stim android refuses with STIM_BUILD_FAILED before Gradle runs when there is no SDK at the resolved path and no android/local.properties. fbb30af
  • Emulator teardown verifies that every emulator process for the AVD exits. A hung emulator gets SIGTERM, then SIGKILL, once its identity is verified; one that survives fails the teardown and keeps its record. 6c840a5
  • A bare project resolves its packages through its own node_modules chain, not NODE_PATH, so a pnpm bin shim no longer resolves another workspace member's expo. 40d49a4
  • logs --errors keeps an Expo error's stack past interleaved bundle response records, and attaches its code frame in --json and --follow. 91c3cb6, 86bdfa5
  • iOS "Synchronous URL loading" diagnostics are recorded as warnings, so a launch does not count them as app errors. 9726693
  • The iOS boot memory-pressure warning repeats only when the level changes. 519b2f6
  • status detects the framework of an app registered only through a named port or stim web. 4b5c2a4
  • Stim starts Stim Desktop without the command's STIM_HOME, so it reads the same home as when you open it yourself. 9158517
  • On Windows, two log files no longer share a log error index key. 10fb09d
  • @stim-cli/core: a failed lock marker write, such as ENOSPC, no longer leaves an empty lock directory that blocks later commands, and the lock timeout refusal prints the command that removes an unowned directory. fbcc163
  • @stim-cli/server: a stim command or control tool call ends at its timeout even while a grandchild process holds its output. 254366e, 1784586

Docs​

  • A monorepo recipe for a Vite web package with a base path and HTTPS. a73170d

Migration notes​

  • stim web does not start Metro for Expo web. Run stim start, then stim web. The stim web --json payload has no devServer field.
  • lastActivityAt in status is rounded down to the minute.
  • status --watch applies a log append no sooner than 15 seconds after the previous refresh, so a log-driven update, such as the error count or device activity, can lag by up to 15 seconds.
  • gc no longer deletes a device only because its name or AVD config.ini matches Stim's format. A device created before the ledger that this home's config does not record in the device pool or as owned by a project is listed under unverifiedDevices with its delete command; run that command yourself to remove it.
  • ios --runtime refuses when the workspace's simulator runs another iOS version. Remove the simulator with stim worktree remove or stim gc --delete, or pass --slot <name> to create one beside it.
  • gc --worktrees and the idle sweep also remove clean worktrees whose pull request was merged or closed. A closed pull request whose remote branch is gone keeps a worktree with local commits.
  • A launch whose app exited before the bundle wait ended now fails instead of returning launched: "unverified" or "bundling".
  • A build whose inputs changed while it ran is installed but not cached, so the next run with the same inputs compiles again.
  • gc --delete trims Metro, client and device logs over 16 MiB to their newest 8 MiB.
  • iOS "Synchronous URL loading" diagnostics no longer appear in logs --errors.
  • stim android refuses before Gradle when no Android SDK is found. Set ANDROID_HOME or write sdk.dir into android/local.properties.
  • In a monorepo, ports, settings, logs, reload, stop and status run from a package that depends on neither react-native nor expo act on the one Stim app registered in the same Git worktree. ports stop or stop there stops the app's ports or workspace. A package that already holds ports keeps them until you release them.
  • Stim starts Stim Desktop without the command's STIM_HOME, so a simulator booted under another STIM_HOME does not appear in it. Pass --simulator-app xcode to see it.

QA​

No-QA stable release explicitly authorized by the maintainer on 2026-09-26. Omitted locally before the version commit: format:check, lint, typecheck, knip, unit tests, test:e2e, test:runtime, test:compat, and the pre-tag native/manual matrix. Retained before the version commit: candidate build, CLI and server version, CLI help, guide agent, web --help, and tarball inspection of all six packages. Retained after push: exact-release-commit CI.

Each included change merged with passing CI. Pull requests record their own real-tool checks.

1.12.0​

New​

  • iosSimulatorApp and androidEmulatorApp default to stim-desktop on macOS while Stim Desktop is installed, found by its bundle id through Launch Services. Without it the defaults stay xcode and emulator, and a set value always wins. stim settings shows such a default as (default: Stim Desktop installed) and --json adds defaultReason. 5ebf6ae
  • ios --plan and android --plan report why they predict a cache miss, as missReason in the plan payload. ae99e71
  • AVDs Stim creates get a hardware keyboard (hw.keyboard=yes) unless android.avdConfig or android.avdConfigFile sets it. A parked AVD from before this default gets the key when it is adopted. f0f550f
  • stim status --json reports structured issues (code, severity, message, remedy) beside warnings, and an owned emulator's deviceProfile. 52bfcb7, 8e072a3
  • The machine setting gc.worktreeGraceMinutes (default 120, env STIM_GC_WORKTREE_GRACE_MINUTES) keeps a merged or idle worktree from gc until that long after its last activity. A booted owned device or an unexpired device lease also keeps a worktree. 4a23ae1
  • @stim-cli/server:
    • Paired devices with control can drive a Stim-owned simulator or emulator with touch, text and hardware buttons in a control session held under a stim device lock lease. c1e6cb4
    • input.rotate and input.posture rotate a device, fold an iPhone Duo and move a foldable emulator's hinge. d64d5ee
    • build.plan runs one plan at a time per workspace. 80ca4de

Fixes​

  • App id detection evaluates a dynamic app.config.* through expo config, so a non-literal bundleIdentifier or package is found before prebuild. b22a317
  • android --device-profile refuses a profile such as pixel_fold on a system image that cannot boot it, before building or creating an AVD. 5dd08fd
  • stim stop closes only agent-device sessions that belong to this workspace's stopped device. 1b1ffb3
  • stim status no longer warns about an idle workspace's shut-down emulator or a supervisor record proven stale. stop --slot forgets that slot's launch record. 52bfcb7
  • A remote EAS start waits for another workspace's session start to finish, with a notice, instead of failing with STIM_LOCK_TIMEOUT after 4 minutes. 3f26806
  • @stim-cli/server:
    • A logs.query or logs.subscribe whose slot or grep cannot be a stim logs argument gets bad-request instead of closing the connection or stopping the server. 85ff2f8
    • Simulator input from the frames helper keeps working on Xcode 27 after a CoreDevice client such as Device Hub has touched the simulator. 370fa2f

Docs​

  • Quitting Device Hub on Xcode 27 shuts down every booted simulator on the machine, not only the ones it opened. ff21e53

Migration notes​

  • With Stim Desktop installed and neither viewer setting set, owned simulators open in Stim Desktop and owned emulators boot without a window. To keep the device windows, run stim settings set iosSimulatorApp xcode and stim settings set androidEmulatorApp emulator.

QA​

No-QA stable release explicitly authorized by the maintainer on 2026-09-26. Omitted locally before the version commit: format:check, lint, typecheck, knip, unit tests, test:e2e, test:runtime, test:compat, and the pre-tag native/manual matrix. Retained before the version commit: candidate build, CLI and server version, guide agent, and tarball inspection of all six packages. Retained after push: exact-release-commit CI.

Each included change merged with passing CI. Pull requests record their own real-tool checks. #1394 was checked against the real Launch Services lookup and a running Stim Desktop with a scratch STIM_HOME.

1.11.0​

New​

  • stim status --json reports each worktree's git state: changed and untracked files, ahead and behind its upstream, and whether the branch is merged. The merged check is cached per worktree, and paths macOS protects are skipped so status never triggers a privacy prompt. #1230
  • A cache miss says why. ios and android print one line naming the changed native inputs, for example cache miss: native dependency added: expo-clipboard, and status --json reports lastBuilds.<platform>.missReason. #1228
  • stim status --json reports each platform's last build, and ios --plan / android --plan predict whether the next build hits the cache. #1178
  • android --device-profile <id> and the android.deviceProfile setting choose the owned AVD's hardware profile (default pixel_6), such as pixel_fold or pixel_tablet. Parked emulators are only reused for the same profile. #1268
  • The machine setting androidEmulatorApp (emulator or stim-desktop) boots new owned emulators without a window on macOS and shows them in Stim Desktop. #1292
  • stim stop --slot default stops only the workspace's default device. #1176
  • stim device lock can lock the workspace's own simulator or emulator. #1261
  • stim logs --source agent lists agent-device actions (taps, typing, screenshots) as log records, attributed to the device they drove. #1179
  • stim-server:
    • Paired devices granted the control scope (stim-server pair --control or stim-server devices grant <id> --control) can run reload and stop. Every action is audited, and stim-server log prints the audit. #1187
    • machine.get reports the Mac's memory used and CPU usage, and machine.history returns the last hour of samples. #1206, #1253, #1267
    • build.plan predicts the next build. #1189
    • Device frames come from a native helper, compiled on first use, instead of repeated simctl screenshots. Clients that decode H.264 receive hardware-encoded video. #1254, #1265
    • Frames report the lit iPhone Duo panel and the posture of iPhone Duo and Android foldables. #1234, #1277
    • The status payload also exposes the home folder and the per-volume machine usage used by the phone app. #1182

Fixes​

  • stim stop interrupts an ios or android build it would leave nothing to deploy to, instead of waiting silently for it to finish. A billable remote session is ended without waiting on the build, and any command that waits on the workspace says what it is waiting for. #1300
  • ios --remote eas and android --remote eas create the remote session after the local build is ready, so a long build no longer leaves the EAS host's daemon idle until it exits. #1216
  • stim ios with iosSimulatorApp set to stim-desktop opens Stim Desktop in the background instead of bringing it to the front. #1256

QA​

No-QA stable release explicitly authorized by the maintainer on 2026-09-25. Omitted locally before the version commit: format:check, lint, typecheck, knip, unit tests, test:e2e, test:runtime, test:compat, and the pre-tag native/manual matrix. Retained before the version commit: candidate build, CLI and server version, guide agent, and tarball inspection of all six packages. Retained after push: exact-release-commit CI.

Each included change merged with passing CI. Pull requests record their own real-tool checks: throwaway simulators and emulators, scratch STIM_HOME runs, and a real stim stop interrupting a local iOS compile for #1300. The @stim-cli/server tarball now carries the Swift sources of its frame helper, which the server compiles on first use.

1.10.1​

Fixes​

  • stim-server pairs through the tailscale serve route on port 7443 by default and refuses to pair while any serve route is exposed through Tailscale Funnel. #1168
  • stim-server reports a slow or timed-out device frame capture as delayed instead of failed. #1170
  • The default iOS simulator is a numbered iPhone on the newest runtime that has one, instead of a special model such as iPhone Duo; ios.deviceType and --device-type are unaffected. #1174

QA​

No-QA patch release explicitly authorized by the maintainer on 2026-09-25. Omitted locally before the version commit: format:check, lint, typecheck, knip, unit tests, test:e2e, test:runtime, test:compat, and the pre-tag native/manual matrix. Retained before the version commit: candidate build, CLI and server version, guide agent, and tarball inspection of all six packages. Retained after push: exact-release-commit CI.

Each included change merged with passing CI. #1174 was checked against the real simctl list runtimes output on a Mac whose newest runtime offers only iPhone Duo.

1.10.0​

New​

  • stim ios and stim android start the workspace's dev server when it is not running (including after an idle stop), through the same path as stim start; they no longer refuse with STIM_NO_METRO. Release configurations, *Release variants, and --no-metro-check still skip Metro. JSON results report devServer: { started, reason } when a run started it. #1163
  • Disk and memory budgets: before ios, android, and start build or boot anything, Stim checks free disk on the workspace and $STIM_HOME volumes and committed memory. Below budget.minFreeDiskGb (default 20) it reclaims in a safe order (idle devices, idle dev servers, build outputs of idle workspaces, cache entries unused for 14 days) and reports each step; below budget.hardFloorDiskGb (default 5) it refuses with STIM_LOW_DISK. doctor reports the budget. #1132
  • Idle dev servers stop themselves after metro.idleStopMinutes (default 60, 0 disables) with no bundle request, client log, or Stim command, never during a build, a driven device, a device lock, or an open bundle request; status shows stopped (idle). #1138, #1144
  • gc reports every Stim-managed worktree with a reason, and gc --delete removes worktrees whose branch was merged (merge, rebase, or squash) with no uncommitted or unpushed work. With --older-than N, gc --delete now also removes only parked devices parked at least N days ago instead of emptying the pool. #1133, #1161
  • Device activity: stim status reports each device as driven (by agent-device, a stim device lock, or a driver process), active, idle, or unknown; gc lists idle devices and gc --idle <duration> shuts them down. #1101
  • @stim-cli/server (stim-server), a new read-only server for remote clients: QR pairing with single-use tokens and revocable device tokens bound to a Tailscale node, and live status, logs, stats, settings, and device screenshots over a WebSocket. It powers the Stim mobile app (not yet published). #1139, #1145, #1150
  • @stim-cli/core exposes a state entry point with the read side of Stim's state (paths, config and workspace state, settings registry, ledgers, logs, payload types). #1131
  • Parked simulators are erased and parked emulators have their user data wiped, shrinking each parked device from about 2 GB to a few MB; adoption now cold-boots and reinstalls. #1140
  • worktree warm warns when the carried node_modules were installed from a different lockfile (npm, pnpm, Yarn Classic, Yarn Berry), with the install command to run. #1126
  • guide agent tells agents to leave the machine clean, and the website documents a Claude Code SessionEnd hook that runs stim stop. #1128

Fixes​

  • Regenerate a stale, gitignored ios/ or android/ before a cache-miss Expo build, so an app built from an old native dir is never stored under the new fingerprint; read the iOS scheme and Android package after regeneration. #1142, #1148
  • gc and every boot, shutdown, park, and delete trust only devices Stim created (a new creation ledger, or the exact legacy naming format), never any stim-* name. #1105
  • Remote iOS installs on EAS Simulator sessions work again (agent-device refused them under the proxy lease); a failure after a billable session started names the session and stim stop. #1127
  • Refuse early when eas-cli is too old: below 21.6.0 for --remote eas, below 18.9.0 for --eas-profile. #1095, #1129
  • Remote Android launches report the public Metro origin and skip adb-only steps. #1130
  • stop and gc --idle verify a simulator actually shut down and report failures with a remedy; parking waits for the shutdown to settle before erasing. #1158, #1156
  • status lists worktrees with no environment for every registered repository, not only the current one, and no longer lists a worktree whose inner package has an environment. #1106
  • settings --json reports environment-provided values with their registry type. #1152
  • The release smoke test waits for npm to serve new versions before failing. #1109

QA​

No-QA stable release explicitly authorized by the maintainer on 2026-09-25, although this release changes package boundaries (a sixth package, @stim-cli/server) and the Release workflow, which RELEASE.md assigns to the full lane. Omitted locally before the version commit: format:check, lint, typecheck, knip, unit tests, test:e2e, test:runtime, test:compat, and the pre-tag native/manual matrix. Retained before the version commit: candidate build, CLI version/help/guide, and tarball inspection of all six packages. Retained after push: exact-release-commit CI, including its format, lint, build, typecheck, knip, unit, cross-platform e2e, and published-runtime jobs.

Most included changes were reviewed by a fresh agent (and several by Codex) and merged with passing CI; some were merged without a fresh review at the maintainer's request. Pull requests record their own real-tool checks (throwaway simulators and emulators, scratch STIM_HOME runs, real EAS Simulator sessions for #1127 and #1130). The Release workflow publishes @stim-cli/server for the first time; its name was reserved with a 0.0.0 placeholder and a trusted publisher.

1.9.0​

New​

  • stim status --watch streams status: with --json it prints one complete payload per line, immediately and then on every change, and never repeats an identical payload. It reacts to Stim state changes, adb track-devices, and simulator state, and exits cleanly on SIGINT, SIGTERM, or a closed stdout. #1080
  • stim status reports the build in progress for each workspace: platform, slot, phase (prepare, cache-lookup, wait, prebuild, pods, compile, install, launch), elapsed time, and an estimate from the median of comparable previous runs. A build whose process is gone shows as stale. #1078
  • stim settings reads and writes settings by scope: stim settings --json lists every setting's effective value, origin layer, and per-layer values; get, set, and unset take --scope machine|workspace|repo|committed. One registry now defines every setting, and the package ships a JSON Schema (dist/settings.schema.json) that .stim.json can reference with "$schema". #1079
  • stim status lists each workspace's remote EAS Simulator sessions with their preview URL; plain status marks them billable. #1077
  • stim gc --json prints the report as JSON, and gc reclaims workspace build outputs, orphaned workspace directories, and finished worktrees. #1043, #1021
  • iosSimulatorApp: "stim-desktop" and stim ios --simulator-app stim-desktop open owned simulators in Stim Desktop instead of Device Hub. #1057
  • stim ios --device installs on Wi-Fi-paired iPhones. #1020
  • Owned Android emulators boot with their gRPC endpoint enabled on 127.0.0.1 (console port + 3000) with token authentication, so tools can stream frames from headless emulators. #1064

Performance​

  • stim status is about 20 times faster (about 3.3 s to 0.18 s on a machine with 21 workspaces): error counts come from a per-workspace index that reads only newly appended log bytes, and device, git, and port lookups run once per call and in parallel. The first run after upgrading builds the index. #1081
  • Workspace NDJSON logs are capped, and the timeline is read once per query. #995

Fixes​

  • Decode iOS provisioning profiles without touching the keychain. #1036
  • Use the agent-device proxy lease for remote devices, and name remote sessions after the workspace. #1029, #1037
  • Keep the Metro port reserved while its supervisor is live, keep other projects' registry entries when reusing a port, and stop or keep the port of an Expo child orphaned by its supervisor. #984, #983, #1000
  • Keep build cache entries intact during concurrent publishes and swaps, keep build claims held while an orphaned build tool runs, and report a remote cache upload with no destination as failed. #991, #990, #985
  • gc never trims a declared cache that contains a protected root, reports cache entries it cannot remove, and exits 1 when --delete cannot delete or empty an entry. #989, #1009, #994
  • Refuse git-blocked worktree removal before reclaiming devices. #987
  • Bound device tool calls that could hang every workspace, and bound the xcodebuild and pod install transcripts in memory. #986, #1004
  • Key Expo Android debug builds on the device ABI, and claim the console port for the Android boot fallback. #992, #1002
  • logs --follow starts after startup crash capture. #998
  • Refuse outside a project with STIM_NO_PROJECT from every command; refuse relative STIM_HOME and cache path overrides, a config.json that is not a JSON object, and unknown remote, tunnel, Android compilerCache, and pch choices. #1001, #1005, #999, #1012, #1006
  • Keep cache.options from reaching a provider that another settings layer names, and remove the committed .stim.json caches setting. #997, #1017
  • Record doctor runs only for app projects, and print escaped errors without a Node stack trace. #993, #988
  • Settle Windows access denials for vanishing claims, and close the Windows follow-ups from the #871 review. #1034, #1027

Docs​

  • The static skill points agents at npx stim when stim is not on PATH; READMEs are trimmed to landing pages; website gaps are filled and the first troubleshooting codes grouped. #1042, #1013, #1007
  • "Worktrees with no environment" in status is documented as linked worktrees Stim has not registered, which worktree warm does not change. #1065

Stim Desktop​

The repository now contains Stim Desktop (apps/desktop), a macOS app for supervising Stim workspaces: live iOS and Android frames with take-over input, projects, logs, build progress, resource usage, settings, and cleanup actions through the CLI. It is not part of the npm packages and is not distributed yet; build it with apps/desktop/scripts/bundle.sh. #1047

QA​

No-QA stable release explicitly authorized by the maintainer on 2026-09-25. Omitted locally before the version commit: format:check, lint, typecheck, knip, unit tests, test:e2e, test:runtime, test:compat, and the pre-tag native/manual matrix, which scripts/release-qa-matrix.mjs v1.8.0 marks fully required (8 of 8 rows). Retained before the version commit: candidate build, CLI version/help/guide, and tarball inspection. Retained after push: exact-release-commit CI, including its format, lint, build, typecheck, knip, unit, cross-platform e2e, and published-runtime jobs.

Unlike earlier no-QA releases, most changes from #1047 onward were merged without a review and before their CI finished, at the maintainer's request. Main CI was restored to green in #1086 and #1084 before this release. Individual pull requests record their own real-tool checks (throwaway simulators and emulators, a scratch Expo app build for build progress, a scratch STIM_HOME); the native E2E suites were not run against this version. A real EAS Simulator session was not exercised: #1090 tracks an early refusal for eas-cli versions without simulator commands.

1.8.0​

New​

  • Choose Siniulator or Apple's viewer with the machine-wide iosSimulatorApp preference. Override it for one local launch with stim ios --simulator-app siniulator or --simulator-app xcode, including an already running owned simulator. The override does not change the saved preference. 072ddc7c5, 5600b9e26

Fixes​

  • Open owned simulators with the selected Xcode's Device Hub on Xcode 27, with Simulator retained for older Xcode versions. Window opening remains best effort. 642606173
  • Verify Cloudflare Quick Tunnels and remote Metro reachability when the system DNS resolver still has a stale answer for a new tunnel hostname. 9c9ac0d8e

Docs​

  • Restore the documentation site's shared theme context so its theme switch and production build work together. 2951dbcf3

QA​

No-QA stable release explicitly authorized on 2026-09-21. This release contains the same product changes as 1.8.0-rc.1. Omitted locally before the version commit: format:check, lint, typecheck, knip, unit tests, test:e2e, test:runtime, test:compat, and the pre-tag native/manual matrix. Retained before the version commit: candidate build, CLI version/help/guide, and tarball inspection. Retained after push: exact-release-commit CI, including its format, lint, build, typecheck, knip, unit, cross-platform e2e, and published-runtime jobs.

Included changes were reviewed and merged with passing CI in #942, #945, #947, #949, and #952. Their existing validation includes a live Quick Tunnel HTTP/WebSocket check, the docs production build, and Siniulator displaying the existing iPhone Duo with the per-launch override. These observations were made before the version bump; the native/manual matrix was not rerun for this stable release.

The published tarball README is verified. Empty npm registry README metadata is tracked separately in #953.

1.8.0-rc.1​

New​

  • Choose Siniulator or Apple's viewer with the machine-wide iosSimulatorApp preference. Override it for one local launch with stim ios --simulator-app siniulator or --simulator-app xcode, including an already running owned simulator. The override does not change the saved preference. 072ddc7c5, 5600b9e26

Fixes​

  • Open owned simulators with the selected Xcode's Device Hub on Xcode 27, with Simulator retained for older Xcode versions. Window opening remains best effort. 642606173
  • Verify Cloudflare Quick Tunnels and remote Metro reachability when the system DNS resolver still has a stale answer for a new tunnel hostname. 9c9ac0d8e

Docs​

  • Restore the documentation site's shared theme context so its theme switch and production build work together. 2951dbcf3

QA​

Expedited RC authorized on 2026-09-21. Omitted locally before the version commit: format:check, lint, typecheck, knip, unit tests, test:e2e, test:runtime, test:compat, and the pre-tag native/manual matrix. Retained before the version commit: candidate build, CLI version/help/guide, and tarball inspection. Retained after push: exact-release-commit CI, including its format, lint, build, typecheck, knip, unit, cross-platform e2e, and published-runtime jobs.

Included changes were reviewed and merged with passing CI in #942, #945, #947, #949, and #952. Their existing validation includes a live Quick Tunnel HTTP/WebSocket check, the docs production build, and Siniulator displaying the existing iPhone Duo with the per-launch override. These observations were made before the version bump; the native/manual matrix was not rerun for this candidate.

1.7.0​

New​

  • The Android development loop runs on Windows, including owned emulators, Metro supervision, native builds, installation, logs, worktrees, and caches. 183f05b8e, f5a6df25d, f700814d9, 156117682
  • stim ios --remote eas --eas-profile <profile> works from a Windows host using a completed EAS development build and a remote simulator. f1753ee8c, 9a8aaff2f

Removed (breaking)​

  • Node 20 support ended. Running Stim requires Node 22.12.0 or later. 87285918d

Fixes​

  • Windows Android runs retain the selected emulator ABI across repeated slots and wait for the emulator's package manager before installing. 3b5aea298
  • Windows claim acquisition yields between transient publish retries, avoiding a spurious refusal when other processes repeatedly remove and recreate the claim directory. 8e8ac8e10
  • Windows worktree removal handles long paths, and claim acquisition treats an access denial for a disappearing claim as a retryable absence. 7855bbe46, a5ff2b896
  • Windows release APK swaps and named Metro ports work without Unix cp, mv, zip, or lsof commands. 23c13116e
  • Windows Android builds report insufficient native path room before Gradle runs. Supervisor startup no longer inherits a pipe that keeps the parent waiting, and emulator cleanup handles the crash-consent process. 156117682
  • Bare React Native launch remedies no longer suggest opening Expo's development-server picker. 589bfe032
  • Android emulator debug launches route Metro through the existing ADB reverse connection, so a cached install can load its bundle on a newly started emulator. 095797d7f
  • iOS simulator launches recover when CoreSimulator reports no process handle even though the app started, after confirming the app was absent before launch and running afterward. 18c7cae7f
  • Clean worktree removal recognizes @-prefixed branch names when checking for unique commits. 2800d7279

Docs​

  • Restored the root Stim README after it was replaced by the video-project README. 288547ef8
  • Documented Windows host requirements, Android path limits, and the EAS Simulator route for iOS. 631986f02
  • Published refusal-code remedies on the troubleshooting page. dab1187c5

Migration notes​

Upgrade to Node 22.12.0 or later before installing this release. Repository development requires Node 22.18.0 or later. On Windows, use a short project path or a subst drive for Android native builds; the requirements page lists the host tools and the remote iOS path.

1.6.0​

New​

  • Swift joins the shared Xcode compilation cache automatically. When xcrun swift reports Swift 6.4 or newer (Xcode 27) and the project's react-native is 0.87 or newer, stim ios passes SWIFT_ENABLE_COMPILE_CACHE=YES with Swift prefix mappings for the workspace root and DerivedData, so Swift output crosses worktrees like clang output already did. Older toolchains, where prefix-mapped batches crashed swift-frontend, and React Native before 0.87, which disables the explicit modules Swift caching requires, stay off. The pre-build cache note names the outcome (Swift on, Swift on, unmapped, Swift off, 6.3 < 6.4, Swift off, react-native 0.86 < 0.87), and the post-build line counts any Swift targets Xcode still excluded for building without explicit modules. optimizations.ios.swiftCompilationCache is unset by default; true forces the cache on any toolchain, unmapped below Swift 6.4, and false keeps it off. 2ece2d34d

Migration notes​

Preserve existing Stim state when upgrading. The artifact cache key is unchanged for an unset or false swiftCompilationCache; an explicit true keeps its own key as before. On a Swift 6.4 toolchain with React Native 0.87 or newer, the first stim ios after upgrading recompiles Swift targets once to populate the shared cache.

QA​

Native compatibility, simulator/device suites, and manual QA are omitted at the user's request on 2026-09-19. The omission is not a native-compatibility pass. The Swift caching path was verified by hand on 2026-09-18 with Xcode 27.0 on an Expo SDK 58 app across two worktrees, but not by the native e2e suites, whose iOS lane has not yet run on a Swift 6.4 runner.

Retained checks: formatting, lint, candidate build, typecheck, knip, unit tests, end-to-end tests, published-runtime checks, candidate CLI version/help/guide, inspection of all five packed tarballs, and CI on the exact release commit before tagging.

1.5.0​

New​

  • stim guide and stim guide agent open with a STATUS block when doctor is due for a platform in this project, meaning it never ran here, ran more than seven days ago, or ran under another Stim version, and when a newer Stim release exists. Doctor records each run per platform, and the release check reads the npm registry at most once a day; STIM_NO_UPDATE_CHECK disables it. 56decdaf3
  • stim start --reset-cache restarts the owned Metro with Metro's own reset, clearing every store in the app's Metro config including the shared transform store, instead of persisting a cache generation in workspace state. The Expo SDK 54 requirement for a reset is gone. 01f290095

Fixes​

  • Support bare-repository worktree layouts, where every checkout is a linked worktree beside a bare .git. Warm, remove, doctor, storage diagnostics, and status resolve the source checkout as the worktree on the bare repository's HEAD branch, and refuse with the exact Git command to run when HEAD is detached, its branch has no worktree, or that worktree was deleted. 6b5ad313c

Docs​

  • Website benchmark details: scenario picker keyed on the run variant, one full-width comparison card, a terminal clamped to the viewport, and a redesigned timeline toolbar with direct scrubbing. 515203498

Migration notes​

Doctor now writes to Stim state on every run, so a project appears in stim status after stim doctor alone. The --reset-cache workspace-state generation field is no longer read; existing state needs no change.

QA​

Native compatibility, simulator/device suites, and manual QA are omitted at the user's request on 2026-09-18. The omission is not a native-compatibility pass.

Retained checks: formatting, lint, candidate build, typecheck, knip, unit tests, end-to-end tests, published-runtime checks, candidate CLI version/help/guide, inspection of all five packed tarballs, and CI on the exact release commit before tagging.

1.4.0​

New​

  • Reserve named workspace ports for web and API servers with stim ports get <label>. List allocations with stim ports, stop listeners with stim ports stop [label], or release reservations without killing with stim ports release [label]. Stop supports --dry-run; worktree removal and GC reclaim named allocations. Metro remains separate. beeae86b8

Fixes​

  • Accept compatible EAS development builds using the CLI's current app.id field or legacy project.id, while retaining project-identity validation. e1b3c4c25 3b984be4a
  • Give iOS relaunch remedies the commands needed to restart an app. e80a01c08
  • Recover abandoned parked-device operation claims through process identity, while keeping live and unverifiable owners protected. d2037c688
  • Track the native process group during Android AVD creation so interrupted creation can recover once its writers are proven gone. Reserve AVDs outside short config transactions. 5255dff26 4b712ee54
  • Preserve directory lock ownership and recover core locks through the shared process-identity protocol. The core package owns the shared lock implementation. 8a4920418 fbef197ea

Docs​

  • Use generic web and API server examples throughout the named-port guides and website. 9759b4f9b

Migration notes​

Preserve existing Stim state when upgrading. Use the current CLI for cleanup after allocating named ports; older versions do not manage those allocations.

Named reservations do not hold sockets open. Pass the returned port to the server, use strict-port behavior when supported, and verify the bound port. ports stop may stop a listener outside the workspace, so reserve only services that workspace may stop.

QA​

Native compatibility, simulator/device suites, and manual QA are omitted at the user's request on 2026-09-14. This release also includes the unreleased fixes and internal refactors since 1.3.1; the omission is not a native-compatibility pass.

Retained checks: formatting, lint, candidate build, typecheck, knip, unit tests, end-to-end tests, published-runtime checks, candidate CLI version/help/guide, inspection of all five packed tarballs, and CI on the exact release commit before tagging.

1.3.1​

Fixes​

  • Give an Android emulator that remains alive under observed memory pressure one additional boot wait, and retain a bounded diagnostic window after the boot deadline. d7687383a6
  • Close matching local agent-device sessions when giving up owned simulators and emulators, with ownership checks and bounded cleanup. 0fdf4d3bec
  • Allow current worktree refreshes to overlap copies while retaining exclusive ownership for mutations and revalidating after lock escalation. 31839cb137
  • Require an ownership claim before copying warm state and provide actionable storage-permission and blocking-file remedies. 10c93ef708
  • Refuse log queries for workspaces without a timeline instead of reporting a misleading empty success, and suggest a matching registered descendant when available. 66d21f150a
  • Improve test isolation for parallel runs and external temporary directories, and stop cache-suite Gradle daemons before removing their homes. 5b5d1b5201 6d861b2d1c ac63380353 8d59b2d2c9

Docs​

  • Explain how to investigate React Native Test App fingerprint churn with narrowly scoped generated-file exclusions and cache parity checks. 8095bafa3f

Migration notes​

Preserve existing Stim state when upgrading. No state reset is required.

Log queries now return STIM_NO_PROJECT when the workspace has no timeline. Run the command from the intended workspace after starting or building with Stim. Existing empty timelines still return an empty successful result.

Worktree warming no longer copies without an ownership claim. If it returns STIM_CLAIM_UNAVAILABLE, follow the reported process-identity or storage remedy before retrying.

1.3.0​

New​

  • Run multiple simulators, emulators, and physical devices per workspace with --slot <name>. Reuse named assignments, inspect per-slot logs, and stop one slot while preserving Metro and sibling devices. There is no fixed slot count; configured caps and host resources still apply. c531b2ad0d
  • Download compatible EAS development builds with --eas-profile <name> for local or remote targets. Missing builds produce an explicit EAS build command; Stim does not start a cloud build automatically. 18613c83fe

Fixes​

  • Report local iOS boot progress and sampled memory pressure, retaining the highest observed pressure, unavailable readings, and long observation gaps with safe agent recovery guidance. Blocking CLI work can delay observations and timeout handling. fe33ce2f83
  • Prepare profiled native QA simulators sequentially before multi-device workloads, retaining cold-build and simultaneous-device checks and ownership state on preparation failures. Pool QA stops an unused workspace before the next boot. 7d5e18078a fbad065cd9
  • Probe iOS readiness using a simulator-native executable instead of a host macOS binary, while keeping the process-spawn timeout and failure handling. 7d2fca7758
  • Resolve the Android SDK emulator during native QA cleanup when it is not on PATH, retaining fatal enumeration errors so leaked devices cannot be hidden. dfac53fa0a

Docs​

  • Explain device slots on the website with a copyable phone/iPad/hardware testing prompt, command examples, cleanup behavior, and shared-Metro limitations. Feature changes now require corresponding website documentation and example updates. f6e566e54b
  • Document EAS development profiles and add an example agent prompt. b9b8570d25 9760e5524b

Migration notes​

Existing assignments remain in the default slot. Preserve ownership state when upgrading; do not delete it. Use a slot-aware CLI consistently while named assignments exist, since older versions cannot manage them.

Slots share Metro and compatible build caches. Shared Metro events cannot verify an individual slot's Debug launch, so it may be reported as unverified; inspect the intended device before claiming success. Reload remains platform-wide, and named remote sessions are unsupported. Physical-device signing and lease requirements still apply. The parked-device pool policy is unchanged.

QA​

All iOS and Android loop/cache jobs passed in the full native run. Its Expo pool timeout led to the idle-workspace fix; both corrected pool jobs then passed in the focused run, using the unchanged standard runners. Local bare/Expo iOS loop, cache, and pool suites passed, including three simultaneous simulator slots and concurrent build-cache races. The final combined source passed 4,332 unit tests (one skip), 87 end-to-end tests, build, type, lint, runtime, and website checks.

The release owner authorized reusing the completed feature QA rather than repeating it for the version-only release candidate. Packaging inspection, CLI version/help/guide checks, and exact-release-commit CI remain required before tagging. Physical-device installs and authenticated EAS downloads were not exercised in this campaign; their command and lease paths have regression coverage.

1.2.0​

New​

  • Native builds can warm Metro while compilation is still running, reducing the wait before the first development bundle. The optimization is enabled by default and can be configured or disabled through Stim's documented optimization settings. 8a7b70d8c

Fixes​

  • Android emulator recovery is more reliable. Stim can reclaim orphaned AVD data, recover an existing owned emulator, detect when an AVD restarts on a different serial, revalidate ownership before installation, restore port forwarding, and continue on the verified serial. Freshly created emulators also receive a longer first-bundle verification window. cba5f1fd0 748d0053e 750fc98aa 1704b15da
  • New Android emulators use an explicit high-density phone display instead of inheriting an unsuitable default. 28f4a1106
  • Stalled iOS simulator processes are detected earlier, and failure diagnostics report observed host memory pressure without attributing it to the app or claiming it caused the failure. doctor recommends the optional SimSlim setup, while Stim continues to require an explicit profile before changing simulator services. 62404f782
  • Local iOS discovery, boot, installation, Simulator opening, and SimSlim reconciliation now have bounded waits. SimSlim cleanup verifies the owned process identity, handles timeouts and terminal signals, and retains its claim when termination cannot be confirmed. 9f8097583
  • doctor no longer requires expo-dev-client; Expo projects can use Stim without that package when their workflow does not need a development client. ff786f1e6
  • doctor --fix changes the Claude Code sandbox allowance only when the corresponding diagnostic is present. 772b9b356
  • worktree warm skips Finder .DS_Store metadata and ignored .idea directories. 25889af6e 1ddaf82b8

Docs​

  • Agent guidance routes common situations to the matching versioned guide topic, and long waits point to the workflow guidance that explains diagnosis and recovery. d9adbf1cf 1b76f527b
  • Website release-note rendering fences command output that could otherwise be interpreted as page markup. 615800c52

QA​

The release owner authorized reusing the completed 1.2 QA campaign instead of repeating the native and manual matrix on the version-only candidate. This is a disclosed deviation from the stable-release gate in RELEASE.md; the exact candidate still receives the full local preflight, tarball inspection, and exact-release-commit CI before tagging.

The retained campaign exercised all four native loop rows and all four native cache rows across bare React Native and Expo on iOS and Android. All cache rows and both iOS loops passed. Both Android loops completed with clean teardown but recorded partial launch qualification because a warm invocation was unverified; later manual recovery showed the expected UI. Manual Expo iOS and bare Android work covered launch, UI, induced errors, structured logs, recovery, worktree isolation, and exact cleanup. The Expo Android check covered picker recovery, healthy UI, and a successful bundle response. No test device or automation session remains active.

The fixes merged after that frozen candidate have separate evidence:

  • The simulator-memory fix passed 4,219 unit tests, 84 end-to-end tests, final-head CI, two reviews, and a fresh owned iOS 26.5 Expo launch with real spawn and launch calls.
  • Android cold-start and serial-recovery fixes passed integrated static, runtime, unit, and end-to-end checks. A real API 36 emulator was restarted on a new serial; status detected it without mutating the registry, and rerunning restored forwarding and produced a verified launch. The exact emulator and worktree were removed.
  • The final iOS deadline and cancellation tree passed formatting, lint, build, type checking, unused-code checks, 4,248 unit tests with one existing skip, 84 end-to-end tests, runtime verification, two fresh reviews, and required CI. A fresh owned iPhone 17 / iOS 26.5 simulator completed discovery, boot, install, and a verified Expo launch with empty error logs. Controlled subprocess checks covered timeout, SIGINT, SIGTERM, immediate exit, descendant cleanup, and retained claims. The exact simulator, worktree, and test processes were removed. No SimSlim profile was applied and no OOM condition was induced.

Omitted at the release owner's direction: rerunning the pre-tag native and manual matrix after the version and release-note-only changes. Existing unresolved pool, authenticated remote-device, and physical-phone coverage are not claimed by this release.

1.1.0​

New​

  • stim worktree warm --refresh brings the source checkout up to date before copying from it: it fast-forwards that checkout to its upstream and installs when the lockfiles moved, so a warm worktree no longer inherits whatever happened to be sitting in the checkout that seeded it. It refuses a source checkout it cannot move (STIM_MAIN_DIRTY, STIM_MAIN_DETACHED, STIM_MAIN_DIVERGED), never switches a branch under another checkout, and warns when the checkout is not on the default branch, because every worktree seeded from there then carries that branch's dependencies. Opt-in: plain warm is unchanged. 97ede367f
  • One ownership-claim primitive now backs every lock held across a long operation, and build-lock, build-slots and workspace-process-lock were migrated onto it. Liveness comes from a process-identity record rather than a bare PID check, so a recycled PID can no longer make a dead claim look alive; a claim is built privately and renamed into place, so a half-built one is never observable; and a spawned installer is held by its process group, so a package manager's postinstall descendants keep the claim while they are still writing. A state the claim cannot resolve is refused with the path to remove, never silently reaped. a6252b9d7
  • worktree.defaultBranch in the repository-root .stim.json names the default branch when origin/HEAD is missing or wrong. 97ede367f
  • doctor reports the source checkout's fitness as a seed -- uncommitted changes, a detached HEAD, a rebase or merge in progress, divergence from upstream, and a branch that is not the default one -- each naming the refusal it predicts and the git line that clears it. 6d0d16bc0
  • doctor validates the machine config: a setting naming a path that no longer exists, a setting missing a companion it requires, a key Stim no longer reads, and a config that is not valid JSON. Previously these surfaced only when a command read them, and the error named the missing file rather than the setting that named it. e9d520468
  • A native build's compiler-cache statistics now survive the build that failed. --json failure payloads carry a new ccache key on Android and compilationCache on iOS, and every native build prints a cache progress line, failures included. 5197ff384

Fixes​

  • gc --delete could delete a build lock that was still held. It decided whether a lock was orphaned from a bare PID check, and that guard also passed for a claim record it could not parse -- so a corrupt record belonging to a running build was treated as garbage, leaving two workspaces free to compile the same app at once. It now surveys claims through the shared primitive, takes the set's own claim before removing anything, and leaves a claim it cannot resolve alone and reported. a6252b9d7
  • A STIM_HOME the filesystem will not accept writes to no longer stops a plain warm. macOS reports a recursive mkdir against a read-only mount as ENOENT, which is also what a claim set another process just removed gives, so the attempts were spent as if contended and the copy refused naming a contender that was never there. Such a store is now identified as missing rather than contended, and the copy degrades the way it did before claims existed. 68d9e2fe5
  • warm output names what it is talking about. The line reporting a wait attributed the waiter's own elapsed time to the holder's pid, and the line reporting an install named neither the tree it installed in nor the one it copied from. aa09f4015
  • A transient adb shell pm clear failure when adopting a pooled Android emulator is retried rather than failing the launch. The first launch on an emulator resumed from a snapshot could fail while its system services were still restarting, where the identical command succeeded seconds later. 3e2552ac5
  • A cleanup whose staging removal fails now releases the claim it took instead of keeping it. 95d2b7e3f
  • stim reload no longer kills the app it was asked to reload. It reopened the recorded development-client URL, which restarts rather than reloads; on Android that recreates MainActivity, and expo-dev-launcher asserts there that no React context exists yet, so a healthy dev-client app died on every stim reload android while the command reported success because it dispatched without observing the result. Reload now goes through the Metro websocket on both platforms. 35c6bab80
  • A hand-edited config.json no longer crashes a command with a raw TypeError. A missing projects container is adopted, the way a missing repos already was, and a container holding an array or a scalar is reported as STIM_CONFIG_CORRUPT with the file left untouched. That second shape was the quiet one: JSON.stringify drops a string key set on an array, so "projects": [] let a write return the record it appeared to have stored while nothing reached disk. 2efb9db84
  • Owned devices are now distinguished across worktrees. The default label combines the worktree directory and the app directory (pr6460-tlon-mobile), and an iOS name gains a suffix when two workspaces would otherwise collide, serialized under its own lock. Two worktrees of one repository used to compete for a single device named after the app directory alone. An existing owned iOS simulator is renamed when it is next reused. acc1d7c29
  • A CAS toolchain that is missing, unreadable, or unusable no longer refuses the Android build. Stim warns, names the setting and the file it came from, and falls back to ccache where ccache is available. A stale optimizations.android.casToolchain pointing at a deleted directory used to fail every stim android instantly with STIM_BAD_ARG, while doctor --platform android reported clean. e9d520468
  • A non-string optimizations.android.casToolchain no longer makes gc refuse. e9d520468
  • doctor no longer reports the source checkout's branch state to people who do not use worktrees. The source checkout is N commits behind fired unconditionally, so working in a single checkout on a feature branch read as a problem rather than as the job. Those findings are now gated on the repository actually having a linked worktree. Findings about the source checkout's dependencies, Pods and cold build output are not gated and still appear in a single checkout. 6d0d16bc0
  • Command start-up loads only the command being run. 9a23aebd7
  • A long run of benchmark-harness fixes: sessions no longer fail on a refusal that changed nothing, and runs pin their locale so native cache entries are reused.

Docs​

  • guide agent and guide lifecycle name the two ways of working -- a single checkout, and the worktree workflow where the source checkout is a seed that stays clean and current -- and say which rules belong to which. Previously the worktree workflow read as the only one, and its rules about the seed looked universal. 6d0d16bc0
  • Shipped text now calls the repository's primary working tree the source checkout rather than the main checkout. The old name read as the checkout of the main branch, and plenty of repositories have none -- a project whose default branch is develop got The main checkout is 3 commits behind origin/develop. No command, flag, settings key, or error code changed. 99de2b229

Migration notes​

  • Let a running build finish before upgrading. The build lock changed shape, so a 1.0.0 build already in flight and a 1.1.0 build started after the upgrade do not see each other's record and will both compile. Running two Stim versions against one STIM_HOME is not a supported configuration; this is only about the moment of the upgrade, and it clears as soon as the older processes exit.
  • Output that said "main checkout" now says "source checkout". Nothing executable changed: worktree.defaultBranch, worktree.exclude and every other settings key keep their names, and STIM_MAIN_DIRTY / STIM_MAIN_DETACHED / STIM_MAIN_DIVERGED keep theirs. A script matching on the old phrase in human output needs updating. So does one matching on doctor --json, whose finding titles, details and remedies are the same renamed strings; only the field names are untouched.
  • stim worktree warm is unchanged without --refresh, with one exception: it now refuses to copy after an install that did not finish, because copying a half-installed node_modules into a worktree produces a broken worktree silently. stim worktree warm --refresh reinstalls and clears it.
  • --refresh needs a clean source checkout on a branch that can fast-forward. In a worktree workflow that is what keeps the checkout worth copying; if you also work in it directly, omit the flag. It is best-effort about the network: a fetch that fails is reported and the refresh continues against what is already local, and a branch with no upstream is left where it is.
  • A machine whose optimizations.android.casToolchain has rotted keeps building. Stim warns, names the setting and its file, and uses ccache where available; doctor reports the same thing so it is findable before a build rather than during one. CAS itself is unchanged, and a valid toolchain is still used.

Known limitations​

Recorded in #696 rather than fixed, and documented in guide errors and guide lifecycle where a user would meet them:

  • A plain warm that cannot record a claim surveys the claim set and refuses if a refresh holds it, which closes the common case but not a refresh that starts after the copy is already running. Reaching it needs one process unable to write STIM_HOME while another can. That survey also only treats an unreadable .claim file as a blocker, so a refresh whose exclusive/ directory itself cannot be read is not seen.
  • A process-group id carries no identity, so a recycled one reads as alive. It holds a claim too long; it never frees a live writer.
  • An installer descendant that never exits makes --refresh wait, then refuse after five minutes naming the process group and the command to end it, keeping the claim.
  • STIM_HOME or its warm-locks directory existing as a plain file refuses with a remedy that names a path one level too deep.

QA​

Cut with the expedited preflight plus the full caches matrix, at the release owner's direction. RELEASE.md section 3 excludes a stable version from the expedited lane and asks for every applicable row on a minor release, so this is a disclosed deviation from the documented gate rather than a completion of it. What it does not omit any more is the cache evidence.

Why the cache rows specifically: this release migrates the single-flight build lock, the build-slot semaphore and the workspace process lock onto one primitive, and RELEASE.md names the caches row as the required evidence for "build, cache, fingerprint, Pods, Metro, single-flight, or gc behaviour". A lock change without that row is the one thing this release could not claim.

Run on the version commit:

  • pnpm install --frozen-lockfile, release:prep --check (5 packages at 1.1.0, 7 workspace: ranges), build, --version, --help, guide agent, and the tarball inspection.
  • format:check, lint, typecheck, knip, test, test:e2e.
  • node test/e2e/native/run-cache-e2e.mjs --framework expo --platform ios -- 7 pass, 1 skip, 0 fail, 0 missing in 327s. The skip is gradle-cache, which is Android-only.
  • node test/e2e/native/run-cache-e2e.mjs --framework expo --platform android -- 6 pass, 2 skip, 0 fail, 0 missing in 363s. The skips are xcode-cas and pods-reuse, both iOS-only.

Every check passes on the platform where it applies, on real compiles in throwaway workspaces.

Both rows were run on 0bbdead9c, the candidate before the final five commits -- aa09f4015, 3e2552ac5, 68d9e2fe5, 95d2b7e3f and e22636dd0, which are the four fixes listed above plus a docs change. They were not re-run against the tagged tree, at the release owner's direction. Three of the four touch code the suite exercises: the claim store's error classification, warm output wording, and Android pool adoption. What covers them instead is the unit and e2e suites, which did run on the tagged tree and include the tests those commits shipped with, plus exact-release-commit CI.

The evidence that bears directly on the lock migration is single-flight, on both platforms, with two workspaces racing one uncached fingerprint.

iOS, through xcodebuild:

wt3: cacheHit="local" waitedForBuild={"pid":37959,"ms":67452} durationMs=85630
wt4: cacheHit=false waitedForBuild=null durationMs=85659
both racers computed the same key: b75c50589e37f1b49ff92feca64e128fc05852b2-debug-sim

Android, through Gradle:

wt3: cacheHit="local" waitedForBuild={"pid":67582,"ms":23053} durationMs=67306
wt4: cacheHit=false waitedForBuild=null durationMs=63857
build waited 23.1s for .../e2e-cache-4's build -> installed from cache

One workspace compiled and the other waited on the claim and installed what it stored, on each platform. The surrounding checks cover the rest of what the migration touches: gradle-cache proves --build-cache reached the real argv with 651 new entries and 89 FROM-CACHE tasks in the second worktree; xcode-cas proves all five compilation-cache settings reached the real argv with 69% of the cold compile's CAS files reused by a never-compiled workspace; pods-reuse proves a carried, matching Pods skips pod install outright rather than merely running it quickly; and zero-config proves the source checkout was porcelain-clean afterwards with all four worktrees removed without --force.

gc-view reports every cache with a size and calls none of them garbage on both platforms. It does not reach gc --delete, so the repaired deletion guard -- the fix in this release that previously decided deletions from a bare PID check -- has unit coverage only.

Omitted: the pre-tag native loop rows and the manual field-test matrix in RELEASE.md section 3.

Retained after push: exact-release-commit CI, including its format, lint, build, typecheck, knip, unit, cross-platform e2e, and published-runtime jobs.

Three things the next person running this suite should know, each of which currently fails late rather than up front (#702). It needs ANDROID_HOME for the Android row, or the run spends 37s inside a Gradle build before reporting SDK location not found. It needs LANG/LC_ALL set to a UTF-8 locale, or pod install fails inside the iOS row on a unicode normalization error. And it needs no orphaned .avd directory for the AVD names it will claim, a state that is invisible to emulator -list-avds and therefore to Stim's own check (#710). The suite's own exit status is trustworthy: it exits 1 on a failed check, a missing check, and a fatal, including one raised by its own cleanup.

1.0.0​

New​

  • The CLI is now published as stim. All five packages move together to the first stable release, 1.0.0. 256516b0ff
  • Select a shared Xcode app scheme with ios --scheme <name>. Explicit schemes use separate build directories and cache keys, and resolve the actual product even when its name differs. 33ecb66ff9
  • Reset one app's Metro transforms and file-map state with start --reset-cache, preserving other apps and native build caches. 7cc5276586
  • Each monorepo app reads its own committed .stim.json beside its package manifest. App-relative profiles and cache-provider paths resolve from that directory. c9fb9568a4

Fixes​

  • Android's unverified-launch remedy uses the resolved launcher activity or dev-client URL to restart the app. Printed URLs preserve query parameters through both host and device shells. f2dd053af5

  • Android launch previews compact nested Metro error JSON into a readable module diagnosis, code frame and import stack. Previously unseen context is retained; logs and JSON preserve full captured evidence. 912b8fd863

  • Doctor warns about effective multi-architecture Debug simulator settings, including generated Pods and imported configuration. It reports but does not rewrite project policy. 7afb23c8da

  • Doctor identifies linked native libraries whose Git metadata enters the fingerprint and prints narrow ignore entries for projects that do not build from that metadata. 89e9280f0c

  • Android workspaces with the same app-directory name receive distinct owned emulators instead of failing on a name collision. Existing device assignments are preserved. 529f7c68f2

  • Bare iOS reload failures include an exact-app relaunch fallback when no Reload control is visible, preserving the automation session, simulator and Metro port. The remedy warns about losing in-memory state. 71484ba0ae

  • iOS resolves a listed app scheme from the static app.json name when a differently named workspace and multiple schemes make the existing selection ambiguous. The error distinguishes a naming mismatch from an unshared scheme. 5aa719fcfd

  • iOS simulator launches allow up to 60 seconds for CoreSimulator to respond, preventing the previous 10-second deadline from aborting a slow successful launch. App readiness and crash checks remain unchanged. 8221162342

  • A workspace waiting for a shared native build no longer reports failure just because native preparation changed the cache key. It retains the wait result after validating the artifact under its own final key. fd65de16ec

  • stim worktree warm copies ignored state directly into the destination, avoiding the extra staging and publication passes. Same-volume APFS copies request copy-on-write; missing paths are copied and existing entries are kept. 49ae2fce16

  • Doctor checks the actual source and destination volumes when reporting worktree-copy performance. 49ae2fce16

Docs​

  • Refresh startup-crash benchmark results and keep timeline endpoint circles visible. 2d192f66e7, 591df79435
  • Document that warming must finish before editing, installing dependencies, starting Metro, building, or running another warm in the destination. Concurrent destination writes are unsupported. 49ae2fce16

Migration notes​

  • Replace npx stim-cli with npx stim. For global installations, uninstall stim-cli before running npm install --global stim so the old and new packages do not compete for the same executable. The command remains stim; the four @stim-cli/* companion package names are unchanged.
  • Keep worktrees on the source checkout's filesystem for copy-on-write warming. STIM_TMPDIR does not relocate this direct copy. Wait for warm to complete before writing to its destination.
  • In monorepos, move repository-root runtime settings into each relevant app's .stim.json; apps no longer inherit that root file. Keep repository-wide worktree.exclude rules at the main checkout root. Machine-level project and repository overrides retain their precedence.

1.0.0-rc.23​

Fixes​

  • Launch errors show compact, labeled error, component, and native stacks with up to 10 frames each, prioritizing app code. Every stim logs query retains full captured stacks. be15388437, dfd342114e
  • Resolve JavaScript stack locations through the workspace's Metro and preserve the symbolicated context for later queries. Capture attributable native crashes on iOS simulators and Android, combine duplicate reports, and preserve Java cause chains. Native symbolication uses matching symbols where available. dfd342114e
  • Show a warning when the native app stays alive with a JavaScript launch error, and explain when a delayed iOS crash report needs another logs query. Launch JSON and exit-code contracts remain unchanged. dfd342114e

Docs​

  • Clarify stack previews, full captured logs, symbolication limits, and delayed native crash reports in the version-matched guides and website. dfd342114e

QA​

Expedited RC authorized on 2026-09-09. Omitted locally before the version commit: format:check, lint, typecheck, knip, unit tests, test:e2e, test:runtime, and the pre-tag native/manual matrix. Retained before the version commit: candidate build, CLI version/help/guide, and tarball inspection. Retained after push: exact-release-commit CI, including its format, lint, build, typecheck, knip, unit, cross-platform e2e, and published-runtime jobs.

Pre-merge evidence includes JS errors on iOS and Android, Swift fatalError, Java IllegalStateException, JNI abort, and repaired app launches on both platforms. Captured command output. This evidence predates the version bump; the full native matrix is not rerun against the release candidate. Physical and remote devices were not tested.

1.0.0-rc.22​

New​

  • Recycle compatible Android emulators across worktrees. Adoption clears app data, retains matching APKs to skip installation, and recovers from signer or version conflicts. The default pool holds three emulators. ea0292b1
  • Wait for optional [stim:readiness] pending and ready device logs during launch, and report whether readiness completed, timed out, or failed. 689ad047

Removed (breaking)​

  • Remove the legacy caches.injectMetroStore machine setting. Use optimizations.metroSharedCache instead. 2c585854

Fixes​

  • Wait for Metro to deliver the bundle before starting launch-stability checks. a4832aa3
  • Align Android device-log timestamps with the host clock so launch filters include the current app's logs. dd0f2c5e
  • Resolve dynamic imports from nested modules when bare Metro uses a hoisted metro-runtime package. 1fe471f5
  • Restart a stuck Android app when following the unverified-launch remedy. 6230e4aa

Docs​

  • Document build optimizations, Android CAS, emulator recycling, and optional readiness reporting. Refresh the website and publish activity-timed Android and iOS launch-error benchmarks. 2c585854, 1f9fc1e8, c2f848b4

Migration notes​

  • Replace caches.injectMetroStore: false with optimizations.metroSharedCache: false to keep shared Metro caching disabled.
  • Set pool.androidParkedMax to 0 when a fresh emulator is required. Recycling preserves system apps, shared storage, accounts and device settings; app data is cleared at adoption. Older AVDs without recorded creation settings are deleted when removed.

QA​

  • Full preflight: formatting, lint, build, typecheck, knip, 3,737 unit tests, 66 end-to-end tests, Node 20.19.4 runtime checks, website build, and all five packed tarballs passed.
  • Native loop and cache suites passed for bare React Native and Expo on both iOS and Android. Both device-pool suites passed.
  • Manual QA covered readiness success, timeout and error; nested dynamic imports; healthy, broken and repaired logs; UI interactions; and Trailhead on both platforms, including native cache reuse across worktrees.
  • Android release QA verified current JS in the installed, signed and aligned APK after adopting an emulator with a differently signed app.
  • A newly booted Android emulator can initially return unverified. Follow the printed restart remedy and verify the UI. The recovery instruction was corrected during this QA pass. iOS may require the printed UI Reload fallback when Metro cannot identify its peer. Earlier client errors remain visible until the next launch, as documented.
  • Authenticated remote providers, physical-device signing, Android flavors, and iOS release swapping were omitted because those implementations did not change. Shared launch and lease behavior has simulator/emulator and unit coverage.

1.0.0-rc.21​

Fixes​

  • Persist exact process identities for Stim-owned supervisors, log collectors, and managed tunnels instead of inferring host-process ownership through ps. Use unique-pid@0.1.1 so permitted process inspection works in macOS sandboxes that deny boot-session metadata. Cleanup retains unverified records and leaves replacement processes and their device claims alone. dfaeae32

Removed (breaking)​

  • Remove stop --force. Stim no longer terminates externally started Metro servers or infers process ownership from a port, command name, or directory. dfaeae32

Migration notes​

  • Stop a still-running legacy supervisor manually before starting a replacement when its saved state has no verifiable process token. Externally started Metro remains reusable but must be stopped by its owner.
  • Process checks and signals are separate OS operations. On macOS, a token captured without boot identity cannot distinguish a repeated PID and exact start timestamp after a reboot or clock reset. Denied process inspection is not treated as proof that a process exited.

QA​

Expedited RC authorized on 2026-09-07. Omitted locally before the version commit: format:check, lint, typecheck, knip, unit tests, test:e2e, test:runtime, and the pre-tag native/manual matrix. Retained before the version commit: candidate build, CLI version/help/guide, and tarball inspection. Retained after push: exact-release-commit CI, including its format, lint, build, typecheck, knip, unit, cross-platform e2e, and published-runtime jobs.

The included PR passed independent review, 3,602 unit tests, 66 end-to-end tests, and required CI before merge. Forty collector-ownership and reclaim tests also passed inside the actual Codex sandbox with the published library. These checks are not a substitute for the omitted candidate native/manual QA.

1.0.0-rc.20​

When Stim is not installed globally, replace stim with npx stim-cli.

New​

  • Configure build optimizations with machine defaults and repository/project overrides: artifact and remote caches, release bundle swapping, Metro sharing, iOS compilation caching and prefix mapping, and Android compiler cache, PCH, Gradle cache, and target ABI settings. See stim guide settings. a4e2251
  • Opt into experimental Android PCH caching across worktrees using Apple Clang CAS on macOS. This requires a manually prepared compatible compiler/NDK toolchain; see the Android CAS report for setup, evidence, and compatibility limits. f45d9e2

Fixes​

  • Distinguish harmless executable lookups from actual device commands when auditing benchmark isolation, while retaining checks for real invocations. a563dad

Docs​

  • Complete the published Android benchmark coverage and omit private inventory data from benchmark exports. df12e17

Migration notes​

  • Shipping defaults are unchanged: Stim-supplied Android ccache disables PCH. CAS is a mutually exclusive opt-in through optimizations.android.compilerCache and a toolchain manifest.
  • Android compiler/PCH modes use separate generated CMake profiles so switching backends does not retain a previous compiler. Old profiles accumulate; worktree removal reclaims them.
  • CAS is experimental. Automatic toolchain installation, broader project compatibility, and CAS eviction/diagnostics remain unfinished.

QA​

Expedited RC authorized on 2026-09-07. The pre-tag native and manual QA matrix was omitted. The candidate passed formatting, lint, build, typecheck, knip, all 3,664 unit tests, all 66 E2E tests, the runtime check, CLI version/help/guide checks, and inspection of all five tarballs. Publishing remains gated on CI for the exact release commit, including Node 22, Node 24, and the published Node 20.19.4 runtime.

1.0.0-rc.19​

When Stim is not installed globally, replace stim with npx stim-cli.

Fixes​

  • Enforce benchmark runner filesystem isolation for coordinator data and sibling runs, with verified access checks before timing starts. 5751260
  • Correct benchmark auditing of quoted shell commands and separate crash launch/error capture. e2cb47a d53bebf

Docs​

  • Require worktree warming to finish successfully before starting Metro, building, or installing dependencies. A shell job ID is not completion. 35a77e4
  • Refresh validated Android and launch-recovery benchmark results on the website. e62e6f7

QA​

Expedited RC authorized on 2026-09-06. Omitted locally before the version commit: format:check, lint, typecheck, knip, unit tests, test:e2e, test:runtime, and the pre-tag native/manual matrix. Retained before the version commit: candidate build, CLI version/help/guide, and tarball inspection. Retained after push: exact-release-commit CI, including its format, lint, build, typecheck, knip, unit, cross-platform e2e, and published-runtime jobs.

1.0.0-rc.18​

When Stim is not installed globally, replace stim with npx stim-cli.

Fixes​

  • Warming an Android worktree excludes generated autolinking caches, including nested apps, so Gradle resolves native dependencies inside the new checkout. 1fac645
  • Keep large temporary copies on the operation's volume, including worktree warming and cached app preparation. STIM_TMPDIR or machine tempDir overrides placement; doctor reports cross-volume copies. 20232f3
  • Repair stale Android CMake launcher configurations with stim doctor --fix --platform android, including installed native modules. Stop all native builds before repair; custom launchers and shared ccache entries are preserved. 008d057
  • Validate benchmark cache entries with the pinned CLI's fingerprint library. 0079fb3
  • Audit benchmark device-session scoping per shell command, accepting correctly scoped delayed navigation and rejecting unscoped invocations later in a chain. 782b8c5

Docs​

  • Explain the workflows where Stim adds little value on the homepage. fb82b23

QA​

Expedited RC authorized on 2026-09-06 after disk pressure paused the native matrix. The product candidate passed formatting, lint, build, typecheck, knip, 3,605 unit tests, 66 end-to-end tests, runtime smoke, and tarball inspection. Real Gradle autolinking regeneration and CMake launcher repair were verified. Native bare Android and bare/Expo iOS workflow checks passed, as did the bare iOS cache suite.

Omitted before the version commit: the remaining native cache matrix, Expo Android workflow, and manual Release-artifact checks. Final candidate build, version, help, guide, and all five tarball inspections are retained, together with blocking CI on the exact release commit, including Node 22, Node 24, and the Node 20.19.4 published-runtime job.

1.0.0-rc.17​

If Stim is not installed globally, replace stim below with npx stim-cli.

Fixes​

  • Android builds disable CMake precompiled headers by default when Stim supplies ccache, improving compiler-cache reuse across worktrees. Explicit project PCH settings are preserved; builds without Stim's ccache setup and iOS builds are unchanged. The tradeoff is slower cold C++ compilation. (9418d3d)
  • The repository's benchmark harness flags missing or below-target Android compiler-cache evidence, retains failed attempts for investigation, and excludes them from published comparisons. Android preparation and preflight also require a doctor report without cost findings. (872bb1f)

Docs​

  • stim guide agent keeps the workflow, safety rules, and recovery actions up front. Optional cache warm-up, ABI selection, fingerprint fallback, and build-sharing details live in the build and concurrency guides. (1c420aa)

QA​

Expedited RC authorized on 2026-09-06. Omitted locally before the version commit: format:check, lint, typecheck, knip, unit tests, test:e2e, test:runtime, and the pre-tag native/manual matrix. Retained before the version commit: candidate build, CLI version/help/guide, and tarball inspection. Retained after push: exact-release-commit CI, including its format, lint, build, typecheck, knip, unit, cross-platform e2e, and published-runtime jobs.

1.0.0-rc.16​

If Stim is not installed globally, replace stim below with npx stim-cli.

New​

  • stim doctor reports the running CLI version, the first stim executable resolved from PATH, and every distinct Stim installation found on PATH. It flags the setup when the resolved executable is older than the CLI currently running. (0070bae)

Fixes​

  • The benchmark harness accepts ABI-scoped Android APK cache keys when validating a native-change run. (93bfeb1)
  • Benchmark run guards reject the wrong Stim version inside the timed shell, enforce inactivity and hard timeouts across process trees, and preserve failures hidden inside shell chains. (e3410c2)

Docs​

  • The release guide defines an explicitly authorized expedited lane for release candidates, including the checks it may omit and the package, CI, tagging, and publishing safeguards it must retain. (5da0e95)

QA​

Expedited RC authorized on 2026-09-05. Omitted locally before the version commit: format:check, lint, typecheck, knip, unit tests, test:e2e, test:runtime, and the pre-tag native/manual matrix. Retained before the version commit: candidate build, CLI version/help/guide, and tarball inspection. Retained after push: exact-release-commit CI, including its format, lint, build, typecheck, knip, unit, cross-platform e2e, and published-runtime jobs.

1.0.0-rc.15​

If Stim is not installed globally, replace stim below with npx stim-cli.

New​

  • stim worktree warm copies missing ignored dependencies, native output, and local configuration from the main checkout into an existing linked worktree. Existing entries and tracked files are preserved. (6412169)
  • stim reload [ios|android] reloads JavaScript on a running owned simulator or emulator without rebuilding or reinstalling the app. It reports the requested reload, selected device, Metro port, and strategy; app-side completion still needs verification. (43da17e, 24d6d71)
  • Sectioned guide topics support stim guide <topic> <section>, including direct error-code lookup such as stim guide errors STIM_NO_METRO. (d377e92)
  • Owned Android emulators use Quick Boot where supported. Shutdown waits for the emulator and snapshot save to finish, and uncertain process locks prevent deletion. Displayless Linux keeps snapshots disabled. (e6ddc3a)

Removed (breaking)​

  • worktree create, --carry-ignored, and creation-only settings are removed. Use Git to create worktrees, then run stim worktree warm when you need ignored files from main. Tracked edits are no longer copied. (834b338)

Fixes​

  • Warming preserves APFS clones through file publication on macOS, avoiding full data copies while keeping existing files safe. Files remain independent of the main checkout. (0a5c2fa)
  • Benchmark overview links open the selected platform's audit, including Luna's Android JavaScript result. (5d3aced)
  • Android shutdown flushes guest writes before killing the emulator, with a five-second limit so a stalled flush cannot block cleanup. (3abbb87)
  • iOS skips cache lookup and storage when the final fingerprint cannot be computed, preventing an artifact from being stored under its earlier key. (74d9a3e)
  • Commands waiting for a native build keep waiting when another process takes over a failed builder's lock. (70f3108)
  • Android Debug builds target the selected device's ABI and route eligible CMake compilation through ccache. (0e5eb6d, b9743cd)
  • start, ios, and android reject directories that are not React Native or Expo apps; doctor reports the problem as a finding. Invalid relative registry paths are reported as invalid records, and worktree Git commands pass paths as arguments. (360d871, c93997f, b7100dd)

Docs​

  • Native cache QA uses a normally built disposable source checkout and separate empty test caches. (8865207)

  • The agent workflow verifies UI changes on the reported device and checks logs again before cleanup. (0dff4a2)

  • Error-guide headings match emitted messages, and shared-lock guidance covers the records those locks protect. (8576b5e)

  • Setup, platform requirements, device ownership, and reload guidance now agree across the CLI and website. A successful log query is distinguished from a clean log result. (b264ffa, 0ab7174, cc34d57)

  • The skill description names common agent tasks while retaining its single version-matched guide command. (82e7ac9)

  • The benchmark website has separate platform/model selectors, visible unavailable combinations, and published Android results. (ebe5359, 9c54814, 8fde8cc)

Migration notes​

stim worktree create and --carry-ignored are removed. Create the checkout with Git, then warm it from inside the new directory:

git worktree add -b my-feature ../my-feature HEAD
cd ../my-feature
stim worktree warm

If an agent harness already created the worktree, run only stim worktree warm. Warming never applies the main checkout's tracked edits. Remove worktreeDir, worktree.baseRef, worktree.include, and .worktreeinclude from project setup; worktree.exclude and .worktreeexclude still control warming. stim worktree remove works with warmed and unwarmed worktrees and retains Git-created branches.

stim guide errors, lifecycle, facts, and cleanup now print a section index. Add the section name to read its full guidance. The agent, Metro, logs, and settings topics remain whole. (d377e92)

The website build retains two accepted high-severity image-size denial-of-service advisories: GHSA-5p2g-fcmc-qvqq and GHSA-w3rx-r6r6-pgpr. A malformed local image can hang a website build. The audit exceptions do not fix the parser; published Stim packages do not depend on it. Docusaurus tracks a replacement in #12231.

The remaining native-device validation continues after this candidate is published.

1.0.0-rc.14​

New​

  • a19b03a adds a separate JavaScript launch-crash benchmark that measures diagnosis and recovery without mixing those results into readiness charts.
  • ea416bf adds audited Android agent benchmark results alongside the iOS results.
  • c69d4ae adds simulated prompt responses to the benchmark website.

Fixes​

  • bdd19a6 includes available Expo and bare React Native code frames and stacks in human stim logs --errors output. Launch errors now distinguish a live native process from an exited process so agents can avoid unnecessary native rebuilds; an exited process still needs another native launch.

Docs​

  • The benchmark presentation now preserves terminal output, follows timeline playback, uses clearer event labels, aligns interaction video, and improves prompt-card spacing (573546d, 07f670a, 52f486d, 0d56b35, 9b1f822, f660636).
  • a41b6cb updates the documented cache-savings examples to match measured results.
  • 8ae44bd publishes the strict Sol iOS benchmark report.

1.0.0-rc.13​

New​

  • 91724f4 structures successful stim doctor output into labeled project, platform, shared-service, and automatically handled checks, and adds the selected platform to reports with findings.
  • f06cd45 adds stim guide agent as the version-matched coding-agent workflow. The skill installed by npx skills add appandflow/stim is now a tiny one-time router to that CLI guidance.

Fixes​

  • 58140dc removes dangling separators when Stim shortens long owned iOS simulator names.

Docs​

  • The benchmark site now includes an overview, interactive timeline, model selection, playback, valid-attempt filtering, run activity, zoom, deep links, grouped background lanes, Safari gestures, corrected device-session evidence, and clearer result labels (efb389d, 198c996, ff52efc, 0ba0db6, fbadca6, 7ab80fa, e6d8040, 47802ba, f2c2752, 7689578, d91a97e).
  • 82855e0 adds copyable prompts for common build, device, log, cache, and PR-validation workflows and clarifies when agents should use Stim or create a worktree.
  • 3390c6f adds an automated continuous benchmark replay for the website and social sharing, including the audited clock and recreated real-speed device interaction.

1.0.0-rc.12​

Fixes​

  • 98480e9 maps slash-separated worktree names to flat + paths instead of percent-encoded %2F paths, avoiding nested Xcode build failures while preserving the Git branch hierarchy.

Docs​

  • 4f1e3bd defines the benchmark v4 readiness protocol for separate JavaScript and native passes, exact device evidence, failure arms, and timeline artifacts.

Migration notes​

Existing rc.11 %2F worktrees are not migrated. Continue or remove them from their current checkout; newly created slash-named worktrees use +.

1.0.0-rc.11​

New​

  • stim doctor --platform ios|android keeps shared checks while filtering native findings and fingerprint parity to the selected platform. Android-only checks no longer invoke Xcode tooling (901b561).

Fixes​

  • stim worktree create accepts safe slash-separated names such as bench/run-1, preserves the hierarchy in the Git branch, and derives a collision-resistant single-directory checkout and device label (5b17558).

Docs​

  • Documented the v3 agent benchmark matrix across JavaScript and native changes, Stim and direct-tool control arms, and supported coding models (4a1bc15).

Migration notes​

  • Existing stim doctor and slash-free stim worktree create invocations keep their current behavior.

1.0.0-rc.10​

New​

  • Owned iOS simulators can be parked when a worktree is removed and adopted by a later matching workspace. Adoption resets app data, privacy grants, keychain state, and other user apps; ownership and cleanup remain fail-closed across crashes and concurrent garbage collection. status and gc expose the bounded pool, whose machine default is three simulators (9136251).
  • stim stats aggregates build timing and cache effectiveness without changing the build lifecycle (4d0cabb).
  • iOS runs can select an installed simulator with --device-type and --runtime; Android runs can select an installed image with --system-image (c29c6bd).

Fixes​

  • iOS fingerprints are computed while the simulator boots, and a simulator booted by the current run is not redundantly listed again (9dbba71, e66bde6).
  • Long native builds emit phase-named heartbeats sized from the project's last cold build instead of a fixed interval (91a1d6e).
  • Worktree, server, device, and platform-run output uses one consistent phase vocabulary (e759551, 1daa954, b91ca0a).
  • iOS simulator log collection anchors its process predicate to the installed app executable path, avoiding matches from unrelated processes (b3915e0).
  • Workspace overrides move uuid, serialize-javascript, and qs to patched dependency versions (029854b).

Docs​

  • Added the simulator-pool and aggregate-statistics designs, and documented the stacked pull-request workflow used for dependent changes (d8de9c8, 7ebed3e, d017f3c).

Migration notes​

  • No command migration is required. Projects that require a completely fresh simulator system image for every worktree can set pool.iosParkedMax or STIM_POOL_IOS_PARKED_MAX to 0.

1.0.0-rc.9​

Fixes​

  • Android builds are re-fingerprinted after Gradle succeeds, so inputs changed by Gradle or autolinking store the artifact, lastBuild, and JSON cache identity only under the post-build key. If re-fingerprinting fails, Stim installs the APK without caching it or reporting a stale cache identity (2b689ad).
  • Dev-client launches carry Expo's session-only disableFab=1 parameter on iOS and Android. Expo versions that support the parameter hide the floating Tools button without changing the user's persistent preferences; older versions ignore it (7c4dd23).
  • stim android --json includes durationMs, matching the documented iOS payload and reporting the wall time of the complete successful native run (adc927e).
  • A warm iOS run reports the installed-app identity proof separately from Expo dev-client preference preparation, so a slow simulator command is attributed to the step that actually ran (e858950).

1.0.0-rc.8​

New​

  • stim ios --device [udid] builds the iphoneos slice, selects a cabled iPhone, installs and launches it through devicectl, and points a Debug build at this workspace's Metro over the LAN. The phone is used, never owned: the signing gate reads the bundle's own embedded.mobileprovision and refuses one that does not name the target device, and Stim re-signs only copies it makes (02cfcb2, 43fd760).
  • stim logs --source device collects a phone's runtime logs through the devicectl console. That stream renders every os_log level alike, so a line stays info unless a crash marker or devicectl's own error prefix proves otherwise; severity is never guessed (1b79cb2).
  • A launch blocked by the phone's pending Local Network permission is named as such, and the remedy says how to accept the prompt, including from a device tool, because the grant cannot be pre-seeded from this machine (a2b3ee9).
  • stim device lock <ios|android> [id] --for <duration> holds a connected physical device across runs, from 10 seconds to 30 minutes, waiting for another workspace the way a run does. stim device unlock [platform] gives back one platform's lease or every lease this workspace holds (33141aa).
  • ios --device and android --device take a run-scoped lease from just before the install until the run exits, raising the expiry at each device step so a killed run frees the device in bounded time. --wait <seconds> waits for a device another workspace holds; --no-wait proceeds without a lease and says what that costs. status lists leases, stop and worktree remove release this workspace's, and gc --delete deletes expired lease files (3fbb460, 96f3c1f).
  • --device with no id picks from the connected pool: the device this workspace already leases when it is present, otherwise the first free one in case-folded id order. A leased device that is not connected refuses rather than moving silently to another phone (eb02f1a).
  • doctor reports the missing sandbox allowance when a write to STIM_HOME actually fails, naming the absent keys, and doctor --fix applies the findings Stim can repair itself. It writes only per-user files, refuses a settings file it cannot read back, and refuses under Codex, where the only value that clears Stim turns the sandbox off wholesale (6640771).
  • stim worktree create --dir <path> places one worktree where the run needs it, taking precedence over the worktreeDir setting; a relative --dir resolves against the current directory, and a blank one is refused (fdb60b7).

Fixes​

  • A run summary says what it proves: a verified launch no longer prints the iOS connection refusal it already recovered from, the ready line reads bundle loaded, process alive, stable for 3s -- the first screen may still be rendering, the heartbeat holds its 30-second grid without repeating a beat, and preparing an owned simulator or emulator gets its own timed line (3c86cfc).
  • Every known setting is type-checked from one shape table in start, ios, android, worktree create, gc, and doctor, and a known key is no longer also warned about as unknown. Behavior change: a non-string cache.provider is refused before the build instead of reported while the run continues (0c7c691, 278ce61).
  • worktree create judges carried Pods against the Podfile.lock the new worktree ends up with, not the one it started from (c006de1).
  • worktree create rolls back a branch it stranded, refuses --base on a branch that already exists (d60db78), and runs git worktree add under LC_ALL=C so the branch-exists guard matches git's English text on a build with translation catalogs (891e94f).
  • A relative worktreeDir setting resolves against the settings root instead of the process's working directory (0a0603f), and a non-string worktreeDir is refused instead of crashing (af1e2d3).
  • A gradle failure whose report is interleaved with task output surfaces the real cause chain instead of the surrounding tail (c1bc9ba).
  • An owned Android emulator's console port is chosen and recorded under the global config lock before boot, so concurrent workspaces cannot claim the same serial. A failed boot releases only its matching port claim while keeping the owned AVD recorded for gc (e6535b2).
  • The fingerprint ignores android/local.properties and android/.idea, machine state no native build reads, so a cache key can match across checkouts (060aeb2).
  • stop, gc --delete, and worktree remove prove a collector pid is this workspace's collector before signalling it, and report a pid they cannot prove instead of killing a process that reused it (d960785, f373e95).
  • A pool refusal gives each connected device's own reason -- Developer Mode off, unpaired, unreadable lease -- instead of asking which of several to name (15f994c).
  • stop --json, status --json, and doctor --json print their payload on exactly one stdout line, matching the documented contract and the other single-payload JSON commands (daaaeeb).
  • Release: every published tarball carries real inter-package versions, substituted by pnpm at pack time rather than bumped by hand (c96df6b), and the workflow computes its publish dist-tag, so a candidate cut after a stable release lands on next instead of latest (9ef253c).
  • The Expo dev menu stays off an app Stim launches, and on iOS so does its floating Tools button. The dev-client project url carries disableOnboarding=1 on every launch, a phone launch passes -EXDevMenuShowsAtLaunch 0 and -EXDevMenuShowFloatingActionButton 0 as launch arguments through devicectl, the simulator preference write covers the Tools button, and Android adds the EXDevMenuDisableAutoLaunch intent extra; the simulator's onboarding preference write is gone. Android's floating button still shows (#249) (4d432f4, 21ead76).

Docs​

  • The website documents doctor --fix and doctor's sandbox finding (8977ff0).
  • guide errors and the skill name the three things an agent sandbox blocks -- writes to STIM_HOME, the simulator service, the adb server -- and what to allow, instead of leaving an agent to find out by failing (4916eaf).

1.0.0-rc.7​

New​

  • stim ios and stim android skip the install entirely when the device already holds the exact artifact, going straight to launch (c07f97b).
  • A flavored Android project with no configured variant is refused before gradle runs, naming the candidates, instead of after a full assemble (168528b).

Removed (breaking)​

  • The stale-APK mtime refusal is gone: an APK older than the build that just ran is no longer treated as foreign (6941c5d).

Fixes​

  • Android installs use the APK's real applicationId instead of the gradle namespace, so suffixed and flavored ids launch correctly (a4b64d7).
  • The log collector signals its stream through the child handle rather than a pid that can be reused (f342663).
  • The real-xcodebuild diagnostic test carries its sibling's timeout instead of flaking at the 5s default (425cd13).

1.0.0-rc.6​

New​

  • stim android --device [serial] installs and launches on a connected physical Android device after the usual build. With no serial it uses the one connected device and refuses with the candidate list when adb reports several; it cannot be combined with --remote. The device is used, never owned: nothing is recorded, and stop, gc, and teardown never touch it (518965a).
  • CocoaPods runs through bundler when the project pins it: a Gemfile.lock that resolves a cocoapods spec routes pod install through bundle check --dry-run -> bundle install (when gems are missing) -> bundle exec pod install, all frozen so the tracked lockfile is never edited. A Gemfile without a lock, a lock without cocoapods (fastlane-only), or a machine without bundle keeps today's plain pod install. Doctor, worktree warnings, and the Xcode sandbox remedy share the same rule (0c636f6).

Removed (breaking)​

  • stim gc --delete --all is now stim gc --delete --cache all: the flag reaches caches only and its name says so (8e4eee1).

Fixes​

  • Android debug builds reverse port 8081 only when the debug_http_host write fails, so the reserved-port wiring is not undone by a fallback that was not needed (531833f).
  • A damaged Xcode compilation-cache object is named as the build-failure cause, with the cache path to clear (1fbde68).
  • CI: vitest 5 stops spurious worker-exit failures (1e20a7d).

Docs​

  • The cache e2e gate judges Gradle storing by entry names, accepts a warm-machine cache as engagement, and isolates GRADLE_USER_HOME so the storing leg is always measurable (86f9273, bf4640a); the bare fixture installs its pods through bundler (d8b6fa7).

1.0.0-rc.5​

New​

  • @stim-cli/cache, a fifth published package holding the cache provider contract and the local-first tier coordination behind the native artifact cache and the Metro transform cache (fadc2ce). The built-in filesystem provider is the only tier shipped; a remote provider becomes a pure addition that implements the same capabilities. On-disk cache layout, keys, APFS clone behavior, and gc reporting are unchanged, and a project with no provider configured loads nothing new.
  • cache.provider and cache.options settings select an optional second-tier provider through the existing settings layers. A committed cache.provider is executable code and is documented as such. Provider calls are bounded: load, resolve, and upload each carry a timeout with a STIM_CACHE_*_TIMEOUT_MS environment override, Metro reads are concurrency-capped, and repeated failures disable the tier for the run with one warning.
  • Contract-test helpers for provider authors ship in @stim-cli/cache, including checks for abort handling, overwrite semantics, and destination hygiene, runnable against a provider module so apiVersion and factory validation are exercised.

Fixes​

  • A slow first simulator boot no longer fails stim ios with a raw spawnSync /bin/sh ETIMEDOUT. Stim re-enters simctl bootstatus while the device still reports Booting, against a ten-minute deadline, and a failure names the udid and the wait (91900c4).
  • The warm-carry summary no longer flips its dependencies verdict when a later node_modules entry lacks a sibling package.json (bc155c1).
  • stim doctor claims the fingerprint-parity check only where it runs (a checkout without installed dependencies), and its expensive main-checkout probes are pinned behind their preconditions (400e068).
  • CI: the native e2e workflows read the renamed STIM_* environment variables again, restoring the cross-run build cache they silently lost in the rebrand (a49f7e5).

Docs​

  • stim guide errors documents every refusal code the CLI emits, enforced by a contract test that scrapes the engine sources (7c2ff5c).
  • The website documents the real carry exclusions, the real guide topics, the parity check's precondition, and the two shapes of the caches setting (7f32bf2).
  • The CLI and docs carry the Stim rebrand and the redesigned website (cc9276b).

1.0.0-rc.4​

This release candidate improves fresh-agent app launches. It also helps agents prepare the main checkout before they create native worktrees.

Install and run​

npx skills add appandflow/stim

npx --package=stim-cli stim doctor
npx --package=stim-cli stim start
npx --package=stim-cli stim ios # or: npx --package=stim-cli stim android
npx --package=stim-cli stim logs --errors
npx --package=stim-cli stim stop

Self-contained app launch​

stim ios and stim android build or restore the app, install it, launch it, and check its readiness. The Stim skill does not require a separate device automation tool for this workflow.

The iOS launch path sets the workspace Metro address before it opens the app. The Android launch path injects the Metro preferences and uses the launcher activity when a dev-client URL is not available. These paths reduce Expo dev menu and deep-link confirmation friction.

Stim observes the app for three seconds after Metro completes the bundle. The final summary reports the process state, launch errors, device ID, app ID, Metro port, cache result, and log path.

Main checkout diagnostics​

stim doctor checks the main checkout when it runs from a linked worktree. It reports stale dependencies, CocoaPods state, native output, build-cache state, and local commits that are missing from the upstream branch.

The skill asks agents to run doctor before native work. It also recommends one main-checkout build when more native worktrees are expected. A later stim worktree create --carry-ignored can clone that prepared state with APFS copy-on-write support.

Release reliability​

The release workflow retries npm registry checks after publishing. This avoids a false release failure while a new package version propagates through the registry.

All four packages use version 1.0.0-rc.4 and are released together.

1.0.0-rc.3​

This release candidate adds optional SimSlim profiles for Stim-owned iOS simulators. It also prevents ignored-file carry-over from copying a nested Git worktree into a new worktree.

Install and run​

npx skills add appandflow/stim

npx --package=stim-cli stim doctor
npx --package=stim-cli stim start
npx --package=stim-cli stim ios # or: npx --package=stim-cli stim android
npx --package=stim-cli stim logs --errors
npx --package=stim-cli stim stop

Optional SimSlim profiles​

Projects can commit a SimSlim profile and select it in .stim.json:

{
"ios": { "simslimProfile": ".simslim/dev.json" }
}

Install SimSlim once on each Mac with brew install mobai-app/tap/simslim. SimSlim requires an iOS 18 or newer simulator.

stim ios applies the profile only to its owned local simulator. The first change can reboot the simulator. Later matching runs are fast no-ops in SimSlim. Removing the setting restores stock services when Stim applied the profile.

Stim validates the profile path before it creates or boots a simulator. The path must stay inside the repository. stim doctor reports an invalid profile or a missing SimSlim installation, including for apps inside a monorepo.

Safer ignored-file carry-over​

stim worktree create --carry-ignored now reads Git's registered worktrees and excludes them from the copied files. This prevents nested Claude, Codex, and custom worktree locations from being copied into the new worktree.

The check uses Git's worktree registry, so it does not depend on a hardcoded directory list. If Git reports only an ignored parent directory, Stim excludes that full parent. Project-specific exclusions remain available through .worktreeexclude and worktree.exclude.

All four packages use version 1.0.0-rc.3 and are released together.

1.0.0-rc.2​

This release candidate improves the full agent workflow. It makes worktree creation more predictable, reports native cache activity, and gives launch errors a clearer severity.

Install and run​

npx skills add appandflow/stim

npx --package=stim-cli stim doctor
npx --package=stim-cli stim start
npx --package=stim-cli stim ios # or: npx --package=stim-cli stim android
npx --package=stim-cli stim logs --errors
npx --package=stim-cli stim stop

The platform command builds or restores the app, installs it, opens it, and checks its launch state. Plain output streams progress and ends with the full device, app, Metro, cache, and log facts.

Worktree workflow​

stim worktree create <name> now uses the current checkout as its base. Use --base fresh to start from origin/HEAD, or use --base <ref> for a branch, tag, or commit.

The command reports which ignored dependencies, CocoaPods files, and native build output can make the new worktree warm. It also finds nested lockfiles and prints the correct install command and directory.

stim worktree remove removes a Stim-owned branch only when that branch has no unique commits. It keeps the branch when another worktree uses it or its ref changes during cleanup.

Native cache reporting​

The iOS command now reports Xcode compilation-cache hits from Xcode 26. This metric stays separate from the Stim artifact-cache result. The output explains when the Xcode metric did not run or was not available.

Launch diagnostics​

The iOS log collector reduces known harmless UIKit focus messages. It also keeps a TCP refusal unless a later log record proves that the same connection succeeded. The raw log timeline remains complete.

The release also fixes a Node 22 Linux test race in tunnel cleanup.

All four packages use version 1.0.0-rc.2 and are released together.

1.0.0-rc.1​

This release candidate makes the normal agent loop easier to follow and more reliable. Plain output now streams each phase and finishes with the complete device, app, Metro, cache, and log facts. JSON output remains available for scripts that need a stable payload.

Install and run​

npx skills add appandflow/stim

npx --package=stim-cli stim doctor
npx --package=stim-cli stim start
npx --package=stim-cli stim ios # or: npx --package=stim-cli stim android
npx --package=stim-cli stim logs --errors
npx --package=stim-cli stim stop

Launch readiness​

stim ios and stim android wait for Metro to finish the bundle before the launch stability window starts. The commands then observe the app for three seconds and check that its process remains alive.

The final output reports whether the app is ready, still bundling, unverified, or failed. A Metro bundle failure or an exited app process fails the command. Errors observed during the launch window are printed for the agent to assess, even when the app remains alive.

Plain agent output​

The platform commands stream the important build and launch phases as they happen. Their final plain-text summary includes:

  • the full simulator UDID or emulator serial;
  • the app ID;
  • the Metro port and state;
  • the native build cache result;
  • the log directory.

The bundled skill recommends plain output for interactive agent work. It uses JSON only when another program must parse the result.

Owned simulator cleanup​

An explicit stim stop shuts down a Stim-owned iOS simulator even when a device automation session is still active. Stim verifies ownership before the shutdown and never shuts down an unowned simulator.

This lets an agent keep its Agent Device session active through visual proof, run stim stop, and then close the automation session.

Error reporting​

Native prebuild failures now appear in stim logs --errors. Bare React Native Metro logs also retain more launch context for diagnosis.

All four packages use version 1.0.0-rc.1 and are released together.

1.0.0-rc.0​

This is the first release candidate for the stim-cli package family.

Stim gives each React Native or Expo workspace an isolated local development environment. Each workspace gets its own Metro port and its own simulator or emulator. Native builds and Metro transforms use shared caches, so parallel worktrees can reuse work without sharing runtime state.

The CLI is designed for coding agents. Commands never prompt. Status and progress go to stderr. Commands with --json print one machine-readable result to stdout. Failures include stable error codes, focused diagnostics, and specific remedies.

New​

One complete agent loop​

npx skills add appandflow/stim

npx --package=stim-cli stim doctor
npx --package=stim-cli stim start
npx --package=stim-cli stim ios # or: npx --package=stim-cli stim android
npx --package=stim-cli stim logs --errors
npx --package=stim-cli stim stop

doctor reports project settings that can prevent correct or fast builds. start runs the project dev server under a detached supervisor. ios and android build or restore the app, install it, and launch it on the owned device. logs --errors gives the agent a direct pass or fail check. An empty result with exit code 0 means that the current error window is clean.

Isolated workspaces and owned devices​

  • Each project or worktree gets a reserved Metro port.
  • Each environment gets a simulator or emulator named stim-<label>.
  • Stim operates only on devices that it created and recorded as owned.
  • Physical devices and user-created virtual devices are outside the product scope.
  • worktree create creates an isolated git worktree and prints only its path to stdout.
  • worktree remove checks git state before it removes a worktree and its environment.
  • stop stops the supervisor and shuts down the device without deleting it.
  • gc reports stale state. Only worktree remove and gc --delete delete owned resources.

Several agents can work in parallel without sharing a bundler, device, runtime state, or project log stream.

Supervised dev servers and structured logs​

start runs bare React Native Metro in process. It runs the project Expo dev server as a child when the project uses Expo. Both modes use the workspace's reserved port and verify server identity before reporting success.

The CLI merges bundler, client, build, and device records into one NDJSON timeline. logs can filter by time, source, level, text, or the current error window. logs --follow streams new records. status summarizes all managed environments and reports active processes, assigned devices, ports, cache results, and errors.

Shared native build cache​

ios and android calculate a native fingerprint with @expo/fingerprint. A matching artifact can be reused by another worktree. The cache key includes the platform, build configuration or variant, and target class.

  • iOS supports --configuration <name>.
  • Android supports --variant <name>, including product flavors.
  • Release builds skip Metro and contain the current JavaScript bundle.
  • A release cache hit receives the current JavaScript before installation.
  • A failed JavaScript swap falls back to a full build.
  • Concurrent requests for one fingerprint use a single build.
  • A configured Expo build cache provider can act as a second cache tier.

Stim also enables Xcode's compilation cache when the installed Xcode supports it. Android builds use Gradle's shared build cache. Device boot starts in parallel with compilation.

Shared Metro transform cache​

@stim-cli/metro provides a Metro transform cache shared by every worktree. The package also provides the NDJSON reporter used by bare React Native dev servers. Stim injects the shared store without requiring a project change when the installed React Native or Expo version supports that path.

Cache inspection and cleanup​

Cache packages register their storage with the shared cache manifest. stim gc reports registered and detected caches without deleting them.

npx --package=stim-cli stim gc
npx --package=stim-cli stim gc --delete --older-than 30
npx --package=stim-cli stim gc --delete --all

Age-based cleanup removes independent entries. Atomic stores, such as Xcode's content-addressed store, are emptied only with --all.

Machine and project settings​

Project settings can select the iOS device type, iOS runtime, Android system image, Android build variant, worktree defaults, and project label. Machine settings can move cache roots and set optional build or device concurrency limits. Environment variables override machine cache and concurrency settings.

The CLI keeps runtime state under $STIM_HOME, which defaults to ~/.stim. The normal workflow does not require generated project files or an initialization command.

Built-in agent guidance​

The stim-cli npm package includes an agent skill and version-matched guidance. guide prints the reference for the installed CLI version. Topics cover facts, lifecycle, Metro, logs, errors, cleanup, and settings.

Packages​

  • stim-cli - the command-line interface and bundled agent guidance.
  • @stim-cli/core - shared cache paths, cache keys, locks, and cache registration primitives.
  • @stim-cli/metro - the shared Metro transform cache and NDJSON reporter.
  • @stim-cli/expo-build-cache - a local Expo build cache provider for Expo-driven native builds.

All four packages use version 1.0.0-rc.0 and are released together.

Requirements and limits​

  • Node.js 20.19.4 or later on Node 20, or Node.js 22.12.0 or later.
  • Xcode and an installed iOS Simulator runtime for iOS work.
  • Android SDK tools and an installed emulator system image for Android work.
  • Bare React Native and Expo projects are supported.
  • Store signing, distribution builds, and physical devices are outside scope.
  • The package surface can change during the release-candidate cycle.

The source, package documentation, and issue tracker are available at appandflow/stim.