Files
pepa-pi-bot/runtime/critic.test.js
T
mayatnikovandClaude Opus 4.7 4ae63dabe1 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>
2026-05-26 15:16:42 +03:00

51 lines
2.0 KiB
JavaScript

import { test } from "node:test";
import assert from "node:assert/strict";
import { _internal } from "./critic.js";
test("extractJsonObject: bare object", () => {
const got = _internal.extractJsonObject('{"reasoning":"x","success":true,"critique":""}');
assert.equal(got.success, true);
assert.equal(got.reasoning, "x");
});
test("extractJsonObject: fenced markdown", () => {
const got = _internal.extractJsonObject('```json\n{"reasoning":"a","success":false,"critique":"b"}\n```');
assert.equal(got.success, false);
assert.equal(got.critique, "b");
});
test("extractJsonObject: leading OUTPUT: prefix", () => {
const got = _internal.extractJsonObject('OUTPUT:\n{"reasoning":"r","success":true,"critique":""}');
assert.equal(got.reasoning, "r");
});
test("extractJsonObject: junk before object is tolerated", () => {
const got = _internal.extractJsonObject('Some chatter from Pi.\n{"reasoning":"a","success":true,"critique":""}\nMore chatter.');
assert.equal(got.success, true);
});
test("extractJsonObject: nested braces parse correctly", () => {
const got = _internal.extractJsonObject('{"reasoning":"nest {a:1}","success":true,"critique":""}');
assert.equal(got.success, true);
});
test("extractJsonObject: garbage returns null, not throw", () => {
assert.equal(_internal.extractJsonObject("not json at all"), null);
assert.equal(_internal.extractJsonObject(""), null);
assert.equal(_internal.extractJsonObject(null), null);
});
test("buildUserBlock includes milestone + slim snapshot", () => {
const s = _internal.buildUserBlock({
snapshot: { position: { x: 1, y: 2, z: 3 }, health: 10, food: 17, inventory: { dirt: 1 } },
lastResult: { label: "gather.logs", ok: false, code: "no_target", detail: "no reachable log" },
scenarioTail: [{ skillId: "gather.logs", ok: false, code: "no_target" }],
milestone: "chop 1 log",
kind: "stuck-no_food_source",
});
assert.ok(s.includes("chop 1 log"));
assert.ok(s.includes("no_target"));
assert.ok(s.startsWith("INPUT:"));
assert.ok(s.endsWith("OUTPUT:"));
});