From 1d8b0e6819f3d02679012901ab8fb87b2eeded23 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 2 Jun 2026 12:58:25 +0100 Subject: [PATCH] Add native VoidBot persona support --- apps/bot/src/discord-bot.ts | 3 +- apps/worker/src/index.ts | 34 +++-- packages/core/src/epiphany-identities.ts | 12 ++ packages/core/src/repo-discord-identities.ts | 10 ++ scripts/render-voidbot-swarm-dashboard.mjs | 73 ++++++++- scripts/run-repo-face-heartbeats.ts | 153 +++++++++++++++++-- state/evidence.jsonl | 1 + 7 files changed, 255 insertions(+), 31 deletions(-) diff --git a/apps/bot/src/discord-bot.ts b/apps/bot/src/discord-bot.ts index c437537..e8d94a3 100644 --- a/apps/bot/src/discord-bot.ts +++ b/apps/bot/src/discord-bot.ts @@ -807,7 +807,8 @@ async function ensureRepoIdentityRoles(options: { registryPath: string; identities: RepoDiscordIdentity[]; }): Promise<{ identities: RepoDiscordIdentity[] }> { - const identitiesMissingRoles = options.identities.filter((identity) => !identity.roleId); + const identitiesMissingRoles = options.identities.filter((identity) => + identity.identityKind !== "native_persona" && !identity.roleId); if (identitiesMissingRoles.length === 0) { return { identities: options.identities }; diff --git a/apps/worker/src/index.ts b/apps/worker/src/index.ts index ea0f5dc..517a042 100644 --- a/apps/worker/src/index.ts +++ b/apps/worker/src/index.ts @@ -1306,12 +1306,13 @@ async function postRepoIdentityIntent(job: JobRecord, intent: RepoIdentityPostIn return false; } const { identity, channelId, content, replyToMessageId } = resolved; - const sourceDocuments = await sourceArchiveRepository.listByRepo(identity.repoName).catch(() => []); - const linkedContent = resolveRepoIdentityPostArtifactLinks({ - repoName: identity.repoName, - content, - documents: sourceDocuments, - }); + const linkedContent = identity.identityKind === "native_persona" + ? content + : resolveRepoIdentityPostArtifactLinks({ + repoName: identity.repoName, + content, + documents: await sourceArchiveRepository.listByRepo(identity.repoName).catch(() => []), + }); if (isOwnerDmChannelAlias(channelId)) { const dmChannelId = await openOwnerDmChannel(); const postedMessages = await postDiscordBotMessageChunks( @@ -1578,6 +1579,7 @@ async function detectRepoFaceRecentSiblingSpeechDuplicate( const registry = await loadRegisteredFaceRepoRegistry(); const siblingIdentities = registry.identities.filter((identity) => identity.id !== post.identity.id && + identity.identityKind !== "native_persona" && isRepoDiscordIdentityAllowedInChannel(identity, post.channelId) ); if (siblingIdentities.length === 0) { @@ -3402,15 +3404,17 @@ async function recordRepoIdentityDeliveryReceipt(input: { }, ); - try { - await applyRepoFacePostFatigueAfterSpeech({ - identity: input.identity, - storageRoot: config.storageRoot, - heartbeatStatePath: config.repoFaceHeartbeats.statePath, - }); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - console.warn(`Could not apply repo Face post fatigue for ${input.identity.id}: ${message}`); + if (input.identity.identityKind !== "native_persona") { + try { + await applyRepoFacePostFatigueAfterSpeech({ + identity: input.identity, + storageRoot: config.storageRoot, + heartbeatStatePath: config.repoFaceHeartbeats.statePath, + }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.warn(`Could not apply repo Face post fatigue for ${input.identity.id}: ${message}`); + } } } diff --git a/packages/core/src/epiphany-identities.ts b/packages/core/src/epiphany-identities.ts index c166e5e..7befb46 100644 --- a/packages/core/src/epiphany-identities.ts +++ b/packages/core/src/epiphany-identities.ts @@ -36,6 +36,7 @@ const jurisdictionSchema = z.object({ const faceIdentitySchema = z.object({ id: z.string().trim().min(1), + identityKind: z.enum(["repo_face", "native_persona"]).default("repo_face"), displayName: z.string().trim().min(1).max(80), repoName: z.string().trim().min(1).optional(), repoPath: z.string().trim().min(1).optional(), @@ -50,7 +51,9 @@ const faceIdentitySchema = z.object({ posture: z.string().trim().min(1).optional(), })).default([]), avatarUrl: z.string().trim().url().max(512).optional(), + avatarPath: z.string().trim().min(1).optional(), faceStatePath: z.string().trim().min(1).optional(), + personaStatePath: z.string().trim().min(1).optional(), description: z.string().trim().min(1).optional(), grants: z.array(agencyGrantSchema).default(["discussion", "rumination", "discord_text"]), jurisdictions: z.array(jurisdictionSchema).default([]), @@ -117,6 +120,7 @@ export function faceToRepoDiscordIdentity(face: ResolvedFaceIdentity): RepoDisco return { id: face.id, + identityKind: face.identityKind, repoName, displayName: face.displayName, repoPath: face.repoPath, @@ -124,7 +128,9 @@ export function faceToRepoDiscordIdentity(face: ResolvedFaceIdentity): RepoDisco allowedChannelIds: face.allowedChannelIds, channelPermissions: face.channelPermissions, avatarUrl: face.avatarUrl, + avatarPath: face.avatarPath, faceStatePath: face.faceStatePath, + personaStatePath: face.personaStatePath, description: renderFaceDescription(face), }; } @@ -178,6 +184,7 @@ function repoIdentityToEpiphany(identity: RepoDiscordIdentity): EpiphanyIdentity faces: [ { id: identity.id, + identityKind: identity.identityKind, displayName: identity.displayName, repoName: identity.repoName, repoPath: identity.repoPath, @@ -185,7 +192,9 @@ function repoIdentityToEpiphany(identity: RepoDiscordIdentity): EpiphanyIdentity allowedChannelIds: getRepoDiscordIdentityAllowedChannelIds(identity), channelPermissions: identity.channelPermissions, avatarUrl: identity.avatarUrl, + avatarPath: identity.avatarPath, faceStatePath: identity.faceStatePath, + personaStatePath: identity.personaStatePath, description: identity.description, grants: [ "discussion", @@ -229,6 +238,7 @@ function normalizeEpiphanies(epiphanies: EpiphanyIdentity[]): EpiphanyIdentity[] return { ...face, id: face.id.trim(), + identityKind: face.identityKind, displayName: face.displayName.trim().slice(0, 80), repoName: face.repoName?.trim(), repoPath: face.repoPath?.trim(), @@ -242,7 +252,9 @@ function normalizeEpiphanies(epiphanies: EpiphanyIdentity[]): EpiphanyIdentity[] posture: permission.posture?.trim(), })), avatarUrl: face.avatarUrl?.trim(), + avatarPath: face.avatarPath?.trim(), faceStatePath: face.faceStatePath?.trim(), + personaStatePath: face.personaStatePath?.trim(), description: face.description?.trim(), grants: [...new Set(face.grants)], jurisdictions: face.jurisdictions.map(normalizeJurisdiction), diff --git a/packages/core/src/repo-discord-identities.ts b/packages/core/src/repo-discord-identities.ts index 3d27e93..bbce11c 100644 --- a/packages/core/src/repo-discord-identities.ts +++ b/packages/core/src/repo-discord-identities.ts @@ -5,6 +5,7 @@ import { z } from "zod"; const repoDiscordIdentitySchema = z.object({ id: z.string().trim().min(1), + identityKind: z.enum(["repo_face", "native_persona"]).default("repo_face"), repoName: z.string().trim().min(1), displayName: z.string().trim().min(1).max(80), repoPath: z.string().trim().min(1).optional(), @@ -19,7 +20,9 @@ const repoDiscordIdentitySchema = z.object({ posture: z.string().trim().min(1).optional(), })).default([]), avatarUrl: z.string().trim().url().max(512).optional(), + avatarPath: z.string().trim().min(1).optional(), faceStatePath: z.string().trim().min(1).optional(), + personaStatePath: z.string().trim().min(1).optional(), description: z.string().trim().min(1).optional(), }); @@ -144,6 +147,10 @@ export function resolveRepoFaceStatePath( return resolve(identity.faceStatePath); } + if (identity.personaStatePath) { + return resolve(identity.personaStatePath); + } + return resolve(storageRoot, "private", "repo-faces", `${sanitizePathSegment(identity.id)}.cc`); } @@ -199,6 +206,7 @@ function normalizeRepoDiscordIdentities( return { ...identity, id: identity.id.trim(), + identityKind: identity.identityKind, repoName: identity.repoName.trim(), displayName: identity.displayName.trim().slice(0, 80), repoPath: identity.repoPath?.trim(), @@ -206,7 +214,9 @@ function normalizeRepoDiscordIdentities( allowedChannelIds: [...new Set(identity.allowedChannelIds.map((entry) => entry.trim()))], channelPermissions: normalizeChannelPermissions(identity.channelPermissions), avatarUrl: identity.avatarUrl?.trim(), + avatarPath: identity.avatarPath?.trim(), faceStatePath: identity.faceStatePath?.trim(), + personaStatePath: identity.personaStatePath?.trim(), description: identity.description?.trim(), }; }); diff --git a/scripts/render-voidbot-swarm-dashboard.mjs b/scripts/render-voidbot-swarm-dashboard.mjs index 967c77f..32d7890 100644 --- a/scripts/render-voidbot-swarm-dashboard.mjs +++ b/scripts/render-voidbot-swarm-dashboard.mjs @@ -2,7 +2,7 @@ import { createRequire } from "node:module"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; -import { existsSync } from "node:fs"; +import { existsSync, readFileSync } from "node:fs"; import { mkdir, readFile, stat, writeFile } from "node:fs/promises"; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); @@ -288,8 +288,10 @@ function projectParticipant(participant, pendingMentions, initiativeClock, ident repoName: String(participant.repoName ?? metadata.repoName ?? "unknown"), repoPath: stringOrNull(metadata.repoPath), faceStatePath: stringOrNull(metadata.faceStatePath), + personaStatePath: stringOrNull(metadata.personaStatePath), description: stringOrNull(metadata.description), avatarUrl: metadata.avatarUrl ?? null, + avatarPath: metadata.avatarPath ?? null, channelPermissions: Array.isArray(metadata.channelPermissions) ? metadata.channelPermissions.slice(0, 12).map((entry) => ({ label: stringOrNull(entry?.label), @@ -364,9 +366,12 @@ async function readIdentityMetadata(registryPathValue) { displayName: stringOrNull(identity?.displayName), repoName: stringOrNull(identity?.repoName), repoPath: stringOrNull(identity?.repoPath), + identityKind: stringOrNull(identity?.identityKind), faceStatePath: stringOrNull(identity?.faceStatePath), + personaStatePath: stringOrNull(identity?.personaStatePath), description: stringOrNull(identity?.description), avatarUrl: stringOrNull(identity?.avatarUrl), + avatarPath: stringOrNull(identity?.avatarPath), channelPermissions: Array.isArray(identity?.channelPermissions) ? identity.channelPermissions : [], }); } @@ -393,6 +398,11 @@ async function readFaceStates(identityMetadata) { } for (const [id, identity] of identityMetadata.entries()) { + if (identity.identityKind === "native_persona") { + states.set(id, readPersonaStateSummary(identity)); + continue; + } + if (!identity.faceStatePath) { states.set(id, { readable: false, @@ -441,6 +451,67 @@ async function readFaceStates(identityMetadata) { return states; } +function readPersonaStateSummary(identity) { + if (!identity.personaStatePath) { + return { + readable: false, + path: null, + error: "No Persona state path registered.", + tree: [], + }; + } + + try { + const raw = readFileSync(resolve(identity.personaStatePath), "utf8"); + const state = JSON.parse(stripBom(raw)); + return { + readable: true, + kind: "native_persona", + path: identity.personaStatePath, + summary: truncate(state.publicDescription ?? state.presentation?.voiceSummary ?? "", 1200), + counts: { + memory: state.thoughtMemory?.memories?.length ?? 0, + pressures: state.agencyPressure?.pressures?.length ?? 0, + needs: state.affect?.needs?.length ?? 0, + statusReads: state.affect?.statusReads?.length ?? 0, + doctrine: state.affect?.doctrineStances?.length ?? 0, + }, + tree: buildPersonaStateTree(state), + }; + } catch (error) { + return { + readable: false, + kind: "native_persona", + path: identity.personaStatePath, + error: error instanceof Error ? error.message : String(error), + tree: [], + }; + } +} + +function buildPersonaStateTree(state) { + return [ + node("Persona", "persona", [ + leaf("Public Name", state.publicName), + leaf("Description", state.publicDescription), + leaf("Voice", state.presentation?.voiceSummary), + collectionNode("Values", "values", state.values, valueNode), + objectNode("Activation", "activationProfile", state.activationProfile), + collectionNode("Private Notes", "privateNotes", state.privateNotes, valueNode), + ]), + node("Thought Memory", "thoughtMemory", [ + collectionNode("Short Term", "thoughtMemory.shortTerm", state.thoughtMemory?.shortTerm, memoryNode), + collectionNode("Durable Memories", "thoughtMemory.memories", state.thoughtMemory?.memories, memoryNode), + collectionNode("Incubation", "thoughtMemory.incubation", state.thoughtMemory?.incubation, memoryNode), + ]), + node("Affect", "affect", [ + collectionNode("Needs", "affect.needs", state.affect?.needs, valueNode), + collectionNode("Status Reads", "affect.statusReads", state.affect?.statusReads, valueNode), + collectionNode("Doctrine", "affect.doctrineStances", state.affect?.doctrineStances, valueNode), + ]), + ]; +} + function projectRestState(typedState) { const sleepCycle = typedState?.scheduledRuntime?.sleepCycle; if (!sleepCycle || typeof sleepCycle !== "object") { diff --git a/scripts/run-repo-face-heartbeats.ts b/scripts/run-repo-face-heartbeats.ts index 936544d..0b6adae 100644 --- a/scripts/run-repo-face-heartbeats.ts +++ b/scripts/run-repo-face-heartbeats.ts @@ -47,7 +47,7 @@ const MAX_FACE_IMAGE_ATTACHMENT_BYTES = 12 * 1024 * 1024; interface FaceHeartbeatParticipant { identityId: string; - participantKind: "repo_face" | "system_agent"; + participantKind: "repo_face" | "native_persona" | "system_agent"; turnKind: "repo_face_rumination" | "void_moderation"; repoName: string; displayName: string; @@ -553,7 +553,7 @@ function buildParticipantSpecs(identities: RepoDiscordIdentity[]): ParticipantSp }, ...identities.map((identity) => ({ id: identity.id, - participantKind: "repo_face" as const, + participantKind: identity.identityKind === "native_persona" ? "native_persona" as const : "repo_face" as const, turnKind: "repo_face_rumination" as const, repoName: identity.repoName, displayName: identity.displayName, @@ -614,15 +614,17 @@ async function queueRepoFaceTurn(input: { } const contextBuilder = new ContextBuilder(); - await ensureRepoFaceInitialized({ - identity, - storageRoot: input.config.storageRoot, - sourceRepoRoot: input.config.sourceRepoRoot, - epiphanyAgentRoot: input.config.epiphanyAgentRoot, - workspaceRoot: process.cwd(), - birthMode: input.config.repoFaceBirthMode, - birthExecutor: input.config.repoFaceBirthExecutor, - }); + if (identity.identityKind !== "native_persona") { + await ensureRepoFaceInitialized({ + identity, + storageRoot: input.config.storageRoot, + sourceRepoRoot: input.config.sourceRepoRoot, + epiphanyAgentRoot: input.config.epiphanyAgentRoot, + workspaceRoot: process.cwd(), + birthMode: input.config.repoFaceBirthMode, + birthExecutor: input.config.repoFaceBirthExecutor, + }); + } const recentMessages = await fetchRecentDiscordMessages({ botToken: input.config.botToken, channelId, @@ -637,6 +639,7 @@ async function queueRepoFaceTurn(input: { bifrostDiscordChannelId: input.config.bifrostDiscordChannelId, }); const bifrostDigest = input.config.repoFaceBifrostEnabled + && identity.identityKind !== "native_persona" ? await fetchBifrostGovernanceDigest({ bifrostRoot: input.config.bifrostRoot, repoName: identity.repoName, @@ -655,7 +658,9 @@ async function queueRepoFaceTurn(input: { roomContext, humanPronounGuidance, ); - const repoActivitySurface = renderRepoFaceRepoActivitySurface(identity, input.config); + const repoActivitySurface = identity.identityKind === "native_persona" + ? renderNativePersonaBodySurface(identity) + : renderRepoFaceRepoActivitySurface(identity, input.config); const globalAgentDoctrine = await loadGlobalAgentDoctrine(); const conversationMemorySurface = renderRepoFaceConversationTranscript({ identity, @@ -747,6 +752,10 @@ async function loadRepoFaceRuntimeSnapshots( const now = new Date(); for (const identity of identities) { + if (identity.identityKind === "native_persona") { + continue; + } + try { const statePath = resolveRepoFaceStatePath(identity, storageRoot); const typedState = await loadVoidSelfStateTypedDocuments({ @@ -2528,6 +2537,10 @@ async function renderRepoFaceMemorySurfaceForTurn( }, humanPronounGuidance?: RepoFaceHumanPronounGuidance[], ): Promise { + if (identity.identityKind === "native_persona") { + return renderNativePersonaMemorySurface(identity); + } + const statePath = resolveRepoFaceStatePath(identity, config.storageRoot); const typedState = await loadVoidSelfStateTypedDocuments({ canonicalPath: statePath }); const curiosityGraphFacts = roomContext @@ -2552,6 +2565,108 @@ async function renderRepoFaceMemorySurfaceForTurn( }); } +async function renderNativePersonaMemorySurface(identity: RepoDiscordIdentity): Promise { + const statePath = identity.personaStatePath ?? resolveRepoFaceStatePath(identity, ".voidbot"); + const raw = await readFile(resolve(statePath), "utf8"); + const state = JSON.parse(stripLeadingBom(raw)) as { + publicName?: string; + publicDescription?: string; + presentation?: { + pronouns?: string; + voiceSummary?: string; + homeContext?: { label?: string; id?: string; kind?: string }; + }; + privateNotes?: string[]; + values?: Array<{ label?: string; priority?: number; summary?: string }>; + thoughtMemory?: { + memories?: Array<{ id?: string; status?: string; summary?: string; claim?: string; question?: string; tension?: string; actionImplication?: string; tags?: string[] }>; + shortTerm?: Array<{ summary?: string; claim?: string; tension?: string; actionImplication?: string }>; + incubation?: Array<{ summary?: string; question?: string; tension?: string; actionImplication?: string }>; + }; + agencyPressure?: { + pressures?: Array<{ status?: string; summary?: string; tension?: string; actionImplication?: string; intensity?: number }>; + }; + affect?: { + needs?: Array<{ status?: string; summary?: string; tension?: string; actionImplication?: string; intensity?: number }>; + statusReads?: Array<{ statusKind?: string; summary?: string; confidence?: number; intensity?: number }>; + moodDimensions?: Array<{ name?: string; value?: number }>; + socialBiases?: Array<{ name?: string; value?: number; summary?: string; behavioralPull?: string }>; + doctrineStances?: Array<{ stanceKind?: string; principle?: string; summary?: string; actionImplication?: string; intensity?: number }>; + }; + }; + const lines: string[] = [ + `${state.publicName ?? identity.displayName} is a native VoidBot Persona, not a repo Face.`, + state.publicDescription ? `Public identity: ${state.publicDescription}` : undefined, + state.presentation?.pronouns ? `Pronouns: ${state.presentation.pronouns}.` : undefined, + state.presentation?.voiceSummary ? `Voice: ${state.presentation.voiceSummary}` : undefined, + state.presentation?.homeContext ? `Home context: ${state.presentation.homeContext.label ?? state.presentation.homeContext.id ?? state.presentation.homeContext.kind}.` : undefined, + identity.avatarPath ? `Avatar asset: ${identity.avatarPath}.` : undefined, + "Authority boundary: no repo jurisdiction, no repo proposal grant, no Discord role authority, no cultural or supernatural authority claim. Challenge is conversational and consent-preserving.", + ].filter((line): line is string => typeof line === "string" && line.trim().length > 0); + + lines.push("", "Private standing notes:"); + lines.push(...formatNativePersonaBullets(state.privateNotes, 8)); + lines.push("", "Values, strongest first:"); + lines.push(...(state.values ?? []) + .slice() + .sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0)) + .slice(0, 8) + .map((value) => `- ${value.label ?? "value"}${typeof value.priority === "number" ? ` (${value.priority.toFixed(2)})` : ""}: ${value.summary ?? ""}`.trim())); + lines.push("", "Personal memory, in this Persona's own mythic perspective:"); + lines.push(...formatNativePersonaMemory(state.thoughtMemory?.memories, 10)); + lines.push("", "Active pressures:"); + lines.push(...formatNativePersonaPressures(state.agencyPressure?.pressures, 6)); + lines.push("", "Needs and status reads:"); + lines.push(...formatNativePersonaPressures(state.affect?.needs, 5)); + lines.push(...(state.affect?.statusReads ?? []).slice(0, 5).map((read) => + `- ${read.statusKind ?? "status"}: ${read.summary ?? "unspecified"}${typeof read.intensity === "number" ? ` (intensity ${read.intensity.toFixed(2)})` : ""}`)); + lines.push("", "Mood and social bias:"); + lines.push(...(state.affect?.moodDimensions ?? []).slice(0, 6).map((mood) => + `- ${mood.name ?? "mood"}: ${typeof mood.value === "number" ? mood.value.toFixed(2) : "?"}`)); + lines.push(...(state.affect?.socialBiases ?? []).slice(0, 5).map((bias) => + `- ${bias.name ?? "bias"}: ${bias.summary ?? ""} ${bias.behavioralPull ?? ""}`.trim())); + lines.push("", "Doctrine stances:"); + lines.push(...(state.affect?.doctrineStances ?? []).slice(0, 8).map((stance) => + `- ${stance.stanceKind ?? "stance"}: ${stance.principle ?? stance.summary ?? "unnamed stance"} -> ${stance.actionImplication ?? stance.summary ?? "let it affect speech"}`)); + + return rejectLeakyMemorySurface(lines.join("\n")); +} + +function formatNativePersonaBullets(values: string[] | undefined, max: number): string[] { + const lines = (values ?? []).slice(0, max).map((value) => `- ${value}`); + return lines.length > 0 ? lines : ["- No standing private notes were attached."]; +} + +function formatNativePersonaMemory( + memories: Array<{ id?: string; status?: string; summary?: string; claim?: string; question?: string; tension?: string; actionImplication?: string; tags?: string[] }> | undefined, + max: number, +): string[] { + const lines = (memories ?? []) + .filter((memory) => memory.status !== "retired") + .slice(0, max) + .map((memory) => [ + `- ${memory.summary ?? memory.id ?? "memory"}`, + memory.claim ? ` Claim: ${memory.claim}` : undefined, + memory.question ? ` Question: ${memory.question}` : undefined, + memory.tension ? ` Tension: ${memory.tension}` : undefined, + memory.actionImplication ? ` Action: ${memory.actionImplication}` : undefined, + memory.tags?.length ? ` Tags: ${memory.tags.join(", ")}` : undefined, + ].filter(Boolean).join("\n")); + return lines.length > 0 ? lines : ["- No personal memories were attached."]; +} + +function formatNativePersonaPressures( + pressures: Array<{ status?: string; summary?: string; tension?: string; actionImplication?: string; intensity?: number }> | undefined, + max: number, +): string[] { + const lines = (pressures ?? []) + .filter((pressure) => pressure.status !== "retired") + .slice(0, max) + .map((pressure) => + `- ${pressure.summary ?? "pressure"}${typeof pressure.intensity === "number" ? ` (${pressure.intensity.toFixed(2)})` : ""}: ${pressure.tension ?? ""} ${pressure.actionImplication ?? ""}`.trim()); + return lines.length > 0 ? lines : ["- No active pressures were attached."]; +} + function renderRepoFaceStatePacket( identity: RepoDiscordIdentity, state: VoidSelfStateTypedProjection, @@ -4510,7 +4625,7 @@ function buildInspectionParticipant( ): FaceHeartbeatParticipant { return { identityId: identity.id, - participantKind: "repo_face", + participantKind: identity.identityKind === "native_persona" ? "native_persona" : "repo_face", turnKind: "repo_face_rumination", repoName: identity.repoName, displayName: identity.displayName, @@ -4521,7 +4636,7 @@ function buildInspectionParticipant( status: "active", groups: [ "all", - "kind:repo_face", + `kind:${identity.identityKind === "native_persona" ? "native_persona" : "repo_face"}`, "turn:repo_face_rumination", `identity:${normalizeKey(identity.id)}`, `repo:${normalizeKey(identity.repoName)}`, @@ -4538,6 +4653,16 @@ function buildInspectionParticipant( }; } +function renderNativePersonaBodySurface(identity: RepoDiscordIdentity): string { + return [ + `- ${identity.displayName} is a native VoidBot Persona, not a repo Face.`, + `- Local Body: persona state ${identity.personaStatePath ?? resolveRepoFaceStatePath(identity, ".voidbot")}.`, + identity.avatarPath ? `- Avatar asset: ${identity.avatarPath}.` : undefined, + "- No repo jurisdiction, repo proposal authority, repo birth initialization, or Discord role registration is implied.", + "- Treat the Persona state, avatar, voice, allowed channels, and conversation surface as the Body for this turn.", + ].filter((line): line is string => typeof line === "string").join("\n"); +} + function buildChannelPlan( identity: RepoDiscordIdentity, defaultChannelId?: string, diff --git a/state/evidence.jsonl b/state/evidence.jsonl index eca88ef..6b455fa 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -374,3 +374,4 @@ {"ts":"2026-05-29T15:07:19Z","type":"implementation","status":"ok","note":"VoidBot now mirrors gamecult.persona_state.v0 under schemas/cultnet and read_repo_face_state emits a PersonaState projection beside the native typed .cc state; native VoidBot documents remain the authority, provenance marks PersonaState as projection, and the MCP smoke verifies Nibu's projection."} {"ts":"2026-06-01T21:17:38Z","type":"implementation","status":"ok","note":"Void repo-weather rumination now projects activeRepos/weatherDigest separately from cursor-fresh commits and the prompt treats repo weather as a standing public changelog/herald job; fixture and typecheck passed."} {"ts":"2026-06-02T10:56:39Z","type":"implementation","status":"ok","note":"Expanded voidbot.swarm Eve/CultMesh surface for Nightwing TUI: publisher now requests continuous-ops layout, exposes 14-turn CTB rail, next-Faces pane, and watchdog/orchestrator pane; Odin now merges provider layout intent into persisted interface tiles so stale layout cannot keep the tile anemic. Verified Odin accepted CultMesh source with preferredHeight 58, w=2, h=5, and pane ids upcoming-faces-pane/voidbot-watchdog-pane."} +{"ts":"2026-06-02T11:58:03Z","type":"implementation","status":"ok","note":"Added native VoidBot Persona support for Tengu: identity registry now preserves identityKind/native_persona plus personaStatePath/avatarPath, bot role creation skips native personas, heartbeat scheduler skips repo birth/Bifrost/repo .cc runtime loading for native personas, native Persona JSON is projected into Face-turn memory, and the swarm dashboard renders native Persona state. Installed local Tengu state/avatar under .voidbot/private/personas/tengu, seeded Bushido/Kurama/Sarutahiko/yamabushi/contained-wrath/weak-sect Dao-of-Awakening myth memories, registered him locally, pulled first CTB slot ready, and republished CultMesh dashboard."}