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 +171 -10
- package/bin/moshcode.mjs +2 -2
- package/package.json +1 -1
- package/prd/0011-herd-agent-protocol.md +391 -0
- package/prd/README.md +1 -0
- package/src/cli-schema.mjs +100 -6
- package/src/commands.mjs +85 -10
- package/src/cost.mjs +121 -2
- package/src/engines.mjs +32 -0
- package/src/herd-cli.mjs +812 -20
- package/src/herd-eval.mjs +301 -0
- package/src/herd-hooks.mjs +285 -0
- package/src/herd-remote.mjs +365 -0
- package/src/herd-serve.mjs +515 -0
- package/src/herd-state.mjs +167 -10
- package/src/herd-tasks.mjs +377 -0
- package/src/herd.mjs +89 -7
- package/src/templates.mjs +32 -5
- package/src/tools.mjs +74 -0
- package/src/tui.mjs +1 -1
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`
|
|
515
|
-
|
|
516
|
-
|
|
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 === "
|
|
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
|
@@ -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 -->
|