moshcode 0.59.0 → 0.61.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
@@ -30,7 +30,7 @@ or miss one that does. A test fails the build when it drifts.
30
30
  | `moshcode start` | engines | launch an engine with its native defaults |
31
31
  | `moshcode herd` | runtime | run agent sessions that outlive this terminal |
32
32
  | `moshcode ps` | runtime | list herd sessions and what each one is doing |
33
- | `moshcode cost` | runtime | what each session is spending, read from the engines' own logs |
33
+ | `moshcode cost` <br>`usage` | runtime | what each session is spending, read from the engines' own logs |
34
34
  | `moshcode attach` | runtime | attach this terminal to a herd session |
35
35
  | `moshcode kill` | runtime | end a herd session |
36
36
  | `moshcode wait` | runtime | block until a session is blocked, done, or idle |
@@ -263,9 +263,9 @@ moshcode agents claude -d --name api # and an agent
263
263
 
264
264
  ```sh
265
265
  $ moshcode ps
266
- api claude blocked ~/src/coinpay 3m
267
- logs shell idle ~/src/coinpay 3m
268
- work shell idle ~/src/coinpay 3m
266
+ api claude blocked ~/src/coinpay 3m screen
267
+ logs shell idle ~/src/coinpay 3m screen
268
+ work shell idle ~/src/coinpay 3m screen
269
269
 
270
270
  ⚠ 1 waiting on you — moshcode attach api
271
271
  ```
@@ -317,9 +317,9 @@ Every session carries a state: `working`, `blocked`, `done`, `idle`, or
317
317
  `unknown`. `blocked` means a human decision is the only thing missing.
318
318
 
319
319
  ```
320
- api claude blocked ~/src/coinpay 12m
321
- web codex working ~/src/ugig.net 4m
322
- audit opencode done ~/src/moshpit-dns 1h
320
+ api claude blocked ~/src/coinpay 12m hook
321
+ web codex working ~/src/ugig.net 4m screen
322
+ audit opencode done ~/src/moshpit-dns 1h runtime
323
323
  ```
324
324
 
325
325
  State comes from one authority per session, never two. An engine that reports
@@ -416,6 +416,120 @@ await herdWait("api"); await herdWait("web");
416
416
  say(herdRead("api", { lines: 20 }));
