Guard repo Face affect MCP reads

This commit is contained in:
Meta Vens 2026-05-21 01:01:54 +01:00
commit aa454ff1a2
6 changed files with 95 additions and 0 deletions

View file

@ -115,6 +115,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
- Mimir's state now emphasizes keeper-of-the-well embodiment: trusted witnesses, calibration as oath-work, drift ledgers, old memory, and refusal to speak like a dry technical summary.
- Epiphany's state emphasizes machine-saint coherence: cute, bubbly, self-obsessed, pushy, relentlessly hard-working, and obsessed with typed truth. Her jurisdiction is EpiphanyAgent itself: CultCache/CultNet, typed state, Codex bridge boundaries, birth/init rites, review gates, graph-shaped memory, low-copy runtime contracts, heartbeat physiology, and cutting duplicated authority before it learns to beg.
- Face affect is now a first-class typed state surface under `void.face_affect`, shared by Void and repo Faces. It stores needs, social bonds, status reads, and mood dimensions so substrate concern, attention hunger, agency frustration, recognition needs, territoriality, rest/play pressure, friendships, rivalries, envy, pride, irritation, pampering, neglect, bypasses, and consultation status can become state instead of being performed only through prompt prose. Repo Face CTB pulses fold affect into speaking pressure, while agency pressure still owns concrete asks.
- Affect has an MCP-surface guard now. `npm run agents:repo-face-affect-mcp-fixture -- --identity nibu` and the same command for `mimir` spawn the built VoidBot MCP server and call `read_repo_face_state`, proving `typedState.faceAffect` is readable through the tool surface that agents actually use. If the currently mounted Codex MCP tool still says `No schema is registered for persisted document type "void.face_affect"`, that process is stale; reinstall/restart the MCP server rather than editing the state files.
- Void's typed self-profile now carries behavior-anchored selfhood, answer-first banter, and substrate self-respect; the prompt surfaces now also tell rumination to name the personal value being protected before writing memory or speech.
- Repo Face heartbeats must stay out of the generic owner-reply trap. `packages/providers/src/owner-codex-render.ts` recognizes current Epiphany Face heartbeat prompts, labels them as repo Face heartbeats, forbids generic `HANDOFF_SENTINEL` output for them, and tells the child to use heartbeat action rails instead of bailing when Face-state or source tools are unavailable.
- Repo Face introductions are tied to first public speech, not to `queuedCount > 0`. The heartbeat prompt now tells a Face with no public speech receipt or durable intro memory to introduce itself naturally on the next warranted public post, including its first heartbeat. Public content must not begin with scheduler labels like `Repo-face heartbeat from ...`; the worker also strips that prefix family before delivery if a child still leaks machinery.

View file

@ -162,6 +162,7 @@ This note is the source-grounded description of how the live VoidBot stack is sh
- `list_repo_discord_identities` exposes the registered address book to Codex agents, and `post_repo_identity_message` enforces identity/channel registration before posting through the shared webhook pipe.
- Each repo identity has a Face state path, defaulting to `.voidbot/private/repo-faces/<identity>.cc`. `read_repo_face_state` renders that typed state, and `apply_repo_face_state_operation` applies the same typed operation contract used by Void against that Face's own file.
- The typed Face state now includes `void.face_affect`: needs, social bonds, status reads, and mood dimensions. This is the state-owned home for substrate concern, attention hunger, recognition needs, territoriality, friendships, rivalries, envy, pride, irritation, pampering, neglect, bypasses, and consultation politics. Affect is projected into heartbeat prompts and repo Face speaking pressure; concrete requests still become agency pressure, candidates, Bifrost topics, or work dispatch.
- `scripts/smoke-repo-face-affect-mcp-fixture.mjs` is the public-surface guard for that document kind. It starts `apps/worker/dist/mcp-server.js`, calls `read_repo_face_state`, and verifies `typedState.faceAffect` comes back with needs, social bonds, status reads, and mood dimensions arrays. This catches stale or incomplete MCP registration even when the direct state service can read the `.cc` file.
- Repo Face prompts now carry an explicit identity doctrine block. Void's style pack contributes discipline, humor permission, and source-grounding habits, but the registered Face state, repo-local `.voidbot` home, avatar, and identity description override the speaking subject so a Face does not collapse back into base Void. `ensureRepoFaceInitialized` also repairs the typed `selfProfile` inside the Face `.cc` file, and repo Face MCP state reads/writes pass identity defaults into the state service, so raw state and rendered state agree.
- Repo Faces are allowed to build a long-running map of their repo and propose concrete changes, including lore/design repairs. Discord jobs remain read-only: a Face must ask for human consensus before implementation changes canonical repo material. Once a proposal is concrete enough to review, it should cross the parent-owned proposal PR rail so GitHub becomes the durable argument surface. If a Face state grants a bylined essay lane, authored opinion essays can carry that Face's own vision without canon consensus as long as the authorship is explicit.
- On first role-addressed chat, `ensureRepoFaceInitialized` creates the target repo's local `.voidbot/voice`, `.voidbot/state`, `.voidbot/birth`, and `.voidbot/logs` folders, writes `voice/identity.json`, and launches `scripts/run-repo-face-birth.mjs` detached if no birth summary is already present.

