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>
This commit is contained in:
2026-05-27 20:39:25 +03:00
co-authored by Claude Opus 4.7
parent 7f545723b5
commit 21462dfdb1
2 changed files with 81 additions and 11 deletions
+23 -10
View File
@@ -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.
+58 -1
View File
@@ -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,