summaryrefslogtreecommitdiffhomepage
path: root/roadmap.md
blob: 4d9be269592b62c94086775c205e1326bd6bb195 (plain)
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
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
# Roadmap — raylib-jamstack: AI Harness + First-Class Agentic Development

This roadmap has **two intertwined goals**:

1. **Build out the AI harness** — finish the layered context system this repo
   started (`.agents/rules/`, `.agents/knowledge/`), borrowing the proven shape
   from `../dispatch/arch-rewrite` (5-layer harness + orchestrator + NDJSON
   observability) and `../roblox` (`.live/` state mount + cmd/result protocol +
   in-game console), adapted to **mruby + Flecs**.
2. **Make agentic development first-class at runtime** — a running game (desktop
   *or* browser) that an AI agent can introspect, hot-patch, and debug live:
   editable Flecs systems on the fly, a WebSocket eval bridge, full game-state
   read/write, Flecs's own debugging/logging surfaced to the agent, structured
   logging, and an in-game Ruby console.

> Reference reading: the AI-harness article
> (`dev.to/louaiboumediene/the-ai-harness`), `../dispatch/arch-rewrite`
> (`AGENTS.md`, `ORCHESTRATOR.md`, `.dispatch/`, `notes/observability-design.md`),
> `../roblox` (`AGENTS.md` §15, `.live/`, `.opencode/`).

---

## Status — what's shipped (implemented + verified)

- **Part A harness:** H0–H3 + plan-first/session-hygiene done. H4 (subagents)
  deferred; H5/H6 partial/deferred.
- **Part B runtime — desktop AND web, all verified:**
  - **R1** eval bridge — TCP + `.live/bin/eval`; web `jamstack_eval` C export. ✅
  - **R2** structured `Jamstack::Log` — NDJSON ring buffer + sinks, queryable over the bridge. ✅
  - **R3** hot-reloadable Flecs systems (`Flecs::Hot`) — same system id, state survives;
    verified live, **including in a browser tab**. ✅
  - **R4** `.live/` file mount (desktop, `Jamstack::Live`) + **W2** dependency-free Node
    relay (`tools/agent-bridge/server.js`) so the SAME `.live/bin/*` drive a browser tab. ✅
  - **R5** flecs REST/Explorer + stats (`world.enable_rest`/`enable_stats`, desktop);
    **R5a** wasm `ecs_rest_server_init` + flecs's `flecs_explorer_request` wired. ✅
  - **Demo:** `game/ragdoll_demo.rb` — flecs-tracked, bridge-summonable ragdolls
    (mruby↔flecs↔Jolt). Verified live (desktop + browser): `summon_ragdoll(x,y,z)`,
    `$flecs.query`/`lookup` live position sync, and a live `Flecs::Hot` reload.

- **Known issues / immediate next:**
  - ~~**🔴 flecs entity DELETE leak**~~ — **FIXED.** Root cause: 32-bit `mrb_int` on
    wasm32 truncated the generation bits (high 32) of `ecs_entity_t` when ids
    round-tripped through `fl_yield_iter`/`fl_w_delete`/`fl_w_alive`. Fix: force
    `-DMRB_INT64` in `build_config.rb` (both builds) + on `src/main.c` compilation
    (`build.zig`/`build_web.sh`) so the full 64-bit entity id round-trips on every
    target. Verified with a summon/delete/summon recycle test on desktop + web.
  - ~~**ballpit_demo.rb yaw fix**~~ — **FIXED.** Same `cam_yaw += md.x` → `-=` as
    the ragdoll demo.
  - ~~**bin/snapshot / bin/query**~~ — **DONE.** Added `Flecs::World#rest_request`
    (in-process `ecs_http_server_request`, no socket — works on desktop AND web).
    `bin/snapshot` → `/world` → writes `.live/<token>/state.json`; `bin/query <expr>`
    → `/query?expr=<expr>&values=true`. Verified on desktop (running game) + web
    (node headless).
  - **R6 — in-game RmlUi console** — keyboard-input gap now CLOSED:
    `rml_context_process_input` forwards keys (raylib→RmlUi KI_* map), text input
    (`GetCharPressed`→`ProcessTextInput`), and modifiers. Unblocks R6; the console
    itself is the next build on top of it.
  - **H6** (LSP/RBS generator): **DONE for Rl/Rml.** `gen_rbs.rb` generates
    `sig/raylib.rbs` from `raylib_api.json` (650 functions, 34 structs, 331
    constants). Hand-written `sig/rmlui.rbs`, `sig/flecs.rbs`, `sig/jolt.rbs`,
    `sig/jamstack.rbs`. `opencode.json` enables `ruby-lsp` with RBS auto-discovery.
    `sig/jolt.rbs` and `sig/flecs.rbs` are fully typed (public API + the `_` C
    primitives + the `Flecs::Hot` sugar, from the `API_SPEC_*.md` + binding
    sources). Only the genuinely dynamic part is left loose: Flecs component
    *values* are `Hash[Symbol, untyped]` (runtime meta structs ↔ Ruby Hashes,
    no per-component class) — the static surface (lifecycle, phases, system/query
    registration, REST, Hot) is precise, with a `term` alias for the
    Component|Entity|Integer id union.

