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:
@@ -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
@@ -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}`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user