moshcode 0.58.0 → 0.60.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 +299 -6
- package/bin/moshcode.mjs +3 -3
- package/examples/account.mosh +48 -0
- package/examples/aliases.mosh +56 -0
- package/examples/research-desk.mosh +49 -0
- package/package.json +1 -1
- package/prd/0011-herd-agent-protocol.md +391 -0
- package/prd/README.md +1 -0
- package/src/auth.mjs +59 -64
- package/src/cli-schema.mjs +133 -4
- package/src/commands.mjs +389 -39
- package/src/cost-cli.mjs +232 -0
- package/src/cost-pricing.mjs +159 -0
- package/src/cost.mjs +753 -0
- package/src/engines.mjs +32 -0
- package/src/games-breakout.mjs +64 -10
- package/src/games-paddle.mjs +128 -0
- package/src/games-pong.mjs +53 -4
- package/src/games.mjs +164 -12
- package/src/herd-cli.mjs +816 -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 +43 -0
- package/src/tui.mjs +1 -0
|
@@ -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 -->
|
package/src/auth.mjs
CHANGED
|
@@ -166,89 +166,84 @@ export async function loginAuto({ device = false, browser = false } = {}) {
|
|
|
166
166
|
return loginDevice({ open: !isRemoteShell() });
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
/**
|
|
170
|
-
|
|
169
|
+
/**
|
|
170
|
+
* Who is logged in, as a value — the shape `whoami --json` prints, and what
|
|
171
|
+
* moshscript's whoami()/requireLogin() read (src/commands.mjs).
|
|
172
|
+
*
|
|
173
|
+
* Returning rather than printing is the point: a script needs to branch on the
|
|
174
|
+
* account ("do I have credits?", "is this the right email?"), and the only way
|
|
175
|
+
* to get that out of a printing function is to re-parse its stdout. One
|
|
176
|
+
* verified-identity implementation, two callers — the CLI renders it, the
|
|
177
|
+
* script reads it — so the two can never disagree about what "logged in" means.
|
|
178
|
+
*
|
|
179
|
+
* Never throws: an unreachable app is a `status` like any other, because the
|
|
180
|
+
* caller is usually deciding whether to *start* work, not reporting an outage.
|
|
181
|
+
*/
|
|
182
|
+
export async function identity() {
|
|
171
183
|
const creds = loadCreds();
|
|
172
184
|
if (!creds?.token) {
|
|
173
|
-
|
|
174
|
-
console.log(JSON.stringify({
|
|
175
|
-
status: "not_logged_in",
|
|
176
|
-
verified: false,
|
|
177
|
-
api: API(),
|
|
178
|
-
user: null,
|
|
179
|
-
}, null, 2));
|
|
180
|
-
} else {
|
|
181
|
-
console.log("not logged in — run: moshcode login");
|
|
182
|
-
}
|
|
183
|
-
return;
|
|
185
|
+
return { status: "not_logged_in", verified: false, api: API(), user: null };
|
|
184
186
|
}
|
|
185
187
|
const api = creds.api || API();
|
|
188
|
+
// What we know locally, used for every unverified outcome. Deliberately not
|
|
189
|
+
// presented as confirmed: the app is the authority on the account, and these
|
|
190
|
+
// fields are only what the last successful login happened to write down.
|
|
186
191
|
const localUser = {
|
|
187
192
|
id: creds.id ?? null,
|
|
188
193
|
email: creds.email ?? null,
|
|
189
194
|
name: null,
|
|
190
195
|
credits: null,
|
|
191
196
|
};
|
|
192
|
-
const printJson = (value) => console.log(JSON.stringify(value, null, 2));
|
|
193
197
|
try {
|
|
194
198
|
const res = await fetch(`${api}/api/me`, { headers: { authorization: `Bearer ${creds.token}` } });
|
|
195
199
|
if (res.status === 401) {
|
|
196
|
-
|
|
197
|
-
printJson({
|
|
198
|
-
status: "expired",
|
|
199
|
-
verified: false,
|
|
200
|
-
api,
|
|
201
|
-
user: localUser,
|
|
202
|
-
error: { type: "auth", status: 401 },
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
else console.log("session expired — run: moshcode login");
|
|
206
|
-
return;
|
|
200
|
+
return { status: "expired", verified: false, api, user: localUser, error: { type: "auth", status: 401 } };
|
|
207
201
|
}
|
|
208
202
|
// Any other error status still has a body, and it isn't an account — reading
|
|
209
|
-
// it as one
|
|
203
|
+
// it as one reports a made-up identity for a session the app just refused.
|
|
210
204
|
if (!res.ok) {
|
|
211
|
-
|
|
212
|
-
printJson({
|
|
213
|
-
status: "unverified",
|
|
214
|
-
verified: false,
|
|
215
|
-
api,
|
|
216
|
-
user: localUser,
|
|
217
|
-
error: { type: "http", status: res.status },
|
|
218
|
-
});
|
|
219
|
-
} else {
|
|
220
|
-
console.log(`${creds.email || "logged in"} @ ${api} (couldn't verify — the app returned ${res.status})`);
|
|
221
|
-
}
|
|
222
|
-
return;
|
|
205
|
+
return { status: "unverified", verified: false, api, user: localUser, error: { type: "http", status: res.status } };
|
|
223
206
|
}
|
|
224
207
|
const me = await res.json();
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
});
|
|
237
|
-
} else {
|
|
238
|
-
console.log(`${me.email || me.name || "moshcoder"} 🤘 (${me.credits ?? "?"} credits) @ ${api}`);
|
|
239
|
-
}
|
|
208
|
+
return {
|
|
209
|
+
status: "authenticated",
|
|
210
|
+
verified: true,
|
|
211
|
+
api,
|
|
212
|
+
user: {
|
|
213
|
+
id: me.id ?? creds.id ?? null,
|
|
214
|
+
email: me.email ?? creds.email ?? null,
|
|
215
|
+
name: me.name ?? null,
|
|
216
|
+
credits: me.credits ?? null,
|
|
217
|
+
},
|
|
218
|
+
};
|
|
240
219
|
} catch {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
220
|
+
return { status: "unreachable", verified: false, api, user: localUser, error: { type: "network" } };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Print who is logged in (verified against the app). */
|
|
225
|
+
export async function whoami({ json = false } = {}) {
|
|
226
|
+
const me = await identity();
|
|
227
|
+
if (json) {
|
|
228
|
+
console.log(JSON.stringify(me, null, 2));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const who = me.user?.email || me.user?.name;
|
|
232
|
+
switch (me.status) {
|
|
233
|
+
case "not_logged_in":
|
|
234
|
+
console.log("not logged in — run: moshcode login");
|
|
235
|
+
return;
|
|
236
|
+
case "expired":
|
|
237
|
+
console.log("session expired — run: moshcode login");
|
|
238
|
+
return;
|
|
239
|
+
case "unverified":
|
|
240
|
+
console.log(`${who || "logged in"} @ ${me.api} (couldn't verify — the app returned ${me.error.status})`);
|
|
241
|
+
return;
|
|
242
|
+
case "unreachable":
|
|
243
|
+
console.log(`${who || "logged in"} @ ${me.api} (couldn't reach the app to verify)`);
|
|
244
|
+
return;
|
|
245
|
+
default:
|
|
246
|
+
console.log(`${who || "moshcoder"} 🤘 (${me.user.credits ?? "?"} credits) @ ${me.api}`);
|
|
252
247
|
}
|
|
253
248
|
}
|
|
254
249
|
|