- **Agentic-runtime tip:** reach live *objects* via `ObjectSpace.each_object` (e.g. the
  player `Jolt::Character`) instead of rebuilding to expose a global — eval is live; a
  top-level *local* isn't reachable from an eval binding, but the object is.

---

## Guiding principles (adapted to this repo)

Borrowed from the article and `../dispatch` (P1–P8), trimmed to what matters here:

- **P1 — The repo is a harness, not just code.** Agent-facing meta-info (rules,
  knowledge, features, skills, glossary) is a first-class deliverable, maintained
  with the same care as the bindings.
- **P2 — Document only the non-inferable.** If a frontier model could infer it
  from the code, leave it out. Tribal knowledge (ABI traps, WSL, premultiplied
  alpha, flecs wasm stack, generator naming) is gold; generic advice is noise.
- **P3 — Tiny files always loaded; big files on demand.** Rules + AGENTS.md every
  session (~tens of lines); knowledge docs only when touching that area;
  skills only when invoked.
- **P4 — One canonical vocabulary.** A `GLOSSARY.md` with an "aliases to avoid"
  column prevents synonym drift (System vs system vs phase-callback, Component vs
  struct vs tag, mrbgem vs binding vs gem).
- **P5 — Separate code-mutation from runtime-observation** (the `../roblox`
  lesson). The agent **mutates** behavior by editing Ruby files / hot-reloading;
  it **observes** state through a read-mostly `.live/` surface + logs. Keep the
  write path and the read path distinct and obvious.
- **P6 — Single-threaded safety.** mruby is **not** thread-safe and Flecs mutation
  must happen on the main thread. Every agent/console/bridge command is funneled
  through a **frame-polled command queue drained on the main thread**. No command
  ever runs on a socket/JS callback thread directly.
- **P7 — Dev-only by construction.** The eval bridge executes arbitrary Ruby. It
  binds to localhost, is gated behind a build flag / env var, and never ships in a
  release web build.

---

## Where we are today (baseline)

**Harness (partial):**
- `AGENTS.md` (constitution, 62 lines) + `CLAUDE.md` (currently a *duplicate*, not
  a symlink) + `docs/AI_REFERENCE.md` (generated typed API).
- `.agents/rules/` — 6 safety reflexes (toolchain, link order, LTO, mruby rebuild,
  raylib platform objs, generated files).
- `.agents/knowledge/` — 8 per-area docs (build-system, environment, raylib-,
  rmlui-, flecs-, jolt-binding, web-target, testing).
- **Missing harness layers:** `GLOSSARY.md`, per-area orientation (folded into the
  existing `.agents/knowledge/` docs — one tribal-knowledge location, no separate
  `features/` tree), `.agents/skills/` (codified workflows), subagents, the symlink
  trick, an orchestrator/plan-mode workflow.

**Runtime (nothing agentic yet):**
- `src/main.c` boots one `mrb_state`, runs `argv[1]` as **plain-text** Ruby
  (default `game/main.rb`), no REPL/eval-in/hot-reload.
- Platform seam: `Rl.while_window_open` (`mrbgems/raylib/mrblib/raylib.rb`) —
  desktop `until window_should_close?` vs web `emscripten_set_main_loop`.
- Flecs binding exists (`Flecs::World`, systems, queries) but **no game uses it
  yet**; the amalgamation is compiled **with** REST/HTTP but **no Ruby way to
  start it**; no flecs logging exposed.
- `mruby-eval`, `mruby-socket`, `mruby-io` are **already compiled in** (via the
  `default` gembox). `mirb` is intentionally not linked.
- Web: `web/shell.html` (hardcoded entry script, `Module.print → console.log`),
  served by `python3 -m http.server`. No dev server / live reload / WS.

This baseline is good news: the eval primitive (`mruby-eval`) and the I/O
(`mruby-socket`/`mruby-io`) we need already exist; the work is wiring, not new
toolchain plumbing.

---

## Target architecture (the runtime loop)

