Five concrete patterns from Voyager and Mindcraft, applied in our shape
without abandoning the git-as-evolution-substrate that makes pepa
distinct. Plus a first multi-agent surface so two bots from the same
repo can share intent.
1. runtime/critic.js (Voyager critic.txt)
- Spawns `pi -p` with a JSON-only critic prompt before a proposal is
written. {reasoning, success, critique}.
- success=true short-circuits the proposal (bot recovered between
detector tripping and now), saving Pi tokens on false positives.
- critique is spliced into the proposal body via attachCritique() so
the downstream auto-patcher has a sharp spec.
- Graceful: pi missing / timeout / unparseable JSON → proposal still
filed without the critic block.
2. scripts/lint-patch.js (Mindcraft coder._lintCode)
- Pre-flight gate between Pi commit and npm test: node --check, dynamic
import (catches missing named exports), regex extraction of
runSkill("id") calls cross-checked against the live registry.
- Cheaper than npm test, fails fast with a clear reason.
3. runtime/stuck-incident.renderActionTemplate (Voyager action_template.txt)
- All proposal bodies now follow the same fixed-section layout: Task /
Last result / Execution error / State / Metrics / Journal /
Scenarios / Critique / Fix / Edit scope / Forbidden.
4. runtime/skill-library.js (Mindcraft skill_library.getRelevantSkillDocs)
- Word-overlap ranking (Mindcraft's offline fallback) — zero deps,
deterministic. auto-patch.js injects top-3 similar skills into the
Pi prompt as "look at these patterns".
5. runtime/modes.js (Mindcraft modes.js)
- Declarative {name, interrupts, on, active, update(ctx)} chain that
runs BEFORE the curriculum each tick.
- Ships self_preservation (low HP → eat/flee), hunger (food<14 → eat),
night_shelter (night + bed in hand → sleep). Cleaner than ad-hoc
lastFleeAttempt cooldowns in reflex.js.
6. runtime/social/conversation.js + cmd:conv-say/conv-recent/conv-list
- File-JSONL topic channel so two bots from the same repo (different
usernames, different host dirs under state/) can append turns and
read peers. Skeleton — multi-agent collaboration on top later.
Differentiator preserved: every Pi-written skill still lands on main via
auto-patch.js (real git branch + smoke gate + cherry-pick). Voyager
keeps skills in a Chroma JSON, Mindcraft keeps them in RAM — pepa keeps
them as versioned source code reviewable in `git log`.
package.json: 0.0.1 → 0.1.0. 174/174 tests pass. README + AGENTS updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
96 lines
3.2 KiB
JavaScript
96 lines
3.2 KiB
JavaScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { registerMode, tickModes, listModes, setModeEnabled, _resetModes } from "./modes.js";
|
|
|
|
test("tickModes: first mode that fires wins", async () => {
|
|
_resetModes();
|
|
registerMode({
|
|
name: "high",
|
|
interrupts: ["all"],
|
|
update: () => ({ action: { skillId: "from-high" } }),
|
|
});
|
|
registerMode({
|
|
name: "low",
|
|
update: () => ({ action: { skillId: "from-low" } }),
|
|
});
|
|
const out = tickModes({});
|
|
assert.equal(out.mode, "high");
|
|
assert.equal(out.action.skillId, "from-high");
|
|
});
|
|
|
|
test("tickModes: disabled mode is skipped", async () => {
|
|
_resetModes();
|
|
registerMode({ name: "skipme", update: () => ({ action: { skillId: "x" } }) });
|
|
registerMode({ name: "use", update: () => ({ action: { skillId: "y" } }) });
|
|
setModeEnabled("skipme", false);
|
|
const out = tickModes({});
|
|
assert.equal(out.mode, "use");
|
|
});
|
|
|
|
test("tickModes: returns null when no mode fires", async () => {
|
|
_resetModes();
|
|
registerMode({ name: "silent", update: () => null });
|
|
const out = tickModes({});
|
|
assert.equal(out, null);
|
|
});
|
|
|
|
test("tickModes: thrown update doesn't break the chain", async () => {
|
|
_resetModes();
|
|
registerMode({
|
|
name: "throws",
|
|
update: () => { throw new Error("boom"); },
|
|
});
|
|
registerMode({
|
|
name: "next",
|
|
update: () => ({ action: { skillId: "rescued" } }),
|
|
});
|
|
const out = tickModes({});
|
|
assert.equal(out.mode, "next");
|
|
});
|
|
|
|
test("registerMode: same name replaces, doesn't duplicate", async () => {
|
|
_resetModes();
|
|
registerMode({ name: "x", update: () => ({ action: { skillId: "v1" } }) });
|
|
registerMode({ name: "x", update: () => ({ action: { skillId: "v2" } }) });
|
|
const out = tickModes({});
|
|
assert.equal(out.action.skillId, "v2");
|
|
assert.equal(listModes().length, 1);
|
|
});
|
|
|
|
test("standard modes load on import", async () => {
|
|
const mod = await import(`./modes.js?cb=${Date.now()}`);
|
|
const names = mod.listModes().map((m) => m.name);
|
|
assert.ok(names.includes("self_preservation"));
|
|
assert.ok(names.includes("hunger"));
|
|
assert.ok(names.includes("night_shelter"));
|
|
});
|
|
|
|
test("self_preservation: low-HP + food + hasFood → eat", async () => {
|
|
_resetModes();
|
|
const mod = await import(`./modes.js?cb=${Date.now() + 1}`);
|
|
const out = mod.tickModes({ snapshot: { health: 4, food: 10, hasFood: true } });
|
|
assert.equal(out.mode, "self_preservation");
|
|
assert.equal(out.action.skillId, "eat");
|
|
});
|
|
|
|
test("hunger: food below 14 with food → eat", async () => {
|
|
_resetModes();
|
|
const mod = await import(`./modes.js?cb=${Date.now() + 2}`);
|
|
const out = mod.tickModes({ snapshot: { health: 20, food: 12, hasFood: true } });
|
|
assert.equal(out.action.skillId, "eat");
|
|
});
|
|
|
|
test("night_shelter: day → null (skip)", async () => {
|
|
_resetModes();
|
|
const mod = await import(`./modes.js?cb=${Date.now() + 3}`);
|
|
const out = mod.tickModes({ snapshot: { isDay: true, food: 20, hasFood: false, inventory: { red_bed: 1 } } });
|
|
assert.equal(out, null);
|
|
});
|
|
|
|
test("night_shelter: night + bed in hand → sleep", async () => {
|
|
_resetModes();
|
|
const mod = await import(`./modes.js?cb=${Date.now() + 4}`);
|
|
const out = mod.tickModes({ snapshot: { isDay: false, food: 20, hasFood: false, inventory: { red_bed: 1 } } });
|
|
assert.equal(out.action.skillId, "sleep");
|
|
});
|