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:
+62
-25
@@ -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 1–60s. 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}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user