docs(runtime): reflect live reflex bodies + operator chat + self-improvement loop (#7)

Updates docs/runtime.md and README.md to match what's actually shipped:
  - reflex chain priorities and what each body now dispatches
  - operator chat command list (status, come, pause, resume, stop)
  - automatic + manual Pi escalation paths and the no-code-change rule
  - the full self-improvement loop end-to-end (detector → TUI approval
    → propose:apply → supervisor restart) with the rationale for the
    manual propose:apply step
  - new state files layout (proposals/, proposals/approved/, etc.)
  - supervisor.js + bot:bare script flags

No code changes.

Co-authored-by: Yuriy Mayatnikov <mayatnikov@me.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit was merged in pull request #7.
This commit is contained in:
Yuriy Mayatnikov
2026-05-25 16:22:38 +03:00
committed by GitHub
co-authored by mayatnikov Claude Opus 4.7
parent cd14bbf89a
commit 445524b34d
2 changed files with 130 additions and 37 deletions
+15
View File
@@ -96,8 +96,23 @@ The TUI auto-reconnects to the bot if you restart it. Press `q` to leave the TUI
| `r` | Force a fresh status snapshot. |
| `c` | Send a chat message into MC. |
| `a` | Ask Pi (one-shot subprocess). |
| `y` | Open the latest pending proposal (badge appears in status bar). In the panel: `y` approve, `n`/Esc close. |
| `q` | Quit the TUI — bot keeps running. |
### Self-improvement loop (short version)
The bot **proposes its own patches** when something repeatedly fails:
1. Reflex action fails 3× with the same label → markdown proposal lands under `state/<host>/proposals/`.
2. Status bar shows `[proposals N, press y]`.
3. Press `y` to read, `y` to approve. The proposal moves to `proposals/approved/`.
4. Run `npm run propose:apply <filename>` — spawns Pi headless on a fresh feature branch with the proposal + repo conventions. Pi writes a patch and commits.
5. Review the diff, smoke-test locally (`npm run bot`), push + open a PR by hand.
Supervisor (`npm run bot`) watches `runtime/*.js` and hot-restarts the child on file change, so during step 4 you can iterate quickly.
See [`docs/runtime.md`](./docs/runtime.md) for the full lifecycle and the operator-chat command list (`pepa_bot status`, `come`, `pause`, etc.).
## Authentication
Two dimensions:
+115 -37
View File
@@ -90,41 +90,71 @@ TUI; the bot is unaffected.
| `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.
`Enter` submits, blank submit cancels. The status bar shows `[proposals N, press y]` when there's something pending.
## What the reflex loop does today
All reflex bodies are currently **stubs** — they log decisions but don't yet
call into Mineflayer actions. The priority chain is wired:
The chain (highest priority first), wired and dispatching real
Mineflayer actions:
1. `defendReflex`closest hostile within 6 m → log + decision (next:
actually attack / flee).
2. `eatReflex` — food ≤ 16 → log (next: equip food, eat).
3. `sleepReflex`night + bed in inventory → log (next: `bot.sleep`).
4. `idleReflex` — every 10th tick, log heartbeat (HP / food / pos).
1. **`operatorGoalReflex`**if `OPERATOR_USERNAMES` issued a `come` /
`follow` command, satisfy it (walk to the operator's last known
position, reply in chat on arrival or failure).
2. **`defendReflex`**closest hostile within 4 m → `attackNearest`
(equips best melee). Within 12 m + low HP or ≥3 hostiles → `fleeFrom`
along the away-vector.
3. **`eatReflex`** — food < 16 → `eatBestFood` (picks from
FOOD_PRIORITY list, equip + consume). 5 s cooldown.
4. **`sleepReflex`** — night + no hostile within 8 m → `sleepInBed`
(finds nearest placed bed within 16 blocks, paths there, sleeps).
30 s cooldown on failures.
5. **`idleReflex`** — every 20th tick, log heartbeat (HP / food / pos).
Adding a new reflex = a function `(ctx) => { action, ... }` in
`runtime/reflex.js`, inserted at the right priority. Pure script, no LLM.
`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
Reflexes that don't handle a situation simply return `noop`. After N
consecutive tick cycles with no useful action — or when a reflex explicitly
flags "stuck" — the bot will escalate by calling `pi-bridge.js`:
Two escalation paths:
```js
askPi({
prompt: "I've been at the same position for 5 minutes, last reflex chain
fell through, snapshot attached. What's a reasonable next action?",
onChunk, onDone,
});
**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 operator goal, no hostiles in reach, food fine, day or no
bed, etc.) 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.
## Operator chat commands
Players listed in `OPERATOR_USERNAMES` can address the bot in MC chat
by prefixing the message with the bot's name:
```
pepa_bot status → bot replies with HP / food / pos / hostiles / busy
pepa_bot come → bot pathfinds to the operator's current position
pepa_bot pause → reflex loop stops
pepa_bot resume → reflex loop resumes
pepa_bot stop → graceful disconnect + process exit
```
This is **not wired into the reflex loop yet** — the escalation is currently
operator-driven via TUI hotkey `a`. Wiring it up as an automatic fallback is
the next milestone.
Unrecognized commands get a polite "didn't recognize" reply. Operator
identity verification is the server's job (AuthMe on cracked,
online-mode on premium) — the bot trusts the nickname.
## IPC protocol
@@ -159,32 +189,66 @@ 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 (planned)
## Self-improvement loop
When a reflex repeatedly fails (e.g. "tried to navigate to base 3 times,
pathfinder returned noPath each time"), the bot will:
End-to-end and wired. The flow:
1. Write `state/<host>/proposals/YYYY-MM-DD-<slug>.md` describing the gap.
2. Mark a flag in the next `status` event so the TUI surfaces it.
3. Wait for operator approval (TUI key `y` on a proposal — not yet built).
4. On approval: spawn Pi headless with the proposal text + repo context, ask
it to write a new skill / patch, commit on a feature-branch.
5. Hot-reload the affected module (reflex / actions) without dropping the MC
connection.
```
1. reflex chain dispatches an action → action returns { ok: false, detail }
2. bot.js failure tracker accumulates the failure under its label
3. same label fails 3× in a row → writeProposal() → markdown lands in
state/<host>/proposals/<ts>-<slug>.md
4. next IPC STATUS event includes pendingProposals: N
5. TUI shows [proposals N, press y] badge
6. operator presses y, reads the proposal, presses y again to approve
7. proposal moves to state/<host>/proposals/approved/
8. operator runs: npm run propose:apply <filename>
9. script verifies clean working tree, creates feat/proposal-<slug>
branch, spawns `pi -p` with the proposal + repo-conventions prompt
10. Pi commits a patch on that branch (no push, no merge)
11. operator reviews diff, runs `npm run bot` to smoke-test
12. operator pushes the branch and opens a PR by hand
13. supervisor on the running bot picks up runtime/*.js changes and
hot-restarts the child the moment they hit disk
```
This is the "bot writes its own code, asks permission, restarts itself"
loop — the whole point of having Pi as an escalation rather than a runtime.
Not wired yet; tracked under tasks #56#58 history.
Operator is in the loop at three guardrails: approving the proposal,
reviewing Pi's diff, deciding to merge.
### Triggers (today)
Only one detector is wired: "same labelled action fails 3 times in a
row" — for example, three back-to-back `flee from zombie` failures.
30 min cooldown so the same proposal doesn't multiply when the bot
keeps trying.
More triggers worth adding (each as a small follow-up):
- "Pi auto-escalation fired but the snapshot didn't change in the next
N ticks" → bot is fundamentally stuck, propose a code change.
- "death count >K in M minutes at similar coords" → safety regression.
- "operator typed the same chat command twice and the bot couldn't act"
→ missing operator verb.
### Why a manual `propose:apply` step
Approval inside the TUI is cheap — one keypress. Spawning Pi to write a
patch is not (subscription tokens, multiple minutes). Splitting "I want
this addressed" (TUI) from "now actually run the patcher" (CLI) means
you can approve five proposals over a session and dispatch them in a
batch when convenient.
## 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 (defend / eat / sleep / idle, stubs)
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
@@ -192,10 +256,24 @@ runtime/
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, same
as before. The `bot.sock` lives there too.
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