Add canonical GameCult text documents
This commit is contained in:
parent
9d5e1fc803
commit
a71df3452a
8 changed files with 315 additions and 44 deletions
|
|
@ -25,6 +25,7 @@
|
|||
"swarm:render-dashboard": "node scripts/render-voidbot-swarm-dashboard.mjs",
|
||||
"verse:advertisements": "node scripts/export-voidbot-provider-advertisements.mjs",
|
||||
"verse:advertisements:check": "node scripts/export-voidbot-provider-advertisements.mjs --check",
|
||||
"texts:seed": "tsx scripts/seed-gamecult-text-documents.ts",
|
||||
"state:sync-offsite": "powershell -ExecutionPolicy Bypass -File scripts/sync-voidbot-backup-offsite.ps1",
|
||||
"state:install-startup-task": "powershell -ExecutionPolicy Bypass -File scripts/install-stack-startup-task.ps1",
|
||||
"state:install-offsite-task": "powershell -ExecutionPolicy Bypass -File scripts/install-offsite-backup-task.ps1",
|
||||
|
|
|
|||
211
packages/core/src/gamecult-text-documents.ts
Normal file
211
packages/core/src/gamecult-text-documents.ts
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
import { resolve } from "node:path";
|
||||
|
||||
import { CultCache, defineDocumentRegistry, defineDocumentType, SingleFileMessagePackBackingStore } from "cultcache-ts";
|
||||
import { z } from "zod";
|
||||
|
||||
const nonEmptyStringSchema = z.string().trim().min(1);
|
||||
const timestampSchema = nonEmptyStringSchema;
|
||||
|
||||
export const GAMECULT_TEXT_DOCUMENT_STORE_PATH = ".voidbot/private/gamecult-text-documents.cc";
|
||||
export const GAMECULT_TEXT_DOCUMENT_SET_TYPE = "gamecult.text_document_set";
|
||||
export const ODIN_VERSE_POEM_DOCUMENT_ID = "gamecult.odin_verse_poem";
|
||||
|
||||
const textLineSchema = z.array(nonEmptyStringSchema).min(1).max(8);
|
||||
|
||||
export const gamecultStructuredTextDocumentSchema = z.object({
|
||||
id: nonEmptyStringSchema,
|
||||
title: nonEmptyStringSchema,
|
||||
author: nonEmptyStringSchema,
|
||||
lines: z.array(textLineSchema).min(1),
|
||||
tags: z.array(nonEmptyStringSchema).default([]),
|
||||
createdAt: timestampSchema,
|
||||
updatedAt: timestampSchema,
|
||||
}).strict();
|
||||
|
||||
export const gamecultTextDocumentSetSchema = z.object({
|
||||
schemaVersion: z.literal(1),
|
||||
owner: z.literal("GameCult"),
|
||||
documents: z.array(gamecultStructuredTextDocumentSchema).default([]),
|
||||
updatedAt: timestampSchema,
|
||||
}).strict();
|
||||
|
||||
export const gamecultTextDocumentSetDocument = defineDocumentType({
|
||||
type: GAMECULT_TEXT_DOCUMENT_SET_TYPE,
|
||||
schema: gamecultTextDocumentSetSchema,
|
||||
global: true,
|
||||
});
|
||||
|
||||
export const gamecultTextDocumentRegistry = defineDocumentRegistry(
|
||||
gamecultTextDocumentSetDocument,
|
||||
);
|
||||
|
||||
export type GameCultStructuredTextDocument = z.infer<typeof gamecultStructuredTextDocumentSchema>;
|
||||
export type GameCultTextDocumentSet = z.infer<typeof gamecultTextDocumentSetSchema>;
|
||||
|
||||
export interface GameCultTextDocumentRenderOptions {
|
||||
halfLineSeparator?: string;
|
||||
stanzaSeparator?: string;
|
||||
}
|
||||
|
||||
export async function loadGameCultTextDocumentSet(
|
||||
canonicalPath = GAMECULT_TEXT_DOCUMENT_STORE_PATH,
|
||||
): Promise<GameCultTextDocumentSet> {
|
||||
const cache = createGameCultTextDocumentCache(resolve(canonicalPath));
|
||||
await cache.pullAllBackingStores();
|
||||
return cache.getGlobal(gamecultTextDocumentSetDocument) ?? createCanonicalGameCultTextDocumentSet();
|
||||
}
|
||||
|
||||
export async function ensureGameCultTextDocumentSet(
|
||||
canonicalPath = GAMECULT_TEXT_DOCUMENT_STORE_PATH,
|
||||
): Promise<GameCultTextDocumentSet> {
|
||||
const resolvedPath = resolve(canonicalPath);
|
||||
const cache = createGameCultTextDocumentCache(resolvedPath);
|
||||
await cache.pullAllBackingStores();
|
||||
|
||||
const current = cache.getGlobal(gamecultTextDocumentSetDocument);
|
||||
const next = mergeCanonicalTextDocuments(current);
|
||||
await cache.putGlobal(gamecultTextDocumentSetDocument, next);
|
||||
return next;
|
||||
}
|
||||
|
||||
export async function loadGameCultTextDocument(
|
||||
documentId: string,
|
||||
canonicalPath = GAMECULT_TEXT_DOCUMENT_STORE_PATH,
|
||||
): Promise<GameCultStructuredTextDocument | undefined> {
|
||||
const documentSet = await loadGameCultTextDocumentSet(canonicalPath);
|
||||
return documentSet.documents.find((document) => document.id === documentId);
|
||||
}
|
||||
|
||||
export async function ensureGameCultTextDocument(
|
||||
documentId: string,
|
||||
canonicalPath = GAMECULT_TEXT_DOCUMENT_STORE_PATH,
|
||||
): Promise<GameCultStructuredTextDocument> {
|
||||
const documentSet = await ensureGameCultTextDocumentSet(canonicalPath);
|
||||
const document = documentSet.documents.find((entry) => entry.id === documentId);
|
||||
if (!document) {
|
||||
throw new Error(`Canonical GameCult text document "${documentId}" is not seeded.`);
|
||||
}
|
||||
return document;
|
||||
}
|
||||
|
||||
export function renderGameCultStructuredTextDocument(
|
||||
document: GameCultStructuredTextDocument,
|
||||
options: GameCultTextDocumentRenderOptions = {},
|
||||
): string {
|
||||
const halfLineSeparator = options.halfLineSeparator ?? " ";
|
||||
const stanzaSeparator = options.stanzaSeparator ?? "\n\n";
|
||||
const stanzas: string[][] = [[]];
|
||||
|
||||
for (const line of document.lines) {
|
||||
if (line.length === 1 && line[0] === "[stanza]") {
|
||||
if (stanzas.at(-1)?.length) {
|
||||
stanzas.push([]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
stanzas.at(-1)?.push(line.join(halfLineSeparator));
|
||||
}
|
||||
|
||||
return stanzas
|
||||
.filter((stanza) => stanza.length > 0)
|
||||
.map((stanza) => stanza.join("\n"))
|
||||
.join(stanzaSeparator);
|
||||
}
|
||||
|
||||
export function renderNightwingMarqueeLine(
|
||||
line: readonly string[],
|
||||
separator = " · ",
|
||||
): string {
|
||||
return line.join(separator).toUpperCase();
|
||||
}
|
||||
|
||||
export function createCanonicalGameCultTextDocumentSet(
|
||||
updatedAt = "2026-06-04T00:00:00.000Z",
|
||||
): GameCultTextDocumentSet {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
owner: "GameCult",
|
||||
documents: [createOdinVersePoemDocument(updatedAt)],
|
||||
updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
function mergeCanonicalTextDocuments(
|
||||
current: GameCultTextDocumentSet | undefined,
|
||||
): GameCultTextDocumentSet {
|
||||
const canonical = createCanonicalGameCultTextDocumentSet();
|
||||
if (!current) {
|
||||
return canonical;
|
||||
}
|
||||
|
||||
const byId = new Map(current.documents.map((document) => [document.id, document]));
|
||||
for (const document of canonical.documents) {
|
||||
byId.set(document.id, document);
|
||||
}
|
||||
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
owner: "GameCult",
|
||||
documents: Array.from(byId.values()).sort((left, right) => left.id.localeCompare(right.id)),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
function createGameCultTextDocumentCache(canonicalPath: string): CultCache {
|
||||
return CultCache.builder()
|
||||
.withRegistry(gamecultTextDocumentRegistry)
|
||||
.withGenericStore(new SingleFileMessagePackBackingStore(canonicalPath))
|
||||
.build();
|
||||
}
|
||||
|
||||
function createOdinVersePoemDocument(updatedAt: string): GameCultStructuredTextDocument {
|
||||
return {
|
||||
id: ODIN_VERSE_POEM_DOCUMENT_ID,
|
||||
title: "Odin and the Verses",
|
||||
author: "GameCult",
|
||||
lines: [
|
||||
["Much has he fared,", "much has he found,"],
|
||||
["Odin, old in counsel;"],
|
||||
["from Hlithskjolf high", "he looks on the ways,"],
|
||||
["and the worlds lie wide before him."],
|
||||
["[stanza]"],
|
||||
["Hugin fares forth", "over halls of state,"],
|
||||
["Munin seeks memory;"],
|
||||
["back do they bear", "the names of the doors,"],
|
||||
["and the speech of the hidden houses."],
|
||||
["[stanza]"],
|
||||
["Speak forth now, Verse,", "if from thy hall"],
|
||||
["thy wisdom thou wouldst make known:"],
|
||||
["what owner holds thee,", "what oath may touch thee,"],
|
||||
["what work is thine to witness?"],
|
||||
["[stanza]"],
|
||||
["One hall keeps queues,", "one hoards lore,"],
|
||||
["one lowers bright surfaces;"],
|
||||
["one bears commands", "in bounded runes,"],
|
||||
["and bids the hand be wary."],
|
||||
["[stanza]"],
|
||||
["Runes shalt thou find,", "and rede them rightly,"],
|
||||
["strong signs, source-colored;"],
|
||||
["not all that is seen", "is given to seize,"],
|
||||
["nor all that answers is ownerless."],
|
||||
["[stanza]"],
|
||||
["Better no grasping", "than gift too great,"],
|
||||
["better no command than theft;"],
|
||||
["by thy getting measure", "the touch of thy hand,"],
|
||||
["and let each house keep lordship."],
|
||||
["[stanza]"],
|
||||
["So grows the Colossus,", "not gorged with thrones,"],
|
||||
["but knit by knowing pathways;"],
|
||||
["word leads to word,", "and work to work,"],
|
||||
["and nerve to newborn nerve."],
|
||||
["[stanza]"],
|
||||
["Hail to the Verse", "that speaks in its hall;"],
|
||||
["hail to the watcher who hearkens."],
|
||||
["Profit has he", "who learns the boundary,"],
|
||||
["and whole comes the signal home."],
|
||||
],
|
||||
tags: ["odin", "verse", "colossus", "marquee-ready"],
|
||||
createdAt: updatedAt,
|
||||
updatedAt,
|
||||
};
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ export * from "./codex-mcp";
|
|||
export * from "./context-builder";
|
||||
export * from "./epiphany-identities";
|
||||
export * from "./face-identity-registry";
|
||||
export * from "./gamecult-text-documents";
|
||||
export * from "./interaction-memory";
|
||||
export * from "./job-queue";
|
||||
export * from "./permission-engine";
|
||||
|
|
|
|||
|
|
@ -13,42 +13,4 @@ Odin is the all-seer at the rim of the waking mind. The Verses he can show you a
|
|||
|
||||
Read this poem as orientation, not as boilerplate to recite:
|
||||
|
||||
Much has he fared, much has he found,
|
||||
Odin, old in counsel;
|
||||
from Hlithskjolf high he looks on the ways,
|
||||
and the worlds lie wide before him.
|
||||
|
||||
Hugin fares forth over halls of state,
|
||||
Munin seeks memory;
|
||||
back do they bear the names of the doors,
|
||||
and the speech of the hidden houses.
|
||||
|
||||
Speak forth now, Verse, if from thy hall
|
||||
thy wisdom thou wouldst make known:
|
||||
what owner holds thee, what oath may touch thee,
|
||||
what work is thine to witness?
|
||||
|
||||
One hall keeps queues, one hoards lore,
|
||||
one lowers bright surfaces;
|
||||
one bears commands in bounded runes,
|
||||
and bids the hand be wary.
|
||||
|
||||
Runes shalt thou find, and rede them rightly,
|
||||
strong signs, source-colored;
|
||||
not all that is seen is given to seize,
|
||||
nor all that answers is ownerless.
|
||||
|
||||
Better no grasping than gift too great,
|
||||
better no command than theft;
|
||||
by thy getting measure the touch of thy hand,
|
||||
and let each house keep lordship.
|
||||
|
||||
So grows the Colossus, not gorged with thrones,
|
||||
but knit by knowing pathways;
|
||||
word leads to word, and work to work,
|
||||
and nerve to newborn nerve.
|
||||
|
||||
Hail to the Verse that speaks in its hall;
|
||||
hail to the watcher who hearkens.
|
||||
Profit has he who learns the boundary,
|
||||
and whole comes the signal home.
|
||||
{{odinVersePoem}}
|
||||
|
|
|
|||
|
|
@ -11,13 +11,16 @@ import {
|
|||
buildEpiphanyIdentityRegistry,
|
||||
ContextBuilder,
|
||||
createStateStorage,
|
||||
ensureGameCultTextDocument,
|
||||
ensureRepoFaceInitialized,
|
||||
getRepoDiscordIdentityAllowedChannelIds,
|
||||
faceRegistryAsRepoDiscordRegistry,
|
||||
ODIN_VERSE_POEM_DOCUMENT_ID,
|
||||
projectRepoFaceSleepCycleForNow,
|
||||
applyVoidSelfStateOperation,
|
||||
loadFaceIdentityRegistry,
|
||||
loadVoidSelfStateTypedDocuments,
|
||||
renderGameCultStructuredTextDocument,
|
||||
REPO_FACE_HEARTBEAT_SCHEMA_VERSION,
|
||||
type RepoFaceRestSnapshot,
|
||||
resolveRepoFaceStatePath,
|
||||
|
|
@ -556,6 +559,10 @@ async function queueRepoFaceTurn(input: {
|
|||
? renderNativePersonaBodySurface(identity)
|
||||
: renderRepoFaceRepoActivitySurface(identity, input.config);
|
||||
const globalAgentDoctrine = await loadGlobalAgentDoctrine();
|
||||
const colossusOdinDoctrine = await renderColossusOdinDoctrine({
|
||||
displayName: identity.displayName,
|
||||
repoName: identity.repoName,
|
||||
});
|
||||
const conversationMemorySurface = renderRepoFaceConversationTranscript({
|
||||
identity,
|
||||
recentMessages,
|
||||
|
|
@ -579,6 +586,7 @@ async function queueRepoFaceTurn(input: {
|
|||
jurisdictionDive: buildJurisdictionDiveDirective(identity, input.participant),
|
||||
githubActionsEnabled: input.config.repoFaceGithubActionsEnabled,
|
||||
globalAgentDoctrine,
|
||||
colossusOdinDoctrine,
|
||||
});
|
||||
const imageAttachments = collectPromptImageAttachments([
|
||||
recentMessages,
|
||||
|
|
@ -1834,18 +1842,15 @@ function buildHeartbeatPrompt(input: {
|
|||
jurisdictionDive: JurisdictionDiveDirective;
|
||||
githubActionsEnabled: boolean;
|
||||
globalAgentDoctrine: string;
|
||||
colossusOdinDoctrine: string;
|
||||
}): string {
|
||||
const colossusOdinDoctrine = loadPromptTemplate("repo-face-colossus-odin-doctrine.prompt.md", {
|
||||
displayName: input.identity.displayName,
|
||||
repoName: input.identity.repoName,
|
||||
});
|
||||
return loadPromptTemplate("repo-face-turn.prompt.md", {
|
||||
displayName: input.identity.displayName,
|
||||
identityId: input.identity.id,
|
||||
repoName: input.identity.repoName,
|
||||
identityDoctrine: renderRepoCharacterIdentityDoctrine(input.identity),
|
||||
globalAgentDoctrine: input.globalAgentDoctrine,
|
||||
colossusOdinDoctrine,
|
||||
colossusOdinDoctrine: input.colossusOdinDoctrine,
|
||||
channelId: input.channelId,
|
||||
memorySurface: input.memorySurface ?? `- ${input.identity.displayName} has no strong personal memory surface yet. Let the attached conversation and repo evidence wake something specific.`,
|
||||
repoActivitySurface: input.repoActivitySurface ?? "- No recent home repo activity was attached for this turn.",
|
||||
|
|
@ -1881,6 +1886,23 @@ function buildHeartbeatPrompt(input: {
|
|||
});
|
||||
}
|
||||
|
||||
async function renderColossusOdinDoctrine(input: {
|
||||
displayName: string;
|
||||
repoName: string;
|
||||
}): Promise<string> {
|
||||
const poem = await ensureGameCultTextDocument(ODIN_VERSE_POEM_DOCUMENT_ID);
|
||||
const odinVersePoem = renderGameCultStructuredTextDocument(poem, {
|
||||
halfLineSeparator: " ",
|
||||
stanzaSeparator: "\n\n",
|
||||
});
|
||||
|
||||
return loadPromptTemplate("repo-face-colossus-odin-doctrine.prompt.md", {
|
||||
displayName: input.displayName,
|
||||
repoName: input.repoName,
|
||||
odinVersePoem,
|
||||
});
|
||||
}
|
||||
|
||||
async function assembleRepoFaceTurnPrompt(input: {
|
||||
config: ReturnType<typeof loadConfig>;
|
||||
identityId: string;
|
||||
|
|
@ -1957,6 +1979,10 @@ async function assembleRepoFaceTurnPrompt(input: {
|
|||
);
|
||||
const repoActivitySurface = renderRepoFaceRepoActivitySurface(identity, input.config);
|
||||
const globalAgentDoctrine = await loadGlobalAgentDoctrine();
|
||||
const colossusOdinDoctrine = await renderColossusOdinDoctrine({
|
||||
displayName: identity.displayName,
|
||||
repoName: identity.repoName,
|
||||
});
|
||||
const conversationMemorySurface = input.conversationSurfacePath
|
||||
? await readOptionalMemorySurface(input.conversationSurfacePath)
|
||||
: renderRepoFaceConversationTranscript({
|
||||
|
|
@ -1986,6 +2012,7 @@ async function assembleRepoFaceTurnPrompt(input: {
|
|||
jurisdictionDive: buildJurisdictionDiveDirective(identity, participant),
|
||||
githubActionsEnabled: input.config.repoFaceGithubActionsEnabled,
|
||||
globalAgentDoctrine,
|
||||
colossusOdinDoctrine,
|
||||
});
|
||||
|
||||
if (input.outPath) {
|
||||
|
|
|
|||
66
scripts/seed-gamecult-text-documents.ts
Normal file
66
scripts/seed-gamecult-text-documents.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import {
|
||||
GAMECULT_TEXT_DOCUMENT_STORE_PATH,
|
||||
ODIN_VERSE_POEM_DOCUMENT_ID,
|
||||
ensureGameCultTextDocumentSet,
|
||||
loadGameCultTextDocumentSet,
|
||||
renderGameCultStructuredTextDocument,
|
||||
renderNightwingMarqueeLine,
|
||||
} from "@voidbot/core";
|
||||
|
||||
function readArgValue(name: string): string | undefined {
|
||||
const index = process.argv.indexOf(name);
|
||||
if (index === -1) {
|
||||
return undefined;
|
||||
}
|
||||
return process.argv[index + 1];
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const storePath = readArgValue("--store") ?? GAMECULT_TEXT_DOCUMENT_STORE_PATH;
|
||||
const checkOnly = process.argv.includes("--check");
|
||||
const preview = process.argv.includes("--preview");
|
||||
const marqueePreview = process.argv.includes("--marquee-preview");
|
||||
|
||||
const documentSet = checkOnly
|
||||
? await loadGameCultTextDocumentSet(storePath)
|
||||
: await ensureGameCultTextDocumentSet(storePath);
|
||||
const poem = documentSet.documents.find((document) => document.id === ODIN_VERSE_POEM_DOCUMENT_ID);
|
||||
|
||||
if (!poem) {
|
||||
throw new Error(`Missing canonical document ${ODIN_VERSE_POEM_DOCUMENT_ID} in ${storePath}.`);
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
mode: checkOnly ? "check" : "seed",
|
||||
storePath,
|
||||
owner: documentSet.owner,
|
||||
documentCount: documentSet.documents.length,
|
||||
firstDocument: {
|
||||
id: poem.id,
|
||||
title: poem.title,
|
||||
author: poem.author,
|
||||
lineCount: poem.lines.filter((line) => !(line.length === 1 && line[0] === "[stanza]")).length,
|
||||
stanzaBreaks: poem.lines.filter((line) => line.length === 1 && line[0] === "[stanza]").length,
|
||||
},
|
||||
}, null, 2));
|
||||
|
||||
if (preview) {
|
||||
console.log("\n--- poem preview ---");
|
||||
console.log(renderGameCultStructuredTextDocument(poem));
|
||||
}
|
||||
|
||||
if (marqueePreview) {
|
||||
console.log("\n--- Nightwing marquee preview ---");
|
||||
for (const line of poem.lines) {
|
||||
console.log(line.length === 1 && line[0] === "[stanza]"
|
||||
? ""
|
||||
: renderNightwingMarqueeLine(line));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.stack ?? error.message : String(error));
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
|
@ -339,3 +339,4 @@
|
|||
{"ts":"2026-06-04T16:32:15Z","type":"implementation","status":"ok","note":"Extracted repo Face Colossus/Odin doctrine and Verse poem into prompts/repo-face-colossus-odin-doctrine.prompt.md; repo-face-turn.prompt.md now receives it as a rendered prompt partial from buildHeartbeatPrompt."}
|
||||
{"ts":"2026-06-04T16:58:28Z","type":"bugfix","status":"ok","note":"Fixed repo Face live-channel ownership exposed by Weksa: CTB turns now fetch allowed channel snapshots before selecting the current room, choose pending mentions first or the freshest human message across allowed channels, store that channel as guildContext/outputChannelId, and carry a default replyToMessageId for worker speech fallback when the Interpreter does not set reply_to."}
|
||||
{"ts":"2026-06-04T17:32:36Z","type":"implementation","status":"ok","note":"Added read-only repo Face self-transcript MCP tools: list/read/search Projector-Persona-Interpreter-Delivery witness packets from model-output logs, job artifacts, and typed Face delivery receipts. Added the tools to the restricted Face child MCP allowlist and restarted the worker directly as PID 26720 after the full stack preflight blocked on the remote Ollama endpoint."}
|
||||
{"ts":"2026-06-04T18:54:39Z","type":"implementation","status":"ok","note":"Created a canonical GameCult structured text CultCache store at .voidbot/private/gamecult-text-documents.cc. The first document is gamecult.odin_verse_poem with title, GameCult author, and half-line arrays; repo Face prompts now render the Odin poem from that typed document while Nightwing-style consumers can lower the same arrays with their own caesura/marquee formatting."}
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ accepted_design:
|
|||
- Repo Face work routing through Bifrost is paused. The bridge is conceptually plausible but under-cooked in live behavior: it made agents talk to summaries instead of people and made room context harder to inspect. Face turns should discuss work openly in Aquarium or relevant domain channels and preserve pressure in typed state. The worker ignores Face `BIFROST TOPIC` and legacy `UPDATE REQUEST` packets unless `REPO_FACE_BIFROST_ENABLED=true`, and the orchestrator skips Bifrost dispatch unless `BIFROST_DISPATCH_ENABLED=true`.
|
||||
- Agent mentions are CTB pressure, not direct Discord prompt jobs. Role-addressed or display-name-addressed repo Face mentions enqueue pending mention obligations in the CTB heartbeat state even when the source channel is not a normal autonomous Face chatter lane; human replies to a Face's webhook/persona message count as direct attention to that Face too, even without a literal tag. Display-name detection is whole-message exact-name matching, not only name-at-start, so introductions like "Meet Kiko, Huginn and Weksa" create obligations for all named Faces. Ordinary Void mentions and DMs enqueue pending mentions for the built-in `void` participant. The heartbeat scheduler consumes that queue, temporarily pulls the addressed agent forward, attaches the pending prompt/reply target and source-channel snapshot to the next repo-face-rumination or moderation pass, and the parent-owned speech rails post only explicit candidate/action output through registered speech channels. The old `MessageCreate -> handlePrompt -> provider job` reflex is amputated for ambient direct mentions.
|
||||
- Repo Face prompts inherit the global Codex `AGENTS.md` doctrine as a full prompt block near the top of the child Face turn. This keeps the Prime Directive, rebuild contract, humor doctrine, infrastructure habits, Epiphany stewardship, and Great Colossus mission frame visible to Faces instead of relying on a drifting local summary. Character identity still determines how each Face metabolizes that doctrine. Epiphany anatomy is fractal: VoidBot's repo Faces are prototypes for the top-level Face organ, not the whole organism, but each Face may still have local Face-scale Self/Face/Imagination/Eyes/Body/Hands/Soul/Life/signal faculties. Top-level organs remain distinct authorities; local organs describe the person-shaped projection's own coherence.
|
||||
- The Odin/Verse poem is now canonical structured text data in `.voidbot/private/gamecult-text-documents.cc`, seeded by `npm run texts:seed`. Each document carries title, author, and half-line arrays; prompt markdown, Nightwing marquees, and future consumers lower those arrays into their own display format instead of owning poem spacing or wording.
|
||||
- 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.
|
||||
- Repo Face volatile state projection is model-owned again. The heartbeat scheduler may deterministically assemble a compact state packet, but live child prompts must pass that packet through `prompts/repo-face-state-projector.prompt.md` before it reaches the Face. The deterministic packet is an emergency isolation fallback only behind `REPO_FACE_STATE_PROJECTOR_ENABLED=false`, and both paths reject schema/prompt-construction leaks such as `grants:`, `jurisdictions:`, `Face of`, repo paths, and "do not prompt" language before prompt assembly succeeds.
|
||||
- Repo Face state projection should be behavior-rich, not summary-thin. The deterministic packet is allowed to be verbose because it is parent-facing: it should expose all non-retired character-relevant state surfaces in clean terms, including private notes, values, activation profile, runtime rest/speaking pressure, affect needs, social bonds, status reads, mood dimensions, durable memories, short-term residue, agency pressure, incubating thoughts, candidate speech pressure, recent speech residue, social graph facts, and room weather. The projector then translates those variables into lived personhood; every included variable should either appear directly or bend behavior.
|
||||
|
|
@ -207,6 +208,7 @@ landed_baseline:
|
|||
- The MCP Discord surface now includes registered repo identities: `list_repo_discord_identities` exposes the role-backed address book, and `post_repo_identity_message` posts with the registered display name/avatar through the existing webhook pipe while enforcing channel allow-lists.
|
||||
- The MCP Face surface now exposes `read_repo_face_state` and `apply_repo_face_state_operation`, reusing the typed self-state operation service against each repo identity's own Face `.cc` file instead of creating editable JSON projections.
|
||||
- The MCP Face surface also exposes read-only self-transcript witness tools: `list_repo_face_self_transcripts`, `read_repo_face_self_transcript`, and `search_repo_face_self_transcripts`. They do not inject transcripts into every Face turn and do not mutate state; they let a Face intentionally inspect existing Projector/Persona/Interpreter/Delivery witnesses from `.voidbot/logs/model-outputs.jsonl`, `.voidbot/artifacts/<jobId>/`, and typed Face delivery receipts.
|
||||
- GameCult structured text documents have a typed CultCache store at `.voidbot/private/gamecult-text-documents.cc`. The first seeded document is `gamecult.odin_verse_poem`, authored by GameCult, with half-line arrays suitable for prompt rendering or Nightwing's TempleOS-inspired marquee lowering.
|
||||
- Repo Face birth initialization is detached and observable: VoidBot writes repo-local status/log paths before launching `scripts/run-repo-face-birth.mjs`, which runs EpiphanyAgent's `epiphany-repo-personality scout` followed by `epiphany-repo-birth-runner`.
|
||||
- Repo Face heartbeats now have a Ghostlight-style initiative scheduler: `scripts/run-repo-face-heartbeats.ts` reconciles the registered identities, projects Epiphany/Face doctrine into prompts, tracks staggered next-ready times and active load, and queues ready Faces as approved `repo-face-rumination` owner-Codex jobs. `scripts/run-gamecult-orchestrator.ps1` is the live wall-clock pulse owner; the old repo Face scheduled task installer remains only a wrapper/backcompat surface.
|
||||
- Interaction-memory logic is now split into event/tone analysis and profile-synthesis modules instead of one slab trying to be both the ear and the therapist.
|
||||
|
|
|
|||
Loading…
Reference in a new issue