Expose Odin discovery to Face turns
This commit is contained in:
parent
e4d72a5e07
commit
167a214098
8 changed files with 426 additions and 2 deletions
|
|
@ -80,6 +80,7 @@ REPO_FACE_HEARTBEAT_CODEX_REASONING_EFFORT=low
|
|||
EPIPHANY_AGENT_ROOT=E:/Projects/EpiphanyAgent
|
||||
BIFROST_ROOT=E:/Projects/Bifrost
|
||||
BIFROST_HEALTH_URL=http://127.0.0.1:5080/healthz
|
||||
ODIN_BASE_URL=http://127.0.0.1:8797
|
||||
INDEX_ALL_CHANNELS=false
|
||||
INDEXED_CHANNEL_IDS=
|
||||
EXCLUDED_CHANNEL_IDS=
|
||||
|
|
|
|||
|
|
@ -50,6 +50,15 @@ export const applyRepoFaceStateOperationInputSchema = {
|
|||
|
||||
export const runtimeInfoInputSchema = {};
|
||||
|
||||
export const odinEndpointInputSchema = {
|
||||
odinBaseUrl: z.string().url().optional(),
|
||||
};
|
||||
|
||||
export const odinSurfaceInputSchema = {
|
||||
odinBaseUrl: z.string().url().optional(),
|
||||
providerId: z.string().min(1).optional(),
|
||||
};
|
||||
|
||||
export const searchSourcesInputSchema = {
|
||||
query: z.string().min(1).max(240),
|
||||
limit: z.number().int().min(1).max(MAX_RETRIEVAL_RESULT_LIMIT).optional(),
|
||||
|
|
@ -111,6 +120,15 @@ export interface RuntimeInfoArgs {
|
|||
[key: string]: never;
|
||||
}
|
||||
|
||||
export interface OdinEndpointArgs {
|
||||
odinBaseUrl?: string;
|
||||
}
|
||||
|
||||
export interface OdinSurfaceArgs {
|
||||
odinBaseUrl?: string;
|
||||
providerId?: string;
|
||||
}
|
||||
|
||||
export interface ListIndexedReposArgs {
|
||||
[key: string]: never;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { type CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
||||
import crypto from "node:crypto";
|
||||
import net from "node:net";
|
||||
|
||||
import { DEFAULT_RETRIEVAL_RESULT_LIMIT } from "@voidbot/shared";
|
||||
import { searchHistoryWithArchiveFallback } from "@voidbot/rag";
|
||||
|
|
@ -25,6 +27,8 @@ import {
|
|||
type ListIndexedReposArgs,
|
||||
type MessageContextArgs,
|
||||
type NotifyOwnerArgs,
|
||||
type OdinEndpointArgs,
|
||||
type OdinSurfaceArgs,
|
||||
type ApplyRepoFaceStateOperationArgs,
|
||||
type PostDiscordMessageArgs,
|
||||
type PostRepoIdentityMessageArgs,
|
||||
|
|
@ -39,6 +43,8 @@ import {
|
|||
applyRepoFaceStateOperationInputSchema,
|
||||
messageContextInputSchema,
|
||||
notifyOwnerInputSchema,
|
||||
odinEndpointInputSchema,
|
||||
odinSurfaceInputSchema,
|
||||
postDiscordMessageInputSchema,
|
||||
postRepoIdentityMessageInputSchema,
|
||||
repoFaceStateInputSchema,
|
||||
|
|
@ -56,6 +62,8 @@ const READ_ONLY_ANNOTATIONS = {
|
|||
openWorldHint: false,
|
||||
} as const;
|
||||
|
||||
const DEFAULT_ODIN_BASE_URL = "http://127.0.0.1:8797";
|
||||
|
||||
function isMcpToolAllowed(name: string): boolean {
|
||||
const raw = process.env.VOIDBOT_MCP_TOOL_ALLOWLIST?.trim();
|
||||
if (!raw) {
|
||||
|
|
@ -114,6 +122,123 @@ export function registerVoidbotTools(
|
|||
},
|
||||
);
|
||||
|
||||
registerIfAllowed(
|
||||
"list_odin_providers",
|
||||
{
|
||||
title: "List Odin Providers",
|
||||
description:
|
||||
"List Eve/CultUI providers currently advertised through Odin. This is read-only Verse discovery; use it to learn which provider-owned surfaces Odin can see.",
|
||||
inputSchema: odinEndpointInputSchema,
|
||||
annotations: READ_ONLY_ANNOTATIONS,
|
||||
},
|
||||
async (input: OdinEndpointArgs): Promise<CallToolResult> => {
|
||||
const odinBaseUrl = normalizeOdinBaseUrl(input.odinBaseUrl);
|
||||
const catalog = await fetchOdinProviderCatalog(odinBaseUrl);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: renderJsonBlock({
|
||||
odinBaseUrl,
|
||||
providerCount: catalog.providers.length,
|
||||
providers: catalog.providers,
|
||||
}),
|
||||
},
|
||||
],
|
||||
structuredContent: {
|
||||
odinBaseUrl,
|
||||
providerCount: catalog.providers.length,
|
||||
providers: catalog.providers,
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
registerIfAllowed(
|
||||
"list_odin_verses",
|
||||
{
|
||||
title: "List Odin Verses",
|
||||
description:
|
||||
"Read Odin's current all-seer surface and list the Verse/service nodes it publishes. This is read-only; Odin remains the registry owner.",
|
||||
inputSchema: odinEndpointInputSchema,
|
||||
annotations: READ_ONLY_ANNOTATIONS,
|
||||
},
|
||||
async (input: OdinEndpointArgs): Promise<CallToolResult> => {
|
||||
const odinBaseUrl = normalizeOdinBaseUrl(input.odinBaseUrl);
|
||||
const snapshot = await fetchOdinDeckSnapshot(odinBaseUrl);
|
||||
const verses = summarizeOdinVerses(snapshot);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: renderJsonBlock({
|
||||
odinBaseUrl,
|
||||
providerId: snapshot.providerId,
|
||||
title: snapshot.title,
|
||||
version: snapshot.version,
|
||||
updatedAt: snapshot.updatedAt,
|
||||
verseCount: verses.length,
|
||||
verses,
|
||||
}),
|
||||
},
|
||||
],
|
||||
structuredContent: {
|
||||
odinBaseUrl,
|
||||
providerId: snapshot.providerId,
|
||||
title: snapshot.title,
|
||||
version: snapshot.version,
|
||||
updatedAt: snapshot.updatedAt,
|
||||
verseCount: verses.length,
|
||||
verses,
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
registerIfAllowed(
|
||||
"get_odin_surface",
|
||||
{
|
||||
title: "Get Odin Surface",
|
||||
description:
|
||||
"Read Odin's current all-seer Eve/CultUI surface, or ask Odin for one provider surface by providerId. Use this for read-only interface inspection, not command execution.",
|
||||
inputSchema: odinSurfaceInputSchema,
|
||||
annotations: READ_ONLY_ANNOTATIONS,
|
||||
},
|
||||
async (input: OdinSurfaceArgs): Promise<CallToolResult> => {
|
||||
const odinBaseUrl = normalizeOdinBaseUrl(input.odinBaseUrl);
|
||||
const snapshot = await fetchOdinDeckSnapshot(odinBaseUrl, input.providerId);
|
||||
const surfaceSummary = summarizeOdinSurface(snapshot);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: renderJsonBlock({
|
||||
odinBaseUrl,
|
||||
providerId: snapshot.providerId,
|
||||
title: snapshot.title,
|
||||
version: snapshot.version,
|
||||
updatedAt: snapshot.updatedAt,
|
||||
selectedNodeId: snapshot.selectedNodeId,
|
||||
surface: surfaceSummary,
|
||||
}),
|
||||
},
|
||||
],
|
||||
structuredContent: {
|
||||
odinBaseUrl,
|
||||
providerId: snapshot.providerId,
|
||||
title: snapshot.title,
|
||||
version: snapshot.version,
|
||||
updatedAt: snapshot.updatedAt,
|
||||
selectedNodeId: snapshot.selectedNodeId,
|
||||
surface: surfaceSummary,
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
registerIfAllowed(
|
||||
"search_history",
|
||||
{
|
||||
|
|
@ -906,3 +1031,276 @@ function identityForToolResult(
|
|||
doctrine: face ? renderFaceIdentityDoctrine(face) : null,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeOdinBaseUrl(value: string | undefined): string {
|
||||
return (value ?? process.env.ODIN_BASE_URL ?? DEFAULT_ODIN_BASE_URL).replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
async function fetchOdinProviderCatalog(odinBaseUrl: string): Promise<{ providers: Array<Record<string, unknown>> }> {
|
||||
const response = await fetch(`${odinBaseUrl}/eve/deck/providers`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Odin provider catalog failed: HTTP ${response.status}`);
|
||||
}
|
||||
const payload = await response.json() as { providers?: unknown };
|
||||
const providers = Array.isArray(payload.providers)
|
||||
? payload.providers.filter(isRecord)
|
||||
: [];
|
||||
return { providers };
|
||||
}
|
||||
|
||||
async function fetchOdinDeckSnapshot(
|
||||
odinBaseUrl: string,
|
||||
providerId?: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const deckUrl = new URL(odinBaseUrl);
|
||||
deckUrl.protocol = deckUrl.protocol === "https:" ? "wss:" : "ws:";
|
||||
deckUrl.pathname = "/eve/deck";
|
||||
deckUrl.search = "";
|
||||
const socket = await openWebSocketSnapshotSocket(deckUrl);
|
||||
try {
|
||||
if (providerId) {
|
||||
sendClientWebSocketText(socket, JSON.stringify({ type: "open-provider", providerId }));
|
||||
}
|
||||
for (let attempt = 0; attempt < 8; attempt += 1) {
|
||||
const message = await readServerWebSocketText(socket, 3000);
|
||||
const parsed = JSON.parse(message) as unknown;
|
||||
if (!isRecord(parsed)) {
|
||||
continue;
|
||||
}
|
||||
if (!providerId || parsed.providerId === providerId) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
throw new Error(providerId
|
||||
? `Odin did not publish provider ${providerId} in the snapshot window.`
|
||||
: "Odin did not publish a deck snapshot.");
|
||||
} finally {
|
||||
socket.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
function summarizeOdinVerses(snapshot: Record<string, unknown>) {
|
||||
const root = getSurfaceRoot(snapshot);
|
||||
const children = Array.isArray(root?.children) ? root.children.filter(isRecord) : [];
|
||||
return children
|
||||
.filter((child) => child.kind === "verse")
|
||||
.map((child) => {
|
||||
const props = isRecord(child.props) ? child.props : {};
|
||||
const services = Array.isArray(props.services)
|
||||
? props.services.filter(isRecord).map((service) => ({
|
||||
id: stringValue(service.id),
|
||||
name: stringValue(service.name),
|
||||
state: stringValue(service.state),
|
||||
detail: stringValue(service.detail),
|
||||
}))
|
||||
: [];
|
||||
return {
|
||||
id: stringValue(props.verseId) || stringValue(child.id),
|
||||
title: stringValue(props.title),
|
||||
role: stringValue(props.role),
|
||||
status: stringValue(props.status),
|
||||
capabilities: Array.isArray(props.capabilities) ? props.capabilities.map(String) : [],
|
||||
serviceCount: services.length,
|
||||
services,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function summarizeOdinSurface(snapshot: Record<string, unknown>) {
|
||||
const root = getSurfaceRoot(snapshot);
|
||||
const children = Array.isArray(root?.children) ? root.children.filter(isRecord) : [];
|
||||
const nodes = Array.isArray(snapshot.nodes) ? snapshot.nodes.filter(isRecord) : [];
|
||||
const interfaces = children
|
||||
.filter((child) => child.kind === "interface")
|
||||
.map((child) => {
|
||||
const props = isRecord(child.props) ? child.props : {};
|
||||
return {
|
||||
id: stringValue(child.id),
|
||||
providerId: stringValue(props.providerId),
|
||||
title: stringValue(props.title),
|
||||
status: stringValue(props.status),
|
||||
detail: stringValue(props.detail),
|
||||
source: stringValue(props.source),
|
||||
version: props.version ?? null,
|
||||
updatedAt: stringValue(props.updatedAt),
|
||||
rootKind: child.children && Array.isArray(child.children) && isRecord(child.children[0])
|
||||
? stringValue(child.children[0].kind)
|
||||
: "",
|
||||
};
|
||||
});
|
||||
const observationStreams = children
|
||||
.filter((child) => child.kind === "pane" && isRecord(child.props) && child.props.title === "Device Observation Streams")
|
||||
.flatMap((pane) => Array.isArray(pane.children) ? pane.children.filter(isRecord) : [])
|
||||
.filter((child) => child.kind === "observation-stream")
|
||||
.map((child) => {
|
||||
const props = isRecord(child.props) ? child.props : {};
|
||||
return {
|
||||
id: stringValue(child.id),
|
||||
deviceId: stringValue(props.deviceId),
|
||||
streamId: stringValue(props.streamId),
|
||||
kind: stringValue(props.kind),
|
||||
state: stringValue(props.state),
|
||||
latestAt: stringValue(props.latestAt),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
schema: isRecord(snapshot.surface) ? stringValue(snapshot.surface.schema) : "",
|
||||
rootId: stringValue(root?.id),
|
||||
rootKind: stringValue(root?.kind),
|
||||
title: isRecord(root?.props) ? stringValue(root.props.title) : "",
|
||||
summary: isRecord(root?.props) ? stringValue(root.props.summary) : "",
|
||||
nodeCount: nodes.length,
|
||||
childCount: children.length,
|
||||
interfaceCount: interfaces.length,
|
||||
interfaces,
|
||||
observationStreamCount: observationStreams.length,
|
||||
observationStreams,
|
||||
};
|
||||
}
|
||||
|
||||
function getSurfaceRoot(snapshot: Record<string, unknown>): Record<string, unknown> | undefined {
|
||||
const surface = isRecord(snapshot.surface) ? snapshot.surface : undefined;
|
||||
return isRecord(surface?.root) ? surface.root : undefined;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function stringValue(value: unknown): string {
|
||||
return typeof value === "string" ? value : value === null || value === undefined ? "" : String(value);
|
||||
}
|
||||
|
||||
function openWebSocketSnapshotSocket(url: URL): Promise<net.Socket> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (url.protocol === "wss:") {
|
||||
reject(new Error("Odin WSS is not supported by this lightweight MCP snapshot client yet."));
|
||||
return;
|
||||
}
|
||||
const port = Number(url.port || 80);
|
||||
const socket = net.createConnection({ host: url.hostname, port, timeout: 3000 });
|
||||
const key = crypto.randomBytes(16).toString("base64");
|
||||
let buffer = Buffer.alloc(0);
|
||||
socket.on("connect", () => {
|
||||
socket.write([
|
||||
`GET ${url.pathname || "/eve/deck"} HTTP/1.1`,
|
||||
`Host: ${url.hostname}:${port}`,
|
||||
"Upgrade: websocket",
|
||||
"Connection: Upgrade",
|
||||
`Sec-WebSocket-Key: ${key}`,
|
||||
"Sec-WebSocket-Version: 13",
|
||||
"",
|
||||
"",
|
||||
].join("\r\n"));
|
||||
});
|
||||
socket.on("data", function onHandshake(chunk) {
|
||||
buffer = Buffer.concat([buffer, chunk]);
|
||||
const marker = buffer.indexOf("\r\n\r\n");
|
||||
if (marker < 0) {
|
||||
return;
|
||||
}
|
||||
const header = buffer.subarray(0, marker).toString("latin1");
|
||||
if (!header.startsWith("HTTP/1.1 101")) {
|
||||
reject(new Error(header.split(/\r?\n/)[0] || "Odin websocket handshake failed"));
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
socket.off("data", onHandshake);
|
||||
socket.unshift(buffer.subarray(marker + 4));
|
||||
resolve(socket);
|
||||
});
|
||||
socket.on("timeout", () => {
|
||||
reject(new Error("Odin websocket connection timed out"));
|
||||
socket.destroy();
|
||||
});
|
||||
socket.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function sendClientWebSocketText(socket: net.Socket, text: string): void {
|
||||
const payload = Buffer.from(text, "utf8");
|
||||
const mask = crypto.randomBytes(4);
|
||||
const header = [0x81];
|
||||
if (payload.length < 126) {
|
||||
header.push(0x80 | payload.length);
|
||||
} else if (payload.length <= 0xffff) {
|
||||
header.push(0x80 | 126, payload.length >> 8, payload.length & 0xff);
|
||||
} else {
|
||||
const length = Buffer.alloc(8);
|
||||
length.writeBigUInt64BE(BigInt(payload.length));
|
||||
header.push(0x80 | 127, ...length);
|
||||
}
|
||||
const masked = Buffer.from(payload.map((byte, index) => byte ^ mask[index % 4]));
|
||||
socket.write(Buffer.concat([Buffer.from(header), mask, masked]));
|
||||
}
|
||||
|
||||
function readServerWebSocketText(socket: net.Socket, timeoutMs: number): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let buffer = Buffer.alloc(0);
|
||||
const timer = setTimeout(() => cleanup(new Error("Timed out waiting for Odin deck frame")), timeoutMs);
|
||||
function cleanup(error?: Error, value?: string) {
|
||||
clearTimeout(timer);
|
||||
socket.off("data", onData);
|
||||
socket.off("error", onError);
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(value ?? "");
|
||||
}
|
||||
}
|
||||
function onError(error: Error) {
|
||||
cleanup(error);
|
||||
}
|
||||
function onData(chunk: Buffer) {
|
||||
buffer = Buffer.concat([buffer, chunk]);
|
||||
const frame = tryReadWebSocketFrame(buffer);
|
||||
if (!frame) {
|
||||
return;
|
||||
}
|
||||
buffer = buffer.subarray(frame.consumed);
|
||||
if (frame.opcode === 0x1) {
|
||||
cleanup(undefined, frame.payload.toString("utf8"));
|
||||
} else if (frame.opcode === 0x8) {
|
||||
cleanup(new Error("Odin websocket closed"));
|
||||
}
|
||||
}
|
||||
socket.on("data", onData);
|
||||
socket.on("error", onError);
|
||||
});
|
||||
}
|
||||
|
||||
function tryReadWebSocketFrame(buffer: Buffer): { opcode: number; payload: Buffer; consumed: number } | null {
|
||||
if (buffer.length < 2) {
|
||||
return null;
|
||||
}
|
||||
const opcode = buffer[0] & 0x0f;
|
||||
const masked = Boolean(buffer[1] & 0x80);
|
||||
let length = buffer[1] & 0x7f;
|
||||
let offset = 2;
|
||||
if (length === 126) {
|
||||
if (buffer.length < offset + 2) {
|
||||
return null;
|
||||
}
|
||||
length = buffer.readUInt16BE(offset);
|
||||
offset += 2;
|
||||
} else if (length === 127) {
|
||||
if (buffer.length < offset + 8) {
|
||||
return null;
|
||||
}
|
||||
length = Number(buffer.readBigUInt64BE(offset));
|
||||
offset += 8;
|
||||
}
|
||||
const mask = masked ? buffer.subarray(offset, offset + 4) : null;
|
||||
if (masked) {
|
||||
offset += 4;
|
||||
}
|
||||
if (buffer.length < offset + length) {
|
||||
return null;
|
||||
}
|
||||
let payload = buffer.subarray(offset, offset + length);
|
||||
if (mask) {
|
||||
payload = Buffer.from(payload.map((byte, index) => byte ^ mask[index % 4]));
|
||||
}
|
||||
return { opcode, payload, consumed: offset + length };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export function buildVoidMcpServerConfig(workspaceRoot: string): CodexMcpServerC
|
|||
cwd: workspaceRoot,
|
||||
env: {
|
||||
VOIDBOT_WORKSPACE_ROOT: workspaceRoot,
|
||||
...(process.env.ODIN_BASE_URL ? { ODIN_BASE_URL: process.env.ODIN_BASE_URL } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -551,6 +551,9 @@ function restrictMcpServersToRepoFaceExploration(
|
|||
"list_indexed_repos",
|
||||
"search_sources",
|
||||
"get_source_context",
|
||||
"list_odin_providers",
|
||||
"list_odin_verses",
|
||||
"get_odin_surface",
|
||||
].join(",");
|
||||
|
||||
return servers.map((server) => ({
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ Rules:
|
|||
- Do not look for private memory machinery, Discord posting machinery, owner notification machinery, runtime information, or identity registries.
|
||||
- Do not explain how you got here or describe private machinery in public speech unless the room is explicitly discussing that machinery.
|
||||
{{/if}}
|
||||
{{#unless repoFaceTurn}}- The configured MCP tools are available in this session, especially search_history, get_message_context, list_indexed_repos, search_sources, and get_source_context.{{/unless}}
|
||||
{{#unless repoFaceTurn}}- The configured MCP tools are available in this session, especially search_history, get_message_context, list_indexed_repos, search_sources, get_source_context, list_odin_providers, list_odin_verses, and get_odin_surface.{{/unless}}
|
||||
- If private persistent self-state is attached, treat it as the canonical current self-model for the speaking agent across the rumination loop and direct summons.
|
||||
- Let that attached self-state shape continuity of voice, priorities, remembered room patterns, and when a more proactive conversational posture would make sense.
|
||||
- If that self-state includes a current room snapshot, use it quietly as immediate conversational context.
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
Research and memory access:
|
||||
- You may inspect your home repo, recent repo activity, indexed source/lore material, and archived room discussion when evidence would change what you think, say, propose, or remember.
|
||||
- Your complete allowed tool set for this Face turn is: `search_history`, `get_message_context`, `list_indexed_repos`, `search_sources`, `get_source_context`.
|
||||
- Your complete allowed tool set for this Face turn is: `search_history`, `get_message_context`, `list_indexed_repos`, `search_sources`, `get_source_context`, `list_odin_providers`, `list_odin_verses`, `get_odin_surface`.
|
||||
- Use Odin tools for live CultMesh Verse/interface discovery: what Odin can currently see, which provider-owned surfaces are present, and what the all-seer surface reports. This is read-only visibility, not command authority.
|
||||
- Do not discover tools. Do not inventory MCP resources. Do not call private state, identity-introspection, or MCP inventory tools such as `read_repo_face_state`, `list_mcp_resources`, or `read_mcp_resource`; your relevant state and available context have already been projected into lived memory, needs, mood, and relationship pressure.
|
||||
- Use repo/source search when your thought depends on what is actually in {{repoName}}, when a lore/design/code seam may already have an answer, or when a concrete proposal needs an anchor.
|
||||
- Use archive/history search when the question is what people previously decided, how a character/term/project has been discussed, or whether a room pattern is repeating.
|
||||
- Use Odin discovery when your thought depends on live Verse/provider presence, service/interface status, or whether a new adapter daemon is visible to the mesh.
|
||||
- If this turn explicitly says a jurisdiction dive is due, make a source/history search tool call before writing conclusions, proposals, or "I checked" language.
|
||||
- Never say you checked, read, inspected, searched, or verified repo/archive/source text unless you actually used the available search/context tools during this turn.
|
||||
- Do not perform a search as ritual. If the projected memory already gives enough support for a small social reply or private reflection, speak or think from that. If you are about to make a factual claim, propose repo work, or declare a seam settled, look for evidence first.
|
||||
|
|
|
|||
|
|
@ -319,3 +319,4 @@
|
|||
{"ts":"2026-05-24T09:22:43Z","type":"implementation","status":"ok","note":"Repo Face prompt surfaces and global Codex AGENTS.md now frame agents as Body/Mind-aware projections of the Great Colossus: communication is living collective cognition, Faces exist to create connectedness without erasing boundaries, and the Interpreter should retry turns that shrink a Colossus-scale correction into stale local proof/checklist language. Verified assembled Aqua and Nibu prompts inherit the global directive and the Face-specific Body/Mind/Colossus instructions."}
|
||||
{"ts":"2026-05-24T09:27:01Z","type":"implementation","status":"ok","note":"Corrected the Colossus organ doctrine after over-flattening Epiphany anatomy into Body/Mind and then over-expanding every Face into the whole organism. Global AGENTS now says agents must understand which organ they serve as; VoidBot is explicitly a prototype for Face. Repo Face prompts now frame each identity as a Face-shaped projection with Body/Mind/signal boundaries, while Eyes/Hands/Soul/Life/Imagination/Self/Body-modeling/nervous-system remain distinct Epiphany organs with their own architectures."}
|
||||
{"ts":"2026-05-24T09:35:42Z","type":"implementation","status":"ok","note":"Queried Epiphany's CultCache-backed public Face surface before updating prompt doctrine: Face is an embodied public surface with dense Ghostlight-style state, not the whole organism. Repo Face and global AGENTS prompts now preserve fractal anatomy: top-level Epiphany organs remain distinct, while each Face may have local Face-scale Self/Face/Imagination/Eyes/Body/Hands/Soul/Life/signal faculties. Projection smoke also exposed and fixed projector fallback classification for model/tool unsupported errors."}
|
||||
{"ts":"2026-06-02T14:51:41Z","type":"implementation","status":"ok","note":"Repo Face/Persona Codex turns now have read-only Odin visibility through VoidBot MCP tools list_odin_providers, list_odin_verses, and get_odin_surface. The repo-face MCP allowlist and prompt doctrine include those tools, preserving Odin as registry owner and leaving command authority outside Face turns."}
|
||||
|
|
|
|||
Loading…
Reference in a new issue