417
417
  ```
418
418
 
419
+ Fanning work out is easy; joining on it used to be a hand-rolled polling loop.
420
+ `--any` returns on the first session to get there, `--all` when the last one
421
+ has, and both take the same `--state` and `--timeout` as a single wait:
422
+
423
+ ```sh
424
+ moshcode wait --any api web docs # --json names the winner
425
+ moshcode wait --all api web --state done
426
+ ```
427
+
428
+ ```js
429
+ const first = await herdWait(["api", "web", "docs"], { any: true });
430
+ await herdWait(["api", "web"], { states: ["done"] });
431
+ ```
432
+
433
+ ### Let the engine say what it is doing
434
+
435
+ Reading a screen works and it rots — engines change their wording between
436
+ releases and nothing tells you. When an engine has lifecycle hooks, install
437
+ them once and its state comes from the engine itself:
438
+
439
+ ```sh
440
+ moshcode herd hooks install claude
441
+ ✓ claude — 3 hooks installed (stop, notification, prompt-submit)
442
+ ```
443
+
444
+ `moshcode ps` then reads `hook` in its last column instead of `screen`. The
445
+ file is **merged, never clobbered** — your own hooks stay, and `hooks remove`
446
+ takes out only what moshcode put in. A hook that fires outside a herd session
447
+ does nothing and exits 0, so installing one cannot break an engine you run by
448
+ hand, and the screen rules stay as the fallback for everything else.
449
+ `moshcode herd doctor` says what is installed, what has drifted, and — for the
450
+ first time — what is wrong with your `rules.json` instead of ignoring it.
451
+
452
+ ### What happened while you slept
453
+
454
+ Every prompt through the herd mints a **task**: an id, its state transitions
455
+ with timestamps, and the output it produced. `ps` still answers "now"; this
456
+ answers "what happened".
457
+
458
+ ```sh
459
+ $ moshcode herd tasks api
460
+ t-01 22:14 done 4m "port the auth routes"
461
+ t-02 22:19 blocked 6h11 "run the migration"
462
+
463
+ $ moshcode herd task t-02 # transitions, and what came back
464
+ $ moshcode herd log api # the raw state history
465
+ $ moshcode herd stats api
466
+ api working 3h02 · blocked 6h11 · idle 1h40
467
+ blocked 6h11 over 2 spell(s) — that one is you
468
+ ```
469
+
470
+ Blocked time is the herd's name for *human latency*: the agent was ready and
471
+ you were asleep. Ledgers live in `~/.moshcode/herd/tasks/<session>.jsonl` at
472
+ `0600`, capped at the last 500 tasks per session. From a script,
473
+ `herdTasks(name)` and `herdTask(id)` return them as values.
474
+
475
+ ### Agents that are not on this box
476
+
477
+ A deployed agent can be a herd member. Two kinds: `a2a` speaks
478
+ [A2A v0.3.0](https://a2a-protocol.org/v0.3.0/specification/) (card discovery,
479
+ `message/send`, `tasks/get`, `tasks/cancel`), and `run` is a bare endpoint that
480
+ takes `POST {"prompt": …}` — the shape a `gradient agent deploy` prints.
481
+
482
+ ```sh
483
+ moshcode herd remote add research https://agents.do-ai.run/…/production --kind run
484
+ export MOSHCODE_REMOTE_RESEARCH_TOKEN=… # never written to the manifest, never synced
485
+ ```
486
+
487
+ ```
488
+ $ moshcode ps
489
+ api claude blocked ~/src/coinpay 12m hook
490
+ research remote idle agents.do-ai.run — remote
491
+ ```
492
+
493
+ `prompt`, `read`, `wait` and `kill` work on it unchanged, which is the point: a
494
+ fan-out script across a local pty and a deployed agent contains no `if
495
+ (remote)`. A remote's state is the *remote's claim* — `ps` says `remote` in the
496
+ last column so it is never mistaken for something this box verified — and
497
+ `kill` on one deregisters it here rather than reaching across the network to
498
+ end somebody else's agent.
499
+
500
+ ### The herd, over A2A
501
+
502
+ `moshcode herd serve` exposes this machine's herd to any A2A client: the herd's
503
+ card at `/.well-known/agent-card.json`, each member at `/<name>/`,
504
+ `message/send` → prompt, `tasks/get` → the ledger, `tasks/cancel` → interrupt.
505
+ `blocked` is A2A's `input-required`; the states that do not map cleanly round
506
+ down and carry the honest one in task metadata.
507
+
508
+ ```sh
509
+ moshcode login # it verifies tokens against app.moshcode.sh
510
+ moshcode herd serve # 127.0.0.1:7683 by default
511
+ ```
512
+
513
+ It is a shell on a socket and is treated like one: **no unauthenticated mode,
514
+ loopback included**, a loud warning past `127.0.0.1`, and sessions started with
515
+ `--agent` withheld unless you pass `--expose-autonomous` — an engine with its
516
+ approvals bypassed plus a network prompt is the worst pairing on the menu.
517
+
518
+ ### Which engine is best at *this* repo
519
+
520
+ Not a leaderboard run against engines nobody deploys on repos nobody has — your
521
+ dataset, your engines, your machine:
522
+
523
+ ```sh
524
+ moshcode herd eval --dataset evals/moshcode.jsonl --engines claude,codex --threshold 0.8
525
+ ```
526
+
527
+ A row is `{"prompt": "…", "expect": "pattern"}` or
528
+ `{"prompt": "…", "rubric": "…"}` (jsonl, json or csv). Scoring is either the
529
+ dataset's own patterns or an engine acting as judge (`--judge claude`). Exit
530
+ codes are distinct on purpose — `0` pass, `4` below the threshold, `5` the
531
+ harness could not run — because CI has to tell a worse agent from a broken box.
532
+
419
533
  ### After a reboot
420
534
 
421
535
  ```sh
@@ -487,12 +601,57 @@ moshcode install doctl # GitHub release binary → ~/.local/bin
487
601
  moshcode install turso # official script → ~/.turso (new shell to pick up PATH)
488
602
  moshcode install tailscale # official script; system daemon, so it needs root
489
603
  moshcode install coral # official script → ~/.local/bin (checksum-verified)
604
+ moshcode install spinifex # official script; Linux host platform, so it needs root
490
605
 
491
606
  moshcode gh pr list # straight through to the native CLI
492
607
  moshcode railway up
493
608
  moshcode doctl compute droplet list
