Commit Graph
18 Commits
Author SHA1 Message Date
15b6c11002 v0.3.1: survival behaviour overhaul — storyline, biome-aware scout, wedge-relocate, food/perf fixes, monitor TUI (#28)
* docs(v0.3.1): PRD — LLM prompt cost optimization

Design-only commit; no runtime changes. Spec for the next patch iteration.

Goal: cut per-advise() input tokens from ~800 to ≤300, preserving the
LLM's ability to produce valid registered skill ids and useful rationale.

Five proposed changes ranked by impact:

  P1  Compact registry format (saves ~350t/call) — group by namespace,
      comma-list ids, drop human titles. Default mode for advisor;
      verbose mode kept for postmortem/reflect.
  P2  Need-scoped registry (~50t additional) — show LLM only skills
      relevant to the active Maslow need + always-available safety
      skills (survive.flee, pillar-up, recovery.tunnel-out, explore.*).
  P3  Snapshot pruning (~50t) — drop weather/experience/dimension/biome/
      players from the user prompt; the LLM doesn't consult them.
  P4  Prompt caching probe — check if TimeWeb passes through
      prompt_tokens_details.cached_tokens. If yes, restructure prefix
      to maximize cache hits (cached input is ~10x cheaper at OpenAI).
  P5  Per-trigger cost telemetry in scripts/list-improvements.js --stats:
      avg_in / avg_out / cost_₽ / share% per trigger_reason, using
      TIMEWEB_PRICE_IN_RUB_PER_M and TIMEWEB_PRICE_OUT_RUB_PER_M env.

Trigger: TimeWeb admin panel after first day of v0.3.0 live showed
34K tokens / day at low activity. At cap budget that projects to
~480₽/month (101₽/M in, 608₽/M out for gpt-5.4-mini). Manageable
but the savings are mostly free — repeated infra tokens, not signal.

All changes are additive; runtime behaviour stays the same. If the
LLM produces worse advice with the compact registry, flip back via a
single constant in fast-advisor.js.

Acceptance: re-run scripts/check-timeweb.js probe 3 — expect
tokens_in ≤ 300 (was ~800). Live for 1h, check --stats: avg_in ≤ 300
per trigger group. Existing 360 tests still green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(v0.3.1): storyline — canonical Minecraft survival quest

The bot has been stuck in a loop for two days:
  acquire-food (fail: no nearby food) → explore.far → pillar-up (fail) → repeat

Diagnosis: manifesto + LLM advisor both correctly identify "you need
food" but neither expresses *what concretely to do next*. Manifesto is
a priority ladder (need-detection), not a narrative arc.

This commit adds the missing narrative layer — an ordered list of
operational steps that mirror the vanilla Minecraft survival path:

  1. orient_self      — Понять где я
  2. first_wood       — Собрать 8 поленьев
  3. crafting_basics  — Сделать верстак и палки
  4. first_tools      — Деревянные орудия
  5. first_food       — Найти первую еду
  6. shelter_minimal  — Простой шелтер с кроватью
  7. stone_tier       — Каменные орудия
  8. food_security    — Запас еды на 16+
  9. iron_age         — Железо и печь
  10. settle_base     — Постоянная база
  11. village_grow    — Развивать деревню (ongoing)

Each step has:
  - completed(snapshot) → bool — detects achievement from snapshot
  - suggestSkill(snapshot) → { skillId, args? } — concrete next dispatch
  - emergencyPause(snapshot) → bool — defers to manifesto L0 alive
    emergencies (low HP near hostile, lava under foot, food = 0)
  - narration_ru — chat-friendly Russian one-liner spoken on entry

Components:

- runtime/goal/storyline.js — 11-step canonical quest catalogue
- runtime/goal/state.js — pickCurrentStep(snapshot) walks the list,
  returns first non-completed step + its suggestion. 3s cache.
  Validates suggestSkill's skillId against the live registry.
- runtime/reflex.js — curriculumReflex dispatch priority is now:
    1. manifesto (L0 alive emergencies always win)
    2. storyline (concrete operational subgoal)
    3. curriculum plan (legacy fallback)
  Tests pass ctx.disableStoryline=true for isolation.
- runtime/bot.js — snapshot.storyStep populated each tick so
  chatter/advisor/reflect observers see the same view.
- runtime/coach/fast-advisor.js — buildUserPrompt now embeds the
  current step + its suggested skill, so LLM advice is anchored
  ("step 5 first_food, storyline wants survive.acquire-food, but
  recent dispatches show it's failing — try explore.far + scout").
- runtime/coach/advisor-trigger.js — forwards ctx.storyStep into
  advise() and logs step id at trigger time.
- runtime/coach/reflect.js — reflection prompt includes storyline
  progress so 30-min self-assessment is anchored.
- runtime/persona/chatter.js — narrates step.narration_ru on
  transition. Rate-limited via existing maybeNarrateRaw().

New operator CLI:

- scripts/show-story.js — fetches the live snapshot via IPC sock and
  prints step progress with ✓/→/ markers, current skill, inventory.
  Falls back to --plain catalogue view when bot offline.

Token cost impact: ~+30 input tokens per advise() call (one extra
line in user prompt). Trivial vs the value of grounding LLM advice
in a concrete narrative.

Operator usage:

  node scripts/show-story.js          # live progress + which step + why
  node scripts/show-story.js --plain  # static catalogue of all 11 steps

Tests: 376 green (was 360, +16 storyline tests).

Also in this branch (already committed): dev/v0.3.1/PRD.md —
LLM prompt cost optimization design doc.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(v0.3.1): storyline beats manifesto L1+ (only L0 alive emergencies override)

Found in live logs after the previous commit deployed:
  storyline: step 1/11: orient_self → explore.wander
  advisor-trigger: firing because wedged (planned=survive.acquire-food, ...)

Manifesto was still picking survive.acquire-food (L1 food) over the
storyline's orient_self → explore.wander. That's the wrong precedence —
storyline expresses a *concrete operational subgoal* and L1+ manifesto
needs are just "you'd benefit from food" priorities, not emergencies.

New dispatch precedence in curriculumReflex:
  1. manifesto L0 (alive emergencies: lava, low-HP+hostile, food=0)
  2. storyline (concrete narrative subgoal — beats L1+ manifesto)
  3. manifesto L1+ (fallback when storyline has no concrete suggestion)
  4. curriculum plan (legacy fallback)

This way the bot starts following the narrative arc even while
manifesto's L1 food is technically unsatisfied — orient_self runs to
completion before pursuing food explicitly. Storyline already handles
food as step 5 (first_food), so we're not skipping it.

Tests: 378 green (+2 priority-ordering tests):
- L0 manifesto emergency: upstream reflex (defend/modes) catches before
  curriculum dispatch
- storyline beats manifesto when both have suggestions: well-fed bot
  with logs → craft.planks (storyline crafting_basics), not gather.logs
  (manifesto L2)
- updated "manifesto fallback" test to require disableStoryline=true

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(tui): fullscreen monitor-only TUI (opencode-style)

Replaces the old tui/tui.tsx hotkey-heavy dashboard with a read-only
observability screen. Operator actions live in scripts/* now —
TUI is for watching, not driving.

Layout (top to bottom, all auto-resizing to terminal):
  1. Header     — MC/IPC status, pos, HP, food, day/night, hostiles
  2. Storyline  — current step + 11-step quest map (✓/→/○)
  3. Activity   — last N skill dispatches (colour by outcome)
  4. MC Chat    — last N chat lines (cyan for bot, yellow for players)
  5. Advisor    — last N LLM recommendations (trigger + outcome + tokens)
  6. Improvements — open requests from knowledge.improvement_requests
  7. Footer     — 24h token usage + cost in ₽ + q-to-quit

Data sources:
  - IPC sock: snapshot frames, log frames, chat frames (push)
  - SQLite knowledge.db: advisor_recommendations + improvement_requests
    polled every 5s (pull)

Token cost displayed live using TIMEWEB_PRICE_IN_RUB_PER_M /
TIMEWEB_PRICE_OUT_RUB_PER_M env vars (defaults: 101 / 608 for
gpt-5.4-mini).

Switches:
  - npm run tui          → new monitor (this file)
  - npm run tui:legacy   → old action-driven tui/tui.tsx (kept for now)

Implementation notes:
  - Uses ink + alternate-screen-buffer ANSI for proper "opencode-feel"
    fullscreen behaviour; restores prior terminal contents on quit.
  - Skips alt-screen and useInput when stdin/stdout isn't a TTY
    (smoke tests, piped output) — both gracefully degrade.
  - Stable React keys via per-event uid counter, avoids reconciler
    duplicate-key warnings as logs/chat/dispatches stream in.
  - Resize handled via 1s stdout-dimension poll, NOT direct
    'resize' listener (which conflicts with ink's own listener and
    triggers MaxListenersExceededWarning).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ui(tui): compact 4-section monitor (was 6) — fits 1080p without zoom

Operator reported the TUI overflowed the screen unless terminal was
zoomed way out. The 11-step storyline list alone was eating ~13
rows, and each advisor/improvement entry took 2-3 rows. Now:

- Header + storyline collapsed into one panel (2 lines):
    line 1: pepa · ●MC ●IPC · 1m50s · pepa_bot · (697,61,702) · HP 20 · food 5 · ☀ · ⚔60(creeper@58b)
    line 2: story ▓▒░░░░░░░░░ 1/11 orient_self · Понять где я → explore.wander
  The 11-step ladder is now a unicode progress bar (▓ done, ▒ current,
  ░ pending) — same info, fits in one row.

- Advisor entries: one line each instead of two.
    ✓ wedged_60s → survive.flee  802t 1900ms
    (outcome mark / trigger / target skill / tokens / latency)

- Improvements entries: one line each instead of two.
    #1 P2 ×3  Add craft.iron-pickaxe skill
  Description dropped from the row — use `node scripts/list-improvements.js`
  for full text.

- Sections: 4 (was 6).
    [header+story] · [activity | chat] · [advisor | improvements] · [footer]

Tested on a typical 1080p terminal — fits comfortably without zoom.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(v0.3.1): real survival patterns — biome-aware scout, wedge-relocate, escape-pit-safe

Operator reported the bot wandered the same 50×50 patch for 2 hours
without making any progress toward food. Diagnosis showed three root
causes; this commit addresses all five open improvement_requests
the LLM (postmortem + tuner) flagged automatically.

Research basis (`Voyager`, `Plan4MC`, `GITM`, `Mindcraft`):
  - Coverage / commit-to-cardinal exploration when local scan fails
  - Biome-aware strategy switching using a static affordance table
  - Wedge detector above the skill layer that triggers RELOCATE not
    RETRY (per-skill stuck checks reset on re-entry — useless)
  - Time-in-region bbox heuristic + need-duration AND skill-cycle gate

Concrete changes:

1. `runtime/goal/storyline.js`
   - orient_self.completed: added timeout fallback (HP=full + session
     >120s → done) so barren biomes don't block the bot on step 1.
     Closes improvement #2 'Нет навыка оценки когда сменить район'.
   - first_food.suggestSkill: now picks survive.scout-food (new) when
     no passive mob is nearby; falls back to survive.acquire-food only
     when something is in immediate range.

2. `runtime/biome-affordances.js` (new)
   - Static table: 40+ biomes → {has_passive_mobs, has_trees,
     has_water, has_crops, livable}.
   - Unknown biomes return optimistic defaults to avoid regressions.
   - Closes improvement #1 'Нет навыка целевого поиска еды по биому'.

3. `runtime/skills/scout-food.js` (new — survive.scout-food)
   - Tiered strategy: biome check → scan 32 → scan 64 → commit a
     cardinal for 200 blocks rescanning every 16. On cardinal
     exhaustion, returns code:"exhausted" so the curriculum can
     escalate to village.relocate.
   - In barren biomes (desert/ocean/snowy_plains) the scan is
     SKIPPED — bot walks straight toward the nearest neighbour
     biome that affords passive mobs (8-direction biome probe at
     radius 64).

4. `runtime/awareness/wedge-detector.js` (new)
   - Rolling 10-min position bbox tracker. observe() called every
     tick; isWedged() returns true when bbox<50 AND active need
     unmet >5min AND skill cycles ≥3.
   - markRelocationStarted() suppresses further wedge firings
     until the bot has displaced ≥200b — prevents stack overflow
     of relocate calls.
   - Lives ABOVE the skill layer (in runtime/reflex.js), because
     any per-skill stuck check resets on re-entry.

5. `runtime/skills/relocate.js` (new — village.relocate)
   - 300-block walk in least-recently-used cardinal (per-incident
     memory in ctx.recentRelocations).
   - Re-paths every 32 blocks, soft-tolerates pathfinder failures
     (3 consecutive throws → exit with code:"stuck_in_place").
   - Closes improvement #2 + #4 ('low success rate trigger').

6. `runtime/skills/escape-pit-safe.js` (new — recovery.escape-pit-safe)
   - Surveys 4 cardinals AND ceiling height before committing.
     Picks the direction with most open blocks (≥3, no lava).
     Falls through to pillar-up only if ceiling clear ≥4b. Returns
     code:"no_strategy" if both blocked so curriculum can escalate
     to relocate.
   - Closes improvement #3 'Нет навыка для безопасного выхода'.

7. `runtime/reflex.js`
   - Wedge detector wired before manifesto/storyline. If wedge.wedged
     is true, dispatches village.relocate directly and returns —
     bypasses every other branch.
   - ctx.disableWedge flag for tests.

8. `runtime/coach/advisor-trigger.js`
   - LLM provider outage backoff: 3 consecutive http_400 / timeout /
     network_error → suppress advisor for 10 min. Today's TimeWeb
     gpt-5.4-mini was 400'ing for an hour straight; we were spending
     trigger budget on dead calls. Closes improvement implicit gap
     in #5.

9. `runtime/bot.js`
   - Tracks botSpawnedAt; snapshot._sessionMs exposed for storyline
     orient_self timeout fallback.

Tests: 396 green (was 378, +18):
  - runtime/biome-affordances.test.js — 8 tests
  - runtime/awareness/wedge-detector.test.js — 9 tests
  - runtime/goal/storyline.test.js — 1 new test (orient_self timeout)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(coach/trigger-tuner): crash after ~1h — runOnce is sync, not a Promise

The live bot died overnight with:
  TypeError: runOnce(...).catch is not a function
  at trigger-tuner.js:42  →  [supervisor] child exited code=1

attach() wrapped the timer body as `runOnce().catch(...)` but
runOnce() returns a plain {ok, flagged, ...} object (pure SQL, no
await). The first tuner tick (60min after spawn) threw → killed the
whole bot process. Never surfaced before because the bot rarely ran
uninterrupted for a full hour during development.

Fix: guard the synchronous call with try/catch, matching how
persona/chatter.js already does its sync tick. (postmortem.drainOnce
and reflect.runOnce ARE async, so their .catch is correct — audited.)

Regression test added: captures the setInterval callback and invokes
it synchronously, asserting it does not throw.

Tests: 397 green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(v0.3.1): mechanical food/stuck fixes — bot reaches the chicken now

The wedge wasn't only in the manifesto layer; several mechanical bugs
kept the bot in a dead random-walk:

- storyline / manifesto / curriculum: "local food" now means an edible
  passive mob within <=32 blocks. A distant chicken or a cod no longer
  fools the bot into dispatching acquire-food (which then fails on
  no_path). Long-range food goes through scout-food instead.

- scout-food: partial approach to a target now counts as progress
  (approached_target, e.g. moved:14); a blocked heading is NOT counted
  as movement; added blind/tunnel fallback so it doesn't die when the
  pathfinder can't route cleanly.

- acquire-food: on no_path it now also tries a blind/tunnel approach to
  the animal; no_drop routes back into food scouting instead of giving
  up.

- explore.far / relocate / flee: fewer false "done" results (micro-steps
  no longer counted as success), more genuine escapes from stuck.

- scripts/show-story.js: live IPC now actually renders the current
  storyline step.

Verification: scripts/lint-patch.js clean; npm test 404/404 green; bot
relaunched in tmux `pepa`. Live logs show real progress — bot switched
to survive.scout-food, approached the chicken (approached_target
moved:14), then reached survive.acquire-food: hunting chicken. Food
isn't fully closed yet but the remaining issue is concrete pickup/drop,
not dead random-walk.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(v0.3.1): sated bot stops chasing food + perf-leak + fuzzy improvement dedup

Third day of "bot just walks back and forth burning tokens". Root
causes were mechanical, not the manifesto:

1. SATED BOT CHASING FOOD (the big one)
   Bot had food=17 (nearly full) but storyline first_food + manifesto
   L1 required 2+ food ITEMS in inventory, so it looped scout-food /
   acquire-food for hours instead of working. Now both treat a hunger
   bar >= 14 (SATED_FOOD) as satisfied even with empty food inventory —
   a full bot chops wood / makes tools and grabs food opportunistically,
   only hard-pursuing food when actually hungry (< 14).
   manifesto/needs.js foodDetect + goal/storyline.js first_food.completed.

2. perf_hooks MEMORY LEAK (overnight OOM suspect)
   "MaxPerformanceEntryBufferExceededWarning: 1,000,001 measure entries".
   mineflayer/pathfinder emit perf marks we never consume. Added a
   60s reaper in bot.js (performance.clearMeasures/clearMarks). unref'd.

3. IMPROVEMENT QUEUE SELF-DUPLICATING
   The LLM re-filed closed gaps with reworded titles (#5/#8/#9 were
   dupes of implemented #1/#2/#3). Exact-title dedup missed them.
   Replaced with token-set fuzzy match (isDuplicateTitle): jaccard>=0.75
   OR >=3 shared meaningful tokens with jaccard>=0.5. Also: a re-filed
   gap that's already implemented/rejected is NOT resurrected as a new
   open row. Cleared all 5 open requests (now genuinely implemented).

Also confirmed (no change needed):
- canDig=true is a DELIBERATE codebase-wide choice ("without it the bot
  gets permanently stuck", actions.js). The stale memory recommending
  canDig=false is updated. ViaBackwards dig works partially (dug:1
  moved:1.8 observed); false would trap the bot in every pit.
- scout-food already has blind/tunnel fallback + 12s step timeout
  (operator's earlier edits) so trapped-pathfinder degrades instead of
  hanging 30s.

Tests: 407 green (was 404). Updated needs/state/storyline tests for the
SATED_FOOD threshold; added fuzzy-dedup + tokenize/jaccard tests.

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>
2026-05-28 09:43:57 +03:00
mayatnikovandClaude Opus 4.7 86e5294bb8 chore: snapshot pre-v0.2.0 WIP (pathfinder/reflex/metrics/skills improvements)
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>
2026-05-27 13:05:12 +03:00
mayatnikov 49ce0f9bb5 fix(runtime/actions): clear stale collectblock queue after chop timeout 2026-05-26 19:00:09 +03:00
mayatnikov 25e39c7244 fix(runtime/actions): place crafting table beside bot 2026-05-26 14:52:44 +03:00
mayatnikov ecbfa28f3c fix(runtime/actions): cancel timed-out log collection 2026-05-26 14:26:43 +03:00
mayatnikovandClaude Opus 4.7 28f5d9e483 fix(perception): use numeric block ids — callback matchers silently fail under ViaBackwards
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>
2026-05-26 13:45:29 +03:00
mayatnikov 6909715f75 fix(runtime/skills): guard stationary blind fallback 2026-05-26 13:22:30 +03:00
mayatnikov 36e547a896 fix(runtime/actions): require horizontal escape movement 2026-05-26 12:45:13 +03:00
mayatnikovandClaude Opus 4.7 6560c0765c fix(auto-improve): detach auto-patch + recovery-tunnel-out test in suite
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>
2026-05-26 12:43:30 +03:00
mayatnikov 564557450d fix(runtime/skills): recover from wedged pits with tunnel-out 2026-05-26 12:39:23 +03:00
mayatnikovandClaude Opus 4.7 3e3ea3e597 fix(runtime): escape-pit fallback for wedged bot
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>
2026-05-26 12:07:59 +03:00
mayatnikovandClaude Opus 4.7 0aae5e2e68 fix(runtime): wander/explore.far probe-then-go (bot actually moves)
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>
2026-05-26 12:05:14 +03:00
mayatnikovandClaude Opus 4.7 86f0c1799e fix(runtime): pin MC_VERSION=1.21.4 + ground-truth probe + close-loop dig
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>
2026-05-26 11:51:43 +03:00
mayatnikovandClaude Opus 4.7 19dc8e12c6 fix(runtime): unstick wander loop + chop radius + explore.far skill
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>
2026-05-26 11:18:36 +03:00
mayatnikovandClaude Opus 4.7 29542f0559 fix(runtime): unstick scheduler + chop + sleep + bed/shelter/farm skills
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>
2026-05-26 11:12:21 +03:00
313ef081d1 feat(runtime): village progression — crafting, tech-tree reflex, LLM planner (#11)
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>
2026-05-25 17:00:07 +03:00
f5361ad9a4 feat(runtime): autonomous reflex — chop wood + wander + proactive movement (#8)
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>
2026-05-25 16:40:36 +03:00
ee2b4c26eb feat(runtime): real reflex bodies + auto-escalation + operator chat (#5)
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>
2026-05-25 16:11:53 +03:00