moshcode 0.31.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 +104 -0
- package/bin/moshcode.mjs +31 -0
- package/package.json +1 -1
- package/prd/0009-persistent-agent-runtime.md +362 -0
- package/prd/README.md +1 -0
- package/src/cli-schema.mjs +163 -3
- package/src/commands.mjs +102 -0
- package/src/dns-system.mjs +5 -1
- package/src/dns.mjs +167 -1
- package/src/engines.mjs +46 -0
- package/src/herd-cli.mjs +665 -0
- package/src/herd-state.mjs +227 -0
- package/src/herd.mjs +746 -0
- package/src/pty.mjs +6 -2
- package/src/tools.mjs +11 -2
- package/src/trust.mjs +60 -7
- package/src/tui.mjs +67 -4
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
|
|
@@ -643,6 +746,7 @@ chmod +x deploy.mosh
|
|
|
643
746
|
| `ugig(args…)` | drive the ugig workflow CLI |
|
|
644
747
|
| `coinpay(args…)` | drive the coinpay workflow CLI |
|
|
645
748
|
| `c0mpute(args…)` | drive the c0mpute workflow CLI |
|
|
749
|
+
| `c0upons(args…)` | drive the c0upons workflow CLI |
|
|
646
750
|
| `secrets(args…)` | drive the logicsrc secrets CLI |
|
|
647
751
|
| `railway(args…)` | drive the Railway CLI |
|
|
648
752
|
| `gh(args…)` | drive the GitHub CLI |
|
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
|
@@ -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 -->
|