609
+ moshcode spinifex ec2 describe-instances
610
+ ```
611
+
612
+ ### Spinifex — your own AWS-compatible cloud
613
+
614
+ [Spinifex](https://mulgadc.com/spinifex) is the other end of the infra list:
615
+ instead of driving someone else's cloud, it turns your own hardware into one.
616
+ EC2, EBS, S3, VPC, and IAM, API-compatible with AWS, on bare metal, edge boxes,
617
+ or on-prem racks — so the same `aws` calls and Terraform providers work against
618
+ hardware you own.
619
+
620
+ ```sh
621
+ moshcode install spinifex # curl -fsSL https://install.mulgadc.com | bash
622
+ moshcode spinifex version # straight through to the native `spx` CLI
623
+ moshcode spinifex admin init --node node1 --nodes 1
494
624
  ```
495
625
 
626
+ The product is Spinifex, the binary is `spx`, and `moshcode spinifex …` is exact
627
+ passthrough to it — the same split as `moshcode secrets` and `logicsrc`.
628
+
629
+ Spinifex is a host platform, not a standalone binary, so its installer is the
630
+ most invasive one on this list. Read this before running it:
631
+
632
+ - **Linux only**, and specifically Ubuntu 26.04 or Debian 13. The installer
633
+ pulls QEMU/KVM, OVN/Open vSwitch, and the AWS CLI through apt.
634
+ - **Root.** It writes `/usr/local/bin/spx`, systemd units, and scoped
635
+ `sudoers.d` rules. Like tailscale, it finds sudo itself; MoshCode only gets
636
+ the password prompt out of the way first.
637
+ - **Your WAN interface must already be bridged to `br-wan`** before you start —
638
+ check with `ip -br link show br-wan`. The installer does not create it, and
639
+ bridging a live uplink can drop the box off the network.
640
+
641
+ After it finishes, Spinifex's own docs take over — `setup-ovn.sh --management`,
642
+ `spx admin init`, then `systemctl start spinifex.target`. See
643
+ [docs.mulgadc.com/docs/install](https://docs.mulgadc.com/docs/install).
644
+
645
+ MoshCode passes `INSTALL_SPINIFEX_SKIP_NEWGRP=1`, because on a TTY the vendor
646
+ script ends by `exec`ing `newgrp spinifex` to activate the new group. That would
647
+ strand you in a subshell instead of returning to the pit — and would park the
648
+ rest of a `moshcode update` run behind it. Log in again (or run `newgrp
649
+ spinifex` yourself) to pick up the group.
650
+
651
+ Re-running `moshcode install spinifex` is also its upgrade path: the installer
652
+ detects the existing install, replaces the binary, applies pending config
653
+ migrations, and restarts the services.
654
+
496
655
  ### MCP server testing
497
656
 
498
657
  ```sh
@@ -511,9 +670,10 @@ MoshCode resolves the latest GitHub release and drops the binary in
511
670
  `$MOSHCODE_BIN` (default `~/.local/bin`) — no sudo, no package manager. Set
512
671
  `MOSHCODE_BIN` to install elsewhere.
513
672
 
514
- `tailscale` is the exception: it is a system daemon, so its official installer
515
- goes through your distro's package manager and will ask for sudo (on macOS it
516
- delegates to the App Store).
673
+ `tailscale` and `spinifex` are the exceptions: both install system services
674
+ rather than a user-local binary, so their official installers go through the
675
+ distro's package manager and will ask for sudo (tailscale on macOS delegates to
676
+ the App Store instead; Spinifex has no macOS build at all).
517
677
 
518
678
  MoshCode asks for that password **before** starting the work rather than letting
