feat(runtime): skill substrate + dynamic groups + reference skills (Phase 2)

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: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-25 22:15:24 +03:00
co-authored by Claude Opus 4.7
parent f301529f42
commit 0834bb6b73
9 changed files with 705 additions and 1 deletions
+123
View File
@@ -0,0 +1,123 @@
// Dynamic item/block groups derived from bot.registry. The goal is to never
// hard-code a Minecraft version's item table into skill code: skills ask for
// "logs" or "planks" or "beds", and this module returns the set of names that
// actually exist on the connected server.
//
// All helpers are pure: given a bot they return a Set<string>. They tolerate
// missing registries (returning an empty set) so the skill code can degrade
// to `code: "unsupported_version"` rather than crash.
function isItemRegistry(reg) {
return reg && reg.itemsByName && typeof reg.itemsByName === "object";
}
function isBlockRegistry(reg) {
return reg && reg.blocksByName && typeof reg.blocksByName === "object";
}
function pickItems(bot, predicate) {
const reg = bot?.registry;
if (!isItemRegistry(reg)) return new Set();
const out = new Set();
for (const name of Object.keys(reg.itemsByName)) {
if (predicate(name)) out.add(name);
}
return out;
}
function pickBlocks(bot, predicate) {
const reg = bot?.registry;
if (!isBlockRegistry(reg)) return new Set();
const out = new Set();
for (const name of Object.keys(reg.blocksByName)) {
if (predicate(name)) out.add(name);
}
return out;
}
// Wood + stem logs of every available species. The `*_stem` suffix covers
// crimson/warped logs; the `_log` suffix covers regular trees and pale_oak.
export function logs(bot) {
return pickBlocks(bot, (n) => n.endsWith("_log") || n.endsWith("_stem"));
}
export function planks(bot) {
return pickItems(bot, (n) => n.endsWith("_planks"));
}
export function sticks(bot) {
const reg = bot?.registry;
const out = new Set();
if (isItemRegistry(reg) && reg.itemsByName.stick) out.add("stick");
return out;
}
export function beds(bot) {
return pickBlocks(bot, (n) => n.endsWith("_bed"));
}
// Conservative food allow-list. We could derive this from
// minecraft-data's foodsByName, but that includes spider_eye and other
// hazardous items. Until we have an explicit unsafe-food blacklist, keep
// the named cooked/raw/farm staples here and intersect with what exists in
// the connected server's item registry — so pale_oak-era new items don't
// surprise us and pre-1.13 servers don't blow up on missing entries.
const FOOD_ALLOWLIST = [
"bread",
"cooked_beef",
"cooked_chicken",
"cooked_porkchop",
"cooked_mutton",
"cooked_rabbit",
"cooked_salmon",
"cooked_cod",
"baked_potato",
"apple",
"golden_apple",
"carrot",
"beetroot",
"melon_slice",
"sweet_berries",
"glow_berries",
"mushroom_stew",
"rabbit_stew",
"beetroot_soup",
"suspicious_stew",
"dried_kelp",
"pumpkin_pie",
"beef",
"chicken",
"porkchop",
"mutton",
];
export function foods(bot) {
const reg = bot?.registry;
if (!isItemRegistry(reg)) return new Set();
const out = new Set();
for (const name of FOOD_ALLOWLIST) {
if (reg.itemsByName[name]) out.add(name);
}
return out;
}
export function axes(bot) {
const tools = ["wooden_axe", "stone_axe", "iron_axe", "golden_axe", "diamond_axe", "netherite_axe"];
const reg = bot?.registry;
if (!isItemRegistry(reg)) return new Set();
return new Set(tools.filter((n) => reg.itemsByName[n]));
}
export function pickaxes(bot) {
const tools = ["wooden_pickaxe", "stone_pickaxe", "iron_pickaxe", "golden_pickaxe", "diamond_pickaxe", "netherite_pickaxe"];
const reg = bot?.registry;
if (!isItemRegistry(reg)) return new Set();
return new Set(tools.filter((n) => reg.itemsByName[n]));
}
export function swords(bot) {
const tools = ["wooden_sword", "stone_sword", "iron_sword", "golden_sword", "diamond_sword", "netherite_sword"];
const reg = bot?.registry;
if (!isItemRegistry(reg)) return new Set();
return new Set(tools.filter((n) => reg.itemsByName[n]));
}