feat(gc): replace long-running supervisor with cron-driven `pty gc`
The launchd-managed `pty-supervisor` daemon raced /Volumes/SSD's mount
on Mac boot: permanent sessions whose `dist/server.js` lived under the
external volume failed restart with MODULE_NOT_FOUND, exhausted 5
retries x 2s backoff within 10s, then were marked "no metadata" and
never came back without manual `pty up`. A long-running supervisor will
always have this class of bug — it can't be sure its dependencies are
ready when launchd fires it.
Replace it with a stateless reconciliation pass:
pty gc
invoked periodically by launchd (`StartInterval=30` by default) or any
other cron-driver. Three steps:
1. Orphan-children: sessions tagged `parent=<name>` whose parent's
metadata is gone OR whose parent's pid isn't alive get SIGTERM'd
and cleaned up. New user-facing tag.
2. Permanent respawn: every `strategy=permanent` session that's
exited/vanished is respawned via `spawnDaemon`. Sessions tagged
`ptyfile` re-read pty.toml to pick up edits.
3. Sweep: exited/vanished non-permanent sessions get `cleanupAll`'d
(the historic gc behavior).
Each run is independent — no persistent restart bookkeeping, no
MAX_RESTARTS, no backoff state to drift out of sync with reality.
Cron interval IS the rate limit; if /Volumes/SSD isn't mounted, the
invocation exits with ENOENT and the next tick tries again.
Install helper: `pty gc --print-launchd-plist [--interval=N]` prints
a minimal plist to stdout. No FDA wrapper, no compiled C binary, no
esbuild bundling — `<ProgramArguments>[pty, gc]</ProgramArguments>`
with `<StartInterval>`. User redirects to ~/Library/LaunchAgents/
and `launchctl load`s themselves.
Breaking changes:
- Deleted `pty supervisor *` commands entirely. Users who installed
the old supervisor should `launchctl unload` + remove the old
plist (see CHANGELOG for the exact commands).
- Deleted event types `session_restart`, `session_failed`,
`supervisor_start`, `supervisor_stop`. Added `session_respawn`.
- Deleted reserved tag `supervisor.status`. Added user-facing tag
`parent=<name>`.
- Dropped `strategy=temporary` (was functionally identical to no
strategy tag under the new sweep behavior).
- `gc()`'s return shape changed from `string[]` to a structured
`GcResult` with four buckets (`removed`, `killedOrphanChildren`,
`respawned`, `respawnFailed`). Public API.
Net delta: ~1830 lines deleted, ~485 added.
docs: fix stale client API + development docs (onboarding audit) (#63)
Audited README + all repo docs against the code on main. Two files carried
factual errors; the rest (README, CHANGELOG, disk-layout, SKILL, testing)
verified accurate.
docs/client.md:
- gc() documented as `Promise<string[]>` returning removed names; it actually
returns a `GcResult` object. Corrected the signature, example, and added the
GcResult shape.
- getSessionDir() described only `PTY_SESSION_DIR`; it prefers the canonical
`PTY_ROOT` (legacy name still honored).
- isReservedTagKey() listed `supervisor.status`, which is no longer a reserved
key (the supervisor was replaced by cron gc). Removed it.
DEVELOPMENT.md:
- Said the project "ships TypeScript directly, no build step, run via tsx".
Actually: tsx is not a dependency, `npm run build` compiles src/ to dist/
(rewriteRelativeImportExtensions), the published package ships dist/, and
bin/pty runs dist/cli.js with Node. Rewrote the build section, the design-
decision note, the architecture line, and the bin/pty entry; dev-usage
examples now use `node --experimental-strip-types src/cli.ts`.
- `pty run <name> <command>` -> `pty run -- <command>` (actual syntax).
- Socket-override env `$PTY_SESSION_DIR` -> canonical `$PTY_ROOT`.
verify-docs (docs/testing.md executable examples) still passes: 13/13.
lean-core: delete pty state + pty wrap; --help + completions rewrite (#58)
Two lean-core deletes and an accuracy pass on the help / completion
surface. Nathan authorized, cos briefed, usage-check across pty,
eval-sandbox/st-evals, convoy, pty-claude-launcher, cos, pty-relay,
and pty-layout came back clean — no external consumers.
BREAKING (no back-compat shim):
- `pty state` (subcommand + programmatic API + events + metadata field)
gone. CLI subcommands `get`/`set`/`delete`/`keys` removed. Public
API exports `getState`, `getStateKey`, `setState`, `deleteState`,
`listStateKeys` dropped from `@myobie/pty/client`. Event types
`state.set` and `state.delete` removed from `EventRecord`.
`SessionMetadata.state?` field removed — Storage format change;
existing metadata files with a populated `state` object silently
drop the field on the next daemon-side rewrite.
- `pty wrap` / `pty unwrap` / `pty wrap --list` removed. `PTY_BIN_PATH`
env var no longer consumed. `~/.local/pty/bin/` no longer created;
existing shims can be `rm -rf`'d by hand.
Redundant with smalltalk's folder-and-bus persistence (for state) and
orthogonal to the session primitive contract (for wrap).
Accuracy pass:
- `usage()` rewritten. Commands grouped logically (Create / Attach &
interact / Observe / Modify / Lifecycle / Multi / Global); every
flag every current subcommand accepts listed; `<ref>` semantics and
the four env vars (PTY_ROOT / PTY_SESSION_DIR / PTY_ROOT_LEGACY_SILENT
/ PTY_SESSION) documented.
- `completions/pty.{fish,bash,zsh}` rewritten against the same surface.
Every current subcommand + every accepted flag covered; `state`,
`wrap`, `unwrap` removed. All three shells consistent.
Tests: tests/state.test.ts deleted (485 LOC). tests/atomic-writes.test.ts
and tests/events.test.ts shed their state-specific cases.
Suite: 1202 passed, 21 skipped, 0 failed (down from 1231 in main; -29
across the deleted state.test.ts + removed state.* format assertions +
one atomic-writes setState concurrency test).
Delta: +467 / -1223 across 14 files (13 modified, 1 deleted).