feat(herd): sessions that outlive your terminal (PRD 0009) - #342
Merged
Conversation
…ys which one wants you
Implements PRD 0009. `moshcode start claude -d` now runs the engine in a
runtime that outlives the pit and hands the prompt straight back, so the
pit stops being a one-thing-at-a-time shell: `/ps` shows what is running
and what state each session is in, `/attach` steps into one, detaching
leaves it running.
Two substrates behind one interface, because "requires tmux" was the wart
worth removing rather than documenting:
tmux a single named server (socket `moshcode`, not tabs.mjs's per-pid
one). Full fidelity — resizing, scrollback, native attach.
pty no tmux: script(1) allocates the pty, the child is detached with
its stdin on a FIFO held O_RDWR so it never sees EOF when the pit
exits, and attach replays the transcript and relays keystrokes.
Sized from inside via stty; a later resize cannot reach it.
none foreground passthrough, exactly as today, said once.
Semantic state (working/blocked/done/idle/unknown) with herdr's rule that
each session has ONE authority: a live hook report suppresses screen
classification entirely, and expires so a crashed agent cannot read
`working` forever. Screen rules ship beside each engine's install spec,
are anchored to things a terminal draws rather than English words, and
are overridable in ~/.moshcode/herd/rules.json when they rot.
`blocked` can page the operator through the existing notify()/ask()
fan-out, with the reply typed back into the session that was waiting —
the part herdr structurally cannot do. Only transitions notify, so a
session sitting blocked does not page every five seconds.
One surface for humans and agents: every verb takes --json, `wait` exits
0/2/3 so scripts can branch, and moshscript gets herdStart/herdPrompt/
herdWait/herdRead/herdList/herdKill as values rather than exit codes.
Two bugs the survival test caught, both of which would have shipped as
"finished agents report gone": tmux's remain-on-exit was set in a second
call that a fast command could beat, now one invocation via tmux's `;`;
and the pty substrate could not tell a finished agent from a rebooted
box, now the session's own shell records its exit.
Named `herd`, not the `runtime`/`agent <verb>` the PRD first proposed —
`agent` is already an alias of `agents` and a test pins that, and
`runtime` is what src/runtime.mjs already means. PRD updated to match.
Deferred and stated in the PRD: auto-installing the status hook into each
engine (the protocol ships and works), pointing ttyd at a real session,
and scrollback replay across reboots.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ThreatCrush Security Scan91 finding(s) HIGH/CRITICAL: 2 | MEDIUM: 41 | LOW: 48
…and 41 more. Full results in the Security tab. Snippets are redacted; ThreatCrush never prints matched credential material. |
ralyodio
marked this pull request as ready for review
August 9, 2026 17:07
…node exit under it CI caught this; a green local suite did not. Thirteen tests in herd-cli.test.mjs came back `cancelledByParent`, which was the test runner's way of reporting that the file's process had died mid-run. The cause is a real bug in shipped behaviour, not a test artifact. The poll timer in waitFor() was unref'd, copying the instinct from mirror.mjs where a background nicety must never hold the process open. Here that is exactly backwards: `wait` and `watch` exist to keep the process alive. An unref'd timer means node finds nothing scheduled between polls and exits. It does not hang — it is worse. `moshcode wait api --timeout 1h` returned in a millisecond, exit 0, having waited for nothing, and any script branching on it would have read that as "the agent is ready". Measured before: waitFor resolved never and the process exited after 1ms. After: it waits the full duration and settles. End to end, `moshcode wait` now blocks the whole timeout and exits 2, exits 0 the moment the state matches, and exits 3 for a session that does not exist. Also un-unref's the liveness poller in the pty substrate's attach, which was the same trap one step from being sprung: the follow timer beside it is already unref'd, so an attach whose stdin did not hold the loop open would have exited the instant it started. It is cleared on detach, so it never outlives the attach either. The regression test runs waitFor in its own process with nothing else pending, because the test runner itself keeps the event loop alive and hides the bug completely. Verified it fails against the old code before keeping it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Merged
ralyodio
added a commit
that referenced
this pull request
Aug 9, 2026
Bump to v0.33.0, releasing the herd (#342) — agent sessions that outlive the terminal that started them — along with the PRD behind it (#341) and the moshpit pinned-TLS proxy fix (#343), all of which have been sitting on main unreleased. Minor rather than patch: #342 adds six commands (herd, ps, attach, kill, wait, restore) and six moshscript verbs, and changes none of the existing ones. `moshcode start claude` with no -d behaves exactly as it did. This release is what makes any of it reachable. install.sh serves the latest release tarball rather than main, so until a release carries it every installed machine answers `unknown command "ps"` — and the npm channel only moves when publish.yml sees a published GitHub release. No plugin bumps: stocks and crypto are untouched, and neither names a command that moved. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Implements PRD 0009.
The change
moshcode start claude -druns the engine in a runtime that outlives the pit and hands your prompt straight back. The pit stops being a one-thing-at-a-time shell:Close the terminal, drop the SSH link, come back tomorrow —
moshcode psstill answers. In the pit:/ps,/attach,/kill, and the roster prints on the way in.Opt-in throughout.
moshcode start claudewith no-dbehaves exactly as it does today.Two substrates, because "requires tmux" was worth removing rather than documenting
moshcode, nottabs.mjs's per-pid one). Full fidelity — resizing, scrollback, native attach.script(1)allocates the pty, the child is detached with its stdin on a FIFO heldO_RDWRso it never sees EOF when the pit exits.attachreplays the transcript and relays keystrokes. Sized from inside viastty; a later resize cannot reach it.MOSHCODE_HERD=ptyforces the fallback, which is how it gets tested on a box that has tmux.Semantic state
working/blocked/done/idle/unknown, with herdr's rule that each session has exactly one authority: a live hook report suppresses screen classification entirely (and expires, so a crashed agent cannot readworkingforever). Screen rules ship beside each engine's install spec, are anchored to things a terminal draws rather than English words — a test enforces that — and are overridable in~/.moshcode/herd/rules.jsonwhen they rot.blockedcan page you through the existingnotify()/ask()fan-out, with your reply typed back into the session that was waiting. Only transitions notify, so a session sitting blocked doesn't page every five seconds.One surface for humans and agents
Every verb takes
--json.waitexits 0 matched / 2 timed out / 3 gone so scripts can branch. moshscript getsherdStart/herdPrompt/herdWait/herdRead/herdList/herdKillas values rather than exit codes, which is what makes fan-out practical:Two bugs the survival test caught
Both would have shipped as "finished agents report
gone":remain-on-exitwas set in a second call, which a fast command finishes before — now one invocation using tmux's;argument.Naming
herd, not theruntime/agent <verb>the PRD first proposed.agentis already a registered alias ofagentsandtest/help.test.mjspinssuggest("agent") === "agents";runtimeis whatsrc/runtime.mjsalready means. The five most-used verbs are top-level anyway. PRD updated to record the change and why.Verification
send-keys,read,kill. Both engines' first-run prompts were detected asblocked— true positives. Codex's›selector glyph came out of that run and is now in the rules with a test.Deferred, and stated in the PRD
Auto-installing the status hook into each engine's config (the protocol ships and works, so tier 1 is opt-in for now); pointing
console.mjs's ttyd at a real session; scrollback replay across reboots (P2, opt-in by design).🤖 Generated with Claude Code