Version Packages - #14
Merged
Merged
Conversation
github-actions
Bot
force-pushed
the
changeset-release/main
branch
4 times, most recently
from
August 10, 2026 03:11
ac06333 to
83e19d2
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
August 10, 2026 03:46
83e19d2 to
4de1d64
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
Releases
devctl@1.4.0
Minor Changes
#12
f5831ac-devctl config initwrites a devservers.json from the servers the daemon already knows, which is the way back from losing one. The file is routinely gitignored per machine and nothing could regenerate it, so a lost or destroyed config meant retyping every server by hand. What it writes is deliberately portable: an effective or rebound port, a worktree-derived host, a materialized url, an absolute icon path, and the port and host variables devctl injects are all dropped, so a file recovered inside a linked worktree is still correct on someone else's machine.--dry-runshows the content without touching disk, and an existing file is refused unless--force.devctl register --writeappends one server to the file, leaving every other entry alone.devctl config checknow reports the host a start would actually use when it differs from the declared one. A linked worktree gets an ephemeral host, so any origin the app itself pins (an auth callback URL, a CORS allow list, an API key referrer restriction) was already wrong with nothing saying so until the app broke.devctl stopon a server that declares a resource points atdevctl lock, which gets exclusive access without a bounce, andServerStatuscarries the server'slocksso the same fact reaches--jsonconsumers. The session context block namesdevctl lockas the way to exclusive access, and its invitation to report devctl friction now says to describe the problem generically rather than naming the project, since that backlog lives outside it.#18
0069434- devctl installs from Homebrew:brew install --cask quantizor/tap/devctl.brew upgradekeeps it current, and when a newer version ships the menu bar popover shows a quiet notice with a one-click Upgrade button that runs the upgrade in Terminal (a Homebrew install) or links to the release notes (a direct download). A direct DMG download still works exactly as before.A new Settings window, opened from the gear at the bottom of the popover, is the way back to anything you skipped at first run: install or remove the Claude Code and Cursor session hooks per harness, toggle Start at login, and turn the update check on or off. devctl still only edits a harness's settings when you click; it never changes them on its own.
Removing devctl is now a single command.
devctl uninstallunregisters the background agent, removes the agent hooks, and removes the CLI, keeping your data unless you pass--purge; running servers keep going. The Settings window offers the same as a button.devctl doctorreports a harness whose hook is missing or points at a path that no longer exists, and names the command to fix it.#13
5f24f46-devctl lockcan now tell you when a command changed the state it was guarding while a server still held that state open. Alocksentry may name where the resource lives on disk ({"name": "d1", "path": ".wrangler/state/v3/d1"}, alongside the plain"d1"form, which keeps working unchanged). With a path declared,lockfingerprints that state before and after the command. Under--no-pausewith a declaring server still running, a change is aresource-mutatedfailure naming the servers to stop and the command to re-run, because the running server holds the old state open and can write its cached pages back over what the command wrote. Under the default paused mode the same change is just a note.This closes a silent data loss: a migration run under
--no-pausethat wiped and rebuilt a local database reported success while the seeded rows were gone, and nothing in the output distinguished that from a clean run. The check is deliberately modest about its limits, and the contract states them: it flags the risk window rather than the damage, it cannot see state outside the declared path or divergence that never reaches disk, and above 8 MiB it samples a file rather than hashing it whole.#13
5f24f46-devctl lockno longer passes its own options to the guarded command.devctl lock d1 --timeout 300 -- cmdranenv --timeout 300 -- cmdand died withenv: illegal option -- t, because the resource name ended option parsing and everything after it was captured as the command. The command is now taken from after--verbatim, so a nested--, a dash option, and an empty string all survive, while a missing terminator or an unknown option is rejected instead of quietly passed through.A contended
devctl locksays who holds the resource instead of blocking silently for up to five minutes. It names the holder's pid, how long that run has been going, and which servers it paused or left running, then repeats a still-waiting line while it waits. Silence there reads as a hung gate, and the reflex it invites is killing the run that holds the lock, which is the one making progress.--acquire-timeout 0now makes exactly one attempt and fails immediately, which it could not do before: the wait loop never ran its body at a zero budget and failed with a message naming no holder. All oflock's own output moved to stderr, so stdout carries only the guarded command's.#11
ae6a118- Port conflicts now announce themselves at the first surface that can see them.whynames the server and project holding a stopped server's port instead of answering only "not running (stopped)", and the machine-wide status sweep carries the same annotation, so the menu bar app anddoctorsee it too.doctorgains aport-collisionfinding for two unrelated projects that declare one port, which the host-keyed signature table could never report because the hostnames differ while the bind does not; sibling worktrees stay excluded since they rebind by design. A server whose healthcheck is answered by another supervised server now fails withportConflict.state: "foreign"rather than reporting healthy while serving nothing, and a port held by this server and a stranger at once is reported as"shared". A listener outside the process tree that cannot be attributed to a managed server is annotated but left running, because a container-backed or daemonizing server keeps its socket in a process devctl never parented. An unavailablelsofcan never fail a healthy server, since an empty result is not treated as evidence.config checkwarns when a healthcheck declarestype: httpwith nourl, which silently falls back to a TCP probe: a mistyped key otherwise yields a server reporting healthy while its HTTP layer was never checked.doctoralso stops calling a listener unmanaged when another supervised server is the one holding the port.devctl daemon status --jsonnow reportsreachable. Every other command's hint points at this one, and it previously answered{"launchd": "state = running"}with exit 0 when nothing was listening, so a script or agent following the hint was told things were fine.launchd: runningonly means a job is loaded, never that the socket accepts.#10
b7bc910- Lock resume waits until claimed ports are free before re-ensuring, so a pause no longer races a dirty bind. Composite servers declare a port span or named subports so sibling worktrees rebind a whole claim block (relative offsets move, absolute ports stay singleton), anddevctl whykeeps the refusal lines from the last run across ensure retries instead of going blank on exit 0.#15
aa209df-devctl restart <name>is a real command. Agents were writingdevctl stop X && devctl ensure Xby hand, and several assumed the verb already existed. That pair has two problems this fixes: another session'sensurecan land between the two commands, and a refusal (a held resource, a paused server, a config that no longer parses) arrives only after the server is already down, leaving it down. A restart now refuses before it stops anything, and keeps the server's resume-on-boot intent, which a manual stop clears.A server can also list the config files it reads at boot but does not reload on its own, and devctl restarts it when one changes. Without that, a long-lived supervised server keeps running the old config, so a correct fix looks like it did nothing and a test harness keeps checking stale behavior. A server whose framework already reloads its own config declares nothing and behaves exactly as before. A config a server writes during its own startup will not bounce it, one save touching several files is a single restart, and a server that rewrites its own watched file has its watch suspended with a log line naming the culprit rather than restarting forever.
#16
22d5743- A daemon that is coming back up no longer looks like one that is gone. While devctld restores supervised servers at boot it kept its socket closed, so every client gotdaemon-unreachable, which is the same answer a daemon that was never started gives. An agent polling across adaemon installordaemon restartread a busy daemon as a dead one and tried to start another.The daemon now accepts as soon as its listener is up and says which state it is in.
devctl daemon statusreportsrestoringwhile it works, and any other command waits the window out instead of failing, saying on stderr what it is waiting for. Commands that reach the daemon mid-restore are refused withdaemon-startingrather than served against half-restored state, so the ordering guarantee that made the socket closed in the first place is unchanged.#10
b7bc910- Two git worktrees of one repo can both run under supervision without editingdevservers.json. When a sibling checkout already holds the committed port,ensureauto-rebinds to a free port, keeps the main origin on the declared host, and gives the worktree an ephemeralworktree-*.<preferred>.localhostURL; unrelated projects and unmanaged listeners still get a loudport-heldnaming the holder. Session context and ensure/status JSON warn about latent and rebound conflicts so agents use the live URL, andlock --no-pauselets a harness take the mutex without stopping the server it reuses. Discarding a worktree stops its servers and frees the ephemeral host without a manual doctor pass. Bad-state servers in the session-start block still lead with adevctl whyrecommendation and a stderr line count, never the server's own output.Patch Changes
#16
22d5743- A number in devservers.json can no longer take the daemon down. Out-of-range ports were already caught, but the values beside them were not: aportsentry'soffset, aportSpanthat overflows when added to its port, and a healthcheck'shealthyAfter,intervalMs,timeoutMsandunhealthyAfterall reached code that assumed they fit.devctl config checknow reports each of them by name with the range it expected, and refuses the start rather than letting it crash.The
offsetcase was the one nothing could see. It was checked for being too small but never for being too large, soconfig checkcalled the file clean and reported no errors at all, and the failure only arrived later as an unrelated-lookingdaemon-unreachable.One bad project no longer stops the others. Reading status across every project validated each one's config along the way, so a single unusable number anywhere on the machine took down the daemon supervising all of them, and the menu bar's polling brought it straight back to do it again.
A damaged state file is no longer fatal either. A process id too large to be one was read back from disk and used directly, which crashed the daemon on startup, and starting again re-read the same file. Such a value is now treated the way an exited process already was.
devctl lockno longer reports success while protecting nothing. When a project's config could not be read, the lock found no servers declaring the resource, paused none of them, and said it had taken the hold, so the guarded command ran against a live server still holding the resource open. It now refuses.#16
22d5743- A repo's devservers.json can no longer reach outside what it describes. A server name became a path component of the log directory verbatim, so a name containing../made the daemon create directories elsewhere on the machine and write that server's raw output into them. Names are now flattened to a single component, and two names that flatten alike keep separate homes.Session context is devctl's own words again. A server name, url, or head went into the fenced block unescaped, and a JSON key legally holds a newline, so a pulled branch could close the fence and continue as though the harness were speaking. Those values are now kept to one line and the fence is left intact. The port-conflict warning also carried the squatting process's own command line, chosen by that process; it now says which port and what state, which is the part devctl knows.
A port a TCP port cannot hold took the daemon down.
config checkaccepted"port": 70000, and the first probe against it crashed devctld, which under launchd came straight back, re-read the same config, and crashed again. Out-of-range ports, including aportSpanthat runs past the end, are now config errors, and the probe answers that nothing is listening rather than failing.Logs no longer repeat themselves. A server writing while devctl was reading its output had the overlap ingested twice, so lines appeared in duplicate and error tallies counted them twice.
Two crashes in the same moment now raise two notifications. The menu bar tracked how far it had read using a value it advanced mid-pass, so when several servers went down together, only the first was announced.
#16
22d5743-devctl locknow catches a change to a large database file that it used to miss. A lock resource above 8 MiB was fingerprinted by its head, its tail, its size, and its mtime, so a command that rewrote the middle while preserving all four was reported as no change at all. A local sqlite database is exactly the shape that happens to, and not noticing is the worst answer a check that exists to notice can give.A file is now hashed whole at any size, read in chunks so the cost is memory-flat, and the identity no longer claims to be exact when it is not. A directory keeps a byte budget, since its cost is the sum over the whole tree and the fingerprint is taken twice per guarded command.
#19
73280a7- A cloned repo's devservers.json can no longer start itself. devctl's rule is that it never acts on a project's committed config until you approve it by running a server there once, but boot restore skipped that check: after a reboot it would bring back a committed server for a project that was never approved. It now honors the same gate every other path does, so an unapproved project's config stays inert until you start it by hand. An explicitstart,ensure, orupstill records that approval, exactly as before.devctl registernow screens a server the same way the config file is screened. Registering a server directly was the one way into the daemon that skipped validation, so a specdevctl config checkwould reject (an out-of-range port, an empty command, a name containing the reserved::) could still be registered and then started. It is refused up front now.devctl switchvalidates the branch's devservers.json before running that branch's lifecycle commands. A config the daemon would refuse to load no longer has its commands handed to the shell anyway, and an empty lifecycle command is now caught byconfig check.A bad
--greppattern can no longer hang the daemon. A regular expression that nests one unbounded repeat inside another (the classic(a+)+) makes the engine run for minutes on a single log line;devctl logs --grepnow rejects that shape before it runs, with a message that names the fix.devctl no longer hangs waiting on a stuck daemon. A wedged daemon used to leave
devctland the menu bar app blocked with no output and no way out; requests now fail in bounded time and point you atdevctl daemon restart, while a legitimately longensure,wait, or group rollout is given the room it needs.Editing a project's config through the menu bar can only write to a project devctl already tracks, closing a path where a crafted request could have dropped a devservers.json anywhere on disk.
#18
0069434- The installer no longer tells you to fix a PATH that is already fine. It asked the app's own process for its PATH, and the app is launched by Finder, so that was launchd's rather than your shell's: it can never contain~/.local/bin, so the warning appeared for everyone whatever their shell actually had.Asking a login shell was not enough either.
zsh -lwithout-iruns.zshenv,.zprofileand.zloginand skips.zshrc, which is where most tools put themselves. On the machine this was found, that meant devctl handed every server it started a PATH missing~/.local/bin, pnpm, conda and gcloud, so a server script callingdevctlcould not find it. Both the warning and the PATH your servers inherit now reflect what your shell really has.Reading that PATH means running your shell profile, which devctl does not control, so it now gives up after a while and falls back rather than waiting forever. A profile that waits on the network or on a terminal that is not there used to hang the app at launch with nothing on screen.
DEVCTL_RESOLVING_ENVIRONMENTis set while it runs, so a profile can skip whatever needs a real session.b87a4cc- Installing and upgrading devctl from the DMG is clean, with none of the glitches that used to appear around the handoff to Applications.Installing no longer leaves two menu bar apps running. A second copy of the same app doubles everything you see: two icons, two pollers, and two notifications for one crash. Nothing prevented it, and the install hands off by asking macOS for a new instance by name, so any second trigger produced one, whether that was the relaunch button racing the automatic handoff, a squatting copy that would not quit, or opening the app in Finder while it was already running. A copy that finds the same app already running now steps aside on its own, whichever way it was launched.
Upgrading no longer flashes two menu bar icons. Double-clicking the app in the disk image used to briefly show its icon next to the one for the copy already installed, before the handoff finished. The copy on the disk image now runs purely as an installer: it shows only its setup window and never adds a menu bar icon, so the single icon you see stays the installed one throughout. The disk image copy and the Applications copy still run side by side for the moment the handoff needs, since that pair is the one case where two is correct.
The disk image ejects right after you install. The daemon could end up running its program straight from the mounted image instead of the copy in Applications, because the image and the installed app share an identity and macOS sometimes preferred the mounted one; while that lasted the disk reported itself in use and refused to eject until the daemon happened to restart. The daemon now notices at startup when it is running from a mounted volume and relaunches itself from the installed copy first, so the image is free to eject as soon as you have dragged the app to Applications.
Confirming an upgrade now stops rather than replacing the app in Applications while an old copy is still running it, and says which app to quit. It used to try for a few seconds, give up quietly, and replace the bundle anyway, leaving that copy running code no longer on disk.
#16
22d5743-devctl hook installcan no longer erase your harness settings. It merges its session hook into a file it does not own, and it writes that whole file back, so it has to read everything already in it first. When that read failed, for a stray character mid-edit or anything else that stopped the file parsing, it treated the file as empty and wrote it back with only its own hook in it, taking every other hook, permission and setting along with it, then reported a successful install. It now leaves the file alone and says which file it could not read and why.The port shown for a server is the port it is actually on. When a server rebound to a different port to avoid a collision with a sibling checkout, the menu bar and the statusline still showed the port it had asked for, sending you somewhere nothing was listening, while the session context shown to agents had it right. All three now agree.
A version mismatch between
devctland a running daemon is reported rather than skipped. If the opening handshake failed partway, the connection was left half-open and every later request on it went out without the check ever running again.A timestamp before 1970 no longer comes out in a form devctl cannot read back.
#12
f5831ac- A server that loses its port to another supervised server now names that server the way a person reads it. The message carried devctl's internal server id, so it readmanaged server '/Users/me/code/proj::web', and it now says'web' in /Users/me/code/projwith adevctl stopcommand that can be run as printed.#21
327fc60- Server teardown is now safe against pid reuse. Every signal devctl sends while stopping or cleaning up after a server is checked against the identity it recorded for that process while it was alive, so a pid the kernel has since handed to an unrelated process is never signaled. The deliberate-stop path and the crash-cleanup path now share one revalidated sweep, and the crash path, whose process is already gone, no longer directs a signal at its former process group at all.devctl stopnow also cleans up a descendant that escaped into the background. A server that spawns a helper which itself exits, leaving a grandchild reparented away, used to leave that grandchild running after a stop; the stop now sweeps the server's whole session, not only the processes still directly parented to it.The daemon refuses to start rather than erase its own records. If a saved store (the registry, run state, or resource locks) exists but cannot be read, from an I/O error or too many open file descriptors, devctld now exits and lets launchd retry instead of treating the data as absent and overwriting it on the next write. A missing file still starts clean, and an unparseable one is still quarantined and rebuilt.
#10
b7bc910- Installing or restarting the daemon no longer floods the menu bar with false "server crashed" alerts for the expected bounce, and boot restore finishes before clients connect so upgrade re-ensure does not race and leave servers on a bad port.devctl whyalso diagnoses file-backed projects correctly, and an unexpected server exit tears down leftover child processes so the next ensure is not blocked by stale locks.#12
f5831ac- A head or healthcheck url written as a path (/admin) now resolves against the server's own effective base, so it follows a rebind or a worktree host swap. It previously serialized as//:33334/admin, a value that reads as a URL in the menu bar, Spotlight,devctl open, and agent context, and works in none of them.config checknow rejects a head or healthcheck url that is neither an absolute URL nor a path, and one written as a path on a server that declares no port or url to resolve it against, so the mistake is caught while it is still cheap.devctl upnow spawns with the same specensuredoes. It prepared each server's spawn and then re-resolved the supervisor inside its dependency waves, which re-applied the committed config to anything not yet running and discarded the prepared spawn: the rebound port, the worktree host, the substituted{port}/{host}argv, and the injected environment. In a linked worktree that meant the child bound the committed port while devctl reported the rebind, soupin two sibling checkouts left both servers stuck starting. Sibling worktrees now coexist underupexactly as they already did underensure.#16
22d5743- A crashed server no longer leaves its workers running. When a supervised server spawned a helper process and then crashed, that helper could survive forever, holding its port and its files while devctl reported the server as gone. The next start would then fail on a port held by a process nothing was tracking.Two things had to line up, and both are common. A helper started through most process APIs lands in its own process group, so signalling the server's group never reaches it, leaving devctl's record of live descendants as the only way to find it. That record was refreshed when the server started and then not again until its first healthcheck, which for a server declaring no healthcheck is a couple of seconds later. A helper started in between was in no record at all.
Teardown now also sweeps by session, which is the one relationship that survives the server exiting and its helpers being adopted by the system. The record is refreshed throughout startup as well, so the common case is caught before the sweep is needed.
be2e7e4- A non-finite--timeoutno longer crashes the command or the daemon. Passing--timeout inf(or a value large enough to overflow) toensure,wait,restart,up,switch, orlocktook the command down with a runtime error, and the same value sent on to the daemon crashed the daemon itself, which then respawned under launchd. The command now falls back to its normal response deadline, and the daemon clamps any timeout it receives over its socket to a safe bound before use, so a crafted request cannot bring it down either. This matters most for the agents and scripts that drive devctl with computed timeouts.