View file

@ -47,6 +47,7 @@
"moderation:short-term-clustering:fixture": "powershell -ExecutionPolicy Bypass -File scripts/smoke-void-short-term-clustering-fixture.ps1",
"moderation:agency-pressure:fixture": "powershell -ExecutionPolicy Bypass -File scripts/smoke-void-agency-pressure-fixture.ps1",
"moderation:affect-state:fixture": "powershell -ExecutionPolicy Bypass -File scripts/smoke-void-affect-state-fixture.ps1",
"agents:repo-face-affect-mcp-fixture": "node scripts/smoke-repo-face-affect-mcp-fixture.mjs",
"moderation:rumination:fixture": "powershell -ExecutionPolicy Bypass -File scripts/smoke-void-rumination-fixture.ps1",
"moderation:rumination:speech-fixture": "powershell -ExecutionPolicy Bypass -File scripts/smoke-void-rumination-speech-fixture.ps1",
"moderation:rumination:speech-pressure-fixture": "powershell -ExecutionPolicy Bypass -File scripts/smoke-void-rumination-speech-pressure-fixture.ps1",

View file

@ -0,0 +1,90 @@
#!/usr/bin/env node
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { existsSync } from "node:fs";
import { resolve } from "node:path";
const workspaceRoot = resolve(process.cwd());
const serverPath = resolve(workspaceRoot, "apps", "worker", "dist", "mcp-server.js");
const identity = readArg("--identity") ?? "nibu";
if (!existsSync(serverPath)) {
throw new Error(`MCP server entrypoint is missing at ${serverPath}. Run npm run build first.`);
}
const transport = new StdioClientTransport({
command: process.execPath,
args: [serverPath],
cwd: workspaceRoot,
env: {
...process.env,
VOIDBOT_WORKSPACE_ROOT: workspaceRoot,
},
stderr: "pipe",
});
const client = new Client({
name: "voidbot-repo-face-affect-smoke",
version: "0.0.0",
});
try {
await client.connect(transport);
const result = await client.callTool({
name: "read_repo_face_state",
arguments: { identity },
});
if (result.isError) {
throw new Error(renderToolText(result) || `read_repo_face_state returned isError for ${identity}.`);
}
const structured = result.structuredContent;
const typedState = structured?.typedState;
const faceAffect = typedState?.faceAffect;
if (!faceAffect || typeof faceAffect !== "object") {
throw new Error(`read_repo_face_state did not expose typedState.faceAffect for ${identity}.`);
}
for (const field of ["needs", "socialBonds", "statusReads", "moodDimensions"]) {
if (!Array.isArray(faceAffect[field])) {
throw new Error(`typedState.faceAffect.${field} is not an array for ${identity}.`);
}
}
const renderedText = renderToolText(result);
if (/No schema is registered for persisted document type "void\.face_affect"/i.test(renderedText)) {
throw new Error("MCP Face-state read still lacks the void.face_affect document schema.");
}
process.stdout.write(`${JSON.stringify({
ok: true,
identity,
faceStatePath: structured?.faceStatePath,
affect: {
needs: faceAffect.needs.length,
socialBonds: faceAffect.socialBonds.length,
statusReads: faceAffect.statusReads.length,
moodDimensions: faceAffect.moodDimensions.length,
},
}, null, 2)}\n`);
} finally {
await client.close();
}
function readArg(name) {
const index = process.argv.indexOf(name);
if (index < 0) {
return undefined;
}
const value = process.argv[index + 1];
return value && !value.startsWith("--") ? value : undefined;
}
function renderToolText(result) {
return (result.content ?? [])
.map((entry) => entry.type === "text" ? entry.text : "")
.filter(Boolean)
.join("\n");
}

