moshcode 0.32.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,6 +28,12 @@ or miss one that does. A test fails the build when it drifts.
28
28
  |---|---|---|
29
29
  | `moshcode agents` | engines | list engines or launch one autonomously |
30
30
  | `moshcode start` | engines | launch an engine with its native defaults |
31
+ | `moshcode herd` | runtime | run agent sessions that outlive this terminal |
32
+ | `moshcode ps` | runtime | list herd sessions and what each one is doing |
33
+ | `moshcode attach` | runtime | attach this terminal to a herd session |
34
+ | `moshcode kill` | runtime | end a herd session |
35
+ | `moshcode wait` | runtime | block until a session is blocked, done, or idle |
36
+ | `moshcode restore` | runtime | rebuild the herd's sessions after a reboot |
31
37
  | `moshcode install` | engines | install an engine or workflow tool |
32
38
  | `moshcode uninstall` <br>`remove` | engines | take an engine or workflow tool off this machine |
33
39
  | `moshcode upgrade` <br>`update` | engines | update moshcode, engines, or tools |
@@ -102,6 +108,103 @@ is shorthand for `moshcode start claude`. In the TUI, use `/agents <engine>` for
102
108
  autonomous mode or `/start <engine>` for raw mode. Running `moshcode agents` or
103
109
  `/agents` without an engine still lists engines and their install status.
104
110
 
111
+ ## The herd — sessions that outlive your terminal
112
+
113
+ Every launch above hands an engine the whole terminal and waits. That is why
114
+ they feel native, and it is also why the pit can only do one thing at a time and
115
+ why closing the lid kills the work.
116
+
117
+ The herd inverts it. Add `-d` and the session runs in a runtime that outlives
118
+ the pit, so you get your prompt back immediately:
119
+
120
+ ```sh
121
+ moshcode start claude -d --name api # runs in the background, prompt returns
122
+ moshcode agents codex -d # autonomous, and still detached
123
+ moshcode ps # who is running, and who wants you
124
+ moshcode attach api # step in; Ctrl-b d steps back out
125
+ moshcode kill api # end it
126
+ ```
127
+
128
+ Close the terminal, drop the SSH link, come back tomorrow — `moshcode ps` still
129
+ answers, and `moshcode attach` puts you back inside. In the pit the same verbs
130
+ are `/ps`, `/attach`, `/kill`, and the roster prints on the way in.
131
+
132
+ ### Which one needs you
133
+
134
+ Every session carries a state: `working`, `blocked`, `done`, `idle`, or
135
+ `unknown`. `blocked` means a human decision is the only thing missing.
136
+
137
+ ```
138
+ api claude blocked ~/src/coinpay 12m
139
+ web codex working ~/src/ugig.net 4m
140
+ audit opencode done ~/src/moshpit-dns 1h
141
+ ```
142
+
143
+ State comes from one authority per session, never two. An engine that reports
144
+ through a lifecycle hook (`moshcode herd report <name> <state>`) is believed and
145
+ its screen is not second-guessed; everything else is classified from the bottom
146
+ of its screen. Nothing recognisable reads `unknown`, which is a safe answer —
147
+ detection never gates a launch. Patterns that go stale can be fixed in
148
+ `~/.moshcode/herd/rules.json` without waiting for a release.
149
+
150
+ Blocked can also come and find you, using the same notification fan-out as
151
+ `notify()`/`ask()`:
152
+
153
+ ```sh
154
+ moshcode herd notify on --ask # email/SMS/Slack/Telegram/push
155
+ moshcode herd start claude --name watch # then run `moshcode herd watch` in the herd
156
+ ```
157
+
158
+ With `--ask`, whatever you reply is typed into the session that was waiting.
159
+
160
+ ### Driving it from a script or another agent
161
+
162
+ There is no second API — every verb takes `--json`, and that is what a machine
163
+ reads. `wait` exists to be branched on: exit `0` matched, `2` timed out, `3` no
164
+ such session.
165
+
166
+ ```sh
167
+ moshcode herd start claude --name api --json
168
+ moshcode herd prompt api "port the auth routes" --wait
169
+ moshcode herd read api --lines 40
170
+ moshcode wait api --state blocked --timeout 1h
171
+ ```
172
+
173
+ moshscript gets the same surface as values rather than exit codes, which is what
174
+ makes fan-out practical:
175
+
176
+ ```js
177
+ herdStart("claude", { name: "api" });
178
+ herdStart("codex", { name: "web" });
179
+ herdPrompt("api", "port the auth routes");
180
+ herdPrompt("web", "port the dashboard");
181
+ await herdWait("api"); await herdWait("web");
182
+ say(herdRead("api", { lines: 20 }));
183
+ ```
184
+
185
+ ### After a reboot
186
+
187
+ ```sh
188
+ moshcode restore --dry-run # what would come back
189
+ moshcode restore --resume # and ask each engine to reopen its conversation
190
+ ```
191
+
192
+ This brings back the *shape* — the sessions, in their directories, on their
193
+ engines. The processes are new. Work that was in flight is not still running,
194
+ and `--resume` only reaches engines that have a resume flag of their own.
195
+
196
+ ### What it runs on
197
+
198
+ `tmux` when the box has it: real resizing, scrollback, native attach. Without
199
+ tmux, sessions run under `script(1)` with their input on a FIFO — they work and
200
+ they persist, but their size is fixed when they start. With neither, launches
201
+ stay in the foreground and say so once. moshcode does not turn a soft dependency
202
+ into a hard one, so `-d` never fails; at worst it degrades and tells you what
203
+ would fix it.
204
+
205
+ The session manifest and every transcript are written `0600`: engine argv and
206
+ engine output both carry secrets.
207
+
105
208
  ### Parallel pit tabs
