From 21462dfdb1e82d704302b31596dfdaa1510ed5e1 Mon Sep 17 00:00:00 2001 From: Yuriy Mayatnikov Date: Wed, 27 May 2026 20:39:25 +0300 Subject: [PATCH] fix(v0.3.1): storyline beats manifesto L1+ (only L0 alive emergencies override) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- runtime/reflex.js | 33 ++++++++++++++++------- runtime/reflex.test.js | 59 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 81 insertions(+), 11 deletions(-) diff --git a/runtime/reflex.js b/runtime/reflex.js index e386615..b8c4c39 100644 --- a/runtime/reflex.js +++ b/runtime/reflex.js @@ -540,16 +540,29 @@ function curriculumReflex(ctx) { return { action: "dispatched", kind: "curriculum-wander", label: "wander" }; } - // Pick what to dispatch. Order: manifesto > storyline > curriculum. - // Manifesto is highest because L0 alive emergencies (lava, low-HP + - // hostile) must override any narrative aspiration. Storyline beats - // curriculum because it expresses a concrete operational subgoal, - // not just "next milestone". - let skillId = manifestoSkillId ?? storySkillId ?? plan.skillId; - let skillSource = manifestoSkillId - ? `manifesto:${activeNeed.need.id}` - : storySkillId ? `storyline:${storyStep.step.id}` - : "curriculum"; + // Pick what to dispatch. Order: + // 1. manifesto L0 (alive emergencies: low HP near hostile, lava + // under foot, food=0) — absolute priority; do NOT let + // storyline overrule a "you're dying" signal. + // 2. storyline — concrete narrative subgoal ("collect 8 logs", + // "craft wooden pickaxe"). Beats manifesto L1+ because the + // ladder needs operational direction, not just "you need food + // → dispatch acquire-food forever". + // 3. manifesto L1+ — fallback when storyline has no concrete + // pursue (e.g. armor levels with pursue=null). + // 4. curriculum plan — legacy fallback. + const manifestoEmergency = activeNeed?.need?.level === 0; + let skillId, skillSource; + if (manifestoEmergency) { + skillId = manifestoSkillId ?? storySkillId ?? plan.skillId; + skillSource = `manifesto:${activeNeed.need.id}`; + } else if (storySkillId) { + skillId = storySkillId; + skillSource = `storyline:${storyStep.step.id}`; + } else { + skillId = manifestoSkillId ?? plan.skillId; + skillSource = manifestoSkillId ? `manifesto:${activeNeed.need.id}` : "curriculum"; + } // v0.3.0 fast-advisor: if a fresh recommendation is sitting on ctx // (the result of a previous tick's async advise() call), use it. diff --git a/runtime/reflex.test.js b/runtime/reflex.test.js index 2fb8838..321cc65 100644 --- a/runtime/reflex.test.js +++ b/runtime/reflex.test.js @@ -242,9 +242,10 @@ test("curriculum dispatches suggested skill by id", () => { assert.ok(typeof dispatches[0].opts.onComplete === "function"); }); -test("manifesto: hungry bot with no food drives survive.acquire-food (overrides curriculum plan)", () => { +test("manifesto: hungry bot with no food drives survive.acquire-food (manifesto fallback when storyline disabled)", () => { const { ctx, dispatches } = makeCtx({ disableManifesto: false, + disableStoryline: true, snapshot: { connected: true, health: 20, @@ -264,6 +265,62 @@ test("manifesto: hungry bot with no food drives survive.acquire-food (overrides assert.equal(ctx.activeNeed?.need?.id, "food"); }); +test("L0 manifesto emergency: defend/modes layer catches the threat BEFORE curriculum", () => { + // HP=4 + creeper@3m fires `modes` (self_preservation) or defendReflex + // before curriculum even runs — which is the right outcome: alive + // emergencies don't reach the storyline/manifesto branch at all. + const { ctx, dispatches } = makeCtx({ + disableManifesto: false, + disableStoryline: false, + bot: { entities: { z1: { name: "creeper", height: 1.7, position: { x: 3, y: 64, z: 0, distanceTo: () => 3 } } }, entity: { position: { x: 0, y: 64, z: 0 } } }, + snapshot: { + connected: true, + health: 4, + food: 12, + hasFood: false, + inventory: { oak_log: 10 }, + equipment: {}, + nearbyBlocks: { logs: 4 }, + hazards: { footBlock: "grass_block", belowBlock: "dirt", headBlock: "air" }, + closestHostile: { name: "creeper", distance: 3 }, + threats: [{ name: "creeper", distance: 3, position: { x: 3, y: 64, z: 0 } }], + isDay: true, + curriculum: { plan: { skillId: "gather.logs" } }, + }, + }); + const out = runTick(ctx); + assert.notEqual(out?.kind, "curriculum-skill", "an upstream reflex caught the emergency before curriculum"); +}); + +test("storyline beats manifesto L1+ when both have suggestions", () => { + // Snapshot: well-fed (food=20 + 8 bread → manifesto L1 satisfied, L2 + // tools_wood unmet) AND storyline orient_self complete (HP=20, blocks + // visible). Storyline should drive a wood-tier crafting step, not + // manifesto's gather.logs (which would also be valid but less concrete). + const { ctx, dispatches } = makeCtx({ + disableManifesto: false, + disableStoryline: false, + snapshot: { + connected: true, + health: 20, + food: 20, + hasFood: true, + inventory: { bread: 8, oak_log: 10 }, // logs done, no planks + equipment: {}, + nearbyBlocks: { logs: 3 }, + hazards: { footBlock: "grass_block", belowBlock: "dirt", headBlock: "air" }, + isDay: true, + _sessionMs: 60_000, + curriculum: { plan: { skillId: "explore.far" } }, + }, + }); + const out = runTick(ctx); + assert.equal(out.reflex, "curriculum"); + // Storyline step crafting_basics suggests craft.planks (10 logs, no planks yet). + assert.equal(dispatches[0].label, "craft.planks"); + assert.equal(ctx.storyStep?.step?.id, "crafting_basics"); +}); + test("manifesto: well-fed bot with all wood tools defers to curriculum plan", () => { const { ctx, dispatches } = makeCtx({ disableManifesto: false,