Closes a structural gap: the bot now actually REMEMBERS what it
discovered and what it tried. Two stores live under state/<host>/ and
are wired in automatically.
runtime/world-journal.js
- Append-only JSONL of discovered points (chopped, placed, base,
shelter, farm, dead_end). Indexed by 16-block spatial grid; O(neighbors)
nearest() lookups; 6 h age prune; 10k line ceiling with trim.
- leanestQuadrant({x,z}) reports the quadrant the bot has the FEWEST
markers in — used by explore.far to circle rather than retread.
- summary() exposed for the stuck-incident proposal body.
runtime/scenario-memory.js
- Sliding window of (skillId, situationHash, code, ok, detail) tuples.
- situationHash() is a coarse fingerprint (16x8x16 cell + day/night +
food/hp bucket + inv key set + closest hostile). So "same kind of
place + same kind of state" matches.
- shouldSkip({skillId, situation}) → true after ≥3 failures within 30
min UNLESS a more-recent success in the same situation un-locks it.
- recentTailFor() exposed for the stuck-incident body.
Wiring (runtime/bot.js):
- dispatchAction captures situationHash BEFORE the action runs and
records (skillId, situation, code, ok) after — failures are attributed
to the dispatch-time state, not the partial-effect state.
- worldDelta fields (choppedAt, minedAt, placedAt, baseAt, shelterAt,
plantedAt, harvestedAt, tilledAt) auto-flow into the journal.
- no_target + silent_dig_failure also write dead_end markers.
Scheduler / skills now consume memory:
- reflex.js curriculum reflex calls memory.shouldSkip — if the same
(skill, situation) failed 3+ times recently, auto-converts to a
wander hint so the bot leaves and tries elsewhere.
- explore.far calls journal.leanestQuadrant when multiple cardinal
directions are walkable and prefers the less-explored one.
- gather.logs walks to the nearest known "chopped" bucket within 96
blocks before falling through to findBlock — chunks with confirmed
trees are more likely to yield another.
stuck-incident body now includes journal byKind + last 12 scenario
entries so Pi can write a structural fix, not just a guard clause.
Architecturally: this is the foundation for "bot rewrites itself".
The proposals Pi now receives carry real signal about what was tried
and what's around, instead of a single snapshot in isolation.
10 new tests (world-journal × 5, scenario-memory × 5). npm test 134/134.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
27 KiB
Runtime — hybrid script + LLM-on-demand
Status: active. This is the recommended way to run pepa-pi-bot since 2026-05-25. The pure Pi runtime (
pifrom repo root) still works and is documented as a fallback at the bottom of this file.
Phase 0 product pivot (2026-05-25). MC chat is now dialog-only for everyone, including
OPERATOR_USERNAMES. The reflex loop no longer takes commands from chat. Local control lives in the TUI (p/shotkeys); long-term control lives in the repo. Seeplans/autonomous-survival-bot-prd.mdfor the survival-bot pivot.
Why a hybrid runtime?
The original design ran every tick inside Pi — the LLM saw the world, picked one tool, executed it, looped. That gave full self-extension out of the box, but had three problems in practice:
- Slow. A "look around → defend yourself" round-trip took 20–60 seconds because the LLM was in the hot path.
- Expensive. Hostile mob at 4 m? Cost of evasion = one full reasoning pass. Hungry? Same. Idle? Same.
- Invisible. With Pi as the only frontend, you had to
tmux capture-paneto know what the bot was doing.
The hybrid runtime splits the bot into a script-driven layer that handles fast, well-understood things on its own, and a Pi (or Codex) headless escalation that's only invoked when the script gets stuck or needs to write new code for itself.
Architecture
┌────────────────────────────────────────────────────────────────────┐
│ operator │
│ ├── repo edits (.env, skills/, runtime/) │
│ ├── TUI (Ink) — see status, send chat, press [a] to escalate │
│ └── (future) Telegram bridge │
└─────────────┬────────────────────────────────────────────┬─────────┘
│ Unix socket (newline-JSON) │ git
▼ ▼
┌────────────────────────────────────────────────────────────────────┐
│ runtime/bot.js — single long-running Node process │
│ │
│ ┌──────────────────┐ ┌──────────────────────┐ ┌────────────────┐│
│ │ Mineflayer │ │ Reflex loop │ │ IPC server ││
│ │ - MC TCP │ │ - tick every N sec │ │ - Unix socket ││
│ │ - AuthMe handler │◀─│ - priority order: │─▶│ - broadcasts ││
│ │ - chat / events │ │ defend > eat │ │ status/log/ ││
│ │ (dialog-only) │ │ > sleep > tech │ │ chat events ││
│ │ │ │ > autonomous │ │ - accepts ││
│ │ │ │ - NO LLM in path │ │ commands ││
│ └──────────────────┘ └─────────┬────────────┘ └────────────────┘│
│ │ │
│ ▼ on stuck / new scenario │
│ ┌──────────────────────┐ │
│ │ pi-bridge.js │ │
│ │ spawn `pi -p` │ │
│ │ stream stdout to IPC │ │
│ └──────────────────────┘ │
└────────────────────────────────────────────────────────────────────┘
│
│ TCP 25565
▼
Minecraft server
The bot is one process. The TUI is a separate process you can connect and disconnect at will — the bot keeps running. Multiple TUI clients can attach to the same bot simultaneously.
Quickstart
# Once
cd ~/Projects/pepa-pi-bot
npm install
# Terminal 1 — the bot daemon
npm run bot
# Logs go to stdout AND state/<host>/logs/<YYYY-MM-DD>.log
# Terminal 2 — the dashboard
npm run tui
The TUI auto-reconnects to the bot if you restart it. Press q to leave the
TUI; the bot is unaffected.
TUI hotkeys
| Key | Effect |
|---|---|
p |
Pause / resume the reflex loop (MC connection stays). |
s |
Stop the bot process gracefully (disconnect + cleanup + exit). |
r |
Force-broadcast a status snapshot now. |
c |
Enter chat mode — type a message, Enter sends it into MC chat. |
a |
Enter ask-Pi mode — type a prompt, Enter spawns pi -p and streams output into the Pi panel. |
y |
Open the latest pending proposal. In the proposal panel: y approves, n/Esc closes. |
q |
Quit TUI only. Bot keeps running. |
Enter submits, blank submit cancels. The status bar shows [proposals N, press y] when there's something pending.
What the reflex loop does today
The chain (highest priority first), wired and dispatching real Mineflayer actions:
defendReflex— closest hostile within 4 m →attackNearest(equips best melee). Within 12 m + low HP or ≥3 hostiles →fleeFromalong the away-vector.eatReflex— food < 16 →eatBestFood(picks from FOOD_PRIORITY list, equip + consume). 5 s cooldown.sleepReflex— night + no hostile within 8 m →sleepInBed(finds nearest placed bed within 16 blocks, paths there, sleeps). 5 min cooldown on failures.techTreeReflex— deterministic crafting progression (planks → sticks → wooden axe → pickaxe → sword) when prerequisites are in inventory.autonomousReflex— when nothing reactive fires: chop trees until ~16 logs, then wander to discover new chunks.idleReflex— every 20th tick, log heartbeat (HP / food / pos).
There is no operator-goal reflex anymore. MC chat does not create
movement/build/mining tasks (Phase 0 of plans/autonomous-survival-bot-prd.md).
Adding a new reflex = a function (ctx) => { action, ... } in
runtime/reflex.js, inserted at the right priority. Actions live in
runtime/actions.js. Both files trigger a supervisor hot-restart when
saved (see "Self-improvement" below).
When the bot calls Pi
Two escalation paths:
1. Manual — operator presses a in the TUI, types a question, the
bot spawns pi -p "<question>" and streams its stdout into the Pi
panel.
2. Automatic — every tick where the entire reflex chain returns
noop (no hostiles in reach, food fine, day or no bed, nothing to
craft, nowhere to wander) increments a counter. When the counter hits
ESCALATE_AFTER_NOOPS = 20 (≈1 min at tick=3s), the bot fires
askPi with the current snapshot and a fixed system prompt telling Pi
to suggest one next action. 10 min cooldown so a permanently-idle bot
doesn't run the LLM dry.
The auto-escalation prompt explicitly bans code-change proposals — Pi should only suggest what to do with the existing tools. If a deeper problem is happening, the failure-tracker (see Self-improvement) will file a proposal instead.
Observability (Phase 1 — survival-bot pivot)
Every STATUS snapshot now carries fields the TUI uses to answer "what is the bot doing and why isn't it doing more?" without parsing the log stream:
| Field | Meaning |
|---|---|
runtimeState |
finite-state classification: emergency / working / recovering / planning / social / idle (see runtime/state.js). |
activeSkill |
current dispatched action label, or the last one if idle. |
currentMilestone |
first uncompleted line from state/<host>/plan.md (cached 30 s). |
lastResult |
{ label, ok, code, detail, ts } of the most recent dispatched action. |
noProgressReason |
one of waiting_for_day, night_hostile_nearby, no_food_source, inventory_full, no_reachable_target, planner_empty, awaiting_action_cooldown, … emitted when position + inventory have not changed for ≥60 s (see runtime/no-progress.js). |
failuresByCode |
rolling counts of recent failures grouped by class (bug / timeout / feature-gap / other). |
lastEscalation |
{ ts, ageMs } of the most recent Pi auto-escalation. |
reflexPaused |
mirror of the local pause flag (so TUI shows the right state immediately). |
Skill substrate (Phase 2)
Lives under runtime/skills/. A skill is a small composable unit of
survival behaviour with a uniform contract:
export const skill = {
id: "namespace.action",
title: "Human label",
timeoutMs: 45_000,
preconditions(ctx) -> { ok, code?, detail? }
async execute(ctx, args) -> { ok, code, detail, worldDelta }
validate?(ctx, result) -> boolean // optional
recover?(ctx, result) -> any | null // optional
}
Registered skills are dispatched via runSkill(id, ctx, args) from
runtime/skills/index.js. The runner enforces the timeout, normalises
the result shape, runs validate() and calls recover() on failure
so the scheduler can act on the hint (e.g. "switch to wander"). Stable
failure codes the runner itself emits live in RUNNER_CODES
(unknown_skill, precondition_failed, timeout, threw,
validation_failed, done).
Item/block groups are dynamic: runtime/skills/groups.js exposes
logs(bot), planks(bot), sticks(bot), beds(bot), foods(bot),
axes(bot), pickaxes(bot), swords(bot) — every group is derived
from bot.registry, so a version-sensitive item that doesn't exist on
the connected server simply doesn't appear in the set and skills
return code: "unsupported_version" instead of crashing.
Reference skills shipped today: gather.logs, survive.eat,
explore.wander. The reflex loop still calls the older
runtime/actions.js primitives directly — porting more behaviours to
skills lands in later phases.
Run the contract + groups + curriculum tests:
npm test
Survival curriculum (Phase 3)
runtime/curriculum.js exposes a deterministic early-game progression:
wood.16 → wood.planks-and-sticks → wood.tools →
stone.32 → stone.tools → food.basic → storage.chest → shelter.torch
Each milestone has an isDone(inventory, snapshot) predicate and a
suggest(inventory, snapshot) function that names the next skill to
dispatch. isDone is stateful in the sense that reaching a later tier
implies all earlier "gather" milestones are complete (so the bot
doesn't loop back to "gather 16 logs" after crafting them into
planks).
The current curriculum result is on every snapshot as
snapshot.curriculum = { milestone, plan, inventoryFull } so the TUI
can show what the bot is working on and which skill should drive it.
Wiring the scheduler to actually call runSkill(plan.skillId, …) in
the reflex loop is a Phase 4 task; today the reflex still uses
actions.js directly.
Inventory pressure: isInventoryFull(snapshot) is exposed on every
curriculum result; the TUI surfaces [inventory full] next to the
milestone label so the operator can see when a deposit step is needed
before progress continues.
Optional: prismarine-viewer
Set VIEWER_PORT=<port> in .env to launch
prismarine-viewer
in-process. The package is not a default dep — install it explicitly
(npm i prismarine-viewer) before enabling. If missing, the runtime
logs a warning and continues.
Memory: world-journal + scenario-memory (2026-05-26)
Two persistent stores under state/<host>/:
world-journal.jsonl— append-only log of discovered points ({kind, name, at:{x,y,z}, ts}). Skills feed it automatically viaworldDeltaon each successful dispatch — chops, mines, placements, base/shelter location, planted/harvested crops, plusdead_endmarkers onno_target/silent_dig_failure. Indexed by a 16-block spatial grid sonearest({kind, x, z, radius})is O(neighbors). Pruned at 6 h age + 10k line ceiling.leanestQuadrant({x, z})returns the cardinal quadrant the bot has explored LEAST — used byexplore.farto circle rather than retread the same patch.scenarios.jsonl— sliding window of(skillId, situationHash, code, ok, detail, ts)tuples.situationHashis a coarse fingerprint of where + how the bot was (16-cell + 8y bucket, day/night, food bucket, hp bucket, inventory key set, closest hostile name). The curriculum reflex callsmemory.shouldSkip({skillId, situation})— ≥3 failures of the same(skill, situation)within 30 min and the reflex auto-converts into a wander hint instead of re-dispatching the failing skill. A subsequent success in the same situation un-locks it.
Both stores feed stuck-incident proposal bodies: when the LLM is
asked to patch a stuck state, it sees byKind journal counts AND the
last 12 scenario-memory entries, so it can write a structural fix
based on what's actually been tried, not just one snapshot.
Scheduler driven by the curriculum (2026-05-26)
The reflex chain is now: defend → eat → sleep → curriculum → idle.
curriculumReflex reads snapshot.curriculum.plan.skillId (populated
by runtime/curriculum.js each tick) and dispatches it via
runSkill(skillId, ctx). Recovery hints flow back through
ctx.skillBackoff:
- If the skill's
recover()returns{ hint: "wander" }(e.g.gather.logsreturnscode: "no_target"because there's no tree within 32m), the curriculum reflex swaps towanderfor ~60 s. - If the result code is
missing_tool/missing_material/no_target/no_food_source/unsupported_version, that specific skill backs off for 60 s instead of retrying every tick. - Unknown
skillId(curriculum suggested something that isn't registered yet) falls through towander— useful while we wire future skills likevillage.build-shelter.
The old techTreeReflex and autonomousReflex were removed — the
curriculum + craft skills cover their territory, and unit tests in
runtime/reflex.test.js exercise the new dispatch paths.
Chat banter escalation to Pi (2026-05-26)
When social/intent.js classifies an inbound line as
ADDRESSED_BANTER and templates can't answer, bot.js spawns a
one-shot askPi with the bot's runtime state + the last 5 lines from
that speaker (redacted via chatMemory). The reply is capped at 200
chars and sent as a single chat line.
Hard rate limit so banter can't drain the LLM budget: 6 calls per hour, minimum 90 s between calls. Suppressed escalations log once and silently drop.
Base-site scoring + locations (2026-05-26)
runtime/locations.js— atomic JSON store atstate/<host>/locations.json.setLocation(name, {x,y,z,…}),getLocation(name),nearestLocation({x,z}).runtime/base-site.js—scoreCurrentPosition(bot)returns{score, reasons, position}based on wood/stone/water proximity, surface flatness, distance to other players and absence of foreign builds (man-made blocks not in the owned-blocks ledger).runtime/skills/choose-base.js—village.choose-basescores the current spot; ifscore ≥ 8it writeslocations.base, otherwise emitscode: "too_weak"with awanderrecover hint.- The curriculum has a new final milestone
village.base-sitethat firesvillage.choose-baseuntil a base is established.
Compatibility hardening (Phase 7)
Several modules now guard against the live regressions PRD §7 Phase 7 explicitly calls out:
runtime/movement-profiles.js— named profiles (GATHER,TRAVEL,FLEE,BUILD,RETURN_TO_BASE) as pure descriptors;applyProfile(profile, bot)writes a freshMovementsto pathfinder. Stops one skill'scanDig=falsefrom leaking into the next skill's path.runtime/owned-blocks.js— JSONL ledger of blocks this bot placed (and removed).isOwned({x,y,z})for O(1) lookups.ensureDir()makes the dir lazily so first-write doesn't fail.runtime/claim-avoidance.js—classifyArea({blocks, isOwned})returnsplayer_build/natural_or_owned/insufficient_databased on man-made block density vs ownership ratio. Designed for the gather/place skills to call before touching a contested area.runtime/skills/compat.test.js— runsgroups.jsagainst realminecraft-dataregistries for 1.18.2, 1.20.4, 1.21.5; verifies that version-sensitive blocks (e.g.pale_oak_log) only appear where they should.
Self-improvement v2 (Phase 6)
Two classes of proposals now land in state/<host>/proposals/:
- Bug-class failures — same-label action returns
{ok: false}5× in a row, dominated bybug(TypeError, "Cannot read properties") or persistent timeout. Handled by the older tracker inbot.js. - Stuck incidents —
noProgressReasonstays the same for ≥5 min without a productive dispatch. Handled byruntime/stuck-incident.js. The proposal body includes the runtimeState, milestone, suggested skill, slim snapshot, last result, and per-skill success/failure metrics.
Both kinds now persist an editScope in their frontmatter — an
array of repo-relative path prefixes the auto-patcher is allowed to
modify. state-store.readProposalEditScope(filename) reads it back;
hooking scripts/auto-patch.js to refuse cherry-picks that touch
other areas is the remaining follow-up.
Per-skill metrics live in memory only (best-effort) but are surfaced
on snapshot.skillMetrics = { [skillId]: { ok, fail, lastTs } } so
the TUI can show which skills are reliable and which keep failing.
Social layer (Phase 5)
Inbound MC chat is classified via runtime/social/intent.js into one
of GREETING / STATUS_QUESTION / ADDRESSED_BANTER / COMMAND_LIKE
/ UNSAFE_REQUEST / AMBIENT. The classifier uses Unicode-aware
boundaries so "Привет всем" lands as GREETING while
"build me a tower" stays AMBIENT until the bot is addressed.
runtime/social/reply.js turns an intent into a short reply:
GREETING→ one of a small picked-randomly set ("yo" / "привет" / …).STATUS_QUESTION→ live snapshot summary: active skill, current milestone, hp/food/position, no-progress reason, last diary line.COMMAND_LIKE→ dialog-only notice (per Phase 0).UNSAFE_REQUEST→ terse "logged for operator review", plus an entry instate/<host>/escalations.jsonl.ADDRESSED_BANTER→ templates can't reliably answer, so the generator returnsescalate: true. Today bot.js does NOT spawn Pi from this path (keeps the LLM out of the hot path); a rate-limited escalation lands in Phase 6.
runtime/social/memory.js maintains an LRU per-speaker buffer of
recent lines (default 8 per speaker, 16 speakers max) and redacts
password / api-key / JWT-shaped tokens before they ever exit the
runtime.
In-game chat (dialog-only)
As of the Phase 0 survival-bot pivot, MC chat does not drive bot
actions for anyone, including names listed in OPERATOR_USERNAMES.
The bot will:
- reply to greetings (
hi,привет, etc.) and to being addressed by name, rate-limited; - answer status questions (
pepa_bot status,как дела,what are you doing?) from the live snapshot; - detect command-like verbs (
come,follow,build,pause,stop,give, …) when addressed, record them in the diary, and reply once per cooldown that MC chat is dialog-only.
Local control of the bot (pause/resume/stop, sending chat manually,
escalating to Pi) lives in the TUI. Long-term control (skills,
runtime code, proposals) lives in the repo. OPERATOR_USERNAMES is
still used to label speakers in logs (operator <name> vs
player <name>), and remains the right place to plug a future
trusted control channel (e.g. Telegram bridge).
IPC protocol
Socket: state/<MC_HOST>_<MC_PORT>/bot.sock (permissions 0600, removed on
shutdown). Framing: one JSON object per line.
Server → client events (see runtime/ipc-protocol.js):
| Type | Payload |
|---|---|
hello |
{ snapshot, recentLogs } — sent on connect. |
status |
full snapshot from perceive.js. |
log |
{ ts, level, source, text, details } — every log line. |
chat |
{ from, text, kind: "player" | "system" }. |
death |
{ reason, position }. |
error |
{ source, text }. |
ask-pi-chunk |
{ stream: "stdout" | "stderr", text }. |
ask-pi-done |
{ code, durationMs }. |
Client → server commands:
| Type | Payload | Effect |
|---|---|---|
cmd:pause |
{} |
Reflex loop stops ticking. |
cmd:resume |
{} |
Reflex loop resumes. |
cmd:stop |
{} |
Graceful shutdown of the bot. |
cmd:chat |
{ text } |
Sends text into MC chat (rate-limited). |
cmd:ask-pi |
{ prompt } |
Spawns pi -p "<prompt>". |
cmd:snapshot |
{} |
Force a status event now. |
The protocol is intentionally tiny — anyone can write a second client
(a Telegram bridge, a web UI, a one-shot CLI) by reading
runtime/ipc-protocol.js.
Self-improvement loop (fully autonomous)
End-to-end, no operator-in-the-loop. The bot writes proposals when it spots a real bug, applies them with Pi headless, and rolls them back if they break things. The flow:
1. reflex dispatches action → action returns { ok: false, detail }
2. bot.js failure tracker classifies the detail:
bug → TypeError / Cannot read / is not defined …
timeout → "timed out after Ns"
feature-gap → "no reachable log", "no food", "no bed" …
other → anything else
3. 5 consecutive failures with the SAME label, where the run is dominated
by 'bug' or all 'timeout' → writeProposal()
(feature gaps are SKIPPED — reflex routing solves those, not the LLM)
4. runtime/auto-improve.js watcher (poll 2s) sees the new file,
debounces 10s, then spawns scripts/auto-patch.js detached
5. auto-patch.js:
- refuses on dirty tree
- moves proposal pending → approved/ (audit trail)
- creates branch auto/<slug> off main
- runs `pi -p "<patch prompt>"` with 10-min timeout
- if Pi committed AND only touched runtime/ → cherry-pick onto main
- else → discard branch, exit non-zero
6. supervisor's runtime/*.js watcher fires the moment the cherry-pick
lands → child restarts on the new code
7. if the new code crashes >5 times in 60s AND the last commit on main
is younger than 15 min AND it touched runtime/ → supervisor
`git reset --hard HEAD~1` and restarts. Up to MAX_ROLLBACKS times
per supervisor lifetime, then bails out for manual investigation.
Rate limits
- Proposal cooldown: 30 min between proposal files of any kind.
- Auto-improve cooldown: 15 min between finished
auto-patch.jsruns. - Hourly cap: max 4 auto-patches per hour, even if cooldown allows.
- Rollback cap: 3 rollbacks per supervisor lifetime; after that the supervisor exits and waits for human review.
What counts as a bug
runtime/bot.js ships two whitelists (NORMAL_FAILURE_SUBSTRINGS and
BUG_FAILURE_SUBSTRINGS). The proposal trigger fires only when:
- the trailing run of same-label failures contains at least one bug (TypeError / ReferenceError / "Cannot read properties" / etc.),
- OR every failure in the run is a timeout (and they happened on the same operation, so it's probably broken not just unreachable).
Feature gaps like "no reachable log within 32 blocks" are not a bug
— the autonomous reflex sees that result, sets noTreesUntil and
switches to wander. If the script can't solve it via reflex routing,
that's a design issue the operator fixes by editing runtime/reflex.js
directly — not by asking Pi to patch around it.
Manual escape hatches
These still work but should rarely be needed:
- TUI hotkey
yopens the latest pending proposal for inspection. npm run propose:apply <filename>runs the attended version of the patcher — leaves the result on afeat/proposal-<slug>branch without cherry-picking, so the operator can review the diff manually.npm run stopkills everything and clears lock/socket.
File layout
runtime/
supervisor.js forks bot.js, watches runtime/*.js, restart-on-change
bot.js entrypoint — owns MC + tick + IPC + reconnect
config.js reads .env, exposes frozen config + redacted view
log.js ring buffer + stdout + daily file + IPC fan-out
perceive.js snapshot(bot) → JSON
reflex.js priority chain (operator > defend > eat > sleep > idle)
actions.js attackNearest / fleeFrom / eatBestFood / sleepInBed / goTo
state-store.js current-task / diary / proposals on disk
ipc-server.js Unix-socket server
ipc-protocol.js shared contract (event types, command types, framer)
pi-bridge.js spawn `pi -p`, stream stdout
tui/
tui.tsx Ink dashboard (React)
ipc-client.js socket client → EventEmitter
scripts/
propose-apply.js approved-proposal → feat-branch + `pi -p` patcher
Per-server state stays under state/<MC_HOST>_<MC_PORT>/, gitignored:
state/play.xmatic.team_25565/
bot.sock Unix-domain socket (perms 0600, ephemeral)
joined-before.flag AuthMe /register vs /login marker
current-task.json resume anchor — what the bot was doing
goal.md long-term ambition (operator-seeded)
diary/YYYY-MM-DD.md daily journal (one line per milestone)
proposals/ pending self-improvement proposals
proposals/approved/ approved, waiting on propose:apply
logs/YYYY-MM-DD.log full runtime log mirror
Pi-only fallback
The original Pi-driven runtime still works if you prefer the single-process
model — npm run agent from repo root loads AGENTS.md and the existing
extensions in extensions/. The two runtimes share the .env, the
mineflayer deps, and the state/ directory. They MUST NOT run
simultaneously — both will try to claim the same MC nickname and the
server will kick one of them.
If you switch between them frequently, kill one before starting the other:
# stop hybrid
# (in TUI press 's', or just kill `npm run bot`)
# start Pi
npm run agent