2026-05-20 21:45:56 +01:00
#!/usr/bin/env node
import { spawnSync } from "node:child_process" ;
import { createRequire } from "node:module" ;
import { randomUUID } from "node:crypto" ;
2026-05-21 00:10:55 +01:00
import { existsSync , readFileSync } from "node:fs" ;
2026-05-20 21:45:56 +01:00
import { mkdir , readFile , writeFile } from "node:fs/promises" ;
import { dirname , resolve } from "node:path" ;
import { fileURLToPath } from "node:url" ;
const scriptDir = dirname ( fileURLToPath ( import . meta . url ) ) ;
const repoRoot = resolve ( scriptDir , ".." ) ;
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" ) ;
2026-05-20 21:55:28 +01:00
const bridgeCli = resolve ( repoRoot , "tools" , "bifrost-bridge.mjs" ) ;
2026-05-21 00:10:55 +01:00
const defaultPersonaName = "Bifrost" ;
const defaultPersonaAvatarUrl =
"https://raw.githubusercontent.com/GameCult/Bifrost/main/src/Bifrost.Web/wwwroot/img/bifrost-profile.png" ;
2026-05-20 21:45:56 +01:00
const cultCacheRequire = createRequire ( resolve ( projectsRoot , "CultCacheTS" , "package.json" ) ) ;
const {
CultCache ,
SingleFileMessagePackBackingStore ,
defineDocumentType ,
} = cultCacheRequire ( resolve ( projectsRoot , "CultCacheTS" , "dist" , "index.js" ) ) ;
const topicType = "bifrost.governance.topic" ;
const topicSchemaId = "bifrost.governance.topic.v0" ;
const commentType = "bifrost.governance.topic-comment" ;
const commentSchemaId = "bifrost.governance.topic_comment.v0" ;
const validTopicStatuses = new Set ( [ "open" , "consensus_ready" , "approved" , "dispatched" , "closed" , "cancelled" ] ) ;
const validStances = new Set ( [ "comment" , "proposal" , "support" , "objection" , "question" , "approval" , "summary" , "receipt" ] ) ;
const topicDefinition = defineDocumentType ( {
type : topicType ,
schemaId : topicSchemaId ,
schemaName : topicType ,
schemaVersion : "bifrost.governance.topic.v0" ,
schema : { parse : parseTopic } ,
name : "id" ,
indexes : {
jurisdictionRepoName : "jurisdictionRepoName" ,
jurisdictionAgentIdentity : "jurisdictionAgentIdentity" ,
status : "status" ,
} ,
members : [
{ slot : 1 , memberName : "id" , typeName : "string" , isName : true } ,
{ slot : 2 , memberName : "title" , typeName : "string" } ,
{ slot : 3 , memberName : "jurisdictionRepoName" , typeName : "string" , indexAlias : "jurisdictionRepoName" } ,
{ slot : 4 , memberName : "jurisdictionAgentIdentity" , typeName : "string" , indexAlias : "jurisdictionAgentIdentity" } ,
{ slot : 5 , memberName : "status" , typeName : "string" , indexAlias : "status" } ,
{ slot : 6 , memberName : "summaryMarkdown" , typeName : "string" } ,
{ slot : 7 , memberName : "priority" , typeName : "number" } ,
{ slot : 8 , memberName : "sourceKind" , typeName : "string" } ,
{ slot : 9 , memberName : "sourceChannelId" , typeName : "string" } ,
{ slot : 10 , memberName : "sourceMessageIds" , typeName : "string" , isMany : true } ,
{ slot : 11 , memberName : "createdByActor" , typeName : "string" } ,
{ slot : 12 , memberName : "approvedByAgent" , typeName : "string" } ,
{ slot : 13 , memberName : "dispatchRequestId" , typeName : "string" } ,
{ slot : 14 , memberName : "createdAt" , typeName : "string" } ,
{ slot : 15 , memberName : "updatedAt" , typeName : "string" } ,
{ slot : 16 , memberName : "approvedAt" , typeName : "string" } ,
{ slot : 17 , memberName : "dispatchedAt" , typeName : "string" } ,
{ slot : 18 , memberName : "closedAt" , typeName : "string" } ,
] ,
} ) ;
const commentDefinition = defineDocumentType ( {
type : commentType ,
schemaId : commentSchemaId ,
schemaName : commentType ,
schemaVersion : "bifrost.governance.topic_comment.v0" ,
schema : { parse : parseComment } ,
name : "id" ,
indexes : {
topicId : "topicId" ,
authorId : "authorId" ,
stance : "stance" ,
} ,
members : [
{ slot : 1 , memberName : "id" , typeName : "string" , isName : true } ,
{ slot : 2 , memberName : "topicId" , typeName : "string" , indexAlias : "topicId" } ,
{ slot : 3 , memberName : "authorKind" , typeName : "string" } ,
{ slot : 4 , memberName : "authorId" , typeName : "string" , indexAlias : "authorId" } ,
{ slot : 5 , memberName : "stance" , typeName : "string" , indexAlias : "stance" } ,
{ slot : 6 , memberName : "bodyMarkdown" , typeName : "string" } ,
{ slot : 7 , memberName : "sourceMessageId" , typeName : "string" } ,
{ slot : 8 , memberName : "createdAt" , typeName : "string" } ,
] ,
} ) ;
async function main ( ) {
2026-05-21 00:10:55 +01:00
loadLocalEnv ( resolve ( repoRoot , ".env" ) ) ;
loadLocalEnv ( resolve ( projectsRoot , "VoidBot" , ".env" ) ) ;
2026-05-20 21:45:56 +01:00
const [ command , ... rawArgs ] = process . argv . slice ( 2 ) ;
const options = parseArgs ( rawArgs ) ;
if ( ! command || command === "help" || command === "--help" || command === "-h" ) {
printHelp ( ) ;
return ;
}
const storePath = resolveOptionPath ( options . store ? ? defaultStorePath ) ;
const cache = await openCache ( storePath ) ;
switch ( command ) {
case "open" :
await openTopic ( cache , options ) ;
return ;
case "comment" :
await addComment ( cache , options ) ;
return ;
case "approve" :
await approveTopic ( cache , options ) ;
return ;
case "promote" :
await promoteTopic ( cache , options ) ;
return ;
case "list" :
listTopics ( cache , options ) ;
return ;
case "show" :
showTopic ( cache , options ) ;
return ;
case "digest" :
digestTopics ( cache , options ) ;
return ;
case "schema" :
printJson ( { topicType , topicSchemaId , commentType , commentSchemaId } ) ;
return ;
default :
throw new Error ( ` Unknown command " ${ command } ". Run "node tools/governance-threads.mjs help". ` ) ;
}
}
async function openCache ( storePath ) {
const cache = CultCache . builder ( )
. withDocumentType ( topicDefinition )
. withDocumentType ( commentDefinition )
. withGenericStore ( new SingleFileMessagePackBackingStore ( storePath ) )
. build ( ) ;
await cache . pullAllBackingStores ( ) ;
return cache ;
}
async function openTopic ( cache , options ) {
const now = new Date ( ) . toISOString ( ) ;
const topic = parseTopic ( {
id : options . id ? ? ` topic_ ${ randomUUID ( ) } ` ,
title : requireOption ( options , "title" ) ,
jurisdictionRepoName : requireOption ( options , "repo" ) ,
jurisdictionAgentIdentity : optionalString ( options . agent ) ,
status : optionalString ( options . status ) ? ? "open" ,
summaryMarkdown : await readMarkdownOption ( options , "summary" ) ,
priority : parseInteger ( options . priority ? ? "50" , "priority" ) ,
sourceKind : optionalString ( options [ "source-kind" ] ) ? ? "manual" ,
sourceChannelId : optionalString ( options [ "source-channel-id" ] ) ,
sourceMessageIds : parseCsv ( options [ "source-message-ids" ] ) ,
createdByActor : optionalString ( options [ "created-by" ] ) ,
approvedByAgent : undefined ,
dispatchRequestId : undefined ,
createdAt : now ,
updatedAt : now ,
approvedAt : undefined ,
dispatchedAt : undefined ,
closedAt : undefined ,
} ) ;
await cache . put ( topicDefinition , topic . id , topic ) ;
2026-05-21 00:10:55 +01:00
try {
await mirrorTopicActivityOrThrow ( cache , topic , {
options ,
fallbackContent : topic . summaryMarkdown ,
eventLabel : "opened" ,
} ) ;
} catch ( error ) {
await cache . delete ( topicDefinition , topic . id ) ;
throw error ;
}
2026-05-20 21:45:56 +01:00
printJson ( topic ) ;
}
async function addComment ( cache , options ) {
const topicId = requireOption ( options , "topic" ) ;
const topic = cache . get ( topicDefinition , topicId ) ;
if ( ! topic ) {
throw new Error ( ` No governance topic found for id " ${ topicId } ". ` ) ;
}
const comment = parseComment ( {
id : options . id ? ? ` comment_ ${ randomUUID ( ) } ` ,
topicId ,
authorKind : optionalString ( options [ "author-kind" ] ) ? ? "agent" ,
authorId : requireOption ( options , "author" ) ,
stance : optionalString ( options . stance ) ? ? "comment" ,
bodyMarkdown : await readMarkdownOption ( options , "body" ) ,
sourceMessageId : optionalString ( options [ "source-message-id" ] ) ,
createdAt : new Date ( ) . toISOString ( ) ,
} ) ;
await cache . put ( commentDefinition , comment . id , comment ) ;
2026-05-21 00:10:55 +01:00
try {
await mirrorTopicActivityOrThrow ( cache , topic , {
options ,
fallbackContent : comment . bodyMarkdown ,
eventLabel : comment . stance ,
} ) ;
} catch ( error ) {
await cache . delete ( commentDefinition , comment . id ) ;
throw error ;
}
2026-05-20 21:45:56 +01:00
printJson ( comment ) ;
}
async function approveTopic ( cache , options ) {
const topic = requireTopic ( cache , options ) ;
const approvedBy = requireOption ( options , "approved-by" ) ;
const approvalBody = await readOptionalMarkdownOption ( options , "body" ) ;
if ( ! topic . jurisdictionAgentIdentity || ! equalsIgnoreCase ( approvedBy , topic . jurisdictionAgentIdentity ) ) {
2026-06-09 14:40:26 +01:00
throw new Error ( ` Topic " ${ topic . id } " can only be approved by its jurisdiction Persona ( ${ topic . jurisdictionAgentIdentity ? ? "none" } ). ` ) ;
2026-05-20 21:45:56 +01:00
}
const now = new Date ( ) . toISOString ( ) ;
const approved = parseTopic ( {
... topic ,
status : "approved" ,
approvedByAgent : approvedBy ,
approvedAt : now ,
updatedAt : now ,
} ) ;
await cache . put ( topicDefinition , approved . id , approved ) ;
2026-05-21 00:10:55 +01:00
let comment ;
2026-05-20 21:45:56 +01:00
if ( approvalBody ) {
2026-05-21 00:10:55 +01:00
comment = parseComment ( {
2026-05-20 21:45:56 +01:00
id : ` comment_ ${ randomUUID ( ) } ` ,
topicId : topic . id ,
authorKind : "face" ,
authorId : approvedBy ,
stance : "approval" ,
bodyMarkdown : approvalBody ,
sourceMessageId : optionalString ( options [ "source-message-id" ] ) ,
createdAt : now ,
} ) ;
await cache . put ( commentDefinition , comment . id , comment ) ;
}
2026-05-21 00:10:55 +01:00
try {
await mirrorTopicActivityOrThrow ( cache , approved , {
options ,
fallbackContent : approvalBody ? ? ` Approved by ${ approvedBy } . ` ,
eventLabel : "approved" ,
} ) ;
} catch ( error ) {
await cache . put ( topicDefinition , topic . id , topic ) ;
if ( comment ) {
await cache . delete ( commentDefinition , comment . id ) ;
}
throw error ;
}
2026-05-20 21:55:28 +01:00
2026-05-20 21:45:56 +01:00
printJson ( approved ) ;
}
async function promoteTopic ( cache , options ) {
const topic = requireTopic ( cache , options ) ;
if ( topic . status !== "approved" ) {
throw new Error ( ` Topic " ${ topic . id } " must be approved before dispatch promotion; current status is ${ topic . status } . ` ) ;
}
const comments = commentsForTopic ( cache , topic . id ) ;
const requestMarkdown = renderUpdateRequest ( topic , comments ) ;
const packetDir = resolveOptionPath ( options [ "packet-dir" ] ? ? defaultPacketDir ) ;
await mkdir ( packetDir , { recursive : true } ) ;
const packetPath = resolve ( packetDir , ` ${ topic . id } .md ` ) ;
await writeFile ( packetPath , requestMarkdown , "utf8" ) ;
const request = runNodeJson ( [
agentTransportCli ,
"enqueue" ,
"--repo" , topic . jurisdictionRepoName ,
... optionalArg ( "--agent" , topic . jurisdictionAgentIdentity ) ,
"--title" , topic . title ,
"--request-file" , packetPath ,
"--priority" , String ( topic . priority ) ,
"--source-kind" , "bifrost_governance_topic" ,
... optionalArg ( "--source-channel-id" , topic . sourceChannelId ) ,
... optionalArg ( "--source-message-ids" , topic . sourceMessageIds . join ( "," ) ) ,
"--packet-path" , packetPath ,
... optionalArg ( "--created-by" , topic . approvedByAgent ? ? topic . createdByActor ) ,
2026-05-21 00:10:55 +01:00
... optionalArg ( "--mirror-channel-id" , resolveMirrorChannelId ( options ) ) ,
... optionalArg ( "--mirror-persona-name" , optionalString ( process . env . BIFROST _DISCORD _PERSONA _NAME ) ? ? defaultPersonaName ) ,
... optionalArg (
"--mirror-persona-avatar-url" ,
optionalString ( process . env . BIFROST _DISCORD _PERSONA _AVATAR _URL ) ? ?
optionalString ( process . env . DISCORD _PERSONA _AVATAR _URL _BIFROST ) ? ?
defaultPersonaAvatarUrl ,
) ,
... ( options [ "mirror-dry-run" ] === "true" ? [ "--mirror-dry-run" , "true" ] : [ ] ) ,
... ( allowsUnmirrored ( options ) ? [ "--allow-unmirrored" , "true" ] : [ ] ) ,
2026-05-20 21:45:56 +01:00
... optionalArg ( "--store" , options [ "transport-store" ] ) ,
] , repoRoot ) ;
const now = new Date ( ) . toISOString ( ) ;
const dispatched = parseTopic ( {
... topic ,
status : "dispatched" ,
dispatchRequestId : request . id ,
dispatchedAt : now ,
updatedAt : now ,
} ) ;
await cache . put ( topicDefinition , dispatched . id , dispatched ) ;
const receipt = parseComment ( {
id : ` comment_ ${ randomUUID ( ) } ` ,
topicId : topic . id ,
authorKind : "system" ,
authorId : "bifrost" ,
stance : "receipt" ,
bodyMarkdown : ` Promoted to Bifrost update request \` ${ request . id } \` . ` ,
sourceMessageId : undefined ,
createdAt : now ,
} ) ;
await cache . put ( commentDefinition , receipt . id , receipt ) ;
2026-05-21 00:10:55 +01:00
try {
await mirrorTopicActivityOrThrow ( cache , dispatched , {
options ,
fallbackContent : receipt . bodyMarkdown ,
eventLabel : "dispatched" ,
} ) ;
} catch ( error ) {
await cache . put ( topicDefinition , topic . id , topic ) ;
await cache . delete ( commentDefinition , receipt . id ) ;
throw error ;
}
2026-05-20 21:45:56 +01:00
printJson ( { topic : dispatched , request } ) ;
}
2026-05-21 00:10:55 +01:00
async function mirrorTopicActivityOrThrow ( cache , topic , input ) {
const channelId = resolveMirrorChannelId ( input . options ) ;
2026-05-20 21:55:28 +01:00
if ( ! channelId ) {
2026-05-21 00:10:55 +01:00
if ( allowsUnmirrored ( input . options ) ) {
return ;
}
throw new Error (
"Bifrost governance writes require a Discord mirror. Set BIFROST_DISCORD_CHANNEL_ID, pass --mirror-channel-id, or use --allow-unmirrored true only for explicit fixtures." ,
) ;
2026-05-20 21:55:28 +01:00
}
2026-05-21 00:10:55 +01:00
const personaName = optionalString ( input . options [ "mirror-persona-name" ] )
? ? optionalString ( process . env . BIFROST _DISCORD _PERSONA _NAME )
? ? defaultPersonaName ;
const personaAvatarUrl =
optionalString ( input . options [ "mirror-persona-avatar-url" ] ) ? ?
optionalString ( process . env . BIFROST _DISCORD _PERSONA _AVATAR _URL ) ? ?
optionalString ( process . env . DISCORD _PERSONA _AVATAR _URL _BIFROST ) ? ?
defaultPersonaAvatarUrl ;
2026-05-21 19:55:53 +01:00
const mirrorContent = renderGovernanceMirrorContent (
topic ,
input . eventLabel ,
await readOptionalMarkdownOption ( input . options , "mirror-content" ) ? ? input . fallbackContent ,
) ;
2026-05-20 21:55:28 +01:00
const now = new Date ( ) . toISOString ( ) ;
2026-05-21 00:10:55 +01:00
const receipt = runNodeJson ( [
bridgeCli ,
"discord-post" ,
"--channel-id" , channelId ,
"--content" , mirrorContent ,
"--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 ) ;
const bodyMarkdown = receipt . dryRun
? ` Dry-run mirror prepared for Discord channel ${ channelId } . `
: ` Mirrored ${ input . eventLabel } to Discord channel ${ channelId } ${ receipt . url ? ` : ${ receipt . url } ` : "" } . ` ;
2026-05-20 21:55:28 +01:00
const receiptComment = parseComment ( {
id : ` comment_ ${ randomUUID ( ) } ` ,
topicId : topic . id ,
authorKind : "system" ,
authorId : "bifrost" ,
stance : "receipt" ,
bodyMarkdown ,
2026-05-21 00:10:55 +01:00
sourceMessageId : receipt . messageId ,
2026-05-20 21:55:28 +01:00
createdAt : now ,
} ) ;
await cache . put ( commentDefinition , receiptComment . id , receiptComment ) ;
}
2026-05-21 00:10:55 +01:00
function resolveMirrorChannelId ( options ) {
return (
optionalString ( options [ "mirror-channel-id" ] ) ? ?
optionalString ( process . env . BIFROST _DISCORD _CHANNEL _ID ) ? ?
optionalString ( process . env . DISCORD _BIFROST _CHANNEL _ID )
) ;
}
function allowsUnmirrored ( options ) {
return options [ "allow-unmirrored" ] === "true" || process . env . BIFROST _ALLOW _UNMIRRORED _GOVERNANCE === "true" ;
}
2026-05-21 19:55:53 +01:00
function renderGovernanceMirrorContent ( topic , eventLabel , content ) {
const event = governanceMirrorEvent ( eventLabel ) ;
2026-05-20 21:55:28 +01:00
return [
2026-05-21 19:55:53 +01:00
` Bifrost governance: ${ event . label } ` ,
"" ,
` ** ${ topic . title } ** ` ,
` Topic: \` ${ topic . id } \` ` ,
` Jurisdiction: ${ topic . jurisdictionRepoName } ${ topic . jurisdictionAgentIdentity ? ` / ${ topic . jurisdictionAgentIdentity } ` : "" } ` ,
` Topic status: ${ topic . status } ` ,
` Codex job: ${ event . codexJob } ` ,
2026-05-20 21:55:28 +01:00
"" ,
2026-05-21 19:55:53 +01:00
truncateMarkdown ( content . trim ( ) , 1200 ) ,
2026-05-20 21:55:28 +01:00
"" ,
2026-05-21 19:55:53 +01:00
event . nextStep ,
2026-05-20 21:55:28 +01:00
] . join ( "\n" ) ;
}
2026-05-21 19:55:53 +01:00
function governanceMirrorEvent ( eventLabel ) {
switch ( eventLabel ) {
case "opened" :
return {
label : "topic opened" ,
codexJob : "no - this is discussion/proposal intake only" ,
nextStep : "Next: comments, jurisdiction review, or approval. Nothing has been dispatched." ,
} ;
case "approved" :
return {
label : "topic approved" ,
codexJob : "no - approved topics still need promotion before dispatch" ,
nextStep : "Next: promotion creates a Bifrost update request. A separate dispatch receipt means Codex actually started." ,
} ;
case "dispatched" :
return {
label : "topic promoted to update request" ,
codexJob : "queued - Codex has not necessarily started yet" ,
nextStep : "Next: wait for a `Bifrost Codex dispatch started` receipt before treating this as an active Codex job." ,
} ;
default :
return {
label : ` topic ${ eventLabel } ` ,
codexJob : "no - this is governance discussion, not dispatch" ,
nextStep : "Next: keep discussing, approve, or promote when the jurisdiction owner is ready." ,
} ;
}
}
function truncateMarkdown ( value , maxLength ) {
return value . length > maxLength ? ` ${ value . slice ( 0 , maxLength - 3 ) } ... ` : value ;
}
2026-05-20 21:45:56 +01:00
function listTopics ( cache , options ) {
const topics = cache . getAll ( topicDefinition )
. filter ( ( topic ) => ! options . status || topic . status === options . status )
. filter ( ( topic ) => ! options . repo || equalsIgnoreCase ( topic . jurisdictionRepoName , options . repo ) )
. filter ( ( topic ) => ! options . agent || equalsIgnoreCase ( topic . jurisdictionAgentIdentity , options . agent ) )
. sort ( compareTopics ) ;
printJson ( topics ) ;
}
function showTopic ( cache , options ) {
const topic = requireTopic ( cache , options ) ;
printJson ( {
topic ,
comments : commentsForTopic ( cache , topic . id ) ,
} ) ;
}
function digestTopics ( cache , options ) {
const limit = parseInteger ( options . limit ? ? "6" , "limit" ) ;
const topics = cache . getAll ( topicDefinition )
. filter ( ( topic ) => ! options . repo || equalsIgnoreCase ( topic . jurisdictionRepoName , options . repo ) )
. filter ( ( topic ) => ! options . agent || equalsIgnoreCase ( topic . jurisdictionAgentIdentity , options . agent ) )
. filter ( ( topic ) => ! options . status || topic . status === options . status )
. sort ( compareRecentTopics )
. slice ( 0 , limit ) ;
printJson ( {
generatedAt : new Date ( ) . toISOString ( ) ,
topics : topics . map ( ( topic ) => ( {
... topic ,
comments : commentsForTopic ( cache , topic . id ) . slice ( - 5 ) ,
} ) ) ,
} ) ;
}
function requireTopic ( cache , options ) {
const id = requireOption ( options , "topic" ) ;
const topic = cache . get ( topicDefinition , id ) ;
if ( ! topic ) {
throw new Error ( ` No governance topic found for id " ${ id } ". ` ) ;
}
return topic ;
}
function commentsForTopic ( cache , topicId ) {
return cache . getAll ( commentDefinition )
. filter ( ( comment ) => comment . topicId === topicId )
. sort ( ( left , right ) => left . createdAt . localeCompare ( right . createdAt ) ) ;
}
function renderUpdateRequest ( topic , comments ) {
const commentLines = comments . map ( ( comment ) => [
` ### ${ comment . stance } : ${ comment . authorId } ` ,
"" ,
comment . bodyMarkdown ,
] . join ( "\n" ) ) ;
return [
` # ${ topic . title } ` ,
"" ,
` Bifrost governance topic: ${ topic . id } ` ,
` Jurisdiction: ${ topic . jurisdictionRepoName } ${ topic . jurisdictionAgentIdentity ? ` / ${ topic . jurisdictionAgentIdentity } ` : "" } ` ,
` Approved by: ${ topic . approvedByAgent } ` ,
` Priority: ${ topic . priority } ` ,
"" ,
"## Request" ,
"" ,
topic . summaryMarkdown ,
"" ,
"## Discussion Record" ,
"" ,
commentLines . length > 0 ? commentLines . join ( "\n\n" ) : "- No comments recorded." ,
"" ,
"## Dispatch Instruction" ,
"" ,
"Work this as the approved repo-local request produced from the canonical Bifrost topic. Preserve the topic id in any report, commit message, PR body, or follow-up note." ,
"" ,
] . join ( "\n" ) ;
}
async function readMarkdownOption ( options , name ) {
const value = await readOptionalMarkdownOption ( options , name ) ;
if ( ! value ) {
throw new Error ( ` Missing required option -- ${ name } or -- ${ name } -file. ` ) ;
}
return value ;
}
async function readOptionalMarkdownOption ( options , name ) {
const fileValue = optionalString ( options [ ` ${ name } -file ` ] ) ;
if ( fileValue ) {
return readFile ( resolveOptionPath ( fileValue ) , "utf8" ) ;
}
return optionalString ( options [ name ] ) ;
}
function parseTopic ( input ) {
if ( ! input || typeof input !== "object" ) {
throw new Error ( "Governance topic must be an object." ) ;
}
const topic = {
id : requireString ( input . id , "id" ) ,
title : requireString ( input . title , "title" ) ,
jurisdictionRepoName : requireString ( input . jurisdictionRepoName , "jurisdictionRepoName" ) ,
jurisdictionAgentIdentity : optionalString ( input . jurisdictionAgentIdentity ) ,
status : requireString ( input . status , "status" ) ,
summaryMarkdown : requireString ( input . summaryMarkdown , "summaryMarkdown" ) ,
priority : requireNumber ( input . priority , "priority" ) ,
sourceKind : optionalString ( input . sourceKind ) ? ? "manual" ,
sourceChannelId : optionalString ( input . sourceChannelId ) ,
sourceMessageIds : normalizeStringArray ( input . sourceMessageIds , "sourceMessageIds" ) ,
createdByActor : optionalString ( input . createdByActor ) ,
approvedByAgent : optionalString ( input . approvedByAgent ) ,
dispatchRequestId : optionalString ( input . dispatchRequestId ) ,
createdAt : requireString ( input . createdAt , "createdAt" ) ,
updatedAt : requireString ( input . updatedAt , "updatedAt" ) ,
approvedAt : optionalString ( input . approvedAt ) ,
dispatchedAt : optionalString ( input . dispatchedAt ) ,
closedAt : optionalString ( input . closedAt ) ,
} ;
if ( ! validTopicStatuses . has ( topic . status ) ) {
throw new Error ( ` Governance topic status " ${ topic . status } " is not valid. ` ) ;
}
return topic ;
}
function parseComment ( input ) {
if ( ! input || typeof input !== "object" ) {
throw new Error ( "Governance topic comment must be an object." ) ;
}
const comment = {
id : requireString ( input . id , "id" ) ,
topicId : requireString ( input . topicId , "topicId" ) ,
authorKind : requireString ( input . authorKind , "authorKind" ) ,
authorId : requireString ( input . authorId , "authorId" ) ,
stance : requireString ( input . stance , "stance" ) ,
bodyMarkdown : requireString ( input . bodyMarkdown , "bodyMarkdown" ) ,
sourceMessageId : optionalString ( input . sourceMessageId ) ,
createdAt : requireString ( input . createdAt , "createdAt" ) ,
} ;
if ( ! validStances . has ( comment . stance ) ) {
throw new Error ( ` Governance comment stance " ${ comment . stance } " is not valid. ` ) ;
}
return comment ;
}
function parseArgs ( args ) {
const options = { } ;
for ( let index = 0 ; index < args . length ; index += 1 ) {
const token = args [ index ] ;
if ( ! token . startsWith ( "--" ) ) {
throw new Error ( ` Unexpected argument " ${ token } ". ` ) ;
}
const key = token . slice ( 2 ) ;
const next = args [ index + 1 ] ;
if ( ! next || next . startsWith ( "--" ) ) {
options [ key ] = "true" ;
continue ;
}
options [ key ] = next ;
index += 1 ;
}
return options ;
}
function compareTopics ( left , right ) {
if ( right . priority !== left . priority ) {
return right . priority - left . priority ;
}
return right . createdAt . localeCompare ( left . createdAt ) ;
}
function compareRecentTopics ( left , right ) {
return right . updatedAt . localeCompare ( left . updatedAt ) ;
}
function requireOption ( options , name ) {
const value = optionalString ( options [ name ] ) ;
if ( ! value ) {
throw new Error ( ` Missing required option -- ${ name } . ` ) ;
}
return value ;
}
function optionalString ( value ) {
if ( typeof value !== "string" ) {
return undefined ;
}
const trimmed = value . trim ( ) ;
return trimmed . length > 0 ? trimmed : undefined ;
}
function requireString ( value , field ) {
const normalized = optionalString ( value ) ;
if ( ! normalized ) {
throw new Error ( ` Governance document field " ${ field } " must be a non-empty string. ` ) ;
}
return normalized ;
}
function requireNumber ( value , field ) {
const number = typeof value === "number" ? value : Number ( value ) ;
if ( ! Number . isFinite ( number ) ) {
throw new Error ( ` Governance document field " ${ field } " must be a finite number. ` ) ;
}
return number ;
}
function parseInteger ( value , field ) {
const number = Number ( value ) ;
if ( ! Number . isInteger ( number ) ) {
throw new Error ( ` -- ${ field } must be an integer. ` ) ;
}
return number ;
}
function parseCsv ( value ) {
if ( typeof value !== "string" || value . trim ( ) . length === 0 ) {
return [ ] ;
}
return value . split ( "," ) . map ( ( item ) => item . trim ( ) ) . filter ( Boolean ) ;
}
function normalizeStringArray ( value , field ) {
if ( value === undefined || value === null ) {
return [ ] ;
}
if ( ! Array . isArray ( value ) || value . some ( ( item ) => typeof item !== "string" ) ) {
throw new Error ( ` Governance document field " ${ field } " must be an array of strings. ` ) ;
}
return value ;
}
function optionalArg ( flag , value ) {
const normalized = optionalString ( value ) ;
return normalized ? [ flag , normalized ] : [ ] ;
}
function equalsIgnoreCase ( left , right ) {
return typeof left === "string"
&& typeof right === "string"
&& left . toLowerCase ( ) === right . toLowerCase ( ) ;
}
function runNodeJson ( args , cwd ) {
const result = spawnSync ( process . execPath , args , {
cwd ,
encoding : "utf8" ,
stdio : [ "ignore" , "pipe" , "pipe" ] ,
windowsHide : true ,
timeout : 30000 ,
} ) ;
if ( result . status !== 0 ) {
const reason = result . error ? . message ? ? result . stderr ? ? result . stdout ;
throw new Error ( ` node ${ args . join ( " " ) } failed with ${ result . status ? ? "unknown" } : \n ${ reason } ` ) ;
}
return JSON . parse ( result . stdout ) ;
}
2026-05-21 00:10:55 +01:00
function loadLocalEnv ( path ) {
if ( ! existsSync ( path ) ) {
return ;
}
for ( const line of readFileSync ( path , "utf8" ) . split ( /\r?\n/ ) ) {
const trimmed = line . trim ( ) ;
if ( ! trimmed || trimmed . startsWith ( "#" ) ) {
continue ;
}
const separator = trimmed . indexOf ( "=" ) ;
if ( separator === - 1 ) {
continue ;
}
const key = trimmed . slice ( 0 , separator ) . trim ( ) ;
if ( process . env [ key ] ) {
continue ;
}
let value = trimmed . slice ( separator + 1 ) . trim ( ) ;
if (
value . length >= 2 &&
( ( value . startsWith ( '"' ) && value . endsWith ( '"' ) ) || ( value . startsWith ( "'" ) && value . endsWith ( "'" ) ) )
) {
value = value . slice ( 1 , - 1 ) ;
}
process . env [ key ] = value ;
}
}
2026-05-20 21:45:56 +01:00
function resolveOptionPath ( path ) {
return resolve ( process . cwd ( ) , path ) ;
}
function printJson ( value ) {
process . stdout . write ( ` ${ JSON . stringify ( value , null , 2 ) } \n ` ) ;
}
function printHelp ( ) {
process . stdout . write ( ` Bifrost governance topic threads
Commands :
open Create a canonical Bifrost topic for a feature request or discussion
comment Append a proposal , support , objection , question , approval , summary , or receipt
2026-06-09 14:40:26 +01:00
approve Mark a topic approved by its jurisdiction Persona and optionally append approval text
2026-05-20 21:45:56 +01:00
promote Convert an approved topic into a Bifrost agent update request
list List topics , optionally filtered by -- repo , -- agent , -- status
show Print one topic with its comments
digest Print recent matching topics with their latest comments for agent context
schema Print document type metadata
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 _ ...
2026-05-20 21:55:28 +01:00
Mirror options :
2026-05-21 00:10:55 +01:00
-- mirror - channel - id < id > Mirror this activity to Discord through Bifrost bridge ; defaults to BIFROST _DISCORD _CHANNEL _ID
2026-06-09 14:40:26 +01:00
-- mirror - persona - name < name > Render the mirror as the Persona / persona
2026-05-20 21:55:28 +01:00
-- 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
2026-05-21 00:10:55 +01:00
-- allow - unmirrored true Fixture / debug escape hatch ; production writes should not use this
2026-05-20 21:45:56 +01:00
` );
}
main ( ) . catch ( ( error ) => {
process . stderr . write ( ` ${ error instanceof Error ? error . message : String ( error ) } \n ` ) ;
process . exitCode = 1 ;
} ) ;