106
209
 
107
210
  At the mosh prompt, `/new` opens and switches to another independent moshcode
package/bin/moshcode.mjs CHANGED
@@ -28,6 +28,8 @@ import { createPrd, listPrds, authoringPrompt } from "../src/prd.mjs";
28
28
  import { loginAuto, whoami, logout } from "../src/auth.mjs";
29
29
  import { tui } from "../src/tui.mjs";
30
30
  import { consoleCommand } from "../src/console.mjs";
31
+ import { herdCommand, herdStart, splitDetachArgs } from "../src/herd-cli.mjs";
32
+ import { detectSubstrate, substrateNote } from "../src/herd.mjs";
31
33
  import { dnsCommand } from "../src/dns.mjs";
32
34
  import { templateCommand } from "../src/templates.mjs";
33
35
  import { serveCommand } from "../src/serve.mjs";
@@ -117,6 +119,23 @@ function printEngineStatus(json = false) {
117
119
  }
118
120
 
119
121
  async function launchEngine(key, engine, args, { agentMode = false } = {}) {
122
+ const { detach, name, rest: engineArgs } = splitDetachArgs(args);
123
+ if (detach) {
124
+ const substrate = detectSubstrate();
125
+ if (substrate) {
126
+ const code = herdStart([
127
+ key, ...(name ? ["--name", name] : []), ...(agentMode ? ["--agent"] : []), ...engineArgs,
128
+ ]);
129
+ if (code) process.exitCode = code;
130
+ if (!process.stdin.isTTY || process.env.MOSHCODE_NESTED === "1") return;
131
+ return tui();
132
+ }
133
+ // R2: degrade, loudly, once — and then still do the thing that was asked
134
+ // for. A launch that refuses because the box has no tmux would be a worse
135
+ // answer than a launch that works and ends with this terminal.
136
+ console.error(`⚠ ${substrateNote(null)}`);
137
+ }
138
+ args = engineArgs;
120
139
  if (agentMode) {
121
140
  const note = `agent mode: ${key} ${agentLaunchArgs(engine).join(" ")}`;
122
141
  console.error(engine.agentsView
@@ -314,6 +333,18 @@ async function main() {
314
333
  const [key, engine] = resolved;
315
334
  return launchEngine(key, engine, rest.slice(1));
316
335
  }
336
+ // The herd (PRD 0009). `herd` is the namespace; the five verbs people reach
337
+ // for most often are also top-level, because `moshcode ps` is what someone
338
+ // types when they want to know what is running and nobody should have to
339
+ // learn a namespace to ask that.
340
+ if (cmd === "herd") {
341
+ process.exitCode = (await herdCommand(rest)) || 0;
342
+ return;
343
+ }
344
+ if (["ps", "attach", "kill", "wait", "restore"].includes(cmd)) {
345
+ process.exitCode = (await herdCommand([cmd === "ps" ? "ps" : cmd, ...rest])) || 0;
346
+ return;
347
+ }
317
348
  if (cmd === "tools") {
318
349
  const asJson = rest.includes("--json");
319
350
  printStatus(toolStatus(), asJson);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moshcode",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
4
4
  "type": "module",
5
5
  "description": "moshcode — a metal wrapper for coding engines and native UGig/CoinPay workflow CLIs, with OpenPRD and moshscript",
6
6
  "repository": {
@@ -0,0 +1,362 @@
1
+ ---
2
+ openprd: "0.2"
3
+ id: "0009"
4
+ title: "Keep the herd alive — a persistent runtime, semantic agent state, and one control surface for humans and agents"
5
+ status: Accepted
6
+ authors:
7
+ - anthony@profullstack.com
8
+ created: 2026-08-09
9
+ updated: 2026-08-09
10
+ repo: https://github.com/moshcoder/moshcode
11
+ discussion: https://github.com/moshcoder/moshcode/pull/341
12
+ implementation: src/herd.mjs, src/herd-state.mjs, src/herd-cli.mjs
13
+ tags: [runtime, sessions, agents, tui, notify]
14
+ supersedes:
15
+ superseded-by:
16
+ ---
17
+
18
+ ## Problem
19
+
20
+ moshcode is a *launcher*. `openPassthrough` hands the whole terminal to an
21
+ engine with inherited stdio, which is exactly why `moshcode agents claude` feels
22
+ native — and exactly why everything about that session is tied to the terminal
23
+ that started it. Three consequences, all of them daily:
24
+
25
+ **Work dies with the window.** Close the lid, drop the VPN, lose the SSH link,
26
+ reboot for a kernel update — every running engine goes with it. `tabs.mjs`
27
+ already reaches for tmux, but it starts a *private server keyed to the pit's
28
+ pid* (`moshcode-${pid}-${stamp}`, `-f /dev/null`), so the tabs are as mortal as
29
+ the pit that spawned them. There is no way to walk away from a long agent run
30
+ and come back to it.
31
+
32
+ **You cannot see which agent needs you.** The pit knows an engine is running; it
33
+ does not know whether that engine is thinking, waiting on a permission prompt,
34
+ or finished twenty minutes ago. With one engine you notice. With four tabs you
35
+ tab-cycle and squint. The information exists on the screen and moshcode throws
36
+ it away.
37
+
38
+ **The mirror is blind at the only moment that matters.** `mirror.mjs` documents
39
+ its own limit: *"once an engine takes the terminal (`/agents claude`), the child
40
+ writes straight to the tty on its own fd — those bytes never pass through this
41
+ process. The mirror shows the hand-off, not the engine's screen."* So
42
+ `/sessions` on app.moshcode.sh, the PWA, and `moshcode console` all watch a pit
43
+ that has stopped saying anything interesting. `pty.mjs` was built to solve this
44
+ and is not yet load-bearing.
45
+
46
+ [herdr](https://herdr.dev) attacks the same problem from the other end: *"a
47
+ server owns the terminals; every UI is a client of it."* Terminals live in a
48
+ background server, panes carry a semantic agent state (`idle` / `working` /
49
+ `blocked` / `done`), and the CLI and socket API are one surface so agents drive
50
+ it the same way people do. That inversion — the runtime outlives the client — is
51
+ the idea worth taking. Its implementation (a 10MB Rust multiplexer with mouse
52
+ drag, pane splits, and a plugin marketplace) is not.
53
+
54
+ This PRD ports the ideas, not the binary.
55
+
56
+ ## Goals
57
+
58
+ - An agent session survives the terminal that started it — closing the lid, an
59
+ SSH drop, or a reboot costs you nothing but a reattach.
60
+ - Opening the pit answers "what is running and what needs me?" before you type
61
+ anything.
62
+ - A blocked agent reaches the human wherever they are, and the answer comes
63
+ back into the session. moshcode already has `notify()`/`ask()` fan-out to
64
+ email/SMS/Slack/Telegram/push; a runtime that knows what "blocked" means is
65
+ what turns that into an unattended-agent story.
66
+ - The browser (`console`, `/sessions`, the PWA) becomes a real client of the
67
+ same runtime, showing the engine's actual screen instead of the hand-off line.
68
+ - One agent can start, prompt, and wait on another without a human, through the
69
+ same verbs a human types.
70
+ - None of the above changes what `moshcode start claude` feels like today.
71
+
72
+ ## Non-Goals
73
+
74
+ - **Writing a multiplexer.** No pane splits, no mouse drag, no border
75
+ resizing, no right-click menus, no theme engine. tmux exists, is already the
76
+ substrate in `tabs.mjs`, and is better at this than we will be.
77
+ - **A native binary or a compiler in the install path.** `install.sh` untars a
78
+ release and runs node; `pty.mjs` explicitly rejects node-pty for this reason.
79
+ The runtime must hold that line — zero-dependency ESM, capability detection,
80
+ graceful degradation.
81
+ - **A pane-plugin marketplace.** moshcode already ships plugins, skills, and MCP
82
+ servers on the *engine* axis (PRD 0003, PRD 0008). A second plugin system for
83
+ panes is not a gap we have.
84
+ - **Windows.** Same posture as today: POSIX first, and the capability check
85
+ fails soft everywhere else.
86
+ - **A worktree/diff review UI.** Interesting, adjacent, and a different PRD.
87
+ - **Owning the user's tmux.** People with a tmux config keep it. The runtime is
88
+ a separate named server, exactly as `tabPlan()` already reasons about.
89
+
90
+ ## Users
91
+
92
+ - **The solo operator running several agents at once** — the pit's core user.
93
+ Wants to fire off three long tasks, close the laptop, and find out later which
94
+ one stopped to ask a question.
95
+ - **A moshcoder on a rented box.** Runs the pit over SSH on a Hetzner/Railway
96
+ dev box. Every dropped connection currently kills a run. Wants `moshcode` over
97
+ SSH to reattach to what was already going.
98
+ - **The agent itself.** A `moshscript` or a Claude Code session that needs to
99
+ spawn a helper on a second engine, hand it a prompt, and block on the result —
100
+ today that means `tabs.mjs` and hope.
101
+ - **The phone.** Someone away from the desk who gets a push saying "codex is
102
+ blocked on a permission prompt in ~/src/coinpay" and answers from the PWA.
103
+
104
+ ## Requirements
105
+
106
+ ### Phase 1 — the runtime survives you
107
+
108
+ - **R1 [P0] A named, detached runtime.** `moshcode runtime` starts (or reports)
109
+ a single long-lived tmux server on a stable socket (`moshcode`, not
110
+ `moshcode-${pid}-${stamp}`), started with `-f /dev/null` so advertised
111
+ keybindings stay true. Sessions inside it outlive every pit that attaches.
112
+ `moshcode runtime status|stop --json` for the machine.
113
+ - **R2 [P0] Capability detection, and a soft floor.** Follow `pty.mjs`'s
114
+ discipline exactly: probe for a usable tmux once, cache the verdict, and when
115
+ it is missing fall back to today's foreground `openPassthrough` with a single
116
+ honest line (`no tmux — this session ends with the terminal`). moshcode must
117
+ never harden a soft dependency into a hard one.
118
+ - **R3 [P0] Named sessions.** `moshcode start`/`moshcode agents` accept
119
+ `--name <slug>` (`[a-z][a-z0-9_-]{0,31}`, herdr's shape) and default to
120
+ `<engine>-<basename-of-cwd>` with a numeric suffix on collision. The name is
121
+ the handle for everything that follows.
122
+ - **R4 [P0] Attach and detach.** `moshcode attach <name>` drops you into the
123
+ live session; the detach key returns you to the pit and leaves the engine
124
+ running. Detaching is the default exit path; killing is explicit
125
+ (`moshcode kill <name>`). Bare `moshcode` with exactly one detached session
126
+ offers to reattach rather than opening an empty prompt.
127
+ - **R5 [P0] The roster.** `moshcode ps` lists every session: name, engine,
128
+ state, cwd, age, attached-client count. `--json` for automation. The pit's
129
+ banner shows the same roster in miniature when anything is running, so opening
130
+ moshcode answers "what's alive?" before you type.
131
+
132
+ ### Phase 2 — the runtime knows what the agents are doing
133
+
134
+ - **R6 [P0] Semantic state.** Every session carries one of `idle`, `working`,
135
+ `blocked`, `done`, `unknown`. `blocked` means "a human decision is the only
136
+ thing missing" — the permission prompt, the plan approval, the y/n. This is
137
+ the vocabulary the roster, the notifications, and `wait` all share.
138
+ - **R7 [P0] Two-tier detection with one authority per session.** Adopt herdr's
139
+ rule verbatim in spirit: *"each pane has one status authority."*
140
+ - Tier 1, authoritative: engine-native lifecycle hooks. moshcode already
141
+ installs across engines (`plugins.mjs`, `skills.mjs`, `mcp.mjs`, PRD 0003) —
142
+ reuse that machinery to drop a status reporter into each engine's hook
143
+ config, and let it report state directly.
144
+ - Tier 2, fallback: classify the tail of the session's screen against a
145
+ per-engine rule table declared next to `ENGINES` in `engines.mjs`, so a new
146
+ engine ships its detection rules with its install spec.
147
+ - A session with a working tier-1 hook does **not** also run tier-2. Two
148
+ sources of truth is the failure mode herdr calls out and it is a real one.
149
+ - **R8 [P1] Unknown is a first-class answer.** An engine with no hook and no
150
+ matching rule reports `unknown` and still runs perfectly. Detection is a
151
+ feature of the roster, never a gate on launching anything.
152
+ - **R9 [P0] Blocked reaches the human.** A session entering `blocked` (and
153
+ optionally `done`) fires `notify()` through the existing approvals app with
154
+ the session name, engine, cwd, and the tail of the prompt that blocked it.
155
+ When the operator answers via `ask()`, the reply is typed into the session.
156
+ This is the piece herdr structurally cannot do — it can colour a pane; we can
157
+ text you and take the answer back. Off by default, `moshcode notify on`, and
158
+ rate-limited so a chatty engine cannot page someone forty times.
159
+ - **R10 [P1] Wait instead of poll.** `moshcode wait <name> --state blocked,done
160
+ --timeout 30m` blocks until the transition and exits with a distinct code per
161
+ outcome. Both a human in a shell script and an agent in a moshscript get to
162
+ stop screen-scraping.
163
+
164
+ ### Phase 3 — one surface for humans and agents
165
+
166
+ - **R11 [P0] Agent verbs.** `moshcode agent start|prompt|read|send-keys|
167
+ wait|stop`, every one accepting a session name and every one supporting
168
+ `--json`. `prompt --wait` submits input and blocks until the session leaves
169
+ `working` — the single most useful composite. herdr's framing is the target:
170
+ *"the cli and socket api are the same surface agents drive."* moshcode's
171
+ version of that is simply that there is no second surface; the CLI *is* the
172
+ API, and `--json` is how a machine reads it.
173
+ - **R12 [P0] moshscript verbs.** Every verb in R11 is exposed in `runtime.mjs`'s
174
+ vocabulary, so a `.mosh` script can fan work out across engines and join on
175
+ the results. This is what PRD 0004 was for and what it has been missing.
176
+ - **R13 [P1] The browser sees the real screen.** Route a session's output
177
+ through `pty.mjs` into `mirror.mjs`, closing the documented blind spot, and
178
+ point `console.mjs`'s ttyd at `moshcode attach <name>` so the browser terminal
179
+ is a genuine second client of the same live session. Multiple clients on one
180
+ runtime, which is herdr's axis and one moshcode is already three-quarters
181
+ built for.
182
+ - **R14 [P1] Restore the shape after a reboot.** Persist a manifest
183
+ (`~/.moshcode/runtime.json`, mode `0600`) of sessions: name, engine, cwd, and
184
+ the engine's own resume reference where it has one. On the next `moshcode`,
185
+ re-open the sessions in their directories and offer
186
+ `moshcode restore [--resume]` to resume native engine conversations
187
+ (`claude --resume`, `codex resume`, and per-engine equivalents declared in
188
+ `ENGINES`). Structure comes back automatically; *processes* do not, and the
189
+ UI must say so rather than implying the work continued.
190
+ - **R15 [P2] Scrollback replay, off by default.** Restoring screen contents
191
+ across a reboot means writing engine output to disk. The same reasoning that
192
+ put `.moshcode_history` at `0600` — *"the pit records whatever was typed at
193
+ the prompt, and that includes secrets by design"* — applies harder here, since
194
+ engine output includes tokens the user never typed. Opt-in flag, `0600`, and a
195
+ documented retention cap. herdr ships this the same way (`[experimental]
196
+ pane_history`) and that is the right call.
197
+
198
+ ## UX Notes
199
+
200
+ **New verbs, in the existing shape.** The command table is generated from
201
+ `cli-schema.mjs` and a test fails the build on drift, so these land as real
202
+ entries in a new `runtime` group:
203
+
204
+ | command | what it does |
205
+ |---|---|
206
+ | `moshcode herd` | the namespace: status, start, prompt, read, send-keys, report, notify, watch, prune, stop |
207
+ | `moshcode ps` | list sessions with state |
208
+ | `moshcode attach <name>` | attach to a session |
209
+ | `moshcode kill <name>` | end a session |
210
+ | `moshcode wait <name>` | block until a state transition |
211
+ | `moshcode restore` | rebuild sessions from the manifest |
212
+
213
+ The namespace is `herd`, not the `runtime` / `agent <verb>` this document first
214
+ proposed. Two reasons, both found while building it. `agent` is already a
215
+ registered alias of `agents` in `PIT_COMMANDS`, and a test pins
216
+ `suggest("agent") === "agents"` — so `moshcode agent start` would have meant two
217
+ different things depending on where it was typed. And `runtime` is what
218
+ `src/runtime.mjs` already calls the moshscript interpreter. The five verbs
219
+ people reach for most are top-level anyway, which is what the original table was
220
+ really asking for: nobody should have to learn a namespace to ask what is
221
+ running.
222
+
223
+ TUI equivalents follow the existing convention: `/herd`, `/ps`, `/attach <name>`,
224
+ `/kill <name>`, `/wait`, `/restore`. `/agents <engine>` and `/start <engine>`
225
+ keep their meaning and simply gain `-d` / `--name`.
226
+
227
+ **The pit's front door changes.** Today `moshcode` prints a banner and a prompt.
228
+ With anything running it prints the herd first:
229
+
230
+ ```
231
+ mosh ▸ 3 running
232
+ coinpay-fix claude blocked ~/src/coinpay 12m
233
+ ugig-tests codex working ~/src/ugig.net 4m
234
+ dns-audit opencode done ~/src/moshpit-dns 1h
235
+ ```
236
+
237
+ State is the column that earns its place — colour it with the existing `ui.mjs`
238
+ palette (`acid` working, `warn` blocked, `ok` done, `ash` idle). One line per
239
+ session, no box drawing, no full-screen takeover: the pit stays a prompt.
240
+
241
+ **Detach must be discoverable.** The single biggest confusion risk is a user who
242
+ thinks they quit and left an engine burning tokens. Print the detach key on
243
+ attach, and on detach print what is still running and how to get back:
244
+ `detached — coinpay-fix still working · moshcode attach coinpay-fix`.
245
+
246
+ **Naming is optional.** `--name` is there for scripts and for people who want
247
+ it. A user who never types it still gets `claude-coinpay` and can attach by it.
248
+
249
+ **Notifications are opt-in and quiet.** Default off. When on, one notification
250
+ per state transition into `blocked`, coalesced within a window, and never for
251
+ `working`. The approval link deep-links to the session in the PWA.
252
+
253
+ **Degradation is loud once, then silent.** Without tmux, the first session in a
254
+ pit prints one line explaining that sessions end with the terminal, then behaves
255
+ exactly like today. No repeated nagging, no failure.
256
+
257
+ ## Success Metrics
258
+
259
+ - **Survival:** a session started in a pit is still running and reattachable
260
+ after the terminal is closed and the machine is left for an hour — measured by
261
+ an integration test that kills the parent pit and reattaches.
262
+ - **Reattach adoption:** share of pit launches that begin with a reattach rather
263
+ than a cold start. If nobody reattaches, the runtime is not earning its
264
+ complexity.
265
+ - **Time-to-unblock:** median wall-clock between a session entering `blocked`
266
+ and a human answering, before vs after R9. This is the number that says
267
+ whether the notification path is real.
268
+ - **Detection quality:** on the top five engines, `blocked` is reported within
269
+ three seconds of the prompt appearing, with no false `blocked` in a
270
+ thirty-minute unattended run.
271
+ - **Zero-regression:** `moshcode start <engine>` launch time and native feel
272
+ unchanged; the runtime adds no measurable startup cost when no session exists.
273
+ - **Install stays clean:** `install.sh` gains no dependency, and the tmux-less
274
+ path passes the full test suite.
275
+
276
+ ## Risks & Open Questions
277
+
278
+ - **tmux is a soft dependency that this PRD leans on hard.** R2 is the mitigation
279
+ and it must be honoured in code, not just in prose: every runtime call site
280
+ needs a tested fallback. Open question: is a second substrate
281
+ (`script(1)` + a detached child, reusing `pty.mjs`) worth building for
282
+ tmux-less boxes, or is the honest degradation enough? Recommendation: enough,
283
+ for now.
284
+ - **Screen-scraping state is fragile by construction.** Engines change their
285
+ prompts between releases and the rules rot silently. Mitigations: rules live
286
+ beside each engine in `ENGINES` so they version together; `unknown` is
287
+ always safe; and tier-1 hooks are the real answer — prioritise hook coverage
288
+ for `claude`, `codex`, and `opencode` over broad rule tables.
289
+ - **A long-lived runtime is a longer-lived attack surface.** The tmux socket is
290
+ filesystem-permissioned to the user and that is the whole boundary. Anything
291
+ that widens it — R13 pointing ttyd at a session, R15 writing scrollback —
292
+ inherits `console.mjs`'s existing rule: loopback only, token-gated, `0600` on
293
+ disk. A reboot-surviving runtime holding engine scrollback is a materially
294
+ different secret-exposure profile from today's ephemeral pit, and the PRD
295
+ should not pretend otherwise.
296
+ - **Notification fatigue kills the feature.** Get the coalescing window and the
297
+ `blocked` definition right, or users turn it off in a day and never turn it
298
+ back on. Open question: should `done` notify by default? Leaning no — `done`
299
+ is what the roster is for.
300
+ - **Two ways to open a session is a real cost.** Detachable sessions plus the
301
+ existing `tabs.mjs` tmux tabs is one concept too many. Open question: does
302
+ `openNewTab` become a thin wrapper over the runtime (a tab is just an attach
303
+ to a new session), and does that break the "fresh pit, never repeats argv"
304
+ contract? Leaning yes, it should be folded in — a second tmux server per pit
305
+ stops making sense once a stable one exists.
306
+ - **Reboot restore promises more than it delivers.** Users will read "restore"
307
+ as "my agent kept going." It did not. The copy has to distinguish *the shape
308
+ came back* from *the work continued*, and `--resume` has to be an explicit,
309
+ visible act.
310
+ - **Scope.** Phases 1–3 are independently shippable and should ship that way.
311
+ Phase 1 alone — sessions that survive the terminal — is the bulk of the value
312
+ and does not require a single line of state detection.
313
+
314
+ ## Implementation Notes
315
+
316
+ Written after the build, so the document and the code agree.
317
+
318
+ **A second substrate, which this PRD did not ask for.** R2 promised only to
319
+ degrade gracefully without tmux. That was not good enough: `/new` already
320
+ required tmux and it is the wart people notice. So there are two substrates
321
+ behind one interface — tmux when the box has it, and otherwise `script(1)` with
322
+ the session's stdin on a FIFO, reusing the capability detection `pty.mjs`
323
+ already does. The FIFO is opened `O_RDWR` before the spawn so the child is its
324
+ own writer and never sees EOF when the pit exits, which is the whole trick. Its
325
+ one real limit: nothing outside a pty can ioctl its master, so the size is fixed
326
+ at launch (set from inside by `stty`) and a later resize does not reach it.
327
+ `MOSHCODE_HERD=pty` forces it, which is how the fallback is tested on a box that
328
+ has tmux.
329
+
330
+ **Two bugs the survival test caught**, both of which would have shipped as
331
+ "finished agents report `gone`". tmux's `remain-on-exit` was being set in a
332
+ second call, and a fast command finishes before that process starts — fixed by
333
+ making the session and its option one invocation using tmux's `;` argument. And
334
+ the pty substrate could not tell "the agent finished" from "the box rebooted",
335
+ since both are a dead pid — fixed by having the session's own shell record its
336
+ exit code on the way out.
337
+
338
+ **Delivered:** R1–R12 and R14. Both substrates are covered by an integration
339
+ test that starts a session in one process, exits it, and talks to the session
340
+ from another.
341
+
342
+ **Not delivered, deliberately:**
343
+
344
+ - **R7 tier-1 hook installation.** The protocol ships and works —
345
+ `moshcode herd report <name> <state>` takes authority, suppresses screen
346
+ classification entirely while it is live, and expires so a crashed agent
347
+ cannot read `working` forever. What is not built is auto-installing that call
348
+ into each engine's hook config via the `plugins.mjs` / `skills.mjs` fan-out.
349
+ Until then tier 1 is opt-in and tier 2 carries the roster.
350
+ - **R13, the browser as a real client.** `console.mjs` still points ttyd at a
351
+ shell rather than at `moshcode attach <name>`, and `mirror.mjs` keeps its
352
+ documented blind spot. The runtime it would attach to now exists, so this is a
353
+ small follow-up rather than a design question.
354
+ - **R15, scrollback replay.** P2 and opt-in in this document; still the right
355
+ call not to write engine output across a reboot by default.
356
+
357
+ **Rules will rot, and that is planned for.** The shipped patterns are
358
+ conservative and anchored to things a terminal draws — brackets, selectors, line
359
+ anchors — never bare English words, and a test asserts that. `unknown` is
360
+ common and safe. `~/.moshcode/herd/rules.json` lets a rotted pattern be fixed on
361
+ the box it rots on, and a malformed entry there loses that pattern rather than
362
+ the file.
package/prd/README.md CHANGED
@@ -24,4 +24,5 @@ Start one with `moshcode prd "<idea>"` (TUI: `/prd`).
24
24
  | [0006](0006-help.md) | --help | Draft |
25
25
  | [0007](0007-profullstack-site-init.md) | Generate batteries-included Profullstack sites for Moshpit names | Draft |
26
26
  | [0008](0008-ticker-research-and-plugin-marketplace.md) | Bring equity research into the pit, and ship the pit's slash commands as a plugin | Draft |
27
+ | [0009](0009-persistent-agent-runtime.md) | Keep the herd alive — a persistent runtime, semantic agent state, and one control surface for humans and agents | Accepted |
27
28
  <!-- PRD-INDEX:END -->