2026-05-19 17:03:28 +01:00
#!/usr/bin/env node
import { spawnSync } from "node:child_process" ;
import { existsSync , mkdirSync , readFileSync , writeFileSync } from "node:fs" ;
import { dirname , resolve } from "node:path" ;
import { fileURLToPath } from "node:url" ;
const repoRoot = resolve ( dirname ( fileURLToPath ( import . meta . url ) ) , ".." ) ;
const AQUARIUM _CHANNEL _ID = "1501196543150264332" ;
2026-05-21 10:26:07 +01:00
const promptsRoot = resolve ( repoRoot , "prompts" ) ;
2026-05-19 17:03:28 +01:00
function main ( ) {
const options = parseArgs ( process . argv . slice ( 2 ) ) ;
const env = readLocalEnv ( ) ;
const archivePath = resolve (
repoRoot ,
options . archivePath ? ? env . RAG _ARCHIVE _PATH ? ? ".voidbot/rag/messages.json" ,
) ;
2026-05-20 16:48:20 +01:00
const messages = options . liveDiscord
? readLiveDiscordMessages ( env , options )
: readArchiveMessages ( archivePath ) ;
2026-05-19 17:03:28 +01:00
const selected = filterMessages ( messages , options ) . slice ( - options . limit ) ;
2026-05-20 16:48:20 +01:00
const packet = buildConsensusPacket (
selected ,
options ,
options . liveDiscord ? "discord-api:live" : archivePath ,
) ;
2026-05-19 17:03:28 +01:00
const outputPath = resolve (
repoRoot ,
options . out ? ?
` .voidbot/artifacts/chat-consensus/ ${ timestampSlug ( new Date ( ) ) } - ${ slugify ( options . agent ) } .md ` ,
) ;
mkdirSync ( dirname ( outputPath ) , { recursive : true } ) ;
writeFileSync ( outputPath , packet , "utf8" ) ;
const prompt = buildCodexPrompt ( packet , outputPath , options ) ;
const promptPath = outputPath . replace ( /\.md$/i , ".prompt.md" ) ;
writeFileSync ( promptPath , prompt , "utf8" ) ;
process . stdout . write ( ` Wrote consensus packet: ${ outputPath } \n ` ) ;
process . stdout . write ( ` Wrote Codex prompt: ${ promptPath } \n ` ) ;
process . stdout . write (
` Target: ${ options . agent } ; cwd= ${ resolve ( options . cwd ) } ; messages= ${ selected . length } \n ` ,
) ;
2026-05-19 18:17:09 +01:00
if ( options . enqueueBifrost ) {
enqueueBifrostRequest ( {
options ,
packet ,
outputPath ,
promptPath ,
selected ,
} ) ;
}
2026-05-19 17:03:28 +01:00
if ( ! options . execute ) {
process . stdout . write (
` Dry run. Re-run with --execute to feed this to Codex, or pass the prompt file to the target agent. \n ` ,
) ;
return ;
}
const codexExecutable = options . codexExecutable ? ? env . CODEX _EXECUTABLE ? ? "codex" ;
const model = options . model ? ? env . CODEX _MODEL ? ? "gpt-5.4" ;
const reasoningEffort = options . reasoningEffort ? ? env . CODEX _MODEL _REASONING _EFFORT ? ? "medium" ;
const executableArgs = splitCommandArgs ( options . codexExecArgs ? ? env . CODEX _EXEC _ARGS ? ? "" ) ;
const args = [
... executableArgs ,
"exec" ,
"-m" ,
model ,
"-c" ,
'approval_policy="never"' ,
"-c" ,
` model_reasoning_effort= ${ JSON . stringify ( reasoningEffort ) } ` ,
"--skip-git-repo-check" ,
"-s" ,
options . sandbox ,
"-" ,
] ;
const result = spawnSync ( codexExecutable , args , {
cwd : resolve ( options . cwd ) ,
input : prompt ,
encoding : "utf8" ,
stdio : [ "pipe" , "inherit" , "inherit" ] ,
windowsHide : true ,
} ) ;
if ( result . status !== 0 ) {
process . exitCode = result . status ? ? 1 ;
}
}
function parseArgs ( args ) {
const options = {
agent : "codex" ,
channelId : AQUARIUM _CHANNEL _ID ,
cwd : repoRoot ,
hours : 3 ,
limit : 80 ,
sandbox : "read-only" ,
execute : false ,
includeBotPrompts : false ,
} ;
for ( let index = 0 ; index < args . length ; index += 1 ) {
const arg = args [ index ] ;
switch ( arg ) {
case "--agent" :
options . agent = requireValue ( args , ++ index , arg ) ;
break ;
case "--repo" :
options . repo = requireValue ( args , ++ index , arg ) ;
break ;
case "--cwd" :
options . cwd = requireValue ( args , ++ index , arg ) ;
break ;
case "--task" :
options . task = requireValue ( args , ++ index , arg ) ;
break ;
case "--channel-id" :
options . channelId = requireValue ( args , ++ index , arg ) ;
break ;
case "--after" :
options . after = requireValue ( args , ++ index , arg ) ;
break ;
case "--hours" :
options . hours = parsePositiveNumber ( requireValue ( args , ++ index , arg ) , arg ) ;
break ;
case "--limit" :
options . limit = parsePositiveInteger ( requireValue ( args , ++ index , arg ) , arg ) ;
break ;
case "--out" :
options . out = requireValue ( args , ++ index , arg ) ;
break ;
case "--archive-path" :
options . archivePath = requireValue ( args , ++ index , arg ) ;
break ;
2026-05-20 16:48:20 +01:00
case "--live-discord" :
options . liveDiscord = true ;
break ;
2026-05-19 17:03:28 +01:00
case "--codex-executable" :
options . codexExecutable = requireValue ( args , ++ index , arg ) ;
break ;
case "--codex-exec-args" :
options . codexExecArgs = requireValue ( args , ++ index , arg ) ;
break ;
case "--model" :
options . model = requireValue ( args , ++ index , arg ) ;
break ;
case "--reasoning-effort" :
options . reasoningEffort = requireValue ( args , ++ index , arg ) ;
break ;
case "--sandbox" :
options . sandbox = requireValue ( args , ++ index , arg ) ;
break ;
case "--execute" :
options . execute = true ;
break ;
2026-05-19 18:17:09 +01:00
case "--enqueue-bifrost" :
options . enqueueBifrost = true ;
break ;
case "--bifrost-root" :
options . bifrostRoot = requireValue ( args , ++ index , arg ) ;
break ;
case "--bifrost-priority" :
options . bifrostPriority = parsePositiveInteger ( requireValue ( args , ++ index , arg ) , arg ) ;
break ;
2026-05-20 16:48:20 +01:00
case "--bifrost-title" :
options . bifrostTitle = requireValue ( args , ++ index , arg ) ;
break ;
2026-05-19 18:17:09 +01:00
case "--bifrost-store" :
options . bifrostStore = requireValue ( args , ++ index , arg ) ;
break ;
2026-05-19 17:03:28 +01:00
case "--include-bot-prompts" :
options . includeBotPrompts = true ;
break ;
case "--help" :
printHelpAndExit ( ) ;
break ;
default :
throw new Error ( ` Unknown argument: ${ arg } ` ) ;
}
}
if ( options . after && Number . isNaN ( Date . parse ( options . after ) ) ) {
throw new Error ( "--after must be an ISO timestamp." ) ;
}
return options ;
}
2026-05-19 18:17:09 +01:00
function enqueueBifrostRequest ( { options , packet , outputPath , promptPath , selected } ) {
const bifrostRoot = resolve ( options . bifrostRoot ? ? "E:/Projects/Bifrost" ) ;
const transportCli = resolve ( bifrostRoot , "tools/agent-transport.mjs" ) ;
if ( ! existsSync ( transportCli ) ) {
throw new Error ( ` Bifrost transport CLI was not found at ${ transportCli } . ` ) ;
}
const targetRepo = options . repo ? ? inferRepoNameFromCwd ( options . cwd ) ;
if ( ! targetRepo ) {
throw new Error ( "--enqueue-bifrost requires --repo or a target --cwd with a final path segment." ) ;
}
2026-05-20 16:48:20 +01:00
const title = options . bifrostTitle ? ? buildBifrostTitle ( targetRepo , options . task , options . agent ) ;
2026-05-19 18:17:09 +01:00
const priority = String ( options . bifrostPriority ? ? 70 ) ;
const sourceMessageIds = selected
. map ( ( message ) => message . id )
. filter ( Boolean )
. join ( "," ) ;
const args = [
transportCli ,
"enqueue" ,
"--repo" ,
targetRepo ,
"--agent" ,
options . agent ,
"--title" ,
title ,
"--request-file" ,
outputPath ,
"--priority" ,
priority ,
"--source-kind" ,
"discord_consensus" ,
"--source-channel-id" ,
options . channelId ,
"--packet-path" ,
outputPath ,
"--prompt-path" ,
promptPath ,
"--created-by" ,
"voidbot" ,
] ;
if ( sourceMessageIds . length > 0 ) {
args . push ( "--source-message-ids" , sourceMessageIds ) ;
}
if ( options . bifrostStore ) {
args . push ( "--store" , resolve ( options . bifrostStore ) ) ;
}
const result = spawnSync ( process . execPath , args , {
cwd : bifrostRoot ,
encoding : "utf8" ,
stdio : [ "ignore" , "pipe" , "pipe" ] ,
windowsHide : true ,
} ) ;
if ( result . status !== 0 ) {
throw new Error (
` Bifrost enqueue failed with exit ${ result . status ? ? "unknown" } : ${ result . stderr || result . stdout } ` ,
) ;
}
process . stdout . write ( ` Enqueued Bifrost intake request: \n ${ result . stdout } ` ) ;
}
2026-05-20 16:48:20 +01:00
function readLiveDiscordMessages ( env , options ) {
const token = process . env . DISCORD _BOT _TOKEN ? ? env . DISCORD _BOT _TOKEN ;
if ( ! token ) {
throw new Error ( "--live-discord requires DISCORD_BOT_TOKEN in the environment or .env." ) ;
}
const limit = Math . max ( 1 , Math . min ( 100 , options . limit ) ) ;
const url = new URL ( ` https://discord.com/api/v10/channels/ ${ options . channelId } /messages ` ) ;
url . searchParams . set ( "limit" , String ( limit ) ) ;
const child = spawnSync (
process . execPath ,
[
"--input-type=module" ,
"-e" ,
`
const response = await fetch ( $ { JSON . stringify ( url . toString ( ) ) } , {
headers : { Authorization : $ { JSON . stringify ( ` Bot ${ token } ` ) } } ,
} ) ;
if ( ! response . ok ) {
console . error ( await response . text ( ) ) ;
process . exit ( response . status || 1 ) ;
}
process . stdout . write ( JSON . stringify ( await response . json ( ) ) ) ;
` ,
] ,
{
encoding : "utf8" ,
stdio : [ "ignore" , "pipe" , "pipe" ] ,
windowsHide : true ,
} ,
) ;
if ( child . status !== 0 ) {
throw new Error ( ` Discord live fetch failed with exit ${ child . status ? ? "unknown" } : ${ child . stderr || child . stdout } ` ) ;
}
return JSON . parse ( child . stdout ) . map ( ( message ) => ( {
id : message . id ,
channelId : options . channelId ,
authorId : message . author ? . id ? ? "" ,
authorName : message . author ? . global _name ? ? message . member ? . nick ? ? message . author ? . username ? ? "unknown" ,
content : message . content ? ? "" ,
timestamp : message . timestamp ,
} ) ) ;
}
function buildBifrostTitle ( repo , task , agent ) {
const topic = summarizeTaskForTitle ( task ) ;
return topic || ` update request for ${ agent } in ${ repo } ` ;
}
function summarizeTaskForTitle ( task ) {
const normalized = String ( task ? ? "" ) . replace ( /\s+/g , " " ) . trim ( ) ;
if ( ! normalized ) {
return "" ;
}
const match =
normalized . match ( /^Implement the ([^:]+?) as /i ) ? ?
normalized . match ( /^Update ([^:]+?) from /i ) ? ?
normalized . match ( /^Add (.+?)(?:\.|$)/i ) ;
return truncateTitle ( stripTrailingRepoName ( match ? . [ 1 ] ? ? normalized ) , 96 ) ;
}
function truncateTitle ( value , maxLength ) {
return value . length > maxLength ? ` ${ value . slice ( 0 , maxLength - 3 ) } ... ` : value ;
}
function stripTrailingRepoName ( value ) {
return value . replace ( /\s+for\s+[A-Z][A-Za-z0-9._-]+$/g , "" ) ;
}
2026-05-19 17:03:28 +01:00
function buildConsensusPacket ( messages , options , archivePath ) {
const generatedAt = new Date ( ) . toISOString ( ) ;
const transcript = messages . map ( formatTranscriptLine ) . join ( "\n" ) ;
const consensusCandidates = extractConsensusCandidates ( messages ) ;
const openQuestions = extractOpenQuestions ( messages ) ;
const action = options . task ? . trim ( ) || "Read the recent chat consensus and take the next useful repo-local action." ;
return [
` # Recent Chat Consensus Packet ` ,
` ` ,
` Generated: ${ generatedAt } ` ,
` Target agent: ${ options . agent } ` ,
options . repo ? ` Target repo: ${ options . repo } ` : undefined ,
` Target workspace: ${ resolve ( options . cwd ) } ` ,
` Source channel: ${ options . channelId } ` ,
options . after ? ` Source window: after ${ options . after } ` : ` Source window: last ${ options . hours } hour(s) ` ,
` Archive: ${ archivePath } ` ,
` Messages included: ${ messages . length } ` ,
` ` ,
` ## What Needs To Be Done ` ,
` ` ,
action ,
` ` ,
` ## Consensus To Carry Forward ` ,
` ` ,
consensusCandidates . length > 0
? consensusCandidates . map ( ( item ) => ` - ${ item } ` ) . join ( "\n" )
: "- No explicit consensus markers were detected. Treat the transcript below as the authority." ,
` ` ,
` ## Open Questions ` ,
` ` ,
openQuestions . length > 0
? openQuestions . map ( ( item ) => ` - ${ item } ` ) . join ( "\n" )
: "- No explicit question marks were detected in the selected window." ,
` ` ,
` ## Instructions For The Target Codex Agent ` ,
` ` ,
` Use this packet as fresh local consensus from Discord. Prefer repo-local evidence before making edits. If the packet contains a concrete request, act in the target workspace. If it is not enough to act, write down the smallest missing question instead of pretending consensus is stronger than it is. ` ,
` ` ,
` ## Recent Transcript ` ,
` ` ,
"```text" ,
transcript || "(no messages selected)" ,
"```" ,
` ` ,
]
. filter ( ( line ) => line !== undefined )
. join ( "\n" ) ;
}
function buildCodexPrompt ( packet , packetPath , options ) {
2026-05-21 10:26:07 +01:00
return renderPrompt ( "feed-codex-chat-consensus.prompt.md" , {
agent : options . agent ,
repo : options . repo ? ? "" ,
packetPath ,
2026-05-19 17:03:28 +01:00
packet ,
2026-05-21 10:26:07 +01:00
} ) ;
}
function renderPrompt ( name , variables ) {
let template = readFileSync ( resolve ( promptsRoot , name ) , "utf8" ) ;
template = template . replace ( /\{\{#if\s+([A-Za-z0-9_-]+)\s*\}\}([\s\S]*?)\{\{\/if\}\}/g , ( _match , key , body ) =>
variables [ key ] ? body : "" ,
) ;
return template . replace ( /\{\{\s*([A-Za-z0-9_-]+)\s*\}\}/g , ( _match , key ) => String ( variables [ key ] ? ? "" ) ) . trim ( ) ;
2026-05-19 17:03:28 +01:00
}
function extractConsensusCandidates ( messages ) {
const markers = [
/\bsounds good\b/i ,
/\blove it\b/i ,
/\bdefinitely\b/i ,
/\bneed\b/i ,
/\bshould\b/i ,
/\bisn't\b/i ,
/\bis not\b/i ,
/\bis the\b/i ,
/\bcan\b/i ,
] ;
return messages
. filter ( ( message ) => markers . some ( ( marker ) => marker . test ( message . content ? ? "" ) ) )
. map ( ( message ) => ` ${ message . authorName } : ${ singleLine ( message . content ) } ` )
. slice ( - 12 ) ;
}
function extractOpenQuestions ( messages ) {
return messages
. filter ( ( message ) => ( message . content ? ? "" ) . includes ( "?" ) )
. map ( ( message ) => ` ${ message . authorName } : ${ singleLine ( message . content ) } ` )
. slice ( - 12 ) ;
}
function filterMessages ( messages , options ) {
const lowerBoundMs = options . after
? Date . parse ( options . after )
: Date . now ( ) - options . hours * 60 * 60 * 1000 ;
return messages
. filter ( ( message ) => ! message . deletedAt )
. filter ( ( message ) => ( options . includeBotPrompts ? true : readMessageKind ( message ) !== "bot_prompt" ) )
. filter ( ( message ) => ( options . channelId ? message . channelId === options . channelId : true ) )
. filter ( ( message ) => {
const timestampMs = Date . parse ( message . timestamp ? ? "" ) ;
return Number . isFinite ( timestampMs ) && timestampMs > lowerBoundMs ;
} )
. sort ( ( left , right ) => Date . parse ( left . timestamp ) - Date . parse ( right . timestamp ) ) ;
}
function readArchiveMessages ( archivePath ) {
const raw = stripLeadingBom ( readFileSync ( archivePath , "utf8" ) ) ;
const parsed = JSON . parse ( raw ) ;
if ( ! parsed || typeof parsed !== "object" || ! Array . isArray ( parsed . messages ) ) {
throw new Error ( ` Archive at ${ archivePath } does not have a messages array. ` ) ;
}
return parsed . messages ;
}
function readLocalEnv ( ) {
const envPath = resolve ( repoRoot , ".env" ) ;
if ( ! existsSync ( envPath ) ) {
return { } ;
}
return parseDotEnv ( stripLeadingBom ( readFileSync ( envPath , "utf8" ) ) ) ;
}
function parseDotEnv ( raw ) {
const env = { } ;
for ( const line of raw . split ( /\r?\n/ ) ) {
const trimmed = line . trim ( ) ;
if ( ! trimmed || trimmed . startsWith ( "#" ) ) {
continue ;
}
const separatorIndex = trimmed . indexOf ( "=" ) ;
if ( separatorIndex === - 1 ) {
continue ;
}
const key = trimmed . slice ( 0 , separatorIndex ) . trim ( ) ;
let value = trimmed . slice ( separatorIndex + 1 ) . trim ( ) ;
if (
value . length >= 2 &&
( ( value . startsWith ( '"' ) && value . endsWith ( '"' ) ) ||
( value . startsWith ( "'" ) && value . endsWith ( "'" ) ) )
) {
value = value . slice ( 1 , - 1 ) ;
}
env [ key ] = value ;
}
return env ;
}
function readMessageKind ( message ) {
return typeof message . messageKind === "string"
? message . messageKind
: typeof message . metadata ? . messageKind === "string"
? message . metadata . messageKind
: "default" ;
}
function formatTranscriptLine ( message ) {
return ` [ ${ message . timestamp } ] ${ message . authorName } ( ${ message . authorId } ): ${ singleLine ( message . content ) } ` ;
}
function singleLine ( value ) {
return String ( value ? ? "" ) . replace ( /\s+/g , " " ) . trim ( ) ;
}
function stripLeadingBom ( value ) {
return value . charCodeAt ( 0 ) === 0xfeff ? value . slice ( 1 ) : value ;
}
function requireValue ( args , index , flag ) {
const value = args [ index ] ;
if ( ! value || value . startsWith ( "--" ) ) {
throw new Error ( ` ${ flag } requires a value. ` ) ;
}
return value ;
}
function parsePositiveNumber ( value , flag ) {
const parsed = Number . parseFloat ( value ) ;
if ( ! Number . isFinite ( parsed ) || parsed <= 0 ) {
throw new Error ( ` ${ flag } must be a positive number. ` ) ;
}
return parsed ;
}
function parsePositiveInteger ( value , flag ) {
const parsed = Number . parseInt ( value , 10 ) ;
if ( ! Number . isInteger ( parsed ) || parsed <= 0 ) {
throw new Error ( ` ${ flag } must be a positive integer. ` ) ;
}
return parsed ;
}
2026-05-19 18:17:09 +01:00
function inferRepoNameFromCwd ( cwd ) {
const normalized = resolve ( cwd ) ;
return normalized . split ( /[\\/]+/ ) . filter ( Boolean ) . at ( - 1 ) ;
}
2026-05-19 17:03:28 +01:00
function timestampSlug ( date ) {
return date . toISOString ( ) . replace ( /[:.]/g , "-" ) ;
}
function slugify ( value ) {
return String ( value ? ? "codex" )
. trim ( )
. toLowerCase ( )
. replace ( /[^a-z0-9._-]+/g , "-" )
. replace ( /^-+|-+$/g , "" )
. slice ( 0 , 80 ) || "codex" ;
}
function splitCommandArgs ( value ) {
return value
. split ( "," )
. map ( ( entry ) => entry . trim ( ) )
. filter ( Boolean ) ;
}
function printHelpAndExit ( ) {
process . stdout . write ( ` Usage:
node scripts / feed - codex - chat - consensus . mjs -- agent nibu -- cwd E : / P r o j e c t s / A e t h e r i a L o r e - - t a s k " U p d a t e N i b u l o r e n o t e s f r o m t h i s c o n s e n s u s "
Options :
-- agent < name > Target agent label .
-- cwd < path > Target workspace . Defaults to this repo .
-- task < text > Instruction embedded in the consensus packet .
-- channel - id < id > Discord channel id . Defaults to Aquarium .
-- hours < number > Recent time window . Defaults to 3.
-- after < iso > Use messages after an ISO timestamp instead of -- hours .
-- limit < number > Max messages . Defaults to 80.
-- out < path > Packet output path .
2026-05-20 16:48:20 +01:00
-- live - discord Read the latest channel messages from Discord API instead of the archive .
2026-05-19 17:03:28 +01:00
-- execute Feed the generated prompt to codex exec .
2026-05-19 18:17:09 +01:00
-- enqueue - bifrost Enqueue the generated packet into Bifrost intake .
-- bifrost - root < path > Bifrost repo root . Defaults to E : / P r o j e c t s / B i f r o s t .
-- bifrost - priority < number > Priority for -- enqueue - bifrost . Defaults to 70.
2026-05-20 16:48:20 +01:00
-- bifrost - title < text > Explicit title for the queued Bifrost request .
2026-05-19 18:17:09 +01:00
-- bifrost - store < path > Override Bifrost . cc store path , mainly for tests .
2026-05-19 17:03:28 +01:00
-- sandbox < profile > Codex sandbox for -- execute . Defaults to read - only .
` );
process . exit ( 0 ) ;
}
main ( ) ;