```
                 ┌────────────────────── one mrb_state (main thread) ─────────────────────┐
                 │                                                                          │
  AI agent ──┐   │   Rl.while_window_open do                                               │
  console  ──┤   │     Bridge.drain_queue   # eval queued Ruby cmds here (main thread)     │
  (RmlUi)  ──┼──▶│     Hot.progress(dt)     # flecs world.progress; systems dispatch via   │
             │   │                          #   registry → current Ruby proc (swappable)   │
  WS / JS ───┘   │     draw...                                                              │
   bridge        │   end                                                                    │
                 │        ▲                         │                                        │
                 │        │ enqueue                 ▼ emit                                   │
                 │   command queue            Log pipeline (NDJSON ring buffer)             │
                 └────────┼─────────────────────────┼──────────────────────────────────────┘
                          │                          │
        desktop: in-proc non-blocking TCP/WS poll    ├─▶ stdout (→ console.log on web)
        web:     JS calls exported jamstack_eval()    ├─▶ .live/{game,browser}-console
                                                       └─▶ flecs logs (ecs_log_set_level)
                          │
                  Relay (tools/agent-bridge, Bun/Node WS hub)
                          │
                   .live/<token>/  (status.json, state.json, game-console, browser-console, .agent/cmd-*/result-*, bin/*)
```

**Why a queue, not direct calls:** sockets (desktop) and JS WS callbacks (web) run
outside the frame; mruby/Flecs aren't safe to touch there. Everything is enqueued
and drained at one well-defined point each frame (P6). This single design works
identically on both targets — it *is* the new platform seam, sitting right next to
`Rl.while_window_open`.

---

# Part A — Complete the AI Harness

Cheap, high-leverage, mostly docs. Do this first; it makes every later phase
faster because Plan Mode gets grounded context.

## Phase H0 — Harness plumbing & dedupe
- **Symlink trick** (article Layer 6): make the harness tool-agnostic. `.agents/`
  is the single source of truth; symlink `.claude/ → .agents` (and add others as
  tools appear). Replace the duplicated `CLAUDE.md` with a symlink to `AGENTS.md`
  (or a one-line pointer) so the constitution lives in exactly one place.
- Add `.claude/settings.json` (or equivalent): permission allowlist for the build
  commands (`zig build`, `./rebuild.sh`, `build_web.sh`, run scripts) and the
  WebFetch domains we actually use (raylib, flecs.dev, RmlUi, emscripten docs).
- **Acceptance:** editing a rule in `.agents/` is visible from every tool dir; no
  duplicated constitution.

## Phase H1 — Glossary (canonical vocabulary)
- New `GLOSSARY.md` (table: Term | Meaning | Aliases to avoid). Seed with the
  terms that drift in an ECS + bindings repo:
  - `World`, `Entity`, `Component` (runtime struct via meta addon) vs **tag** vs
    **pair**, `System` vs **phase callback**, `query`, `phase` (`ON_UPDATE`...),
    `binding`/`mrbgem` (avoid "gem"/"plugin"), `generator` (`gen_raylib.rb`),
    `platform seam`, `command queue`, `hot-reload` vs **reboot**, `the bridge`,
    `the live mount`.
- **Acceptance:** every new doc/skill uses glossary terms; "aliases to avoid"
  catches the obvious synonyms.

## Phase H2 — Per-module orientation (folded into `.agents/knowledge/`)
**Decision:** keep ONE tribal-knowledge location. Each per-area knowledge doc opens
with an "At a glance" header (Summary / Key files / API+spec pointer / Cross-refs)
above its deep tribal detail — no separate `.agents/features/` tree (a 2nd per-area
location just drifts). Covers the modules that exist; add agentic ones as built
(Part B):
- `raylib-`, `rmlui-`, `flecs-`, `jolt-binding.md`, `build-system.md`, `web-target.md`.
- Later (after Part B): `agent-bridge.md`, `hot-reload.md`, `logging.md`,
  `flecs-observability.md`, `console.md` (all in `.agents/knowledge/`).
- **Rule of thumb:** write the knowledge doc *first* when you start work in an area
  that lacks one — it doubles as the Plan-Mode brief and pays for itself.

## Phase H3 — Skills (`.agents/skills/`, codified workflows)
Each skill is a short SKILL.md procedure for a thing we always forget the steps
of. Initial set, all THIS-repo-specific:
- `/add-binding-fn` — add a raylib fn: edit the **generator**, never `raylib_gen.c`;
  regen; `make clean` if switching target; rebuild order.
- `/add-flecs-system` — define a component (meta struct), register a system via
  the **hot-reloadable** registry (Phase R3), verify with the live mount.
- `/new-demo` — scaffold a `game/*.rb` scene against the platform seam.
- `/build-and-verify` — PATH strip → `./rebuild.sh` (desktop) → offscreen
  render→PNG → web build → node smoke test (mirrors `.agents/knowledge/testing.md`).
