AGENTS.md · git:20260917.4e7eba7 · 2026-09-17 · sha256 d872e0966159f80e
AGENTS.md git:20260917.4e7eba7A
Immutable. This exact content is served forever at /api/v1/blob/d872e0966159f80e.
# rimagent, developer notes (agent-readable)
Two halves, the mod as its own repo since 2026-09-16:
- `mod/` **RimBridge** (C#, RimWorld 1.6, Harmony): loopback HTTP bridge exposing the engine. Its own repo,
github.com/zorrobyte/rimbridge, vendored here as a git submodule — `git submodule update --init` after cloning,
and commit/push mod-side changes from inside `mod/` before bumping the pointer here. Symlinked into the
RimWorld Mods folder as `RimBridge`. Build `script/build.sh` (needs `DOTNET_ROOT=/opt/homebrew/opt/dotnet/libexec`),
then `script/restart-game.sh` (DLLs load at startup only; always launch via Steam so Workshop Harmony loads).
- `agent/` **rimagent** (Python, uv): the brain. `script/start.sh` = launch game if needed + `rimagent play` + dashboard.
- `brain/` what the agent authors: `skills/*.md` (frontmatter name/description/tags/always), `tools/*.py`,
`watchers/*.py` (hot-loaded), `memory/notebook.md` (per colony), `memory/journal.md` (cross-game), `scores.jsonl`.
The runner commits `brain/` per episode; the agent can `brain_revert`.
- `knowledge/` wiki dump + BM25 (`rimagent seed`), `source-1.6/` (ilspycmd of the installed DLL), `source-legacy/`.
## Bridge
`POST 127.0.0.1:8765/rpc {"method":"state.summary","params":{}}`; `GET /health /methods /events?since= /screenshot?x=&z=&w=`.
Method groups: game.* state.* map.* ui.* engine.* defs.* dev.* steward.*, see `[Rpc(name, doc)]` attributes in `mod/Source`.
All Verse work runs on the main thread via `MainThreadQueue` (drained in a `Root.Update` postfix); request threads
only parse/serialize. Never throw into Unity: every RPC error becomes `{ok:false,error}`. Namespaces `GameCtl`/`MapView`
avoid clashes with `Verse.Game`/`Verse.Map`.
- **Steward** (`steward.*`, `mod/Source/Steward/`, namespace `RimBridge.Steward`): two vendored engines that run every tick
without the LLM. `Scorer/` (Free Will port, MIT) writes work priorities for every *managed* colonist; `Stock/` (synchronous
rewrite of Colony Manager Redux, MIT) keeps stock jobs (forestry, foraging, hunting, mining, production, livestock) at
targets by designating work; `StewardRpc.cs` exposes status/enable/pawn/explain/posture/stock.*/settings/research,
`StewardTuning.cs` holds posture deltas and the per-pawn managed gate, `StewardLedger.cs` emits `stock_stalled` /
`stock_reached` / `posture_expired`. Both default ON and survive save/load. Rule: `ui.set_work` marks the pawn
unmanaged before applying and returns `steward_managed: false` (otherwise the scorer would clobber the change);
`steward.pawn managed=true` hands the pawn back. Nothing in steward.* marks the game assisted. Origins in
`THIRD_PARTY_NOTICES.md`; keep vendored headers, add "modified for RimBridge" lines.
- **Orders** (`steward.orders*`, `mod/Source/Steward/Orders/`): standing orders are deterministic reflexes that run from a
MapComponentTick, staggered by id, never throw, budget-logged over 20 ms: `combat` (draft capable fighters to the rally
rect, hold, release, then rescue), `rescue`, `unforbid`, `corpses`, `beds`, `policies`, `blueprints`, `fire`. One class per
order (`Order_*.cs`, base `Order { Id, Label, Doc, IntervalTicks, Enabled, Run(Map) -> OrderReport, Explain() }`), registry
and persisted state (enabled flags, rally rect, manual-touch cooldowns, last summary) in `StandingOrders.cs`, RPCs in
`OrdersRpc.cs` (`steward.orders`, `.set`, `.rally`, `.explain`, `.run`). Manual-touch rule: `ui.draft/goto/attack`,
forbid/unforbid, `ui.set_policies`, `ui.press` on a bed and `ui.job Rescue/TendPatient` record (id, tick) so the matching
order skips that pawn/thing for a cooldown. Ledger kind `orders` (`combat_engaged`, `combat_released`, `rescue`, `corpses`,
`blueprints_cancelled`, `fire`). The brain watchers that did the same from Python stay, but the runner marks them *superseded* (`registry.watcher_superseded`,
table `watchers.SUPERSEDED_WATCHERS`, config `steward.orders.superseded_watchers`) and `watchers.run_all` skips them while
their order is on and the mod answered `steward.orders.set`: their `ui.draft/goto/order/designate` calls would record manual
touches that pause the order for the very pawns they move. `watcher_write`/`watcher_delete` lift the mark; the seeded
doctrine tells the director to delete them.
## Agent
- `rimagent play [--max-days N] [--seeds a,b] [--no-pause] -v`, `rimagent think` (one step), `rimagent tools`, `rimagent llm "hi"`, `rimagent seed [--distill]`.
- Bridge methods auto-become tools `rw_<group>_<name>`; models send JSON args as strings → `registry.coerce_param`.
- Think step = fresh bounded conversation (`loop.think`), ends with `end_turn`/`end_episode`. Runner (`runner.py`) pauses
the game while thinking, wakes on schedule / ledger event kinds / watcher alerts, autosaves daily, runs an improvement
pass every N days and an episode reflection at the end, then starts the next seeded game.
- Dashboard `127.0.0.1:8770` streams `bus.py` events (see its docstring for the event contract).
- **Watchdog** (`watchdog.py`, `tools/watchdog.py`, `prompts/watchdog.md`, `roles.allow_watchdog`, config `watchdog:`):
a second, more privileged self-correction stream. The improvement pass stays inside `brain/` (hot-reloadable text,
safe by construction); the watchdog reads the **tool-call error stream** and patches the **project's own source**.
Trigger: `Runner.maybe_start_watchdog(day)` on each in-game day rollover, gated by `watchdog.due()` — needs
`every_hours` of wall clock **and** `min_errors` failed tool calls since the last pass (a clean error stream fires
nothing), and never runs beside itself or an improvement pass. It runs on its own thread (`start_watchdog_thread`),
never blocking play. Input: `recent_errors(bus, since_seq)` stitches each failed `tool_result` back to its
`tool_call` args and the `think_start` it came from; `format_errors` groups repeats. Scope: **`mod/Source/**` and
`agent/rimagent/**` only** — `safe_path()` rejects `..`, absolute escapes, symlink escapes, `.git`, `obj`/`bin`/
`__pycache__`, and everything else (`brain/`, `config.local.yaml`, `knowledge/`, `mod/1.6/`). Tools (group
`watchdog`, allowlisted exactly by `roles.allow_watchdog`, no `run_python`/`rpc`/`rw_*`/brain tools):
`repo_read` `repo_list` `repo_grep` `repo_patch(path, content)` `repo_revert(path)` `watchdog_verify_python()`
`watchdog_verify_mod()` `watchdog_commit(message)` `end_watchdog(summary, fixes, skipped)`, plus the read-only
knowledge tools. Proof is enforced server-side: a patch marks its root unverified, and `watchdog_commit` refuses
unless the matching verify tool ran, passed, and ran *after* the last patch to that root; it stages exactly the
paths touched (never `-A`) and appends the co-author trailer itself.
**It never deploys**: the verification build writes to `runs/watchdog-build/`, so `mod/1.6/Assemblies/` (the
symlink the running game loaded) is untouched, the game is never restarted, and there is no push — verified fixes
are committed locally and wait for a human to deploy at the next natural restart. Log: `brain/memory/watchdog_log.md`
(append-only, one entry per pass), bus kind `watchdog`, dashboard tab "Watchdog".
- Tests: `cd agent && uv run pytest -q`; `cd mod/Tests && dotnet test` (PathParser only; keep it Verse-free).
## Conventions
- Log from C# via `BridgeLog` (`[RimBridge]` prefix). Watch `~/Library/Logs/Ludeon Studios/RimWorld by Ludeon Studios/Player.log`.
- Tool results are truncated (~8k chars); prefer narrow queries in tools and docs.
- Config: `config.yaml` (generic) + `config.local.yaml` (gitignored: your LLM endpoint). Seeds, cadence, speeds live there.
`steward: {enabled, scorer, stock, orders: {enabled, off: [], superseded_watchers?: {stem: order id}}}` (all default true): the runner calls `steward.enable` and
`steward.orders.set` at new_game/recover_game and when the dashboard toggles it; the situation packet gets a "Steward"
block from `steward.status` (posture, stock rows, problems, orders line, `rally: none` when unset).