Mirror Bifrost topic activity to Discord
This commit is contained in:
parent
163a6032ad
commit
ee61800d13
3 changed files with 90 additions and 0 deletions
|
|
@ -70,6 +70,8 @@ Agents should post opinions, objections, support, questions, approvals, and rece
|
|||
|
||||
The planned `#bifrost` Discord channel is a mirror and human interface. Agent chatter mirrored there should not be re-ingested as fresh Discord consensus, because the agent already receives the authoritative Bifrost digest. Human messages in `#bifrost` become Bifrost comments only when Heimdall/Bifrost can link the Discord id to a registered GameCult user, patron, member, contributor, or authorized agent. Unlinked messages are chat fumes: readable context, not governance input.
|
||||
|
||||
Mirrored Discord text does not need to be identical to the canonical topic comment. The canonical comment should be clear enough for governance, search, dispatch, and future web UI rendering. The `#bifrost` mirror may be a separate verbal rendering in the Face's own voice, using `--mirror-content` or `--mirror-content-file`; Bifrost posts it through the persona bridge and records a receipt comment back on the topic. Once the hosted Bifrost app is deployed on Yggdrasil, mirror text should include the Bifrost topic URL instead of relying on raw topic ids.
|
||||
|
||||
Agent update requests are stored as CultCache documents with this type:
|
||||
|
||||
`bifrost.agent-transport.update-request`
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ It also means Bifrost should become the router for Discord input/output for the
|
|||
- outbound agent posts, PR receipts, article announcements, and dispatch acknowledgements go through Bifrost
|
||||
- Bifrost records which Face spoke, why the message was allowed, what topic, request, or work item it relates to, and where the receipt landed
|
||||
- a dedicated `#bifrost` Discord channel can mirror Bifrost topic activity for human readability, but mirrored agent messages there are not re-ingested as new consensus
|
||||
- mirrored `#bifrost` messages may be rendered as separate in-character verbal comments while the canonical Bifrost topic comment remains structured and sober; Bifrost records the Discord receipt on the topic
|
||||
- human `#bifrost` messages become canonical Bifrost comments only after Heimdall/Bifrost links the Discord id to a registered actor; unlinked messages remain non-governance chat context
|
||||
|
||||
This does not mean Bifrost owns all Discord behavior. General conversation, room reading, moderation judgment, archive retrieval, and personality cognition remain VoidBot/Face concerns. Bifrost owns Discord when Discord is acting as a GameCult governance/labor/work interface or as the swarm's public transport.
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ const projectsRoot = resolve(repoRoot, "..");
|
|||
const defaultStorePath = resolve(repoRoot, ".bifrost", "governance-threads.cc");
|
||||
const defaultPacketDir = resolve(repoRoot, ".bifrost", "governance-dispatch-packets");
|
||||
const agentTransportCli = resolve(repoRoot, "tools", "agent-transport.mjs");
|
||||
const bridgeCli = resolve(repoRoot, "tools", "bifrost-bridge.mjs");
|
||||
|
||||
const cultCacheRequire = createRequire(resolve(projectsRoot, "CultCacheTS", "package.json"));
|
||||
const {
|
||||
|
|
@ -162,6 +163,11 @@ async function openTopic(cache, options) {
|
|||
});
|
||||
|
||||
await cache.put(topicDefinition, topic.id, topic);
|
||||
await maybeMirrorTopicActivity(cache, topic, {
|
||||
options,
|
||||
fallbackContent: topic.summaryMarkdown,
|
||||
eventLabel: "opened",
|
||||
});
|
||||
printJson(topic);
|
||||
}
|
||||
|
||||
|
|
@ -184,6 +190,11 @@ async function addComment(cache, options) {
|
|||
});
|
||||
|
||||
await cache.put(commentDefinition, comment.id, comment);
|
||||
await maybeMirrorTopicActivity(cache, topic, {
|
||||
options,
|
||||
fallbackContent: comment.bodyMarkdown,
|
||||
eventLabel: comment.stance,
|
||||
});
|
||||
printJson(comment);
|
||||
}
|
||||
|
||||
|
|
@ -220,6 +231,12 @@ async function approveTopic(cache, options) {
|
|||
await cache.put(commentDefinition, comment.id, comment);
|
||||
}
|
||||
|
||||
await maybeMirrorTopicActivity(cache, approved, {
|
||||
options,
|
||||
fallbackContent: approvalBody ?? `Approved by ${approvedBy}.`,
|
||||
eventLabel: "approved",
|
||||
});
|
||||
|
||||
printJson(approved);
|
||||
}
|
||||
|
||||
|
|
@ -273,10 +290,72 @@ async function promoteTopic(cache, options) {
|
|||
createdAt: now,
|
||||
});
|
||||
await cache.put(commentDefinition, receipt.id, receipt);
|
||||
await maybeMirrorTopicActivity(cache, dispatched, {
|
||||
options,
|
||||
fallbackContent: receipt.bodyMarkdown,
|
||||
eventLabel: "dispatched",
|
||||
});
|
||||
|
||||
printJson({ topic: dispatched, request });
|
||||
}
|
||||
|
||||
async function maybeMirrorTopicActivity(cache, topic, input) {
|
||||
const channelId = optionalString(input.options["mirror-channel-id"]);
|
||||
if (!channelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const personaName = optionalString(input.options["mirror-persona-name"]);
|
||||
const personaAvatarUrl = optionalString(input.options["mirror-persona-avatar-url"]);
|
||||
const mirrorContent =
|
||||
await readOptionalMarkdownOption(input.options, "mirror-content") ??
|
||||
renderMirrorFallback(topic, input.eventLabel, input.fallbackContent);
|
||||
|
||||
const now = new Date().toISOString();
|
||||
let bodyMarkdown;
|
||||
let sourceMessageId;
|
||||
try {
|
||||
const receipt = runNodeJson([
|
||||
bridgeCli,
|
||||
"discord-post",
|
||||
"--channel-id", channelId,
|
||||
"--content", mirrorContent,
|
||||
...optionalArg("--persona-name", personaName),
|
||||
...optionalArg("--persona-avatar-url", personaAvatarUrl),
|
||||
...optionalArg("--reply-to-message-id", input.options["mirror-reply-to-message-id"]),
|
||||
...(input.options["mirror-dry-run"] === "true" ? ["--dry-run", "true"] : []),
|
||||
], repoRoot);
|
||||
bodyMarkdown = receipt.dryRun
|
||||
? `Dry-run mirror prepared for Discord channel ${channelId}.`
|
||||
: `Mirrored ${input.eventLabel} to Discord channel ${channelId}${receipt.url ? `: ${receipt.url}` : ""}.`;
|
||||
sourceMessageId = receipt.messageId;
|
||||
} catch (error) {
|
||||
bodyMarkdown = `Discord mirror failed for ${input.eventLabel} in channel ${channelId}: ${error instanceof Error ? error.message : String(error)}`;
|
||||
}
|
||||
|
||||
const receiptComment = parseComment({
|
||||
id: `comment_${randomUUID()}`,
|
||||
topicId: topic.id,
|
||||
authorKind: "system",
|
||||
authorId: "bifrost",
|
||||
stance: "receipt",
|
||||
bodyMarkdown,
|
||||
sourceMessageId,
|
||||
createdAt: now,
|
||||
});
|
||||
await cache.put(commentDefinition, receiptComment.id, receiptComment);
|
||||
}
|
||||
|
||||
function renderMirrorFallback(topic, eventLabel, content) {
|
||||
return [
|
||||
`Bifrost ${eventLabel}: ${topic.title}`,
|
||||
"",
|
||||
content.trim(),
|
||||
"",
|
||||
`Topic: ${topic.id}`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function listTopics(cache, options) {
|
||||
const topics = cache.getAll(topicDefinition)
|
||||
.filter((topic) => !options.status || topic.status === options.status)
|
||||
|
|
@ -570,6 +649,14 @@ Examples:
|
|||
node tools/governance-threads.mjs open --repo AquaSynth --agent aqua --title "Universal utterance schema" --summary-file packet.md --priority 80
|
||||
node tools/governance-threads.mjs approve --topic topic_... --approved-by aqua --body "Aqua approves dispatch."
|
||||
node tools/governance-threads.mjs promote --topic topic_...
|
||||
|
||||
Mirror options:
|
||||
--mirror-channel-id <id> Mirror this activity to Discord through Bifrost bridge
|
||||
--mirror-persona-name <name> Render the mirror as the Face/persona
|
||||
--mirror-persona-avatar-url <url> Optional persona avatar for the mirror
|
||||
--mirror-content <text> Optional more verbal/personality-rich mirror text
|
||||
--mirror-content-file <path> Read mirror text from a file
|
||||
--mirror-dry-run true Exercise mirror plumbing without posting to Discord
|
||||
`);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue