v0.4.0 vNext — closed-loop world model + settlement contract (#29)
* feat(v0.4.0): vNext — closed-loop world model + settlement contract
Implements the vNext architecture from the research doc: demote the noisy
multi-rail planner in favour of a closed loop (world truth → invariant check)
plus a single utility-driven goal authority.
L1 services (fix no_drop / silent pathfinder hang first):
- InventoryLedger: diff-based "did I actually get it" verifier; acquire-food
now confirms via ledger.gainedSince instead of the unreliable count/event.
- MotionService.gotoSafe: wall-clock timeout + progress watchdog +
path_update(noPath/timeout) → structured {reached|stuck|timeout|nopath}.
L3 plan — unify the three competing rails (curriculum/manifesto/storyline):
- Settlement Contract: ordered M0–M9 milestones, each invariant-checked
against an authoritative world view (early steps delegate to the proven
curriculum; late game adds farming).
- InvariantChecker + predicate library; GoalManager selects the lowest unmet
milestone via utility argmax (food-urgency preempts, DEPS-style).
- Wired into the scheduler: bot.js precomputes snapshot.contract; reflex.js
consumes it in place of the storyline rail. Manifesto L0 still preempts.
Eval + robustness:
- Village Score (single 0..1 metric) on the snapshot + TUI "build" line.
- survive.dig-in skill + dusk_dig_in mode (exposed at night, no bed → cover).
- approach_block helper (GoalNear + lookAt, avoids GoalLookAtBlock #341).
+28 new tests (450 total green). LLM remains entirely off the tick path.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(v0.4.0): finish vNext plan — anti-loop, skill-graph, worldDelta diff, flee→motion
Completes the remaining v0.4.0 plan items and one fix motivated by a live
in-game observation (flee hanging 30s against a persistent zombie).
- flee → MotionService.gotoSafe: structured {stuck|timeout|nopath} in ~4s with
a blind-retreat fallback, instead of the observed 30s pathfinder hang + 3
watchdog replans. Movements setup guarded so it is unit-testable.
- QW5 anti-loop (runtime/anti-loop.js): same skill failing >=3x in 5min →
30min blacklist (reflex shouldSkip) + one-shot improvement_request
(bot.js drainFired -> writeProposal).
- 4.1 closed-loop worldDelta: runSkill snapshots inventory before execute and
attaches the real delta (_invObserved) to every successful result; opt-in
skill.expectGain asserts the claimed gain or returns world_unchanged.
- 3.6 skill-graph (Plan4MC): declarative requires/produces for ~20 skills;
prerequisitesMet/canRun/runnableFrontier; GoalManager annotates suggestions
with blockedBy when prereqs are unmet.
+22 tests (472 total green). Live smoke confirmed dig-in works and no new
errors; flee loop is what this commit's flee migration addresses.
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>
This commit was merged in pull request #29.
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { runSkill, _registerForTest } from "./index.js";
|
||||
import { createInventoryLedger } from "../services/inventory-ledger.js";
|
||||
|
||||
function botWithMutableInv(initial) {
|
||||
let inv = initial;
|
||||
return {
|
||||
bot: { inventory: { items: () => inv } },
|
||||
set(next) { inv = next; },
|
||||
};
|
||||
}
|
||||
|
||||
test("expectGain passes and attaches the observed inventory delta", async () => {
|
||||
const m = botWithMutableInv([{ name: "cobblestone", count: 0 }]);
|
||||
const ledger = createInventoryLedger();
|
||||
ledger.update(m.bot);
|
||||
const teardown = _registerForTest({
|
||||
id: "test.mine-ok",
|
||||
title: "t", timeoutMs: 1000,
|
||||
preconditions: () => ({ ok: true }),
|
||||
execute: async () => { m.set([{ name: "cobblestone", count: 5 }]); return { ok: true, code: "done", worldDelta: {} }; },
|
||||
expectGain: { matcher: "cobblestone", min: 1, label: "cobblestone" },
|
||||
});
|
||||
const res = await runSkill("test.mine-ok", { bot: m.bot, ledger });
|
||||
teardown();
|
||||
assert.equal(res.ok, true);
|
||||
assert.equal(res.worldDelta._invObserved.cobblestone, 5);
|
||||
});
|
||||
|
||||
test("expectGain fails with world_unchanged when the world did not move", async () => {
|
||||
const m = botWithMutableInv([{ name: "cobblestone", count: 0 }]);
|
||||
const ledger = createInventoryLedger();
|
||||
ledger.update(m.bot);
|
||||
const teardown = _registerForTest({
|
||||
id: "test.mine-liar",
|
||||
title: "t", timeoutMs: 1000,
|
||||
preconditions: () => ({ ok: true }),
|
||||
execute: async () => ({ ok: true, code: "done", worldDelta: {} }), // claims ok, gains nothing
|
||||
expectGain: { matcher: "cobblestone", min: 1, label: "cobblestone" },
|
||||
});
|
||||
const res = await runSkill("test.mine-liar", { bot: m.bot, ledger });
|
||||
teardown();
|
||||
assert.equal(res.ok, false);
|
||||
assert.equal(res.code, "world_unchanged");
|
||||
});
|
||||
|
||||
test("no ledger in ctx → no validation, skill passes untouched", async () => {
|
||||
const teardown = _registerForTest({
|
||||
id: "test.no-ledger",
|
||||
title: "t", timeoutMs: 1000,
|
||||
preconditions: () => ({ ok: true }),
|
||||
execute: async () => ({ ok: true, code: "done", worldDelta: { foo: 1 } }),
|
||||
expectGain: { matcher: "diamond", min: 1 },
|
||||
});
|
||||
const res = await runSkill("test.no-ledger", { bot: { inventory: { items: () => [] } } });
|
||||
teardown();
|
||||
assert.equal(res.ok, true);
|
||||
assert.equal(res.worldDelta.foo, 1);
|
||||
});
|
||||
|
||||
test("observed delta is attached even without expectGain", async () => {
|
||||
const m = botWithMutableInv([{ name: "oak_log", count: 2 }]);
|
||||
const ledger = createInventoryLedger();
|
||||
ledger.update(m.bot);
|
||||
const teardown = _registerForTest({
|
||||
id: "test.observe-only",
|
||||
title: "t", timeoutMs: 1000,
|
||||
preconditions: () => ({ ok: true }),
|
||||
execute: async () => { m.set([{ name: "oak_log", count: 6 }]); return { ok: true, code: "done", worldDelta: null }; },
|
||||
});
|
||||
const res = await runSkill("test.observe-only", { bot: m.bot, ledger });
|
||||
teardown();
|
||||
assert.equal(res.ok, true);
|
||||
assert.equal(res.worldDelta._invObserved.oak_log, 4);
|
||||
});
|
||||
Reference in New Issue
Block a user