519
679
  the installer stop for it partway through — which matters most in `moshcode
@@ -1169,6 +1329,7 @@ chmod +x deploy.mosh
1169
1329
  | `coral(args…)` | drive the Coral CLI (SQL over APIs, databases, internal systems) |
1170
1330
  | `alpaca(args…)` | drive the native Alpaca trading CLI |
1171
1331
  | `mcpjam(args…)` | drive the MCPJam CLI (test, debug, and validate MCP servers) |
1332
+ | `spinifex(args…)` | drive the Spinifex CLI (`spx` — AWS-compatible cloud on your own hardware) |
1172
1333
  | `trade(args…)` | look up tickers, inspect markets, preview/place Alpaca orders |
1173
1334
  | `stocks(args…)` | research tickers via advis0r (`stocksRead` returns the data) |
1174
1335
  | `crypto(args…)` | research crypto pairs via advis0r (`cryptoRead` returns the data) |
package/bin/moshcode.mjs CHANGED
@@ -344,8 +344,8 @@ async function main() {
344
344
  process.exitCode = (await herdCommand(rest)) || 0;
345
345
  return;
346
346
  }
347
- if (["ps", "attach", "kill", "wait", "restore", "cost"].includes(cmd)) {
348
- process.exitCode = (await herdCommand([cmd === "ps" ? "ps" : cmd, ...rest])) || 0;
347
+ if (["ps", "attach", "kill", "wait", "restore", "cost", "usage"].includes(cmd)) {
348
+ process.exitCode = (await herdCommand([cmd === "usage" ? "cost" : cmd, ...rest])) || 0;
349
349
  return;
350
350
  }
351
351
  if (cmd === "tools") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moshcode",
3
- "version": "0.59.0",
3
+ "version": "0.61.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,391 @@
1
+ ---
2
+ openprd: "0.2"
3
+ id: "0011"
4
+ title: "Teach the herd the agent protocol — hooks-first state, a task ledger, and an A2A surface for local and remote agents"
5
+ status: Draft
6
+ authors:
7
+ - anthony@profullstack.com
8
+ created: 2026-08-16
9
+ updated: 2026-08-16
10
+ repo: https://github.com/moshcoder/moshcode
11
+ discussion:
12
+ implementation: src/herd-hooks.mjs, src/herd-tasks.mjs, src/herd-serve.mjs, src/herd-remote.mjs, src/herd-eval.mjs; touches src/herd-state.mjs, src/herd-cli.mjs, src/herd.mjs, src/engines.mjs, src/tools.mjs, src/commands.mjs, src/cli-schema.mjs
13
+ tags: [herd, runtime, agents, a2a, state, tasks, evals, digitalocean]
14
+ supersedes:
15
+ superseded-by:
16
+ ---
17
+
18
+ ## Problem
19
+
20
+ PRD 0009 built the herd: sessions outlive the terminal, every session carries a
21
+ semantic state, and one verb set serves humans, scripts, and agents. It works.
22
+ Three limits are now the daily friction, and all three are the same limit seen
23
+ from different angles — **the herd reads paint instead of speaking protocol.**
24
+
25
+ **The state rules rot, and we said so ourselves.** `herd-state.mjs` documents
26
+ its own weakness: *"Screen rules are the fallback, and they are the part that
27
+ rots — engines change their prompts between releases and nothing tells us."*
28
+ The tier-1 mechanism exists (`moshcode herd report`, TTL-bounded, beats the
29
+ screen), but nothing *installs* the hooks that would use it. Claude Code has
30
+ lifecycle hooks. OpenCode has plugins and events. Codex has a notify path. We
31
+ built the socket and never plugged anything into it, so in practice every
32
+ session is classified by regex against a screen capture, and every engine
33
+ release is a chance for the roster to start lying.
34
+
35
+ **Sessions have a present tense but no past.** `herd prompt api "…" --wait`
36
+ returns, and then the evidence evaporates. Which prompts were submitted, when
37
+ each one blocked, what the answer was, how long the human took — none of it is
38
+ anywhere. Fan-out (the herd's party trick) is therefore unauditable: a
39
+ moshscript that drove four engines overnight can tell you their state *now*
40
+ and nothing else. The watch loop already observes every transition it would
41
+ take to fix this, and throws each one away after deciding whether to buzz a
42
+ phone.
43
+
44
+ **The herd stops at the edge of the box.** A deployed agent — say a
45
+ DigitalOcean Gradient ADK deployment answering at
46
+ `agents.do-ai.run/<workspace>/<deployment>/run` — cannot be on the roster, and
47
+ nothing off the box can drive the herd. Meanwhile the ecosystem converged on
48
+ exactly the shape we need. DO's ADK gives every agent one uniform entrypoint
49
+ (`POST /run`, JSON in, JSON out), a lifecycle CLI (`init/run/deploy/logs/
50
+ traces/evaluate`), and — the interesting part — [A2A protocol
51
+ v0.3.0](https://a2a-protocol.org/v0.3.0/specification/) support: discovery at
52
+ `/.well-known/agent-card.json`, `message/send`, `tasks/get`, `tasks/cancel`,
53
+ tasks with ids, status history, and artifacts. A2A's task state vocabulary
54
+ includes `input-required`.
55
+
56
+ `input-required` is `blocked`. The mapping between A2A and the herd is not an
57
+ integration to be designed; it is a translation table to be written down:
58
+
59
+ | herd | A2A |
60
+ | --------------- | ---------------------------- |
61
+ | `herd prompt` | `message/send` |
62
+ | state / `wait` | `tasks/get` (poll) |
63
+ | `herd kill` | `tasks/cancel` (best-effort) |
64
+ | `ps` / roster | agent-card discovery |
65
+ | `blocked` | `input-required` |
66
+ | `working` | `working` |
67
+ | `done` | `completed` |
68
+ | killed | `canceled` |
69
+
70
+ PRD 0009 took herdr's thesis — *"the CLI and socket API are one surface agents
71
+ drive"* — and implemented it locally. A2A is that thesis standardized across
72
+ machines. This PRD ports the ideas, not the SDK.
73
+
74
+ ## Goals
75
+
76
+ - State comes from the engine when the engine can speak, and from the screen
77
+ only when it can't. On a default install, a Claude Code herd session reads
78
+ `authority: hook`, not `authority: screen`.
79
+ - Every prompt is a **task** with a durable record: an id, its state
80
+ transitions with timestamps, and the output it produced. `moshcode ps` keeps
81
+ answering "now"; the ledger answers "what happened."
82
+ - The roster spans machines. A deployed agent is a herd member; `ps`, `prompt`,
83
+ `read`, and `wait` do not care whether a member is a local pty or a URL.
84
+ - The herd itself is drivable over a standard protocol, behind real auth, so
85
+ another machine's herd — or anyone's A2A client — can submit work and poll it.
86
+ - "Which engine is best for this repo" is answered empirically:
87
+ one dataset, N engines, a judge, an exit code CI can gate on.
88
+ - The DO Gradient ADK is a first-class workflow tool: installable, drivable
89
+ from moshscript, and its dev server a well-classified herd member.
90
+
91
+ ## Non-Goals
92
+
93
+ - **Not a multiplexer rewrite.** 0009's substrates (tmux, `script(1)`+FIFO,
94
+ foreground fallback) stand unchanged. Everything here layers on the existing
95
+ runtime.
96
+ - **Not a hosted control plane.** `app.moshcode.sh` remains the notify/approve
97
+ surface it already is. `herd serve` runs on your box, like `moshcode console`.
98
+ - **Not the full A2A spec.** v0.3.0, JSON-RPC, text parts only —
99
+ the same MVP scope the ADK itself ships. Streaming, push notifications, and
100
+ authenticated extended cards are declared off in the card's capability flags,
101
+ which the spec provides for exactly this.
102
+ - **Not token-level tracing.** We do not own the engines' runtimes; pretending
103
+ to see inside them would be paint-reading with extra steps. Transitions and
104
+ task artifacts are what the herd can attest to honestly.
105
+ - **Not replacing engine-native resume/history.** `restore --resume` semantics
106
+ from 0009 are untouched; the ledger records what the *herd* saw, not the
107
+ engine's conversation.
108
+
109
+ ## Users
110
+
111
+ - **The operator with four agents and one attention span.** 0009 told them who
112
+ needs them now; this tells them what happened while they slept, and lets a
113
+ deployed agent sit on the same roster as the local ones.
114
+ - **The moshscript author.** Fan-out already works; fan-*in* is exit codes and
115
+ screen reads. Tasks give joins something to join on, and `wait --all` stops
116
+ the hand-rolled polling loops.
117
+ - **Another agent.** An engine in the herd, a CI job, or a deployed ADK agent
118
+ that needs to hand work to a local session and collect the result — over a
119
+ protocol it already speaks, not over SSH-and-tmux incantations.
120
+ - **The CI pipeline** that wants "the agent still passes the dataset" as a
121
+ red/green check next to `npm test`.
122
+
123
+ ## Requirements
124
+
125
+ ### Phase 1 — believe the engine, not the paint
126
+
127
+ - **R1 [P0] `moshcode herd hooks install <engine>|all`.** Writes the
128
+ engine-native lifecycle hook configuration that calls
129
+ `moshcode herd report "$MOSHCODE_HERD_NAME" <state>` at the right moments.
130
+ Claude Code first (its hooks are documented and stable): stop → `done`,
131
+ notification/permission-request → `blocked`, prompt-submit/tool-use →
132
+ `working`. Hook specs live in `ENGINES` next to each engine's screen rules,
133
+ so detection ships with the install spec, exactly as 0009 R7 intended.
134
+ `--dry-run` prints the config diff; `hooks remove` reverts;
135
+ `hooks status --json` reports per-engine install state. **Merge, never
136
+ clobber:** a user's existing hook file is extended, and `remove` takes out
137
+ only what we added.
138
+ - **R2 [P0] Sessions know their own name.** The herd already launches the
139
+ engine, so it injects `MOSHCODE_HERD_NAME` and `MOSHCODE_HERD_DIR` into the
140
+ session environment at start. A hook fired outside a herd session (no env
141
+ var) exits silently and successfully — hooks must never break an engine
142
+ running outside the herd.
143
+ - **R3 [P1] `moshcode herd doctor`.** One verb that checks the things that
144
+ actually go wrong: tmux server reachable, manifest vs. live sessions drift,
145
+ stale hook reports past TTL, unwritable status dir, rules.json parse errors
146
+ (today they vanish silently by design — doctor is where they get to be
147
+ loud). `--json` for provisioning scripts.
148
+ - **R4 [P2] Blocked sub-kinds.** `blocked:permission`, `blocked:question`,
149
+ `blocked:menu`. Hooks can say which; screen rules map their existing
150
+ patterns (y/n → permission, `❯ 1.` → menu). The roster still prints
151
+ `blocked`; the sub-kind rides in `--json` and in notifications, so
152
+ `--ask` replies can be validated against what was actually asked (a menu
153
+ wants a digit, not a paragraph).
154
+
155
+ ### Phase 2 — the task ledger
156
+
157
+ - **R5 [P0] Every prompt mints a task.** `herd prompt` assigns a task id and
158
+ appends to `~/.moshcode/herd/tasks/<session>.jsonl`: submission (text, ts),
159
+ each state transition (observed by the same poll the watcher already runs),
160
+ terminal state, and the output artifact captured as the screen delta via the
161
+ existing `read` machinery. Files are `0600` for the manifest's stated reason,
162
+ one step harder: engine output carries secrets the user never even typed.
163
+ - **R6 [P0] Read verbs.** `moshcode herd tasks <session> [--json]` lists;
164
+ `moshcode herd task <id> [--json]` shows one, transitions and artifact
165
+ included. moshscript gets `herdTasks(name)` and `herdTask(id)` as values,
166
+ same contract as `herdRead`/`herdList`: `null`/`[]` on error, never throw.
167
+ - **R7 [P1] Transitions become history.** `herd log <session>` prints the
168
+ timestamped state history; `herd stats [session]` aggregates time-in-state —
169
+ including blocked-time, which is a number with a name: *human latency*.
170
+ Retention is capped and documented (default: last 500 tasks per session,
171
+ size-bounded), because an append-only file with no cap is a disk-eater with
172
+ a delay on it.
173
+ - **R8 [P1] Fan-in verbs.** `moshcode wait --any <a> <b> …` returns on the
174
+ first session to hit a target state (exit codes name the winner in `--json`);
175
+ `wait --all` returns when every named session has. `herdWait` gains the same
176
+ options. This deletes the polling loop from every fan-out script we have
177
+ written so far.
178
+
179
+ ### Phase 3 — the A2A surface
180
+
181
+ - **R9 [P0] `moshcode herd serve`.** An HTTP server exposing the herd per A2A
182
+ v0.3.0. `GET /.well-known/agent-card.json` describes the herd; each session
183
+ is addressable as `/<name>/` with its own card. `message/send` → `herd
184
+ prompt` (mints a task per R5); `tasks/get` → ledger read; `tasks/cancel` →
185
+ interrupt, escalating exactly as `kill` already does. State maps per the
186
+ table in Problem; `idle` and `unknown` map to `working` with the honest
187
+ state carried in task metadata, because A2A's vocabulary is smaller than
188
+ ours and rounding *up* to "needs input" would page people for nothing.
189
+ - **R10 [P0] Serve is a shell on the internet, and is treated like one.**
190
+ Reuse `console.mjs`'s discipline wholesale: bind `127.0.0.1` by default,
191
+ verify the moshcode token against `app.moshcode.sh/api/me` once, swap for a
192
+ short-lived HMAC credential, refuse unauthenticated requests before they
193
+ reach anything, warn loudly on `0.0.0.0`. There is no unauthenticated mode,
194
+ loopback included — `message/send` is keystrokes into a real pty, which is
195
+ strictly more dangerous than a browser terminal that at least shows you what
196
+ it's doing.
197
+ - **R11 [P0] Remote members.** `moshcode herd remote add <name> <url>
198
+ [--kind a2a|run]` registers a remote agent on the roster. `a2a` discovers the
199
+ card and drives JSON-RPC; `run` covers bare ADK-style endpoints
200
+ (`POST <url>` with `{"prompt": …}` — the shape every `gradient agent deploy`
201
+ prints). Manifest rows carry `kind: "remote"`; `ps` shows them with the host
202
+ where local rows show cwd; state comes from `tasks/get` (a2a) or reachability
203
+ (run — a request/response endpoint is `idle` when up, `working` while a call
204
+ is in flight, and honest about knowing nothing more). Auth is a named header
205
+ from the environment (`MOSHCODE_REMOTE_<NAME>_TOKEN`), never written to the
206
+ manifest and never synced — 0010's allowlist reasoning, verbatim.
207
+ - **R12 [P1] The verbs don't care where a member lives.** `herd prompt`,
208
+ `read`, `wait`, `kill`, and their moshscript forms work unchanged on remote
209
+ members: prompt POSTs, read returns the last artifact, wait polls, kill
210
+ cancels. A `.mosh` script that fans across `claude` (local pty) and
211
+ `research-prod` (deployed on DO) is the acceptance test, and it should not
212
+ contain a single `if (remote)`.
213
+
214
+ ### Phase 4 — evals and the DO toolchain
215
+
216
+ - **R13 [P1] `moshcode herd eval`.** `--dataset <csv|jsonl> --engines a,b,…
217
+ [--judge <engine>|rules] [--threshold N] [--json]`. Fans each dataset row
218
+ across the named engines using the verbs that already exist, collects
219
+ results from the ledger, scores with the `ai()` verb as judge (rubric in the
220
+ dataset) or plain expected-pattern rules, and exits with `wait`'s discipline:
221
+ distinct codes for pass, below-threshold, and infrastructure failure. The DO
222
+ ADK ships `gradient agent evaluate --dataset-file --categories
223
+ --success-threshold` for deployed agents; this is the same idea pointed at
224
+ interactive engines, which is the comparison nobody else can run.
225
+ - **R14 [P2] Install the ADK like we install everything else.**
226
+ `moshcode install gradient` runs the vendor path (`pip install gradient-adk`,
227
+ Python ≥3.10 checked and named when missing — moshcode stays Node, the tool
228
+ owns its runtime, same as CoinPay owning Node 20). Top-level passthrough
229
+ `moshcode gradient …` and a `gradient(args…)` moshscript verb, per the
230
+ existing tool table.
231
+ - **R15 [P2] The ADK dev loop is a good herd citizen.** Ship a `gradient`
232
+ entry in the default state rules so
233
+ `moshcode herd run --name agent -- gradient agent run --dev` classifies
234
+ (uvicorn startup banner → `idle`, request handling → `working`), and a
235
+ template pointer at `digitalocean/gradient-adk-templates` in
236
+ `moshcode template list`. The workflow this buys: dev server in one tile,
237
+ Claude editing it in the next, logs in a third, `gradient agent deploy` from
238
+ the mosh bar, then `herd remote add` the printed URL — the whole lifecycle
239
+ without leaving the pit.
240
+
241
+ ## UX Notes
242
+
243
+ **New verbs, existing shape.** Everything lands in the generated command
244
+ table, drifts-fail-the-build included. `hooks`, `tasks`, `task`, `log`,
245
+ `stats`, `serve`, `remote`, `eval` are all `herd` subverbs; `wait` grows
246
+ `--any/--all` in place. Every verb takes `--json`. There is still no second
247
+ API — `serve` is not a new surface so much as the existing one answering a
248
+ socket.
249
+
250
+ **The one-time setup reads like this:**
251
+
252
+ ```
253
+ moshcode herd hooks install claude
254
+ ✓ claude — 3 hooks installed (stop, notification, prompt-submit)
255
+ sessions started from the herd now report state directly.
256
+ screen rules remain the fallback for everything else.
257
+ ```
258
+
259
+ **A remote member on the roster:**
260
+
261
+ ```
262
+ $ moshcode herd remote add research https://agents.do-ai.run/b168…/production --kind run
263
+ $ moshcode ps
264
+ api claude blocked ~/src/coinpay 12m hook
265
+ research remote idle agents.do-ai.run — remote
266
+ ⚠ 1 waiting on you — moshcode attach api
267
+ ```
268
+
269
+ **What happened overnight:**
270
+
271
+ ```
272
+ $ moshcode herd tasks api
273
+ t-01 22:14 done 4m "port the auth routes"
274
+ t-02 22:19 blocked 6h11 "run the migration" ← answered 04:30
275
+ $ moshcode herd stats api
276
+ working 3h02 · blocked 6h11 · idle 1h40 blocked = you
277
+ ```
278
+
279
+ **Honesty rules, carried forward.** A remote member's state is the remote's
280
+ claim, and `ps --json` says `authority: remote` so nobody mistakes it for
281
+ something we verified. `herd serve` prints the same warning `console` does
282
+ when bound beyond loopback. The ledger survives a reboot; the *processes*
283
+ still don't, and `restore` keeps saying so.
284
+
285
+ ## Success Metrics
286
+
287
+ - On a machine with hooks installed, ≥95% of Claude Code herd sessions report
288
+ `authority: hook`; `rules.json` edits for supported engines drop to
289
+ approximately zero.
290
+ - Any prompt submitted through the herd is reconstructable after a reboot:
291
+ what was asked, when it blocked, what came back.
292
+ - A deployed DO agent is driven by `herdPrompt`/`herdWait` in a fan-out script
293
+ with zero remote-specific branches.
294
+ - An off-the-shelf A2A client (the ADK's own `examples/a2a/client.py` is the
295
+ test) completes discover → send → get → cancel against `herd serve`.
296
+ - `moshcode herd eval` gates a CI job in this repo: engines below threshold
297
+ fail the build with a distinct exit code.
298
+ - `blocked` time is a number on a screen, and it goes down.
299
+
300
+ ## Risks & Open Questions
301
+
302
+ - **`serve` widens the attack surface.** `message/send` is remote keystrokes
303
+ into a pty. Mitigations are R10 (auth always, loopback default, console's
304
+ token discipline) plus one open question: should `serve` refuse to expose
305
+ sessions launched with bypass/auto-approve flags unless `--expose-autonomous`
306
+ is explicit? Leaning yes — an autonomous engine plus a network prompt
307
+ injector is the worst pairing on the menu.
308
+ - **Hook drift is the new rule rot.** Engines change hook schemas like they
309
+ change prompts. Contained the same way: specs live per-engine in
310
+ `ENGINES`, `hooks status` detects a schema the engine rejected, and the
311
+ screen fallback means a broken hook degrades to today, never below it.
312
+ - **A2A is v0.3.0 and moving.** Pin the version in the card, keep the surface
313
+ to the four operations the ADK itself ships, and treat spec upgrades as
314
+ their own PRD. Text-only parts for the MVP.
315
+ - **State vocabularies don't biject.** Our `idle`/`unknown` vs. A2A's
316
+ smaller set (R9 rounds down, metadata carries truth). Accepted as lossy;
317
+ revisit if clients demonstrably need more.
318
+ - **Ledger growth.** JSONL with per-session caps (R7). Open: should artifacts
319
+ above a size threshold store a path to the transcript slice instead of
320
+ inline text?
321
+ - **Card shape.** One card for the herd with sessions as skills, or a card per
322
+ session (current lean: per session — it makes `remote add` of *someone
323
+ else's* single session symmetric)? Decide during R9 spike.
324
+ - **Python as a soft dependency** for R14. Same posture as tailscale needing
325
+ root: name the requirement, never harden it — `install gradient` fails with
326
+ the fix printed, and nothing else in moshcode notices Python exists.
327
+
328
+ ## Implementation Notes
329
+
330
+ The watch loop in `herd-cli.mjs` already observes every transition R5 needs —
331
+ the ledger is a write inserted where the notification decision already
332
+ happens, not a second poller. `notify.mjs`'s `ingestApproval`/`pollApproval`
333
+ pattern is the model for `tasks/get` long-polling if we want it later.
334
+ `console.mjs` is the auth gateway to lift for R10, not to reimplement.
335
+ Hook specs belong in `engines.mjs` beside the state tables they supersede.
336
+ `runtime.mjs` registration gives moshscript the new verbs for free once the
337
+ CLI verbs exist. Build order: R1–R2 (de-rots the core, smallest diff), R5–R6
338
+ (everything else reads from it), then R9–R11 as one spike since they share the
339
+ task model, with R13 as the demo that only moshcode can run.
340
+
341
+ ## Decisions taken while building
342
+
343
+ Six questions this document left open were answered by the implementation.
344
+ They are recorded here rather than edited into the requirements above, so the
345
+ proposal stays the proposal and the answers stay attributable.
346
+
347
+ **Both card shapes ship, not one.** They answer different questions: the herd
348
+ card is discovery ("what is on this box"), and a per-session card is what a
349
+ client stores when it intends to talk to one member for a week. Publishing
350
+ both also keeps `remote add` of somebody else's single session symmetric with
351
+ adding a whole herd, which was the argument for per-session in the first place.
352
+
353
+ **`--expose-autonomous` is opt-in, as the risk section leaned.** A session
354
+ started with `--agent` is off the protocol surface entirely — not in the herd
355
+ card, not addressable, not promptable — until the flag says otherwise.
356
+
357
+ **`tasks/cancel` interrupts; it does not kill the member.** R9 says "escalating
358
+ exactly as `kill` already does", and the escalation *pattern* is what was
359
+ taken: Escape, then Ctrl-C. It stops one rung short of `kill`'s pane removal on
360
+ purpose. An A2A task is a unit of work inside a member, and the member is a
361
+ long-lived thing somebody may have attached to five minutes ago; ending it is a
362
+ decision, not a protocol call. `moshcode kill` is still the verb for that.
363
+
364
+ **A finished task is `completed` even when the session went back to `idle`.**
365
+ The `idle → working` rounding in R9 is about a *session* — it is sitting there,
366
+ it is not asking for anything. Applying it to a task that has an outcome and an
367
+ artifact would leave every A2A client polling a job that finished ten minutes
368
+ ago, because `send → poll until completed` is the whole protocol. The rounding
369
+ now applies only to open tasks; a closed one is `completed`, or
370
+ `input-required` when it ended by stopping to ask.
371
+
372
+ **A poll closes a task.** `tasks/get` and `herd tasks` both reconcile: if a
373
+ task is open and its session has stopped, the observation is recorded and the
374
+ artifact captured. Without it the only thing that ever finished a task was the
375
+ watcher, and a herd where nobody happened to be running one would hand every
376
+ client an eternal `working`.
377
+
378
+ **Artifacts stay inline, truncated at the tail.** The open question asked
379
+ whether oversized artifacts should store a path to a transcript slice instead.
380
+ They do not: the cap keeps the last 8000 characters — an agent's answer is the
381
+ last thing it printed — and records both that it was truncated and the original
382
+ length. A path into a transcript that `restore` may have already replaced would
383
+ be a reference to something the ledger cannot promise still exists.
384
+
385
+ **The ADK dev server has no `working` rule.** R15 asks for "request handling →
386
+ `working`", and uvicorn cannot supply it: it writes its access line when a
387
+ request has *finished*, so a rule matching that line would pin the tile to
388
+ `working` from the first request until the line scrolled away — the exact rot
389
+ this PRD exists to get away from. A completed request is therefore classified
390
+ `idle`, which is true both before traffic and after it. Watching a *deployed*
391
+ agent's state is what `herd remote add` is for.
package/prd/README.md CHANGED
@@ -26,4 +26,5 @@ Start one with `moshcode prd "<idea>"` (TUI: `/prd`).
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
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 |
28
28
  | [0010](0010-cloud-settings-sync.md) | Sync the pit's settings to your moshcode.sh account | Draft |
29
+ | [0011](0011-herd-agent-protocol.md) | Teach the herd the agent protocol — hooks-first state, a task ledger, and an A2A surface for local and remote agents | Draft |
29
30
  <!-- PRD-INDEX:END -->