docs+chore: v0.2.0 README refresh + auto-patch opens PR (not direct merge)

- README: update lede, architecture block, self-improvement section, status.
  Mentions knowledge.db, coach/advice loop, persona narration, and the new
  PR-based auto-patch flow.
- scripts/auto-patch.js: replace cherry-pick-to-main with `git push` +
  `gh pr create`. The operator is now the only one who can merge into main
  (enforced by branch protection rules on the remote). Legacy direct-merge
  path remains behind PEPA_AUTO_PATCH_MERGE=cherry-pick for emergencies.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 13:31:27 +03:00
co-authored by Claude Opus 4.7
parent 9c2c56f30e
commit 042793a53f
2 changed files with 102 additions and 31 deletions
+25 -13
View File
@@ -1,10 +1,12 @@
# pepa-pi-bot
> A universal, autonomous, **self-extending** Minecraft player. Built on [Mineflayer](https://github.com/PrismarineJS/mineflayer) with a hybrid runtime: a fast script-driven reflex loop for the everyday, headless [Pi](https://pi.dev) escalation for the hard bits, and a **git-as-evolution-substrate** loop where the bot writes its own new skills and cherry-picks them onto `main` after passing a real `npm test` smoke gate. Works against **any** Minecraft Java server — vanilla, Paper, Spigot, Fabric, Forge, online-mode or cracked, modded or vanilla.
> A universal, autonomous, **self-learning** Minecraft player. Built on [Mineflayer](https://github.com/PrismarineJS/mineflayer) with a hybrid runtime: a fast script-driven reflex loop for the everyday, headless [Pi](https://pi.dev) escalation for the hard bits, a **SQLite-backed knowledge base** the bot reads and writes as it plays, and a **git-as-evolution-substrate** self-improvement loop where Pi writes new skills, runs `npm test`, and opens a PR for operator review. Works against **any** Minecraft Java server — vanilla, Paper, Spigot, Fabric, Forge, online-mode or cracked, modded or vanilla.
The bot is **not a finished application**. It is a seed: a Mineflayer body, a tiny reflex brain, persistent memory (`world-journal`, `scenario-memory`), a Voyager-style critic + Mindcraft-style modes/skill-library, and a self-improvement loop. The bot is expected to grow its own toolset over time — writing new reflexes, installing skills, adapting its behaviour as it plays.
The bot is **not a finished application**. It is a seed. v0.2.0 added the learning substrate: every death is recorded with full context and asynchronously analysed by Pi into generalised lessons; the dispatcher consults those lessons before each action and routes around mistakes; the bot narrates its life in Russian MC chat to feel like a player, not a script. Earlier substrate (v0.1.x): Mineflayer body, reflex chain, persistent `world-journal` / `scenario-memory`, Voyager-style critic + Mindcraft-style modes/skill-library, scoped auto-patch loop.
**Related work**: conceptually close to [Voyager](https://github.com/MineDojo/Voyager) (NVIDIA, GPT-4) and [Mindcraft](https://github.com/mindcraft-bots/mindcraft) (multi-agent LLM framework). The differentiator is that pepa stores its growing skill library as **versioned source code on `main`**, not as JSON in RAM — every Pi-written skill goes through `git checkout -b → npm test → cherry-pick`, making the loop auditable and rollback-safe.
**Related work**: conceptually close to [Voyager](https://github.com/MineDojo/Voyager) (NVIDIA, GPT-4) and [Mindcraft](https://github.com/mindcraft-bots/mindcraft) (multi-agent LLM framework). The differentiators:
- The growing skill library lives as **versioned source code on `main`** (not as JSON in RAM). Every Pi-written skill goes through `git checkout -b → npm test → PR for operator review`, making the loop auditable and rollback-safe.
- Learned lessons live in **a per-server SQLite knowledge base** (`state/<host>/knowledge.db`), with recipes, mob intel, block intel, lessons, deaths, post-mortems, points-of-interest, cached wiki pages, and a chat log. The dispatcher reads from it before acting.
The name `pepa-pi-bot` is just the project's name (`pepa` from the original test server, `pi` from the original runtime). The bot itself is server-agnostic.
@@ -36,12 +38,16 @@ Most Minecraft AI bots ship as monolithic projects: hard-coded actions, fixed pr
│ ├── Modes chain (self_preservation > hunger > shelter) │
│ │ priority interrupts before curriculum dispatch │
│ ├── Reflex loop (defend > eat > sleep > curriculum) │
│ │ pure script — no LLM in the hot path
│ │ consults coach/advice before every dispatch
│ ├── perception.js — numeric-id findBlocks (VB-safe) │
│ ├── knowledge/ — SQLite store (recipes, mob/block intel,│
│ │ lessons, deaths, post-mortems, POI, chat log) │
│ ├── coach/postmortem — death → DB → Pi → lessons │
│ ├── coach/advice — recall lessons → override / avoid │
│ ├── persona/chatter — Russian narration in MC chat │
│ ├── world-journal + scenario-memory (persistent JSONL) │
│ ├── stuck-incident → critic (Pi) → proposal │
│ ├── auto-improve → auto-patch → npm test → cherry-pick
│ ├── social/conversation — file-JSONL multi-agent topics │
│ ├── auto-improve → auto-patch → npm test → PR (review)
│ └── pi-bridge — spawn `pi -p` only on demand │
└──────────────────────┬───────────────────────────────────┘
│ TCP 25565 (any host/port)
@@ -113,15 +119,19 @@ The TUI auto-reconnects to the bot if you restart it. Press `q` to leave the TUI
### Self-improvement loop (short version)
The bot **proposes its own patches** when something repeatedly fails:
The bot **proposes its own patches** when something repeatedly fails. As of v0.2.0 the loop ends in a **PR for operator review**, not a direct merge to `main`:
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.
1. Reflex action fails 5× with the same label, or `noProgressReason` stays stuck → markdown proposal lands under `state/<host>/proposals/`.
2. `runtime/auto-improve.js` watcher (2 s poll, 10 s debounce) picks it up and spawns `scripts/auto-patch.js` detached.
3. `auto-patch.js`: refuses on dirty tree, moves proposal `pending → approved/`, creates branch `auto/<slug>` off main, runs `pi -p "<patch prompt>"` with 10-min timeout.
4. If Pi committed and only touched the proposal's `editScope`, AND `npm run lint-patch` + `npm test` both pass → `git push origin auto/<slug>` + `gh pr create` against `main`.
5. **Operator reviews the PR on GitHub and merges.** Branch protection on `main` requires approval — auto-patch cannot merge itself.
6. Supervisor watches `runtime/**/*.js` and hot-restarts the child on file change once the merged commit is pulled.
Supervisor (`npm run bot`) watches `runtime/*.js` and hot-restarts the child on file change, so during step 4 you can iterate quickly.
Manual paths still work:
- TUI hotkey `y` opens the latest pending proposal for inspection.
- `npm run propose:apply <filename>`*attended* version: same flow but leaves the branch local without opening a PR.
- `PEPA_AUTO_PATCH_MERGE=cherry-pick` env reverts to the legacy direct-to-main behaviour (not recommended; bypasses review).
See [`docs/runtime.md`](./docs/runtime.md) for the full lifecycle. **Note (2026-05-25):** MC chat is now dialog-only — operator commands (`come`, `pause`, `stop`, …) are no longer dispatched from chat; use the TUI for local control. See `plans/autonomous-survival-bot-prd.md` for the survival-bot pivot.
@@ -218,6 +228,8 @@ These are mirrored in `AGENTS.md` and re-stated at the top of any system prompt
🌿 **Survival-bot pivot (2026-05-25)** — the bot is becoming a self-sufficient survival resident of the configured server. **MC chat is dialog-only**; operator/player chat commands are recorded but not dispatched (TUI is the only local control plane). The hybrid runtime now has enriched perception, priority modes, a skill-driven curriculum, food acquisition, bed/sleep, base/chest/shelter/farm skills, persistent skill metrics, scenario memory, and a scoped auto-patch loop with `npm test` smoke gating. Full plan: `plans/autonomous-survival-bot-prd.md` (local-only, gitignored).
🌿 **v0.2.0 — self-learning agent (2026-05-27).** SQLite-backed knowledge base at `state/<host>/knowledge.db` with seeded recipes / mob intel / block intel / 12 starter lessons. Every death is captured with full context into a `deaths` table; a periodic Pi-coach loop (≤3 calls/hour, 12-min cooldown) batches unanalysed deaths and extracts generalised lessons. The reflex chain consults `coach/advice` before every dispatch — high-confidence lessons can override (`attack creeper``survive.flee`) or back-off. Bot narrates major events in Russian MC chat (≤8 lines/hour, ≥75 s gap). Auto-patch now opens a **PR for operator review** instead of cherry-picking onto `main``main` is protected, the operator is the only approver. Design: [`docs/v0.2.0-self-learning.md`](./docs/v0.2.0-self-learning.md).
Full plan: [`docs/roadmap.md`](./docs/roadmap.md). Memory layout: [`docs/memory-model.md`](./docs/memory-model.md). Day-to-day judgement: "Operating principles" in [`AGENTS.md`](./AGENTS.md).
## License
+77 -18
View File
@@ -3,18 +3,26 @@
//
// Unattended sibling of propose-apply.js. Picks an *unapproved* proposal,
// moves it to approved/, branches off main, runs `pi -p` headless, and if Pi
// commits something — cherry-picks the commit back into main. The point is
// to close the self-improvement loop with no operator interaction.
// commits something — pushes the branch to origin and opens a GitHub PR for
// operator review. The operator is the only one who can merge into main
// (enforced by branch protection rules).
//
// This is a v0.2.0 behavioural change. Earlier versions cherry-picked
// auto-patch commits straight onto main, which caused chaotic merge races
// against operator work. Now main is review-only.
//
// Exit codes:
// 0 patch applied cleanly (commit on main)
// 0 PR opened cleanly (or already exists)
// 1 pi spawned but produced no commit (no change to repo)
// 2 preflight failed (dirty tree, missing proposal, etc.)
// 3 pi exited non-zero
// 4 cherry-pick conflict — left in unresolved state on a branch
// 4 push or `gh pr create` failed — branch left on disk for inspection
// 5 optional fallback path: PR open disabled and cherry-pick conflict
//
// Override:
// PEPA_AUTO_PATCH_MERGE=cherry-pick ← legacy direct-merge (NOT recommended)
//
// Designed to be launched by runtime/auto-improve.js as a detached child.
// We deliberately avoid touching anything outside the repo and don't push.
import { spawn, spawnSync } from "node:child_process";
import fs from "node:fs";
@@ -249,19 +257,70 @@ pi.on("exit", async (code) => {
}
log("info", "smoke gate passed");
// Cherry-pick onto main.
git(["checkout", "main"]);
const cherry = git(["cherry-pick", newHead]);
if (cherry.status !== 0) {
log("error", `cherry-pick failed: ${cherry.stderr}`);
// Leave the branch around for operator inspection; abort the failed
// cherry-pick so main is clean.
git(["cherry-pick", "--abort"]);
exit(4, `cherry-pick conflict — see branch ${branch}`);
// v0.2.0 default: push branch + open PR. The operator approves the merge.
const mode = (process.env.PEPA_AUTO_PATCH_MERGE || "pr").toLowerCase();
if (mode === "cherry-pick") {
// Legacy direct-to-main path (NOT recommended; bypasses operator review).
git(["checkout", "main"]);
const cherry = git(["cherry-pick", newHead]);
if (cherry.status !== 0) {
log("error", `cherry-pick failed: ${cherry.stderr}`);
git(["cherry-pick", "--abort"]);
exit(5, `cherry-pick conflict — see branch ${branch}`);
}
git(["branch", "-D", branch]);
log("info", `patch applied to main as ${git(["rev-parse", "HEAD"]).stdout.trim().slice(0, 8)} (cherry-pick mode)`);
exit(0, `applied ${filenameArg}`);
}
// Success — delete the feature branch (the commit is on main now).
git(["branch", "-D", branch]);
log("info", `patch applied to main as ${git(["rev-parse", "HEAD"]).stdout.trim().slice(0, 8)}`);
exit(0, `applied ${filenameArg}`);
// Default: push the auto/<slug> branch to origin and open a PR.
// Operator reviews + merges on GitHub.
log("info", `pushing branch ${branch} to origin`);
const push = git(["push", "-u", "origin", branch]);
if (push.status !== 0) {
log("error", `git push failed: ${push.stderr}`);
git(["checkout", "main"]);
exit(4, `push failed — branch left on disk: ${branch}`);
}
// Build a PR body from the proposal file + Pi's commit message.
const proposalBody = (() => {
try { return fs.readFileSync(proposal.path, "utf8"); } catch { return ""; }
})();
const piCommitMsg = git(["log", "-1", "--format=%B", newHead]).stdout.trim();
const prTitle = `auto-patch: ${slug}`.slice(0, 70);
const prBody = [
"## Auto-generated patch",
"",
"This PR was produced by `scripts/auto-patch.js` from the proposal below.",
"Tests passed before push. Review the diff and merge when you're happy.",
"",
"## Pi commit",
"",
"```",
piCommitMsg.slice(0, 4000),
"```",
"",
"## Proposal",
"",
proposalBody.slice(0, 8000),
].join("\n");
const prArgs = [
"pr", "create",
"--base", "main",
"--head", branch,
"--title", prTitle,
"--body", prBody,
];
const pr = spawnSync("gh", prArgs, { cwd: REPO_ROOT, encoding: "utf8" });
if (pr.status !== 0) {
log("error", `gh pr create failed: ${pr.stderr || pr.stdout}`);
git(["checkout", "main"]);
exit(4, `pr create failed — branch ${branch} pushed but no PR`);
}
const prUrl = (pr.stdout || "").trim().split("\n").pop();
log("info", `PR opened: ${prUrl}`);
git(["checkout", "main"]);
exit(0, `pr opened ${prUrl}`);
});