1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257 | # PROMPT: raise a resident bot for the ardegazu.ro suite (ClojureScript)
You are an agentic developer working inside the ardegazu.ro workspace. This
scaffold (made by `ardz new-bot <name>`) is a complete headless suite
peer: it holds one suite identity, shows up as a friend, answers invites into
any suite room, and — as a fleet — summons its siblings into self-play
matches. Your job is to give it its behaviors, optionally its learned brains,
and put it on a server and in the hub. Follow the flow in order. Verify each
stage before moving on.
The build canon is `dev/docs/CLJS.md` (in the suite's dev repo); bot.git is
the live worked example of everything this scaffold strips.
---
## 1. BOT SPEC (filled in by the requester)
```
BOT NAME: <the repo/ident/unit name, e.g. "duhul" — [a-z0-9]>
FLEET SIZE: <how many instances; 1 is fine, ≥2 unlocks self-play>
APPS TO JOIN: <standing rooms, usually none — invites do the work; e.g.
"a public chat room" means one rooms[] entry in its config>
BEHAVIORS: <what it does in chat / on boards / in games beyond the stock
brains — e.g. "quotes poetry on the hour", "draws spirals">
RL: <yes/no; if yes, WHICH GAMES learn — each learned game runs
train-kit's NEW-BRAIN-PROMPT.md as a sub-flow (§5)>
PERSONA: <name flavor + a one-line tagline in en, ro AND hu — it goes
on the hub's bot card verbatim>
```
Everything below is fixed house style — do not ask the requester about it.
## 2. House architecture (non-negotiable)
- **The two-stack rule. Never "simplify" this.** The suite's Node kits carry
two libp2p generations whose WebRTC transports load two different builds of
the same native library (libdatachannel) — two copies in one process abort
it (ThreadSafeCallback cancellation). In this scaffold the rule is enforced
**by build structure**: four separate shadow-cljs `:esm` builds, one per
process entry, each its own module graph. `__NAME__.main`/`__NAME__.rooms`
(the `:main` build → `dist/main.js`) require ONLY `ardegazu-peer-kit`
(stack A: games, identity, social); `__NAME__.worker` (the `:worker` build
→ `dist/worker.js`) requires ONLY `ardegazu-rooms-kit` (stack B:
chat/board), and every chat/board room runs in its own forked worker
process. `__NAME__.behaviors` is shared by both builds and therefore
requires NEITHER kit — every client object arrives inside the ctx
**argument** (the CLJS replacement for TS's `import type`-only rule).
npm deps stay external (`:js-provider :import`) so the native builds are
never bundled, and `deploy/check-dist.sh` greps each emitted bundle for
the wrong kit's specifier. If you ever see both kits required into one
build's graph, that is a bug, not a refactor.
- **The boot order in `__NAME__.main` is law:** `installOrigin` →
`installBrowserGlobals` → `init-policies` → `loadIdentity`. The kits read
the origin and browser globals at import-time-adjacent moments; identity
loads last so everything it touches is already in place. Add your own
startup work after `loadIdentity`, never between these.
- **The origin is process-wide and production-only.** Bots present the
allowlisted browser origin `https://chat.ardegazu.ro` to the managed relay
(`origin` in the config — leave it). There is NO dev relay for bots:
peer-kit's relay endpoint is compile-time, and the :9090 dev relay serves
the browser stacks only. Local runs (§6) talk to the production relay with
a throwaway seed, briefly, then delete their state.
- **Seeds are the only irreplaceable state.** A bot's seed file IS its
identity — its friendships, its hub card, its history. Set up
`deploy/backup-state.sh` on day one; the tarball lands mode 0600 and never
enters any repo. Everything else (kv.json, social.json, room stores,
models) is rebuildable or re-learnable.
- **The relay budget: ~20 connections per IP.** One connection per social
agent plus one per open room, for the whole fleet behind one IP. The room
reaper in `__NAME__.main` and the matchmaker's caps
(`maxConcurrentSelfPlay`, `maxHumanRoomsForSelfPlay`) keep the count honest
— don't remove them, and don't add standing rooms casually.
- **Provenance drives policy.** Every room is joined as `config`, `human`,
`sibling` or `selfplay`, and that one word decides its fate: the reaper's
timers (sibling rooms die after their match, human rooms after a long
idle, config rooms never), the exploration epsilon (self-play explores,
humans get the greedy policy), and link sharing (only fleet rooms are
advertised in presence — human rooms' links are the humans' capabilities,
never ours to hand out).
- **The trainer/bot split (RL bots only).** Live bots are inference-only:
a tiny forward pass over plain-JSON checkpoints, no ML framework in the
bot process. The trainer (the `:train` build, an hourly oneshot) is the
only writer. `@tensorflow/tfjs-node` is an optional peer of train-kit
behind a dynamic import — it is NOT in this scaffold's dependencies and
gets installed only at the trainer fill-in stage (§5). Dropping RL is pure
deletion.
- **Deterministic committed dist.** `dist/` is committed (the VPS never
builds; JVM ≥ 17 + shadow-cljs live on the dev machine only) and
`deploy/check-dist.sh` gates publish on it matching a fresh cold-cache
build byte-for-byte — `scripts/build.mjs` wipes `.shadow-cljs/` and
normalizes gensym numbering to make that hold. Never commit a warm build.
- **Anonymity is absolute.** The repo gets published. Every commit is
`<name> <name@noreply.local>` (`ardz new-bot` set the repo-local git
config — verify it). The publish gate decompresses every mirror object and
scans it against private patterns from `~/.config/cod-sursa/idpat`. No
real names, emails, hostnames or local filesystem paths in any tracked
file, ever — and never track `.shadow-cljs/`, `.cpcache/`, `.nrepl-port`
or `*.js.map` (they embed local paths; the scaffold's `.gitignore` already
covers them).
## 3. Scaffold
```sh
ardz new-bot <name> # already done if you are reading this in a scaffold
cd <name>
git config user.name && git config user.email # → <name> / <name>@noreply.local
grep -rI "_[_]NAME_[_]" . && echo LEFTOVER-TOKENS || echo clean
```
The grep must print `clean` — a leftover placeholder token means the
substitution pass missed a file; fix it before anything else. Check
`dev/apps.tsv` gained the bot's row (`<name> … kit … n`).
## 4. Wire behaviors
Behaviors live in `src/__NAME__/behaviors.cljs`: a flat REGISTRY keyed
`"<kind>:<name>"` (`"chat:echo"`, `"board:sign"`, `"game:default"`) with a
`:default` fallback per kind. **The registry ships empty** — the worked
echo/sign/game examples sit in the namespace's comment block, and bot.git's
`src/bot/behaviors.cljs` is the live version. A room's config entry picks a
behavior by name; invites use the defaults.
- Keep behaviors small and event-driven; `(.-nick ctx)` is the bot's display
name in that room, `(.-log ctx)` the room-tagged logger.
- Chat: mind the replicated-history trap — echo/react only to messages with
`ts` after your boot, or the bot replays years of history at every join.
- Board: make writes idempotent (check for your own mark before adding it)
— bots rejoin rooms constantly.
- Games: the clients play their stock brains on their own; a game behavior
only adds flavor (log lines, reactions to receipts).
Then prove the chassis:
```sh
npm install && npm run check && npm run build
```
All three must pass before you continue (`check` is the shadow compile of
all four builds; `build` assembles the committed `dist/`) — and note
`npm install` pulled no tensorflow: that stays true until §5 says otherwise.
Run `npm test` after the first build: the worker-respawn race tests exercise
the committed dist.
## 5. RL fill-in (skip entirely if RL: no)
Per learned game, run **train-kit's `docs/NEW-BRAIN-PROMPT.md`** (the brain
spec, gym porting, features, recorder, trainer stages live there) with these
scaffold-local targets instead of the bot.git paths it names:
- Recorder + policy math → `src/__NAME__/rl/<game>_policy.cljs` (the
ε-greedy hook + transition logging; bot.git's `src/bot/rl/*_policy.cljs`
are the worked examples).
- Provider → `src/__NAME__/policies/<game>.cljs`: a `register-policy` call
owning that game's CheckpointStore — the complete worked example sits in
the comment block of `src/__NAME__/policies/index.cljs`. Then ONE require
in that namespace. Nothing in `__NAME__.rooms` or `__NAME__.main` changes
— that is the point of the registry.
- Trainer slot → `src/__NAME__/rl/train.cljs`: the config block, the
train-<game> fn (ingest → gym self-play → fit → gate), the dispatch on
`--game/--gym-only/--ingest-only`. Export any pure policy fns worth
black-box testing through the `:main` build's `:exports`.
- `npm install @tensorflow/tfjs-node` — now, and only now (tabular-only
brains can even skip it).
- Add the game to `fleet.matchmaker.games` in the deployed configs (hostable
games only — a game is self-playable only if peer-kit ships a host sim).
Dropping RL instead: `rm -rf src/__NAME__/rl deploy/*trainer*`, drop the
`:train` build from `shadow-cljs.edn` and its copy line from
`scripts/build.mjs`, keep the empty `src/__NAME__/policies/` stub. Nothing
else references them; `npm run check` stays green.
## 6. Local smoke test (production relay, throwaway identity)
Make a scratch config — throwaway `dataDir`, one game secret and one chat
secret you minted in a browser:
```json
{
"name": "<name>-test",
"dataDir": "./state",
"rooms": [
{ "app": "seance", "secret": "<fragment from a game room URL>" },
{ "app": "chat", "secret": "<fragment from a chat room URL>" }
]
}
```
With npm's `ignore-scripts` on, fetch the chat/board native prebuild first
(the worker dies at spawn with MODULE_NOT_FOUND otherwise):
`(cd node_modules/@ipshipyard/node-datachannel && npx prebuild-install -r napi)`.
`node dist/main.js ./dev.json` and expect, in order:
1. `friend link: https://ardegazu.ro/#add=…` — identity up.
2. `joined seance room (…)` — stack A works.
3. `joined chat room via worker (…)` — stack B works, in its own process:
the two-stack rule proven live, no native abort.
Poke it from the browser tabs (with a chat behavior wired in §4 it should
answer; with the registry still empty it just sits in the room), then Ctrl-C
— it must log `leaving…` and exit cleanly — and `rm -rf ./state`. Keep the
run to minutes: it is holding real relay connections.
## 7. Deploy (systemd fleet)
```sh
npm run build # dist/ is what runs on the box (commit it — the VPS never builds)
rsync -a --exclude node_modules --exclude .git . server:/opt/ardegazu-__NAME__/
ssh server 'cd /opt/ardegazu-__NAME__ && npm install &&
sh deploy/install-fleet.sh <name…>' # no args = fleet of one
```
`install-fleet.sh` creates the static user, keeps any existing seeds, mints
missing ones, cross-links every bot's friend link into its siblings'
configs, enables the matchmaker only for fleets of ≥2, installs the trainer
units only if they still exist, and starts everything ~20 s apart (staggered
so the relay never sees a connection burst). Watch it come up:
```sh
journalctl -u 'ardegazu-__NAME__@*' -f
```
Then back up the fresh seeds immediately: `sh deploy/backup-state.sh server`.
## 8. Publish the repo + the hub card
1. Commit — targeted adds, never `-A` — including the rebuilt `dist/`
(`deploy/check-dist.sh` must pass on the committed state).
2. `ardz publish-src <name>` (runs `deploy/publish-repo.sh`: check-dist,
bare mirror, the idpat anonymity gate) and verify
`git clone https://git.ardegazu.ro/<name>.git` in a temp dir.
3. The hub card: add an entry to `home/client/catalog.json` `bots[]` —
```json
{ "id": "<name>", "name": "<name>",
"pub": "…", "x": "…", "xs": "…",
"tagline": "<persona line>",
"i18n": { "ro": { "tagline": "…" }, "hu": { "tagline": "…" } } }
```
`pub`/`x`/`xs` are the three dot-separated pieces of the friend link
fragment the bot logged at startup (`#add=<pub>.<x>.<xs>&n=<name>`).
Schema stays v1 — never bump `v` for additive fields.
4. `ardz release home`, then commit home's `client/version.json` bump.
## 9. Definition of done
- [ ] `grep -rI "_[_]NAME_[_]" .` finds nothing; `git log` is 100% `<name> <name@noreply.local>`
- [ ] `npm run check`, `npm run build` and `npm test` green — with tfjs-node present only if a game learns
- [ ] `deploy/check-dist.sh` green on the committed state (dist fresh, two-stack clean, no local paths)
- [ ] Smoke test showed all three lines, including `joined chat room via worker` (the two-stack proof), and exited cleanly on Ctrl-C
- [ ] Fleet up on the box: every instance logs its friend link; `journalctl` quiet of restarts
- [ ] Seeds backed up (0600 tarball, outside any repo)
- [ ] Invite the bot from the hub as a friend → it accepts and joins the invited room
- [ ] RL bots only: episode JSONL growing under each instance's `episodes/`; one trainer run wrote a candidate checkpoint
- [ ] `git clone https://git.ardegazu.ro/<name>.git` works; anonymity gate passed
- [ ] Hub shows the bot card in all three languages; `dev/apps.tsv` row in place
|