feat(runtime): v0.1.0 — adopt Voyager critic + Mindcraft modes/library/lint

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>
This commit is contained in:
2026-05-26 15:16:42 +03:00
co-authored by Claude Opus 4.7
parent 25e39c7244
commit 4ae63dabe1
18 changed files with 1196 additions and 113 deletions
+62 -25
View File
@@ -46,7 +46,9 @@ import { runSkill } from "./skills/index.js";
import { classifyIntent, INTENTS } from "./social/intent.js";
import { generateReply } from "./social/reply.js";
import { createChatMemory } from "./social/memory.js";
import { createStuckIncidentDetector } from "./stuck-incident.js";
import { openConversation, peekConversation, listConversations } from "./social/conversation.js";
import { createStuckIncidentDetector, attachCritique } from "./stuck-incident.js";
import { requestCritique } from "./critic.js";
import { createSkillMetrics } from "./skill-metrics.js";
import { createWorldJournal } from "./world-journal.js";
import { createScenarioMemory, situationHash } from "./scenario-memory.js";
@@ -415,6 +417,37 @@ function maybeFileProposal(label) {
appendDiary(`proposal filed: ${filename} (${summary})`);
}
// Async pre-flight critic — wrapper around writeProposal that asks Pi
// "did the bot actually fail?" first. Runs detached so reflex keeps
// ticking while critic burns 160s. If critic.success=true we drop the
// proposal entirely; otherwise the critique is spliced into the body.
async function filePostCritique(incident, channel) {
const critique = await requestCritique({
snapshot: lastSnapshot,
lastResult,
scenarioTail: scenarioMemory.recentTailFor({ n: 12 }),
milestone: lastSnapshot?.curriculum?.milestone?.title,
kind: incident.kind,
});
if (critique?.success) {
info(channel, `critic says already-recovered (${(critique.reasoning || "").slice(0, 100)}) — skipping proposal`);
return;
}
try {
const body = attachCritique(incident.body, critique);
const { filename } = writeProposal({
kind: incident.kind,
summary: incident.summary,
body,
editScope: incident.editScope,
});
warn(channel, `filed ${filename}: ${incident.summary}`);
appendDiary(`${channel}-proposal filed: ${filename} (${incident.summary})`);
} catch (e) {
warn(channel, `writeProposal failed: ${e.message}`);
}
}
// ---- chat (dialog-only via social/) ----------------------------------------
//
// MC chat is dialog-only (Phase 0 of survival-bot PRD). Phase 5 routes
@@ -784,18 +817,7 @@ function tick() {
now,
});
if (stuck?.fire) {
try {
const { filename } = writeProposal({
kind: stuck.kind,
summary: stuck.summary,
body: stuck.body,
editScope: stuck.editScope,
});
warn("stuck", `filed ${filename}: ${stuck.summary}`);
appendDiary(`stuck-proposal filed: ${filename} (${stuck.summary})`);
} catch (e) {
warn("stuck", `writeProposal failed: ${e.message}`);
}
void filePostCritique(stuck, "stuck");
}
// Second fast-track trigger: explicit wedged loop (escape-pit ran N
@@ -810,18 +832,7 @@ function tick() {
now,
});
if (wedged?.fire) {
try {
const { filename } = writeProposal({
kind: wedged.kind,
summary: wedged.summary,
body: wedged.body,
editScope: wedged.editScope,
});
warn("wedged", `filed ${filename}: ${wedged.summary}`);
appendDiary(`wedged-proposal filed: ${filename}`);
} catch (e) {
warn("wedged", `writeProposal failed: ${e.message}`);
}
void filePostCritique(wedged, "wedged");
}
ipc?.broadcast(EVENT_TYPES.STATUS, lastSnapshot);
@@ -941,6 +952,32 @@ function handleCommand(msg, send) {
tryDispatch();
break;
}
case COMMAND_TYPES.CONV_SAY: {
const { topic: topic_, text, intent, position } = msg.payload ?? {};
if (!topic_ || !text) { send(EVENT_TYPES.ERROR, { source: "conv", text: "topic and text required" }); return; }
try {
const h = openConversation(topic_, { speaker: cfg.username });
const turn = h.append({ text, intent, position: position ?? lastSnapshot?.position });
send(EVENT_TYPES.LOG, { ts: new Date().toISOString(), level: "info", source: "conv", text: `say to ${topic_}`, details: turn });
} catch (e) { send(EVENT_TYPES.ERROR, { source: "conv", text: e.message }); }
break;
}
case COMMAND_TYPES.CONV_RECENT: {
const { topic: topic_, n } = msg.payload ?? {};
if (!topic_) { send(EVENT_TYPES.ERROR, { source: "conv", text: "topic required" }); return; }
try {
const turns = peekConversation(topic_, n ?? 10);
send(EVENT_TYPES.LOG, { ts: new Date().toISOString(), level: "info", source: "conv", text: `recent ${topic_}`, details: { topic: topic_, turns } });
} catch (e) { send(EVENT_TYPES.ERROR, { source: "conv", text: e.message }); }
break;
}
case COMMAND_TYPES.CONV_LIST: {
try {
const topics = listConversations();
send(EVENT_TYPES.LOG, { ts: new Date().toISOString(), level: "info", source: "conv", text: "list", details: { topics } });
} catch (e) { send(EVENT_TYPES.ERROR, { source: "conv", text: e.message }); }
break;
}
default:
warn("ipc", `unknown command type: ${msg.type}`);
}