- `/add-knowledge` — where a newly-discovered gotcha goes (rule vs knowledge vs
  feature) so scar tissue gets crystallized, not lost.
- Later: `/agent-eval`, `/hot-reload-system`, `/inspect-state`, `/debug-with-logs`.

## Phase H4 — Subagents (scoped) — **DONE**
Scoped subagents in `.opencode/agent/`:
- **`backend-engineer`** — generalized C/C++ developer: new native features
  ("backend"), binding fixes/additions, generators, build system, and
  **performance migrations** (Ruby → C). Three job types, one agent.
- **`gameplay-ruby`** — `game/**` Ruby + `mrblib/*.rb` sugar. Uses the live web
  bridge (`bin/eval`, `bin/snapshot`, `bin/query`, ObjectSpace) to test. Never
  touches C/C++ or generators.
- **`reviewer`** — read-only; checks against rules + glossary + API specs.
  Permission: edit deny, bash ask.

New skill: `.agents/skills/ruby-to-native/SKILL.md` — the performance migration
workflow: profile via `bin/eval` → write C → swap call site → verify via web
bridge → update types. Covers bulk array processing, struct batch ops, and
keeping Ruby flexibility (don't over-migrate).

`AGENTS.md` updated with web-first workflow + subagent section.

## Phase H5 — Orchestration & cadence (lightweight)
Adopt the lighter half of `../dispatch`'s workflow:
- A short **plan-first** note in `AGENTS.md`: write a brief → Plan Mode → review
  the *plan* not the code → execute via a skill.
- `tasks.md` (live milestone log) + a `HANDOFF.md` convention for cross-session
  continuity. Skip the full prompts/​reports orchestrator unless multi-agent waves
  become routine.
- **Acceptance:** a feature can be driven from a one-paragraph brief + a skill,
  with ≤2–3 corrections.

## Phase H6 — LSP & editor intelligence (mruby + bindings)
Give the agent (and humans) language-server feedback over Ruby game code — made
*accurate* for mruby and our C-defined bindings.

**The catch (why this isn't just `lsp: true`):** opencode ships `ruby-lsp`, but it
assumes **CRuby**. Pointed at `game/**` it flags `Rl`/`Rml`/`Flecs`/`Jolt` as
*undefined constants* (they're C-defined — no Ruby source to index) and offers
CRuby-stdlib completions mruby lacks. opencode's own docs warn LSP "is not always a
net positive"; raw-enabled here it mostly emits **misleading** diagnostics.

**The leverage:** `gen_ai_reference.rb` already parses `raylib_api.json` /
`raymath_api.json` and has the exact machinery — a C→Ruby type map (`rtype`),
method naming (`ruby_method`), and fully-typed signatures (`sig`). The typed data
to teach an LSP our bindings already exists in machine form.

**Steps:**
- **Generate RBS** (`sig/*.rbs`, **generated — never hand-edit**, like
  `AI_REFERENCE.md`/`raylib_gen.c`): extend `gen_ai_reference.rb` (or a sibling
  `gen_rbs.rb`) to emit signatures for `Rl`/`Rml` reusing the existing type map;
  regenerate in the same step as AI_REFERENCE. `ruby-lsp` consumes RBS natively →
  completion / hover / signature-help / go-to-def on the bindings, and the
  undefined-constant noise disappears. Flecs/Jolt RBS comes later (their API lives
  in `API_SPEC_FLECS.md` etc., not `raylib_api.json`) — hand-write or add an emit.
- **`opencode.json` `lsp` block:** enable `ruby-lsp` for `.rb`; pick a rubocop
  noise policy (disable style cops or scope them); wire it to the `sig/` dir (RBS
  auto-discovery / `rbs_collection`).
- **mruby-core accuracy (deferred):** a slim hand-maintained RBS of mruby's actual
  core subset would kill the residual CRuby-stdlib false positives — high upkeep,
  low ROI; defer until the noise actually bites.
- **C/C++ side (bonus, separate):** opencode auto-installs `clangd` for the binding
  sources, but it needs a `compile_commands.json` we don't emit (zig/emscripten).
  Optional until we can emit one; not part of the mruby ask.

**Sequencing:** enabling `ruby-lsp` is only clearly net-positive *after* the RBS
stubs exist — before that it mainly adds noise. Land the generator with the config,
or gate the config behind it.

**Acceptance:** hover on `Rl.draw_text` in a `game/*.rb` shows the typed signature;
`Flecs::World` resolves; no false "undefined `Rl`" diagnostics; the agent can use
LSP nav on bindings.

---

# Part B — First-Class Agentic Runtime

This is the substance of the request: hot-reloadable Ruby in the browser, a WS
eval bridge, full state read/write, Flecs debugging/logging, structured logging,
and an in-game console. Built bottom-up; each phase is independently useful.

## Phase R1 — Eval-in + frame-polled command queue (foundation)
The primitive everything else rides on.
- Keep the `mrb_state` from `src/main.c` alive after boot (already is) and expose a
  re-entrant eval that runs **on the main thread only**.
- **C surface:** `jamstack_eval(const char *code) -> char *json` that does
  `mrb_load_string_cxt` on the persistent state and returns
  `{ ok, result (inspect), stdout, error, backtrace }` as JSON. Capture
  `mrb->exc`, format the backtrace, reset the exception so the loop survives a bad
  eval. On web, export via `EMSCRIPTEN_KEEPALIVE` + `cwrap`/`ccall`.
- **Ruby surface:** a `Bridge` (or `Console`) module with a thread-safe-by-frame
  command queue: producers `enqueue(code, id)`; `Bridge.drain` runs each queued
  command via eval inside `while_window_open`, before `world.progress`, and routes
  results back. Cap per-frame drain to bound frame time.
- **Desktop input path:** non-blocking TCP poll (`mruby-socket`, accept+recv with
  no blocking) each frame → enqueue. (WS upgrade comes in R4; raw TCP/line-JSON is
  fine to start.)
- **Web input path:** JS pushes into the queue via the exported C function (R4
  wires the actual WS; here just prove `jamstack_eval` works from `Module.ccall`).
- **Acceptance:** with the game running, send `Rl.get_fps` and a multi-line script;
  get the value, stdout, and a clean error+backtrace for a deliberate exception —
  the loop keeps running.

## Phase R2 — Structured logging pipeline (for AI debugging)
- Ruby `Log` module: levels (`debug/info/warn/error`), structured fields, tags,
  monotonic frame counter. Emits **NDJSON** (one JSON object per line) — the
  format `../dispatch/notes/observability-design.md` standardizes on and that
  agents can grep/tail trivially.
- **Ring buffer** in memory (last N entries) queryable via eval
  (`Log.tail(50)`, `Log.grep(/.../)`) so an agent can pull recent context without
  a file.
- **Two console streams (P5 — keep them distinct, not merged):**
  - **`game-console`** — the structured `Log` NDJSON above (game + engine
    *intent*): mruby exceptions, flecs logs, gameplay events. All targets. Flows
    *through* the runtime/bridge.
  - **`browser-console`** — the **actual platform console**: every line the
    browser would print to DevTools (JS exceptions, unhandled rejections,
    Emscripten `printErr`, and **wasm aborts** — OOM/stack-overflow/asserts).
    Web only; desktop analog is process stdout/stderr (raylib `TraceLog`, flecs,
    C `fprintf(stderr)`, uncaught mruby).
  - **Critical — runtime independence:** the lines worth most (aborts) are the
    ones that *kill* mruby, so a Ruby-side sink would miss them. Capture
    browser-console in **pure JS** (R4), buffer, and flush to the relay over WS
    regardless of module health. Both streams normalize to NDJSON, `source`-tagged;
    `bin/tail-log [--game|--browser]` can merge them by timestamp.
  - **Scope (deferred):** `browser-console` only exists on **web**, so it rides with
    R4 — and the capture work (in-page JS shim, and *especially* a browser
    extension) is **deferred until a real need appears** (judged low-odds). **R2
    delivers `game-console`:** the Ruby `Log` NDJSON plus tee-ing desktop process
    stdout/stderr (raylib/flecs/uncaught mruby) into the same stream.
- **Capture mruby exceptions** from the loop and from eval into game-console with
  backtraces.
- **Flecs logs:** call `ecs_log_set_level(n)` (bind it) and install a flecs
  OS-API log callback that funnels flecs's own tracing into this same pipeline, so
  ECS internals and game logs interleave with timestamps.
- **Acceptance:** an agent can answer "what errored in the last 5s and on which
  entity" purely from the NDJSON stream.

## Phase R3 — Hot-reloadable Flecs systems & features (the core ask)
Goal: **edit systems and add/modify features on a running browser game without
resetting** — entities and component data survive; only behavior swaps.

**Mechanism (the key design):** don't re-register Flecs systems on reload (that
loses identity and forces component re-lookup). Instead:
- A **system registry** keyed by name. `define_system(name, with:, phase:, &blk)`:
  - First call: registers a Flecs system whose C callback dispatches to a **stable
    Ruby dispatcher** that looks up the *current* proc by name in the registry.
  - Subsequent calls (reload): just **replace the proc** in the registry. Same
    system id, same matched tables, same entity/component state → new logic.
- `Hot.reload_string(code)` / `Hot.reload_file(path)`: re-`eval` a Ruby module of
  systems; `define_system` is **idempotent** so re-running a file just swaps procs.
- Convention so reload is clean: game code is organized into reloadable units —
  `game/components.rb`, `game/systems/*.rb`, `game/features/*.rb` — each
  re-runnable top-to-bottom without side effects beyond (re)registration.

**What survives vs what needs a reboot — document this table in `hot-reload.md`:**

| Change | Hot-reload? | Why |
|---|---|---|
| Edit a system's body / add/remove a system | ✅ swap proc | state untouched |
| Add a **new** component (new meta struct) + entities | ✅ additive | new id, no layout change |
| Change an existing component's struct layout | ⚠️ reboot (or migrate) | existing entities hold old layout |
| Add/rename a query, change phases | ✅ re-register that system | cheap |
| Change C/C++ binding, generator, build flags | ❌ reboot | native ABI / relink |
| raylib/native resource re-init (window, GPU) | ❌ reboot | native lifetime |

- **Web reboot path:** since C/C++ changes need a reboot anyway, the web flow is
  "rebuild wasm + reload page". Ruby-only changes never reboot — that's the win.
- **Acceptance:** in the browser, change a movement system's speed and add a brand
  new system while entities keep their positions; verify via the live mount that
  state persisted and behavior changed.

## Phase R4 — Agent bridge (WebSocket) + `.live/` mount
Turn R1's queue into the channel the AI and console actually use. Mirrors
`../dispatch` (WS transport + collector) and `../roblox` (`.live/` + cmd/result).

- **Web side — `web/agent-bridge.js`** (loaded by `shell.html`, dev builds only):
  - `cwrap` `jamstack_eval`; expose a WS client that connects out to the relay.
  - On `{id, code}` → push into the wasm command queue → return
    `{id, ok, result, stdout, error}`.
  - **Browser-console capture (runtime-independent; the shim is installed *first*
    in `shell.html`, before the Emscripten module script, so early `.wasm`/`.data`
    load failures are caught):** tee `console.*`, `window.onerror`,
    `unhandledrejection`, `Module.print`/`printErr`, and `Module.onAbort` → buffer
    → forward over WS as `{type:"browser-log"}`. Two completeness tiers:
    - **A (baseline, always on):** the in-page JS shim above. Catches everything
      routed through JS incl. all wasm aborts; misses a few browser-internal lines
      (some WebGL/CSP/deprecation). Works in any tab, no install; also the only
      tier that captures pre-attach / early-boot errors.
    - **B (exact) — Chrome extension via `chrome.debugger`: DEFERRED / unlikely.**
      It would attach CDP to the human's *real* tab for the literal, complete
      DevTools console (incl. the browser-internal lines A misses), but it's a lot
      of work (MV3 service worker, the "being debugged" banner, one-debugger-per-tab,
      Chrome-only) for a payoff we probably never need. Revisit only if A proves
      insufficient in practice.
    Even tier A is built only once a web console is actually wanted; until then the
    browser stream is out of scope (desktop uses process stdout/stderr).
- **Desktop side:** promote R1's raw TCP to a minimal WS server in-process
  (`mruby-socket`, non-blocking, framed), or just keep line-delimited JSON over TCP
  if simpler — same queue, same protocol shape.
- **Relay — `tools/agent-bridge/server.js`** (Bun/Node WS hub):
  - The running game connects as the **runtime**; agents/CLI and the human console
    connect as **clients**. Routes eval requests to the runtime, broadcasts logs +
    state snapshots to clients.
  - Maintains the `.live/` surface so **file-based agents** work without speaking
    WS (the `../roblox` pattern):
    Per-instance, keyed by play token (`.live/<token>/`) so parallel games don't
    collide; all writes atomic (temp + `rename`) so agents never read a half file:
    - `status.json` — connected?, target, frame, fps, entity count, token,
      uptime (cheap heartbeat).
    - `game-console` — structured `Log` NDJSON (R2): mruby/flecs/gameplay.
    - `browser-console` — the real browser DevTools stream (R2 capture); web only.
    - `state.json` — throttled world snapshot (Flecs REST JSON, Phase R5).
    - `.agent/cmd-<id>.json` → `result-<id>.json` — write-a-command,
      poll-for-result (the **only** agent-writable path).
    - `bin/*` — helper scripts: `eval`, `query <entity>`, `snapshot`,
      `tail-log [--game|--browser]`, `hot-reload <path>` (wrap the cmd/result
      protocol so an agent just runs a command).
- **`.live/` mechanism (no FUSE — unlike `../roblox`):** real files, because here a
  host-side writer always exists. **Desktop:** the game has `mruby-io`, so it writes
  `.live/` directly and scans `.agent/` for new `cmd-*` inside the R1 frame drain —
  no relay needed for file-based agents (verify `mruby-dir` for `readdir`, else add
  it or route through the relay). **Web:** the tab can't touch host disk, so the
  relay is the writer/bridge. FUSE was a `../roblox` workaround for a *sandboxed*
  runtime with no host helper; that constraint doesn't exist here, and FUSE is a
  poor fit for our WSL environment besides.
- **Security (P7):** localhost only; bridge + relay enabled by a build flag/env
  (`JAMSTACK_BRIDGE=1`); never included in a release web build.
- **Acceptance:** from a shell, `.live/bin/eval 'world.count(Position)'` returns a
  number; `.live/bin/tail-log` streams live errors; the same works against a
  browser tab.

## Phase R5 — Flecs observability (REST / Explorer / stats)
Surface Flecs's built-in debugging so the agent (and a human) get a full ECS view.
- **Bind it:** `Flecs::World#enable_rest(port: 27750)` → sets the `EcsRest`
  singleton; enable `FLECS_STATS`/`FLECS_MONITOR` for per-system timing and world
  stats; bind `ecs_log_set_level`.
- **Desktop:** `EcsRest` starts the HTTP server on `:27750` (separate thread,
  handled by flecs). Connect the **hosted Flecs Explorer** in *remote* mode
  (`flecs.dev/explorer?remote=true&host=localhost:27750`) for a full entity /
  query / stats UI — zero UI code on our side.
- **Web spike (R5a — research, do early):** flecs's `ecs_http` uses POSIX sockets
  and **cannot bind a listening port in the browser**, so the desktop REST path
  won't work in wasm. Plan: call `ecs_http_server_request()` (feed a synthetic
  request string, get the JSON reply, **no socket**) from a bound Ruby/C function,
  and ship those JSON replies over the **R4 WebSocket**. The relay re-exposes them
  on a local HTTP port so the hosted Explorer can connect to the browser game
  too. Validate that the REST module's request handler is reachable without the
  socket server thread.
- **Feed `.live/state.json`** from the same REST JSON (entities, components,
  stats) so file-based agents see structured world state.
- **Acceptance:** desktop game inspectable in the Explorer; browser game's world
  queryable as JSON through the bridge; per-system timing visible.

## Phase R6 — In-game Ruby console (RmlUi) — **DONE**
A user-facing REPL console with the same powers as the agent bridge (it shares
the `mrb_state`, so "full game state access" is free).
- ~~RmlUi panel (`game/ui/console.rml` + `.rcss`): scrollback + input, toggle key
  (e.g. backtick `` ` ``).~~ **DONE.** `Jamstack::Console` (in
  `mrbgems/rmlui/mrblib/console.rb`) manages the panel: toggle with backtick,
  Enter to eval, Up/Down for command history, auto-scroll, error backtraces.
- ~~Executes any Ruby via `Bridge`/`Console.eval`~~ — **DONE.** Uses `eval(code,
  binding)` with the game script's binding; full access to local variables and
  game state. Pretty-prints results via `inspect`, errors with class + message +
  backtrace.
- Works on desktop and web (it's just Ruby + RmlUi). **Verified on desktop**
  (eval, results, errors, history, toggle all pass). Web build compiles cleanly.
- ~~Note the **RmlUi keyboard-input gap**~~ — **CLOSED.** `rml_context_process_input`
  now forwards keys (raylib→RmlUi KI_* map), text input (`GetCharPressed`→
  `ProcessTextInput`), and modifiers. See `.agents/knowledge/rmlui-binding.md`.
- **Acceptance:** open console in the browser, type `world.each(Position){|e,p| ...}`
  and a hot-reload command; see results inline; close and keep playing.

## Phase R7 — Crystallize the new tribal knowledge — **DONE**
The runtime work generates exactly the kind of scar tissue the harness exists to
hold. Close the loop:
- ~~Knowledge docs: `agent-bridge.md`, `hot-reload.md`, `logging.md`,
  `flecs-observability.md`, `console.md`.~~ — **DONE.** All exist; `console.md`
  created with the R6 REPL console details (toggle, variable propagation,
  tab completion, caret control, key identifiers, HTML escaping).
- ~~New rules: "all eval/console/bridge commands run on the main thread"~~ —
  **DONE.** `.agents/rules/main-thread-eval.md`.
- Skills: `/agent-eval`, `/hot-reload-system`, `/inspect-state`,
  `/debug-with-logs` — deferred (low ROI; the knowledge docs cover the workflows).
- ~~Update `AGENTS.md` with "Agentic dev loop" section.~~ — **DONE.** Also
  updated the rules count (6→7) and the knowledge file table (added `console.md`).

---

## Dependency / sequencing summary

```
Part A (H0→H5)  ── can proceed in parallel with Part B; do H0–H3 first for leverage
Part B:
  R1 eval+queue ─┬─▶ R2 logging
                 ├─▶ R3 hot-reload  (needs R1)
                 └─▶ R4 bridge+.live (needs R1; consumes R2 logs)
  R5 flecs obs ──▶ feeds R4 (.live/state.json);  R5a wasm spike research early
  R6 console ────▶ needs R1 (+ RmlUi keyboard fix);  reuses R2
  R7 docs ───────▶ after each of R1–R6 lands (continuous)
```

**Suggested order:** H0–H3 → R1 → R2 → R3 → R4 → R5 + R5a → web W1 + W2 →
~~FIX flecs delete leak~~ ✅ → ~~bin-snapshot~~ ✅ → ~~ballpit yaw fix~~ ✅ →
~~H6 RBS generator~~ ✅ → ~~R7 tribal knowledge~~ ✅ → ~~H4 subagents~~ ✅ →
H5 remaining.
Next: H5 (tasks.md / HANDOFF.md cross-session convention).

## Open research questions / spikes
- **R5a (highest risk):** confirm `ecs_http_server_request()` works without the
  socket server thread in the emscripten build, and that the hosted Explorer can
  drive it through the relay. Fallback: ship raw REST JSON to the agent only (no
  Explorer UI on web).
- **WS in mruby on desktop:** is an in-process non-blocking WS server via
  `mruby-socket` worth it, or keep line-JSON over TCP and let the relay speak WS to
  clients? (Lower risk: latter.)
- **Component-layout migration:** is a "migrate on reload" path worth building, or
  is reboot-on-layout-change an acceptable permanent constraint? (Default: reboot.)
- ~~**RmlUi keyboard input** (R6 dependency)~~ — **DONE.** Gap scoped + closed:
  `rml_context_process_input` forwards keys, text input, and modifiers. See
  `.agents/knowledge/rmlui-binding.md`.
- **Snapshot cost:** how often can `.live/state.json` be regenerated from REST JSON
  without hurting frame time on large worlds? (Make it on-demand + throttled.)
- **Subagent granularity (H4):** one **game-code** subagent (summoned for any
  `game/**` edit) vs a finer split (`gameplay-ruby`, …). **Deferred** — lean toward a
  single game subagent first; revisit only if context bloat or coordination
  problems appear. The **game/engine boundary** (GLOSSARY "Code boundaries") holds
  either way, so this is just how many agents share the game side.
- **Parallel sessions vs ONE running game (R4):** the harness article's git-worktree
  parallelism assumes stateless file editing + independent test runs. Our hot-reload
  model is a **single process / single `mrb_state`** (P6). Two agents hot-reloading
  into the same game stomp each other. Plan: parallel worktrees must each spawn their
  **own** game instance (own process/port/bridge, own `.live/` dir keyed by the play
  token). Decide the per-instance `.live/` layout before R4.
- **Browser-console capture: DEFERRED.** Only matters on web; the in-page shim (A)
  is built if/when a web console is actually wanted, and the Chrome extension (B) is
  judged unlikely to be worth the effort (Playwright also rejected as too heavy).
  Until then, "console" means desktop process stdout/stderr + the Ruby `game-console`.
- **Flecs entity-delete leak (🔴 urgent):** the full 64-bit `ecs_entity_t` carries
  generation bits that flecs bumps on entity recycling; if `mrb_as_int` truncates or
  the binding strips them, `alive?`/`delete` operate on a stale id. Fix needs
  inspection of `fl_yield_iter`'s id yield, `fl_w_delete`, and `fl_w_alive` in
  `flecs_bindings.c`. (Observed: delete of 30 entities after a re-summon left 30/30
  in the query with `alive?`=false before any mruby-side delete call.)
- **Expose the bridge as an MCP server (R4):** wrap `jamstack_eval` / `query_state` /
  `tail_log` / `load_level` / `hot_reload` as MCP tools so any MCP-capable client gets
  first-class runtime access — the runtime analog of the symlink trick's
  tool-agnosticism — alongside the file-based `.live/bin/*`. Evaluate vs. raw WS.

## Non-goals (for now)
- No multi-threaded eval or background system execution (P6).
- No shipping the eval bridge in production web builds (P7).
- No hot-reload of C/C++/generator/build changes — those reboot, by design.
- No full `../dispatch` prompts/​reports orchestrator until multi-agent waves are
  routine (H5 keeps it light).