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>
67 lines
2.4 KiB
JavaScript
67 lines
2.4 KiB
JavaScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import {
|
|
openConversation,
|
|
listConversations,
|
|
peekConversation,
|
|
_resetConversations,
|
|
} from "./conversation.js";
|
|
|
|
function topic() { return `_t_${Date.now()}_${Math.floor(Math.random() * 1e6)}`; }
|
|
|
|
test("two bots in the same topic see each other's turns", () => {
|
|
_resetConversations();
|
|
const t = topic();
|
|
const alice = openConversation(t, { speaker: "alice" });
|
|
const bob = openConversation(t, { speaker: "bob" });
|
|
alice.append({ position: { x: 1, y: 2, z: 3 }, intent: "chop", text: "I'm chopping oak" });
|
|
bob.append({ position: { x: 10, y: 2, z: 3 }, intent: "mine", text: "I'm at the stone wall" });
|
|
const seenByAlice = alice.recent({ n: 10 });
|
|
assert.equal(seenByAlice.filter((t) => t.kind === "turn").length, 2);
|
|
const fromsByBob = bob.recent({ excludeSelf: true })
|
|
.filter((t) => t.kind === "turn")
|
|
.map((t) => t.from);
|
|
assert.deepEqual(fromsByBob, ["alice"]);
|
|
});
|
|
|
|
test("peers() returns every speaker seen in the topic", () => {
|
|
_resetConversations();
|
|
const t = topic();
|
|
openConversation(t, { speaker: "x" }).append({ text: "hi" });
|
|
openConversation(t, { speaker: "y" }).append({ text: "hello" });
|
|
openConversation(t, { speaker: "z" }).append({ text: "yo" });
|
|
const peers = openConversation(t, { speaker: "x" }).peers();
|
|
assert.deepEqual(peers.sort(), ["x", "y", "z"]);
|
|
});
|
|
|
|
test("listConversations enumerates active topics", () => {
|
|
_resetConversations();
|
|
openConversation(topic(), { speaker: "a" });
|
|
openConversation(topic(), { speaker: "b" });
|
|
const all = listConversations();
|
|
assert.ok(all.length >= 2);
|
|
});
|
|
|
|
test("recent() respects n", () => {
|
|
_resetConversations();
|
|
const t = topic();
|
|
const h = openConversation(t, { speaker: "a" });
|
|
for (let i = 0; i < 15; i++) h.append({ text: `msg${i}` });
|
|
const last5 = h.recent({ n: 5 }).filter((x) => x.kind === "turn");
|
|
assert.equal(last5.length, 5);
|
|
assert.equal(last5[last5.length - 1].text, "msg14");
|
|
});
|
|
|
|
test("peekConversation works without an open handle", () => {
|
|
_resetConversations();
|
|
const t = topic();
|
|
openConversation(t, { speaker: "lurker" }).append({ text: "hi" });
|
|
const peeked = peekConversation(t, 5);
|
|
assert.ok(peeked.find((p) => p.text === "hi"));
|
|
});
|
|
|
|
test("openConversation throws without topic or speaker", () => {
|
|
assert.throws(() => openConversation(null, { speaker: "x" }));
|
|
assert.throws(() => openConversation("t", {}));
|
|
});
|