Adds an explicit hierarchical needs catalogue that the reflex consults
on every tick. The bot now pursues tangible intermediate goals (food,
wood tools, shelter, stone tools, ...) instead of inheriting whatever
the curriculum thought was "next".
Ladder:
L0 alive HP>5, food>0, not in lava, not panic-near hostile
L1 food ≥6 food items in inventory (or sated + any food)
L2 tools_wood wooden_pickaxe + wooden_axe + wooden_sword
L3 shelter_basic bed placed nearby or in inventory
L4 tools_stone stone-tier triplet
L5 armor_basic any chestplate (pursue=null until craft.leather-*
lands; ladder gracefully skips)
L6 food_security ≥16 food items
L7 tools_iron iron-tier triplet (pursue=gather.stone for now)
L8 armor_iron iron chestplate (pursue=null for now)
L9 village_seed bed + chest in nearby blocks
L10 village_full never detected, falls through to curriculum
Each need has detect(snapshot) → bool and pursue(snapshot) →
{skillId, args} | null. The ladder picks the LOWEST unsatisfied
pursuable need. Needs whose pursue is null get recorded as
blockedNeeds and the walk continues — no stalling on missing skills.
Wired into curriculumReflex: manifesto takes precedence over
curriculum.plan when it has a concrete suggestion. Tests can pass
ctx.disableManifesto=true to exercise the curriculum branch
in isolation (existing reflex tests keep passing this way).
Pi self-reflection prompt now includes
"activeNeed (Maslow ladder L0-L10): L2 tools_wood → gather.logs"
so Pi advises at the right level instead of giving generic guidance.
skillId returned by pursue() is validated against the live registry
(rc.1 plumbing) — manifesto cannot accidentally dispatch a
hallucinated skill name.
Tests: 315 green (was 279 on rc.1, +36 new):
- runtime/manifesto/needs.test.js — 24 tests (per-need detect/pursue,
helper sums)
- runtime/manifesto/state.test.js — 10 tests (ladder walk, hostile
takeover at L0, armor skipping, caching)
- runtime/reflex.test.js — 2 integration tests (manifesto overrides
curriculum plan; well-fed bot pursues tools_stone)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Roots out the v0.2.x failure mode: Pi-extracted lessons routinely named
hallucinated skill ids (relocate.surface, choose.safe.surface,
survive.shelter, gather.visible_log, …). All 47 Pi-lessons in the live DB
had applied_count=0 because normalisePreferSkill couldn't find them.
Fix:
1. runtime/skill-registry.js — single source of truth derived from
skills/index.js. Exports listSkillIds, isRegistered, and a
prompt-ready block (skillRegistryPrompt) grouped by namespace.
2. Pi prompts (coach/postmortem, coach/reflect) embed the live registry
with a "USE ONLY THESE, never invent" instruction. Lessons are
filtered at write-time too — anything not in the registry and not a
known mode name gets dropped.
3. coach/advice.js — normalisePreferSkill now returns null for unknown
ids, hardening consult() against any hallucinations that slip
through. Warn-logged for visibility.
Also lays the LLM substrate for the rest of v0.3.0:
- runtime/llm/provider.js — OpenAI-compatible chat client. Configured
via PEPA_FAST_LLM_{BASE_URL,API_KEY,MODEL,TIMEOUT_MS}. Safe no-op
unless API_KEY is set. Supports JSON-mode.
- runtime/coach/fast-advisor.js — tactical advisor tier (scaffold).
Exposes advise() that asks the fast LLM what to do RIGHT NOW when
the reflex is wedged/stuck. Rejects hallucinated skill ids using the
registry. Rate-limited 6/h, 30s cooldown. Not auto-triggered yet —
wired into reflex in rc.3 (awareness layer).
Tests: 279 green (+24 vs rc.3): 5 registry, 9 provider, 10 advisor.
See dev/v0.3.0/PLAN.md for the full iteration design (manifesto needs
ladder, event-driven awareness, skill pre-emption) and STATUS.md for
shipped/pending tracking.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* v0.2.0-rc.3: pillar-up escape + advice everywhere + danger POI
Closes the gap rc.2 left open. Live observation showed:
- Pi-coach extracted 5 high-quality lessons (do not explore.far at
night near zombies, etc.) but none of them fired (applied_count=0
across the board). Root cause: dispatcher consulted advice only on
the main curriculum path; the bot was falling into the wander/
explore.far FALLBACK after each gather attempt bailed, which
bypassed consult().
- Bot was wedged in a pit on (608, 90) with stone walls. recovery.
tunnel-out kept failing ("Digging aborted") because mining stone
with fists takes ~10s/block; pathfinder watchdog kills it.
This patch:
1. survive.pillar-up (runtime/skills/pillar-up.js) — new escape skill.
Places a placeable block under the bot and jumps onto it; repeats
up to 8 steps. No pickaxe required. Works in dirt/cobble/planks/
sand/gravel/wool/etc. The bot's vertical exit from any pit it can
stand in.
2. Wedged-emergency reflex (runtime/reflex.js). At the top of
curriculumReflex, if noProgressReason is wedged-like AND position
hasn't shifted ≥16 blocks in 60s AND no hostile in 6m AND pillar
block in inventory → dispatch survive.pillar-up. 2-min cooldown
between attempts.
3. consult() now also runs on the WANDER/explore.far fallback path
(runtime/reflex.js curriculumReflex). Pi-coach lessons can finally
take effect. If the fallback skill is overridden to a non-eligible
skill but the bot has a placeable block, falls back to pillar-up.
Outcomes feed reportAdviceOutcome so confidence stays grounded.
4. recordPOI("danger") on death (runtime/coach/postmortem.js). Spatial
memory now flags where the bot died, expires after 6h. POI table
was empty in rc.2.
5. SAFE_OVERRIDES extended (runtime/coach/advice.js): adds
survive.pillar-up and village.choose-base so coach lessons can
route there.
Tests: 255/255 green (+9 pillar-up).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* v0.2.0-rc.3 fixup: relax wedged-escape trigger
Drop the WEDGED_REASONS check — noProgressReason is a string that
may or may not be set when the bot is stuck. Fire pillar-up purely on
"no horizontal progress ≥ 60s, no hostile in 6m, placeable block in
inv". Pillar-up is a constructive no-op when it's not needed (places
one dirt under self) so the false-positive cost is small.
Live observation: rc.3 was deployed and bot was wedged with tunnel-out
repeatedly aborted on stone, but wedged-escape never fired because
the runtime's noProgressReason wasn't in my whitelist. Removing the
gate lets the trigger actually engage.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Yuriy Mayatnikov <mayatnikov@me.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
P0 (correctness):
1. PEPA_HEADLESS=1 guard in extensions/mineflayer-bridge.ts. When `pi -p`
spawns a subprocess (banter, coach, planner, reflect, auto-patch), the
bridge no longer attempts a second MC connect — the hybrid runtime
already owns the nickname. runtime/pi-bridge.js sets the env var on
every spawn. Root cause of the "two pepa_bot's racing for the slot"
bug seen in reply-pi stderr.
2. Test state isolation in runtime/config.js. When running under the node
test runner (detected via execArgv/argv) — or when PEPA_STATE_DIR is
set — stateDir redirects to /tmp/pepa-test-state-<pid>/. log.js,
scenario-memory, world-journal, and knowledge.db all follow.
`npm test` no longer pollutes live scenarios.jsonl, world-journal.jsonl,
or daily log files. Verified empirically: post-fix run added 0 test
rows to the live scenarios file. Cleaned ~550 historical test rows
from live state in the same change.
3. defendReflex outcome reporting (runtime/reflex.js). Previously a
creeper-rule override marked the lesson succeeded=false BEFORE the
flee skill returned. Now dispatchDefendFlee accepts {lessonId} and
the onComplete fires reportAdviceOutcome with the actual flee result.
4. Mode-name → skill-id translation in runtime/coach/advice.js. Pi-coach
occasionally returns prefer_skill values that are mode names
("night_shelter", "self_preservation", "hunger"). normalisePreferSkill
maps these to SAFE_OVERRIDES entries before dispatch. Also handles
"tunnel-out", "survive_flee", "survive flee" shapes.
New behavior:
5. Self-reflection loop (runtime/coach/reflect.js). Every 30 min, the
bot asks Pi: "Are you making progress, or stuck in a loop? What
should you do differently?" Pi answers with a verdict
(progress/loop/recovering/idle/emergency), summary, next-action, and
0-N new lessons. The reflection is written to
state/<host>/reflections/<ts>.md and lessons land in the DB with
source="pi-reflect". Rate-limited to 2 calls/hour. Wired through
bot.js with the existing askPi + lastSnapshot accessor.
Tests: 246/246 green (+9 new: 6 advice mode-name + 4 reflect).
Co-authored-by: Yuriy Mayatnikov <mayatnikov@me.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This closes the learning loop. Lessons in knowledge.db now actually
influence behaviour:
- runtime/coach/advice.js: consult({plannedSkillId, snapshot}) reads
knowledge.topAdvice() and returns 'override' / 'avoid' / 'proceed'.
When a lesson says "avoid <skill>" with prefer="survive.flee" (etc.),
the dispatcher swaps in the alternative.
- runtime/reflex.js:
* curriculumReflex now consults advice before dispatch; on 'avoid'
backs off the planned skill + sets wander hint; on 'override'
dispatches the lesson's preferred alternative.
* defendReflex (dist≤4 melee branch) consults advice too — so a
creeper at 4m honours the starter rule "attack creeper → flee".
Failure outcomes feed back via markApplied so confidence stays
grounded.
SAFE_OVERRIDES whitelist contains only known runSkill targets
(survive.flee, survive.sleep, survive.eat, recovery.tunnel-out,
explore.far/wander, village.build-shelter); unknown prefers fall back
to plain 'avoid'.
7 advice tests; total suite 237 green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Baseline for the v0.2.0 self-learning iteration. All 205 tests pass on this
state. Subsequent commits in this branch layer the knowledge base,
post-mortem coach, and persona narration on top.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Problem (live 2026-05-26 screenshot): player drops a block in front of
the bot mid-path. mineflayer-pathfinder computes the path once when
goto() is called and never recomputes for world changes. Bot pushes
forward against the new block until the 30–60 s goto timeout fires,
visible as the bot just standing there pressing W.
Fix — runtime/pathfinder-watchdog.js: per-bot poll loop (2 s tick) that
runs while bot.pathfinder.goal is non-null. Tracks horizontal position.
If movement < 0.5 blocks for > 6 s after an initial 1.5 s grace,
forces a replan: setGoal(null) + setGoal(<same goal>) on a 250 ms
delay. That makes the planner rebuild the path against the current
world, so it routes around the new block — or, with canDig=true in our
profiles, digs through it. Capped at 3 replans per goal so a genuinely
unreachable target still bubbles up to the caller's timeout.
Side benefit: catches mineflayer-pathfinder issue #222 ("path hangs
on unreachable goal") much earlier than our 45 s goto wrappers.
Wired into bot.js on the "spawn" event and stopped on gracefulExit.
7 new unit tests cover the polling math + replan cap. 197/197 green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the canned-template "yo" path for greetings / status / addressed
banter with a Pi roundtrip that takes a real persona, the bot's current
in-game context, the operator's diary tail, AND the last 8 chat turns
with THIS specific player. Per-player cooldown (8 s) + per-message
chat-rate cooldown survive the existing throttle so a chatty player
can't drain Pi tokens.
runtime/social/chat-history.js — append/recent per player into
state/<host>/chat/<player>.jsonl, 1000-line rolling cap. Each entry
stores { ts, dir, text, snap? } where snap is a compact position +
activeSkill + milestone at the time of the turn, so Pi can later say
"помнишь когда мы тогда у воды лес рубили". Survives restarts and
auto-patch cherry-picks.
runtime/social/reply-pi.js — Russian-first system prompt locking the
bot as "pepa_bot, автономный игрок-фермер" on play.xmatic.team, one-
line answers, no emojis, no AI/bot self-mentions, no sycophancy. Spawns
`pi -p`, sanitises the response (strips pepa: prefix, code fences,
quotes, multi-line), caps at 200 chars before sending into MC chat.
Graceful: timeout/parse-fail → returns null, caller falls through to
the existing template path so the bot never goes mute.
bot.js handleChat:
- Skip messages from our own username (defensive — never reply to self).
- Record every inbound line into chat-history.
- For non-COMMAND_LIKE / non-UNSAFE intents, try piReply first; on
success, send + record outbound; on null/throw, fall through to the
templated generateReply.
14 new unit tests (sanitiseReply edge cases, history rotation, snap
compaction, prompt content). 190/190 green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three live-verification surfaces on top of v0.1.0:
1. cmd:screenshot { reason?, frames? } → runtime/viewer.takeScreenshot
- Headless POV render via prismarine-viewer.headless to
state/<host>/screenshots/<ISO>-<reason>.mp4 (1 frame ≈ ~10 KB).
- Lazy-loads the heavy GL stack on first call so the bot doesn't pay
the cost on startup or in TUI-only sessions.
- Returns { ok, path, error } over IPC LOG event.
- Known limitation: needs node-canvas. node-canvas v3 (current npm
default) is incompatible with prismarine-viewer's API; v2 doesn't
build under Node 24 (node-pre-gyp fail). So today the feature is
wired and the IPC contract is stable, but the underlying render
fails fast with "createCanvas is not a function". A future cleanup
can either fork the renderer or pin a Node 20 toolchain.
2. cmd:force-incident { kind?, reason? } → filePostCritique path
- Operator-triggered demo of the critic → proposal → auto-improve →
auto-patch chain. Was previously only observable when the bot
genuinely got stuck. Now a single IPC call exercises the full
loop on demand.
- Verified live 2026-05-26: critic call returned a real, useful
critique ("attack zombie returns done while target is alive →
blocks gather.logs"), proposal landed with all sections including
the Critic block, auto-improve picked it up within 10 s.
3. prismarine-viewer + canvas added to dependencies so npm install
builds the deps once and the IPC surface is always available.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five concrete patterns from Voyager and Mindcraft, applied in our shape
without abandoning the git-as-evolution-substrate that makes pepa
distinct. Plus a first multi-agent surface so two bots from the same
repo can share intent.
1. runtime/critic.js (Voyager critic.txt)
- Spawns `pi -p` with a JSON-only critic prompt before a proposal is
written. {reasoning, success, critique}.
- success=true short-circuits the proposal (bot recovered between
detector tripping and now), saving Pi tokens on false positives.
- critique is spliced into the proposal body via attachCritique() so
the downstream auto-patcher has a sharp spec.
- Graceful: pi missing / timeout / unparseable JSON → proposal still
filed without the critic block.
2. scripts/lint-patch.js (Mindcraft coder._lintCode)
- Pre-flight gate between Pi commit and npm test: node --check, dynamic
import (catches missing named exports), regex extraction of
runSkill("id") calls cross-checked against the live registry.
- Cheaper than npm test, fails fast with a clear reason.
3. runtime/stuck-incident.renderActionTemplate (Voyager action_template.txt)
- All proposal bodies now follow the same fixed-section layout: Task /
Last result / Execution error / State / Metrics / Journal /
Scenarios / Critique / Fix / Edit scope / Forbidden.
4. runtime/skill-library.js (Mindcraft skill_library.getRelevantSkillDocs)
- Word-overlap ranking (Mindcraft's offline fallback) — zero deps,
deterministic. auto-patch.js injects top-3 similar skills into the
Pi prompt as "look at these patterns".
5. runtime/modes.js (Mindcraft modes.js)
- Declarative {name, interrupts, on, active, update(ctx)} chain that
runs BEFORE the curriculum each tick.
- Ships self_preservation (low HP → eat/flee), hunger (food<14 → eat),
night_shelter (night + bed in hand → sleep). Cleaner than ad-hoc
lastFleeAttempt cooldowns in reflex.js.
6. runtime/social/conversation.js + cmd:conv-say/conv-recent/conv-list
- File-JSONL topic channel so two bots from the same repo (different
usernames, different host dirs under state/) can append turns and
read peers. Skeleton — multi-agent collaboration on top later.
Differentiator preserved: every Pi-written skill still lands on main via
auto-patch.js (real git branch + smoke gate + cherry-pick). Voyager
keeps skills in a Chroma JSON, Mindcraft keeps them in RAM — pepa keeps
them as versioned source code reviewable in `git log`.
package.json: 0.0.1 → 0.1.0. 174/174 tests pass. README + AGENTS updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause of "bot just stands still": every gather.* skill was using
bot.findBlock({ matching: (b) => names.includes(b.name) }), and under
mineflayer 1.21.4 + ViaBackwards the Block objects fed into the
callback have a wrong .name field (Block.type / numeric id is still
correct — this is mineflayer issue #2347). Every search returned null,
every skill reported "no_target", reflex looped wander → tunnel-out
forever. The bot's logs said "dispatch ok" while the operator watched
it pace in circles.
Proven live with a new diag.match skill on play.xmatic.team:
findBlocks({matching: numericIds}) → 50 hits
findBlocks({matching: (b) => b.name === ...}) → 0 hits ← the bug
findBlock({matching: (b) => b.name === ...}) → null ← the bug
findBlock({matching: numericIds}) → dark_oak_log @ (606,62,110)
After this fix the same bot from the same spawn dispatches gather.logs
and reaches the chop loop ("chop: dark_oak_log at 606,62,110 (tool=fists)")
instead of returning "no reachable log within 64 blocks".
Changes:
- runtime/perception.js (new): findBlocksByName / findNearestBlockByName
centralise the numeric-id workaround for any future skill.
- runtime/actions.js: chopNearestTree, sleepInBed, placeCraftingTable now
use perception. Also load mineflayer-tool plugin alongside collectblock
(collectblock 1.6 hard-requires bot.tool to dispatch a dig).
- gather-stone, gather-wool, deposit-surplus rewritten to numeric-id
search. gather-wool also loads mineflayer-tool.
- diagnose-scan.js (new): two diagnostic skills — diag.scan reports
findBlocks counts per radius for common blocks; diag.match cross-tests
the four matcher styles so this regression can be re-proven on demand.
- runtime/skills/index.js: registers diag.scan + diag.match.
Memory: project_findblock_callback_broken_under_viabackwards.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When Pi writes a multi-file runtime patch, the supervisor's file watcher
can fire between two consecutive writes, kill the bot mid-edit, and load
a half-saved file with a SyntaxError. Loop until the operator stops it.
scripts/auto-patch.js now creates state/auto-patch.lock with its PID
right after the branch checkout (before spawning pi -p), and removes
it on every exit path. runtime/supervisor.js defers any watch-triggered
restart while the lock holder is alive, polling every 2 s; once the
lock drops it waits 1.5 s for the final write to settle, then runs
\`node --check\` on the changed file and only restarts if it parses.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pi-authored follow-up from a second auto-patch cycle — the original
"does not count jumping in place as escape" test forgot to provide a
dig() mock, so the in-place jump path crashed when the test exercised
escape-pit's "dig above" fallback. Adding the mock makes the test
actually verify the assertion it claims.
138/138 tests now.
Co-Authored-By: pepa_bot self-improvement loop <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two bugs the live self-improvement run exposed:
1) Auto-patch was spawned with detached:false, so when supervisor
restarted bot.js (file change after Pi's commit landed on the
auto branch), the auto-patch child was killed mid-way — Pi's
commit lived in the auto branch but never got cherry-picked.
Recovered manually this round via reflog + cherry-pick. Now
detached:true + child.unref() + a per-run log at
state/_auto-patch-last.log so the operator can read Pi's full
output later.
2) Pi's recovery-tunnel-out.test.js was created but not in npm test
script; tests would have stayed unrun forever. Added.
Also commits the Pi-authored skill (eb29591 cherry-picked):
- runtime/skills/recovery-tunnel-out.js (+ test)
- improvements to runtime/actions.js + runtime/skills/explore-far.js
- wired into runtime/skills/index.js
npm test 137/137.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the self-improvement loop: when escape-pit + wedged-jump +
blind fallback all run 3× in a row without freeing the bot, fire a
dedicated proposal at the auto-improver. Pi gets the full context
(journal byKind, last 12 scenario-memory entries, current slim
snapshot) and is asked to either improve escapePit() (dig forward +
down + side, not only up) OR add a brand-new recovery.tunnel-out skill.
- runtime/stuck-incident.js: new checkWedged() path with separate
cooldown (10 min) from the no-progress path. noteResult() ingests
every dispatched action's detail.mode to count wedged completions.
- runtime/bot.js: dispatchAction calls stuckIncident.noteResult(res)
after each result; tick() calls checkWedged() and files the proposal
via writeProposal({editScope:[runtime/actions.js, runtime/skills/,
runtime/reflex.js]}).
This is the architectural piece: a bot wedged in a 1×1 hole now
generates a proposal that Pi can act on (with edit-scope guard rails
+ npm test smoke gate from PR #19), instead of looping wedged-jump
forever.
Verified live: bot now also picks direction from journal —
"explore.far: journal says leanest quadrant=NE → prefer N" — first
time the bot uses persistent memory to choose where to go next.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
When probe-cardinal shows all 4 directions blocked (the bot is in a
1×1 pit, surrounded by leaves, or in a corridor corner), don't just
hold forward+jump — actually dig the block above the bot's head,
jump into the new gap, repeat up to 3 times. Both wander and
explore.far now call escapePit() in this branch.
Observed live: bot fell into a pit at (623,71,106) after first
explore.far and looped wedged-jump→still-wedged→wedged-jump for 60s
before this fix. With escape-pit, the bot now actually breaks out.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ground-truth finding (diag.physics):
forward N:0.03 E:3.38 S:0 W:3.26 → forward WORKS in unobstructed dirs
jump ΔY=1.25 → jump WORKS (vanilla height)
dig untested (no soft block within 6 of spawn)
So the bot CAN move and jump — the previous "stands still" symptom was
our wander/explore code picking blocked random angles and trusting a
pathfinder that times out on this server's terrain. Each retry just
picked another random direction, often the same blocked one.
- runtime/actions.js wander: probe 4 cardinal yaws for 800ms each,
measure actual Δ, commit to the best one for the remaining budget.
Falls back to "wedged-jump" (forward+jump 2.5s) only when ALL four
cardinals are <0.5 blocks.
- runtime/skills/explore-far.js: same probe-then-go shape, scaled to a
~48-block long walk in the best direction. Replaces the static
NE/SE/SW/NW quadrant rotation that ignored what was actually
walkable.
- runtime/movement-profiles.js: canDig back to true on gather/travel/
flee. The earlier "everything false" defensive default was based on
a wrong hypothesis (silent dig failure) — diag.physics + server-side
inspection (no anti-cheat plugin, spawn-protection=0) showed dig is
fine.
- runtime/compat.test.js: assertions follow profile defaults.
- runtime/skills/diagnose-physics.js: forward probe now tries 4
cardinals and returns trials + bestDir + bestDist so it can be used
to debug "wedged" reports later.
Verified live: bot now actually walks 47 blocks north after
probe.cardinal showed N:2.4 free. First end-to-end real movement on
play.xmatic.team since this session started.
npm test 124/124.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two-pronged response to user-confirmed "bot stands still, doesn't actually
chop" on play.xmatic.team:
1. Pin protocol — .env now sets MC_VERSION=1.21.4. minecraft-data has
wrong packet ID mappings for protocol 775 (server 26.1.2 via
ViaBackwards 5.9.1) — see mineflayer#3888 and #3717. 1.21.5 also has
an enchants decoder bug that breaks bot.dig. 1.21.4 is the last
protocol mineflayer 4.37.1 can speak cleanly through VIA.
2. Don't trust dig success — runtime/actions.js chopNearestTree and
runtime/skills/gather-stone.js now lookAt(face center)+forceLook,
await collectBlock, then re-read the target block. If the log/stone
is STILL there, return ok:false code:"silent_dig_failure" and
blacklist the position. Prevents the curriculum from reporting
"wood.16 in progress" while the world hasn't actually changed.
3. Defensive default — runtime/movement-profiles.js: canDig=false on
every profile until dig is confirmed working live. Otherwise
pathfinder schedules paths through must-dig blocks and the bot loops.
4. Ground-truth probe — runtime/skills/diagnose-physics.js dispatches
forward/jump/dig probes and writes the result to the diary. New
IPC command cmd:run-skill lets the operator (or a future curriculum
trigger) fire any skill on demand; it waits for the current action
to finish before dispatching. /tmp/pepa-runskill.mjs is a one-shot
client.
Live probe on play.xmatic.team confirmed: forward Δ=0.003 over 2s
(BROKEN — server rejects movement packets), jump ΔY=0.42 (likely
physics jitter, not a real jump). Strongly suggests an anti-cheat
plugin gating bot-style movements server-side — beyond protocol pin.
npm test 124/124.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to the iteration-1 fixes. Live smoke on play.xmatic.team
revealed the bot was spawning into a tree-less plain (no log within
32 blocks of spawn), looping wander→gather→no_target→wander
forever inside a 16-block box.
- runtime/actions.js: chopNearestTree search radius 32 → 64 (still no
trees on this spawn, but a normal biome will be served well by it).
wander now has a blind-walk fallback when pathfinder times out
(look+forward+jump for 3 s) so the bot at least unsticks from leaves
or pillars. Pathfinder timeout reduced 30 s → 15 s.
- runtime/skills/explore-far.js: new explore.far skill — walks ~48
blocks in a quadrant (NE/SE/SW/NW, rotating per call) so successive
hints actually circle the spawn instead of bouncing in place. Blind
walk fallback included.
- runtime/reflex.js: when the scheduler is told to wander twice in a
row by gather.* recover hints, it now dispatches explore.far instead
so the bot actually leaves the patch it's stuck in. Resets the
consecutiveWanderHints counter on any success.
- runtime/reflex.js (sleep): no longer dispatches when the bot has
neither a bed in inventory NOR a known shelter/base location —
saved one dispatch + 5-min cooldown per restart at night.
- runtime/reflex.js (eat): inventory check + lastEatAt always updated
fix the eat-spam loop observed live (every tick fired "eat" → "no
food in inventory" → again).
- runtime/skills/chop-logs.js: recognise "no log within ..." as
no_target so the recover hint switches the bot to wander/explore.
npm test 124/124.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Recovers the bot from the live-server symptoms reported 2026-05-26:
1) constant supervisor reconnects, 2) chop "clicks once and stops",
3) sleep does nothing without a bed and so blocks night-skipping for
other players, 4) curriculum reflex always fell through to wander.
Supervisor (#38):
- runtime/watch-filter.js: pure predicate excluding *.test.js + the
supervisor itself; recursive:true so skills/ + social/ edits also
restart. Burned a working main once when test files counted toward
the rollback threshold.
- runtime/supervisor.js: watch-triggered restarts no longer count
toward the crash-loop rollback path. Watcher is now recursive.
Chop / mine (#39):
- runtime/actions.js + runtime/skills/gather-stone.js: replaced raw
pathfinder.goto + bot.dig with mineflayer-collectblock's
bot.collectBlock.collect — handles approach, repositioning, LoS,
dig and pickup as one primitive. Old version "swung once" because
GoalGetToBlock often parked the bot in leaves above the log.
Sleep + bed (#40):
- runtime/actions.js: sleepInBed now ALSO places a carried bed on
solid ground next to the bot and sleeps on it. Critical so the bot
stops blocking player night-skipping the moment it owns a bed.
Bed pipeline (#41):
- runtime/skills/gather-wool.js: gather.wool skill — mines wool block
if any nearby, otherwise shears or attacks the nearest sheep.
- runtime/skills/craft.js: craftBedSkill (any colour the bot has ≥3
wool of, plus 3 planks, plus a table).
- runtime/curriculum.js: new milestone survive.bed sits between
wood.tools and stone.32 so the bot gets a bed BEFORE everything else.
Test fixture updated to include a red_bed in post-survive.bed stages.
Village / shelter / wheat (#42, #43):
- runtime/skills/build-shelter.js: village.build-shelter — real 3×3×3
resumable hut blueprint around the recorded base, places one block
per loop, idempotent so an interrupted build resumes correctly,
marks each placed block in the owned-blocks ledger.
- runtime/skills/deposit-surplus.js: village.deposit-surplus opens
the nearest chest and transfers surplus stacks while keeping a
reserve of tools/food/bed.
- runtime/skills/farm-wheat.js: farm.wheat does one step per call
(till adjacent-to-water grass, plant seeds, or harvest ripe wheat).
- runtime/curriculum.js: village.shelter milestone after base-site.
Scheduler glitch (root of "always wander"):
- runtime/bot.js: curriculum + locations are now computed BEFORE
runTick. Previously they were stamped AFTER, so reflex.js saw
snapshot.curriculum=undefined every tick and fell through to the
wander fallback. Verified live: scheduler now dispatches
gather.logs/gather.stone/craft.* by id via runSkill.
Eat-spam:
- runtime/reflex.js: eatReflex now checks inventory for actual food
and updates lastEatAt on EVERY dispatch (not only successes), so a
failed eat respects the 5 s cooldown instead of firing every tick.
npm test 123/123. Validated live on play.xmatic.team (curriculum
dispatched gather.logs via runSkill, recover hint switched to wander
when no log in range).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three closures of remaining PRD follow-ups, one merge:
1. Reflex scheduler now drives behaviour from the curriculum.
- reflex.js: replaced ad-hoc techTreeReflex + autonomousReflex with
curriculumReflex that dispatches the skill suggested by
snapshot.curriculum.plan via runSkill. Per-skill backoff for
missing_tool / missing_material / no_target / no_food_source /
unsupported_version. recover() hint with `{hint:"wander"}` swaps
the next tick to wander for 60 s.
- Chain is now: defend > eat > sleep > curriculum > idle.
- reflex.test.js: 11 new tests covering busy/disconnected,
defend/eat preemption, dispatch by id, unknown-skill fallback,
per-skill + wander-hint backoffs, onComplete updating backoff.
2. Pi escalation for ADDRESSED_BANTER with hard rate limit.
- bot.js: when generateReply returns {escalate:true}, spawn askPi
with bot state + last 5 lines from that speaker (redacted via
chatMemory). Reply capped at 200 chars, sent as one chat line.
- Rate cap: 6 calls/hour, 90 s min gap. Suppressed escalations
log once and silently drop.
3. Phase 4 substrate.
- runtime/locations.js: atomic JSON store
(state/<host>/locations.json) with setLocation / getLocation /
nearestLocation / removeLocation; 6 tests.
- runtime/base-site.js: scoreCurrentPosition(bot) + pure scoreSite
bundle (wood / stone / water / flatness / no-players /
no-foreign-builds, owned-blocks excluded from claim penalty);
6 tests.
- runtime/skills/choose-base.js: village.choose-base skill — scores
the current spot, writes locations.base if score ≥ 8, otherwise
returns code:"too_weak" with a wander recover hint.
- curriculum.js: new final milestone village.base-site fires
village.choose-base until a base location exists.
- bot.js: stamps snapshot.locations from listLocations() each tick
so the curriculum can read it without coupling to disk.
docs/runtime.md updated with three new sections.
npm test now 116/116.
Co-authored-by: Yuriy Mayatnikov <mayatnikov@me.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 7 of plans/autonomous-survival-bot-prd.md. Five small modules
that close the recurring "shared state" and "version-pinned list"
failure modes the PRD flags in §7 and §5.4.
New:
- runtime/movement-profiles.js: named profiles (GATHER, TRAVEL, FLEE,
BUILD, RETURN_TO_BASE) as pure descriptors via PROFILE_DEFAULTS,
plus applyProfile(profile, bot) that hands a fresh Movements to
pathfinder. Avoids the "flee left canDig=false on the shared
Movements, next chop got stuck in canopy" regression.
- runtime/owned-blocks.js: JSONL ledger of blocks this bot placed/
removed (state/<host>/owned-blocks.jsonl); isOwned({x,y,z}) for
O(1) lookups; ensureDir() makes the parent dir lazily.
- runtime/claim-avoidance.js: classifyArea({blocks, isOwned}) returns
player_build / natural_or_owned / insufficient_data based on
man-made block density vs ownership ratio; shouldAvoid(area) helper.
Designed for gather/place skills to call before touching contested
area.
- runtime/skills/compat.test.js: runs runtime/skills/groups.js against
real minecraft-data registries for 1.18.2, 1.20.4, 1.21.5; spot-
checks that pale_oak_log only appears on 1.21+ etc.
- runtime/compat.test.js: 10 tests covering movement descriptors,
isManMadeBlockName, classifyArea, owned-blocks markPlaced/dedup/
isOwned/markRemoved.
npm test now 79/79.
Co-authored-by: Yuriy Mayatnikov <mayatnikov@me.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 6 of plans/autonomous-survival-bot-prd.md. Expand the
self-improvement loop so the bot can spot and report no-progress
stagnation, not just exception-class failures.
New:
- runtime/stuck-incident.js: detector fires a structured proposal when
the same noProgressReason persists past 5 min (cooldown 30 min).
Body includes runtimeState, milestone, suggested skill, slim
snapshot, last action result, per-skill success/failure metrics
and a forbidden-paths list. Pure module — caller (bot.js) writes
the proposal.
- runtime/skill-metrics.js: in-memory per-skill ok/fail counters
surfaced on snapshot.skillMetrics for the TUI and the incident
body.
- runtime/stuck-incident.test.js: 6 tests covering null reason,
threshold gating, cooldown, reason change resetting the timer,
body composition and metrics snapshot.
Wiring:
- runtime/state-store.js: writeProposal accepts {editScope: string[]}
and persists it in the frontmatter; readProposalEditScope() reads
it back so future auto-patch.js can refuse cherry-picks that touch
other areas.
- runtime/bot.js: tick() invokes the stuck detector each tick,
records skill ok/fail via skillMetrics, stamps snapshot.skillMetrics
and writes the stuck proposal via writeProposal({editScope}).
dispatchAction now records into skillMetrics for both the
resolved-result and the exception path.
npm test now 46/46.
Co-authored-by: Yuriy Mayatnikov <mayatnikov@me.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 5 of plans/autonomous-survival-bot-prd.md. Make the bot feel
present in chat without ever becoming a command executor.
New: runtime/social/
- intent.js: classifyIntent({text, botName}) returns one of GREETING /
STATUS_QUESTION / ADDRESSED_BANTER / COMMAND_LIKE / UNSAFE_REQUEST /
AMBIENT. Unicode-aware word boundaries so cyrillic + latin both work
("Привет всем" → GREETING, "build me a tower" → AMBIENT unless
addressed).
- reply.js: generateReply({intent, speaker, snapshot, diaryTail}) →
short templated response, or {send: null, escalate: true} for the
caller to decide whether to spend Pi tokens.
- memory.js: createChatMemory() — per-speaker LRU buffer of recent
lines; redacts password / api_key / JWT-shaped tokens at append
time, so the buffer can be safely fed back into any future prompt.
- social.test.js: 12 tests (intent edges, memory eviction, redaction,
reply routing). npm test now 40/40.
state-store.js additions:
- readDiaryTail(n) — reads the last N lines of today's diary; used by
status replies.
- writeEscalation({from, request, whyUnsure, wouldHave}) /
listEscalations() — JSONL log under state/<host>/escalations.jsonl
for UNSAFE_REQUEST classifications and future operator review.
bot.js: handleChat() now routes through social/intent + social/reply
(replacing the Phase-0 inline regexes), records every line into
chatMemory, and writes an escalation when classifyIntent returns
UNSAFE_REQUEST. Command-like notice + dialog-only behaviour from
Phase 0 are preserved.
Co-authored-by: Yuriy Mayatnikov <mayatnikov@me.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 3 of plans/autonomous-survival-bot-prd.md. Gives the bot a
deterministic path from empty inventory through stone-tier tools and
basic storage, without an LLM call per tick.
New:
- runtime/curriculum.js: ordered milestone chooser
(wood.16 → wood.planks-and-sticks → wood.tools → stone.32 →
stone.tools → food.basic → storage.chest → shelter.torch). Each
milestone exposes isDone(inventory, snapshot) and suggest() returning
a { skillId } plan the scheduler can dispatch via runSkill. isDone
uses "stage reached" escapes so progress is monotonic — crafting
planks doesn't bounce the chooser back to "gather 16 logs".
- runtime/skills/gather-stone.js: gather.stone with pickaxe-required
precondition, blacklist on failed paths, registry-aware matching
(stone / cobblestone / deepslate / cobbled_deepslate / andesite /
diorite / granite).
- runtime/skills/craft.js: factory + concrete skills for craft.planks,
craft.sticks, craft.wooden-axe/-pickaxe/-sword, craft.stone-axe/
-pickaxe/-sword, craft.furnace, craft.chest, craft.torch (torch
requires coal or charcoal preflight).
Tests:
- runtime/curriculum.test.js: 14 tests covering chooser ordering,
per-milestone skill suggestion, inventoryFull threshold, monotonic
advancement across stage transitions.
- npm test now runs the full suite: 28/28 passing.
Wiring:
- runtime/bot.js: lastSnapshot.curriculum carries the next milestone
+ suggested skill on every tick; lastSnapshot.currentMilestone
prefers the curriculum title over the planner.md line.
- tui/tui.tsx: milestone line shows the curriculum's suggested skill
and an [inventory full] flag when isInventoryFull fires.
Reflex.js still calls actions.js directly; wiring the scheduler to
runSkill(plan.skillId, …) lands in Phase 4.
Co-authored-by: Yuriy Mayatnikov <mayatnikov@me.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2 of plans/autonomous-survival-bot-prd.md. Establishes the
composable skill contract from PRD §5.2 and ports three reference
skills so future phases can layer survival behaviour on top instead of
adding more ad-hoc branches to reflex.js.
New: runtime/skills/
- index.js: skill registry + runSkill(id, ctx, args) wrapper. Enforces
preconditions, hard timeout, normalises {ok, code, detail, worldDelta}
on every result, runs validate() and calls recover() on failure.
Stable failure codes live in RUNNER_CODES (unknown_skill,
precondition_failed, timeout, threw, validation_failed, done).
- groups.js: registry-derived item/block sets — logs/planks/sticks/beds
derived by suffix; foods intersects a curated allowlist with the live
bot.registry; axes/pickaxes/swords scoped to whatever the connected
server's item table actually ships. Empty set instead of throwing on
missing registry, so skills can emit code:"unsupported_version".
- chop-logs.js: gather.logs reference skill (wraps chopNearestTree).
- eat.js: survive.eat (wraps eatBestFood, preconditions check carrying
edible food from the registry-derived set).
- wander.js: explore.wander (wraps wander).
- contract.test.js + groups.test.js: 14 tests covering precondition
gating, timeout firing recover(), execute exceptions, validate
flipping ok→false, dynamic group filtering across mock registries.
package.json: `npm test` runs the new contract + groups suites.
docs/runtime.md: documents the skill contract, runner, dynamic groups
and the reference skills.
Reflex.js still calls actions.js directly — wiring the scheduler to
runSkill() lands in later phases when the survival curriculum kicks in.
Co-authored-by: Yuriy Mayatnikov <mayatnikov@me.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 of plans/autonomous-survival-bot-prd.md. The bot must always be
able to answer "what am I doing and why am I not doing more?" without
parsing the log stream.
New modules:
- runtime/state.js: pure FSM classifier emitting emergency / working /
recovering / planning / social / idle from snapshot + reflex context.
- runtime/no-progress.js: sliding-window detector that watches position
and inventory; when both are unchanged for 60 s+, emits one stable
reason code from REASONS (waiting_for_day, night_hostile_nearby,
no_food_source, inventory_full, no_reachable_target, planner_empty,
awaiting_action_cooldown).
- runtime/viewer.js: optional prismarine-viewer launcher behind
VIEWER_PORT. Lazy import so the dep is not required by default.
Wiring:
- runtime/bot.js: tick() now computes runtimeState + noProgressReason
every tick and stamps them on the snapshot along with activeSkill,
currentMilestone (read from plan.md, cached 30 s), lastResult,
failuresByCode and lastEscalation.
- runtime/bot.js: dispatchAction records lastResult and lastFailureAt
for the recovering-state classifier.
- runtime/planner.js: exports isPlannerBusy(), readNextMilestone()
and planExists() so the runtime can show planning state + current
milestone without spawning extra Pi calls.
- runtime/config.js: adds VIEWER_PORT support.
TUI:
- tui/tui.tsx: StatusBar gains a state badge, current-skill row,
milestone row, no-progress reason warning, last-result line with
ok/fail color, failures-by-class summary and last-escalation age.
Co-authored-by: Yuriy Mayatnikov <mayatnikov@me.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 0 of plans/autonomous-survival-bot-prd.md: change the product
direction from operator-driven remote control to autonomous survival
resident. MC chat is dialog-only for everyone, including
OPERATOR_USERNAMES — commands like come/follow/build/pause/stop are
recorded in the diary but not dispatched. TUI remains the only local
control plane.
Runtime changes:
- Remove operatorGoalReflex from reflex.js (the come-here chat command).
- Replace handleOperatorChat in bot.js with a dialog-only handleChat
that answers greetings/status questions and records command-like
verbs (en+ru) without dispatching them.
- Default MC_VERSION to "auto" in runtime/config.js; mineflayer
receives `false` to trigger version auto-detection.
- Update auto-escalation prompt's reflex chain summary.
Docs:
- AGENTS.md: product pivot notice up top; chat-driven scope-trust is
flagged as legacy/Pi-only.
- README.md / docs/runtime.md: replace operator-chat command list with
dialog-only description; update reflex chain summary.
- docs/roadmap.md: Phase 2/3 marked superseded by the PRD where they
assumed chat-driven control.
Co-authored-by: Yuriy Mayatnikov <mayatnikov@me.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the loop "стой и кидай proposals" → "копит ресурсы, строит,
работает к глобальной цели". Three pieces:
1. Crafting primitives (runtime/actions.js).
craftPlanks (4 per log, any wood type), craftSticks (4 per 2 planks),
placeCraftingTable (crafts a table from planks if needed + places at
reference block + reuses an existing table within 4 m), craftWoodenAxe,
craftWoodenPickaxe, craftWoodenSword. Each uses bot.recipesFor()
+ bot.craft() with a 15s timeout. Returns the same {ok, detail}
contract as the other actions.
inv.{getItemCount, getAnyPlanksCount, getAnyLogCount} helpers
exported so the reflex layer can read inventory cheaply without
pulling mineflayer state through every reducer.
2. Tech-tree reflex (runtime/reflex.js).
New techTreeReflex between sleep and autonomous. Inventory-driven
progression: log+0 planks → planks; planks+0 sticks → sticks;
planks+sticks+no axe → wooden_axe; +no pickaxe → wooden_pickaxe;
+no sword → wooden_sword. 5 s cooldown so we don't fire on every
tick.
Pure script, no LLM. The progression is exactly what a player
does in the first 10 min on a new world; making it scripted means
the bot never burns tokens on it.
3. LLM planner (runtime/planner.js).
Background timer (every 15 min, with a 30 s warm-up after start).
Reads goal.md + plan.md + a slim snapshot, prompts Pi to output a
fresh plan.md to stdout. Stripped of code fences and written
verbatim to state/<host>/plan.md. Capped at 16 KB.
The plan is markdown the operator can read or edit by hand. Numbered
milestones, ✓ prefix for completed ones, kept short. The reflex
layer doesn't auto-execute LLM text — but the planner sets the
long-horizon shape that future reflexes (build house, plant farm)
can read.
5 min timeout on the pi subprocess. If it crashes or times out, the
next 15-min tick just retries — no propagation to the reflex loop.
The progression now looks like, roughly:
chop log (autonomous) →
craft planks → craft sticks → wooden_axe (tech-tree) →
chop faster (autonomous, has axe now) →
wooden_pickaxe + wooden_sword (tech-tree) →
mine stone … (next PR: stone tools, farm site selection,
house frame)
Smoke-tested: all three modules import cleanly, exports check out.
Co-authored-by: Yuriy Mayatnikov <mayatnikov@me.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Operator feedback: "бот должен быть полностью автономным — сам себя
улучшать и чинить, в этом и есть смысл; все что я вижу пока что он
стоит на месте и кидает proposals на каждый чих — это кардинально не
то что я хочу". Acted on:
1. Trigger filter — proposals only on real bugs.
runtime/bot.js classifies failure detail into bug / timeout /
feature-gap / other. The 5-in-a-row trigger fires only when the run
contains a bug (TypeError / Cannot read / is not defined …) OR is
entirely timeouts on the same operation. Feature gaps like "no
reachable log within 32 blocks", "no food in inventory", "no bed in
range", "no target in reach" are SKIPPED — the reflex layer routes
around them (noTreesUntil → wander, etc). The LLM has no business
patching code for missing inventory.
Threshold raised 3 → 5 in a row. Cooldown unchanged (30 min).
2. Auto-apply, no operator-in-the-loop.
New runtime/auto-improve.js polls proposals/ every 2s. When it sees
a new .md and 10s have passed since first sighting (debounce),
spawns scripts/auto-patch.js detached.
New scripts/auto-patch.js: refuses on dirty tree, moves proposal
pending → approved/, branches `auto/<slug>` off main, runs `pi -p`
with 10-min timeout. If Pi committed AND every changed file is
under runtime/ → cherry-picks onto main. Otherwise discards the
branch. No push, no PR. Audit trail in state/<host>/proposals/approved/.
Rate limit: 15-min cooldown between finished runs + 4/hour hard cap.
3. Auto-rollback on bad patches.
runtime/supervisor.js: when MAX_RESTARTS_PER_MINUTE is exceeded
AND `git log -1 HEAD` is younger than 15 min AND HEAD touched
runtime/, runs `git reset --hard HEAD~1`. Up to MAX_ROLLBACKS=3
lifetime, then exits 1 for manual investigation. Restart counters
are reset after a successful rollback so the next attempt isn't
immediately killed.
4. current-task.json slim.
No longer stores the full perception snapshot (was ~3 KB per write
× every action). Position only — sufficient as a resume anchor.
Slim snapshot still goes into the proposal markdown for context.
docs/runtime.md — rewrote the self-improvement section: full flow
diagram, classification rules, all rate-limit knobs, manual escape
hatches kept but documented as rarely-needed.
Also cleared 5 stale proposals from previous smoke tests so the first
production run isn't burning Pi tokens on stale bugs that have since
been fixed.
Co-authored-by: Yuriy Mayatnikov <mayatnikov@me.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User report 2026-05-25: launched 'npm run bot' fresh, MC server kicked
every login with "Игрок с данным никнеймом уже играет на сервере" and
the bot fell into a perpetual reconnect-then-kicked loop. Root cause:
a smoke-test supervisor from an earlier shell was still running in the
background, holding the pepa_bot session open. Two supervisors racing
on the same nickname is undefined behaviour from the server's side and
results in this exact failure mode.
Changes:
runtime/supervisor.js — acquires state/<host>/supervisor.pid before
spawning the child. If another supervisor is alive (kill -0 check), the
new one exits with a clear message telling the operator how to recover.
On SIGINT/SIGTERM/exit the lock is released; stale pidfiles are detected
when the recorded PID is no longer alive.
scripts/stop.sh — emergency cleanup helper:
- kills any supervisor or bot.js processes matching this repo
- removes pidfile + bot.sock
- reminds the operator to wait ~30s for the MC server to drop the old
session before re-launching
package.json — new `npm run stop` script.
Smoke-tested:
- first 'node runtime/supervisor.js' acquires lock, writes pid
- second call refuses with diagnostic message
- first SIGTERM ⇒ pidfile removed automatically
Co-authored-by: Yuriy Mayatnikov <mayatnikov@me.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the "bot stands on a tree doing nothing" problem reported live
when the operator launched the TUI after PR #6 landed. The reactive
chain (operator > defend > eat > sleep > idle) was passive by design:
day-time, full HP and food, no hostile within 4 m ⇒ every reflex
returned noop. The bot perched in dark-oak canopy and never moved.
Changes
runtime/actions.js:
- chopNearestTree: find any *_log within 32 blocks, equip best axe
(falls back to fists), path to the block, dig. Per-bot 5-min
blacklist of unreachable log positions so we don't grind on the
same impossible target.
- wander: pick a random offset 6-16 blocks away and path there.
- setMovementsForGather / setMovementsForTravel: every action that
uses pathfinder now sets its own Movements profile (canDig=true)
instead of inheriting whatever the previous caller left. The old
behaviour caused chop to inherit flee's canDig=false and get stuck
in the canopy.
- fleeFrom now uses canDig=true too — the user observed the bot
permanently stuck on a leaf block because escape required digging.
runtime/reflex.js:
- new autonomousReflex between sleep and idle. Cooldown 10s. Picks
chop when log count < 16, else wander. When chop reports "no
reachable log within 32 blocks" we switch to wander for 60s so we
don't re-fire chop against the same impossible position.
- defendReflex tightened: only flee when closest is ≤8m (or ≤12m
on low HP). Avoids the "82 distant hostiles ⇒ constant flee
loop" pathology observed at this spawn.
- flee cooldown: same mob name within 60s ⇒ noop, so we yield to
other reflexes if flee keeps timing out.
- sleepReflex retry cooldown raised 30s → 5min. Sleeping fails
permanently if no bed is around; the short retry blocked
autonomous behaviour every tick.
- ctx.lastReflex now records {name, label, ts} after each
dispatched/completed reflex so the TUI can show what the bot
just decided.
runtime/bot.js:
- per-tick snapshot adds lastReflex and busy fields for the TUI.
- maybeReplyToPlayer: light canned greetings (yo/hey/hi/привет)
to non-operators when they address the bot. 30s cooldown so we
don't spam.
tui/tui.tsx:
- status bar shows either "▸ busy: <label>" while an action is
in flight, or "last reflex: <name> (<label>) Ns ago" when idle.
Gives an at-a-glance answer to "what is the bot doing right now?"
Smoke-tested live (play.xmatic.team, 2026-05-25T13:30-13:39):
- bot did dispatch chop tree (oak_log at 617,82,95)
- real bug surfaced and 3-fail rule filed a proposal automatically
- after the runtime fix, chop returns "no reachable log" gracefully
- bot switched to wander on the next tick
- position moved from (623.31, 85, 95.12) to (623.41, 86.02, 96.7)
— the first observable movement in this dark-oak spawn
Co-authored-by: Yuriy Mayatnikov <mayatnikov@me.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the self-improvement loop end-to-end:
reflex fails 3× → proposal file → operator approves in TUI →
`npm run propose:apply <file>` spawns Pi on a feature branch →
Pi commits the patch → supervisor watches runtime/*.js and
restarts the child on change.
runtime/state-store.js — atomic current-task.json writes, daily diary
append, proposals/ + proposals/approved/ helpers.
runtime/bot.js:
- ctx.dispatch writes current-task.json on start and updates it on
completion / failure / throw.
- failure tracker: 3 consecutive same-label failures → writeProposal()
with the snapshot, labels, and a suggested-next-step section.
30-min cooldown prevents proposal spam.
- on startup, surfaces resume info (previous task + pending proposal
count); on death, clears current-task.json + writes diary line.
- new IPC commands: PROPOSAL_LATEST returns the newest pending
proposal body; PROPOSAL_APPROVE moves it to proposals/approved/.
tui/tui.tsx — status bar shows `[proposals N, press y]` badge when
bot.pendingProposals > 0. Hotkey 'y' opens the proposal panel; 'y'
approves, 'n'/Esc closes.
scripts/propose-apply.js — given an approved proposal filename, creates
a `feat/proposal-<slug>` branch and spawns `pi -p` with the proposal
+ repo-conventions prompt. Refuses on dirty tree. No auto-push, no
auto-merge — operator reviews the diff and decides.
runtime/supervisor.js — forks bot.js as a child, watches runtime/*.js,
restarts on file change or on child exit code 42. Rate-limited at 5
restarts/minute. SIGINT/SIGTERM forward cleanly. `npm run bot` now
goes through the supervisor; `npm run bot:bare` skips it.
Smoke-tested: supervisor spawned, bot connected to MC, spawned at
expected coords, diary line written, state cleanup on SIGTERM correct.
Co-authored-by: Yuriy Mayatnikov <mayatnikov@me.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
runtime/actions.js — Mineflayer wrappers with hard timeouts and structured
{ok, detail} returns:
- attackNearest: equip best melee, lookAt, single swing per call
- fleeFrom: lazy-load pathfinder, walk N blocks away (canDig=false to
avoid burrowing through walls under panic)
- eatBestFood: scan inventory by FOOD_PRIORITY, equip + consume
- sleepInBed: find nearest placed bed within 16 blocks, path to it, sleep
- goTo: pathfinder.goto for operator come/follow
runtime/reflex.js — bodies now dispatch real actions via ctx.dispatch:
- operator-goal (highest): satisfy come/follow command
- defend: ≤4m attack, ≤12m + low HP/many hostiles flee
- eat: food < 16 + 5s cooldown
- sleep: night + safe + 30s retry cooldown
- idle: heartbeat every 20th tick
Reflex returns "skipped" when ctx.busy so we don't count busy ticks as
either productive or noop in the escalation counter.
runtime/bot.js:
- ctx.dispatch fire-and-forget wrapper with busy gate, onComplete hook
- consecutiveNoops counter; after ESCALATE_AFTER_NOOPS (=20, ~1 min at
tick=3s), askPi with the current snapshot. 10-min cooldown.
- operator chat handler: parses `<botname> <verb>` messages from
OPERATOR_USERNAMES. Verbs: status, pause, resume, stop, come.
- Death drops any pending operator goal.
Smoke-tested live against play.xmatic.team:25565: bot connected, logged
in via AuthMe, reflex chain dispatched flee/sleep, hard timeout fired
when pathfinder couldn't reach the flee target (expected — no usable
ground path in dark_forest at this spawn).
Co-authored-by: Yuriy Mayatnikov <mayatnikov@me.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(mindcraft-skills): hard timeout on every skill call
mc_avoid_enemies (and 7 other tools) wrapped only in safeCall without a
withTimeout. When mindcraft's underlying pathfinder/pvp goal couldn't be
satisfied, the call never resolved — the Pi tick loop blocked forever.
Observed live: mc_avoid_enemies pending >10 minutes after one mc_observe.
safeCall now takes timeoutMs (default 30s) and wraps withTimeout itself,
so every tool gets a hard ceiling. Per-tool overrides:
- goToPosition / goToNearestBlock: 120s / 90s (unchanged from before)
- defendSelf / avoidEnemies: 45s
- stay: secs*1000 + 10s
- craft / consume / pickup / place: 30s
- equip: 15s
collectBlock still uses its bespoke per-iter 75s loop.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(runtime): script-driven reflex daemon + Ink TUI dashboard
Pure-Pi runtime had three failure modes in practice:
- slow: 20-60s per decision because LLM was in the hot path
- expensive: every tick (defend, eat, idle) paid for a reasoning pass
- invisible: required tmux capture-pane to know what the bot was doing
New runtime/ layer is a long-running Node daemon that owns the MC
connection, ticks a priority-ordered reflex chain (defend > eat > sleep
> idle) with NO LLM in the hot path, and exposes status + commands over
a Unix-socket IPC. tui/ is an Ink dashboard that attaches over IPC and
can detach freely — multiple TUI clients can connect at once.
Pi/Codex are still available, but as on-demand escalation: TUI hotkey
'a' spawns `pi -p "<prompt>"` as a subprocess and streams stdout into
the dashboard. The self-improvement loop (proposals → operator approval
→ Pi-driven patch → hot reload) is documented in docs/runtime.md but
not yet wired.
Reflex bodies are stubs today — they log decisions but don't drive
Mineflayer actions yet. The priority chain, IPC contract, and TUI are
fully working; subsequent commits will fill in defend/eat/sleep bodies
and wire automatic escalation.
Run with `npm run bot` + `npm run tui`. Pi-only fallback stays at
`npm run agent`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Yuriy Mayatnikov <mayatnikov@me.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>