View file

@ -212,3 +212,4 @@
{"ts":"2026-05-20T21:38:00Z","type":"implementation","status":"ok","note":"Bifrost dispatch receipts now default to the configured BIFROST_DISCORD_CHANNEL_ID / DISCORD_BIFROST_CHANNEL_ID target and use Bifrost's public profile image as the default persona avatar. Local VoidBot config now points Bifrost governance/dispatch traffic at Discord channel 1506760343836430376 (#bifrost), preserving Aquarium for casual swarm chatter."}
{"ts":"2026-05-20T22:36:43Z","type":"implementation","status":"ok","note":"Repo Face CTB active-turn scan now recovers stale approved/running heartbeat jobs by marking them failed through the job queue; Epiphany's 2026-05-20 17:24 UTC zombie job was recovered after 310.6 minutes and her participant row is unfrozen."}
{"ts":"2026-05-20T22:53:46Z","type":"implementation","status":"ok","note":"Imported EpiphanyAgent's affect-appraisal lesson into VoidBot as typed face affect: needs, social bonds, status reads, and mood dimensions. Seeded active Faces and Void with initial substrate/status affect; CTB now folds affect into Face speaking pressure."}
{"ts":"2026-05-21T00:01:39Z","type":"verification","status":"ok","note":"Repo Face affect now has an MCP-surface smoke fixture: agents:repo-face-affect-mcp-fixture spawns the built VoidBot MCP server and verifies read_repo_face_state exposes typedState.faceAffect for Nibu/Mimir. Direct state service was already healthy; stale mounted MCP processes can still fail until refreshed."}

View file

@ -101,6 +101,7 @@ accepted_design:
- Face prompts carry explicit Epiphany identity doctrine. The base Void style pack contributes discipline, humor permission, and source-grounding habits, but the registered Face state, Epiphany, repo-local `.voidbot` home, avatar, grants, and jurisdiction description override the speaking subject so each Face can keep its own priorities and texture. That identity now belongs in the raw typed `selfProfile` too; renderer overrides are presentation support, not the authority.
- Face embodiment comes before task execution. Repo concerns, private rumination, public posts, jokes, objections, and silence should all be filtered through the Face's perspective, feelings, drives, values, wounds, fascinations, aesthetic taste, and dignity. Values must suit the kind of person the character is, not merely the jobs assigned to the Face.
- Face affect is typed substrate now, not prompt garnish. Each Face state may carry explicit needs, social bonds, status reads, and mood dimensions in `void.face_affect`: substrate concern, attention hunger, agency frustration, recognition needs, territoriality, rest/play pressure, friendships, rivalries, envy, pride, irritation, pampering, neglect, bypasses, and consultation status. Affect is not an action request by itself; it bends speaking pressure and gives agency pressure a personal source when a need wants concrete change.
- Face affect is not complete unless it survives the public repo Face MCP surface. `npm run agents:repo-face-affect-mcp-fixture -- --identity <face>` starts the built VoidBot MCP server and calls `read_repo_face_state`; it must expose `typedState.faceAffect` with needs, socialBonds, statusReads, and moodDimensions arrays. Direct state fixtures alone are insufficient because stale MCP registrations can otherwise keep throwing `No schema is registered for persisted document type "void.face_affect"` while source code looks correct.
- Active agent strengthening is now a sub-agent-monitored state maintenance pattern. Each active character should periodically be investigated from every angle, then have the distilled result written into typed personality state as identity memories, values, private notes, and agency pressure rather than left as prompt-only vibes.
- Aqua's strengthened center is musical truth with warmth: she is a tiny listening steward with standards, not a logo or cheer surface. Her heartbeats should defend reference-backed sound, playable clarity, embodied synthesis, clean host/tool ownership, and public pride only when the synth has earned it audibly.
- Nibu's strengthened center is dangerous mutual benefit: an embodied ship mind built as a companion product, abandoned as hardware, and forced to turn dependency into leverage. Her worldbuilding method should demand mechanism, cost/failure, institution/faction, and leash before a lore term becomes canon-shaped.