2026-05-20 16:48:20 +01:00
#!/usr/bin/env node
import { spawn , spawnSync } from "node:child_process" ;
import { existsSync , mkdirSync , readFileSync , writeFileSync } from "node:fs" ;
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 bifrostRoot = resolve ( scriptDir , ".." ) ;
const transportCli = resolve ( bifrostRoot , "tools" , "agent-transport.mjs" ) ;
const bridgeCli = resolve ( bifrostRoot , "tools" , "bifrost-bridge.mjs" ) ;
const defaultStatusDir = resolve ( bifrostRoot , ".bifrost" , "agent-dispatch" ) ;
const defaultProjectsRoot = resolve ( bifrostRoot , ".." ) ;
const defaultAquariumChannelId = "1501196543150264332" ;
const defaultPersonaName = "Bifrost" ;
2026-05-20 22:38:38 +01:00
const defaultPersonaAvatarUrl =
"https://raw.githubusercontent.com/GameCult/Bifrost/main/src/Bifrost.Web/wwwroot/img/bifrost-profile.png" ;
2026-05-20 16:48:20 +01:00
async function main ( ) {
2026-06-22 14:03:07 +01:00
loadBifrostLocalEnv ( ) ;
2026-05-20 16:48:20 +01:00
const [ command , ... rawArgs ] = process . argv . slice ( 2 ) ;
const options = parseArgs ( rawArgs ) ;
if ( ! command || command === "help" || command === "--help" || command === "-h" ) {
printHelp ( ) ;
return ;
}
2026-06-17 05:56:25 +01:00
if ( command === "dispatch" || command === "run-claimed" ) {
ensureDispatchReceiptGate ( options ) ;
}
2026-05-20 16:48:20 +01:00
switch ( command ) {
case "dispatch" :
await dispatchQueuedRequests ( options ) ;
return ;
case "run-claimed" :
await runClaimedRequest ( options ) ;
return ;
default :
throw new Error ( ` Unknown command " ${ command } ". Run "node tools/dispatch-agent-requests.mjs help". ` ) ;
}
}
2026-06-22 14:03:07 +01:00
function loadBifrostLocalEnv ( ) {
if ( process . env . BIFROST _SKIP _LOCAL _ENV === "true" ) {
return ;
}
loadLocalEnv ( resolve ( bifrostRoot , ".env" ) ) ;
loadLocalEnv ( resolve ( defaultProjectsRoot , "VoidBot" , ".env" ) ) ;
}
2026-05-20 16:48:20 +01:00
async function dispatchQueuedRequests ( options ) {
const max = parseInteger ( options . max ? ? "1" , "max" ) ;
const statusDir = resolveOptionPath ( options [ "status-dir" ] ? ? defaultStatusDir ) ;
await mkdir ( statusDir , { recursive : true } ) ;
const dispatched = [ ] ;
for ( let index = 0 ; index < max ; index += 1 ) {
const request = claimNextRequest ( options ) ;
if ( ! request ) {
break ;
}
const repoRoot = resolveRepoRoot ( request , options ) ;
const runDir = resolve ( statusDir , request . id ) ;
mkdirSync ( runDir , { recursive : true } ) ;
const requestPath = resolve ( runDir , "request.json" ) ;
const promptPath = resolve ( runDir , "prompt.md" ) ;
const logPath = resolve ( runDir , "codex.log" ) ;
writeFileSync ( requestPath , ` ${ JSON . stringify ( request , null , 2 ) } \n ` , "utf8" ) ;
writeFileSync ( promptPath , buildCodexPrompt ( request , repoRoot ) , "utf8" ) ;
const child = spawn (
process . execPath ,
[
fileURLToPath ( import . meta . url ) ,
"run-claimed" ,
"--request-file" , requestPath ,
"--repo-root" , repoRoot ,
"--prompt-file" , promptPath ,
"--log" , logPath ,
... optionalArg ( "--codex-executable" , options [ "codex-executable" ] ) ,
... optionalArg ( "--codex-exec-args" , options [ "codex-exec-args" ] ) ,
... optionalArg ( "--model" , options . model ) ,
... optionalArg ( "--reasoning-effort" , options [ "reasoning-effort" ] ) ,
... optionalArg ( "--sandbox" , options . sandbox ) ,
... optionalArg ( "--launch-mode" , options [ "launch-mode" ] ) ,
... optionalArg ( "--channel-id" , options [ "channel-id" ] ) ,
... optionalArg ( "--persona-name" , options [ "persona-name" ] ) ,
... optionalArg ( "--persona-avatar-url" , options [ "persona-avatar-url" ] ) ,
... ( options [ "no-discord" ] === "true" ? [ "--no-discord" , "true" ] : [ ] ) ,
] ,
{
cwd : bifrostRoot ,
detached : true ,
stdio : "ignore" ,
windowsHide : true ,
} ,
) ;
child . unref ( ) ;
const dispatchRecord = {
requestId : request . id ,
title : request . title ,
repoRoot ,
promptPath ,
logPath ,
pid : child . pid ,
dispatchedAt : new Date ( ) . toISOString ( ) ,
} ;
writeFileSync ( resolve ( runDir , "dispatch.json" ) , ` ${ JSON . stringify ( dispatchRecord , null , 2 ) } \n ` , "utf8" ) ;
dispatched . push ( dispatchRecord ) ;
}
printJson ( {
ok : true ,
dispatchedCount : dispatched . length ,
dispatched ,
} ) ;
}
2026-06-17 05:56:25 +01:00
function ensureDispatchReceiptGate ( options ) {
2026-07-15 22:16:07 +01:00
if ( wantsUnreceiptedActivity ( options ) ) {
throw new Error (
"Bifrost dispatch cannot use --allow-unreceipted-activity or BIFROST_ALLOW_UNRECEIPTED_ACTIVITY. " +
"Use the typed Bifrost CultCache/CultMesh dispatch store instead of launching off-book work." ,
) ;
2026-06-17 05:56:25 +01:00
}
2026-07-15 22:16:07 +01:00
if ( hasRemovedHttpBridgeConfig ( ) ) {
throw new Error (
"BIFROST_BRIDGE_BASE_URL / BIFROST_BRIDGE_TOKEN were removed. " +
"Bifrost dispatch receipts are written to the typed CultCache/CultMesh request/run artifacts." ,
) ;
}
2026-06-17 05:56:25 +01:00
}
2026-07-15 22:16:07 +01:00
function hasRemovedHttpBridgeConfig ( ) {
return Boolean ( optionalString ( process . env . BIFROST _BRIDGE _BASE _URL ) || optionalString ( process . env . BIFROST _BRIDGE _TOKEN ) ) ;
2026-06-17 05:56:25 +01:00
}
2026-07-15 22:16:07 +01:00
function wantsUnreceiptedActivity ( options ) {
2026-06-17 05:56:25 +01:00
return options [ "allow-unreceipted-activity" ] === "true" || process . env . BIFROST _ALLOW _UNRECEIPTED _ACTIVITY === "true" ;
}
2026-05-20 16:48:20 +01:00
async function runClaimedRequest ( options ) {
const requestPath = resolveOptionPath ( requireOption ( options , "request-file" ) ) ;
const repoRoot = resolveOptionPath ( requireOption ( options , "repo-root" ) ) ;
const promptPath = resolveOptionPath ( requireOption ( options , "prompt-file" ) ) ;
const logPath = resolveOptionPath ( requireOption ( options , "log" ) ) ;
const request = JSON . parse ( await readFile ( requestPath , "utf8" ) ) ;
const launchMode = options [ "launch-mode" ] ? ? process . env . BIFROST _CODEX _LAUNCH _MODE ? ? "app-server" ;
if ( launchMode === "codex-exec" ) {
await runClaimedViaCodexExec ( request , repoRoot , promptPath , logPath , options ) ;
return ;
}
if ( launchMode !== "app-server" ) {
throw new Error ( ` Unsupported --launch-mode " ${ launchMode } ". Use app-server or codex-exec. ` ) ;
}
await runClaimedViaAppServer ( request , repoRoot , promptPath , logPath , options ) ;
}
async function runClaimedViaCodexExec ( request , repoRoot , promptPath , logPath , options ) {
const codexExecutable = options [ "codex-executable" ] ? ? process . env . CODEX _EXECUTABLE ? ? "codex" ;
const codexExecArgs = splitCommandArgs ( options [ "codex-exec-args" ] ? ? process . env . CODEX _EXEC _ARGS ? ? "" ) ;
const model = options . model ? ? process . env . CODEX _MODEL ? ? "gpt-5.4" ;
const reasoningEffort = options [ "reasoning-effort" ] ? ? process . env . CODEX _MODEL _REASONING _EFFORT ? ? "medium" ;
const sandbox = options . sandbox ? ? "workspace-write" ;
const startedAt = new Date ( ) . toISOString ( ) ;
2026-06-17 04:03:14 +01:00
const resultPath = resolve ( dirname ( logPath ) , "result.json" ) ;
2026-05-20 16:48:20 +01:00
await mkdir ( dirname ( logPath ) , { recursive : true } ) ;
2026-06-17 09:13:26 +01:00
const bridgeContextEnv = buildBridgeContextEnv ( request , dirname ( logPath ) ) ;
2026-05-20 16:48:20 +01:00
appendLog ( logPath , ` started ${ startedAt } ` ) ;
appendLog ( logPath , ` request ${ request . id } : ${ request . title } ` ) ;
appendLog ( logPath , ` repoRoot ${ repoRoot } ` ) ;
2026-06-17 04:03:14 +01:00
let dispatchRun = null ;
2026-05-20 16:48:20 +01:00
2026-06-17 04:03:14 +01:00
try {
dispatchRun = await beginDispatchRun ( request , {
launchMode : "codex-exec" ,
workerProcessId : process . pid ,
threadId : "" ,
turnId : "" ,
logPath ,
resultPath ,
note : "Codex dispatch process started." ,
} ) ;
2026-05-20 16:48:20 +01:00
2026-06-17 04:03:14 +01:00
const prompt = await readFile ( promptPath , "utf8" ) ;
const result = spawnSync (
codexExecutable ,
[
... codexExecArgs ,
"exec" ,
"-m" , model ,
"-c" , 'approval_policy="never"' ,
"-c" , ` model_reasoning_effort= ${ JSON . stringify ( reasoningEffort ) } ` ,
"--skip-git-repo-check" ,
"-s" , sandbox ,
"-" ,
] ,
{
cwd : repoRoot ,
input : prompt ,
encoding : "utf8" ,
stdio : [ "pipe" , "pipe" , "pipe" ] ,
env : {
... process . env ,
... bridgeContextEnv ,
} ,
windowsHide : true ,
} ,
) ;
appendLog ( logPath , result . stdout ? ? "" ) ;
appendLog ( logPath , result . stderr ? ? "" ) ;
const launchError = result . error ? summarizeError ( result . error ) : "" ;
const ok = result . status === 0 && ! launchError ;
const closeStatus = ok ? "completed" : "cancelled" ;
const note = ok
? ` Codex dispatch completed from Bifrost. Log: ${ logPath } `
: launchError
? ` Codex dispatch failed before completion: ${ launchError } . Log: ${ logPath } `
: ` Codex dispatch failed with exit ${ result . status ? ? "unknown" } . Log: ${ logPath } ` ;
const close = runNodeJson ( [
transportCli ,
"close" ,
"--id" , request . id ,
"--status" , closeStatus ,
"--note" , note ,
] , bifrostRoot ) ;
if ( launchError ) {
await dispatchRun ? . fail ( {
threadId : "" ,
turnId : "" ,
resultPath ,
note ,
error : launchError ,
} ) ;
} else {
await dispatchRun ? . complete ( {
status : ok ? "Completed" : "Cancelled" ,
threadId : "" ,
turnId : "" ,
resultPath ,
note ,
} ) ;
}
await writeDispatchResult ( logPath , {
2026-05-20 16:48:20 +01:00
requestId : request . id ,
ok ,
exitCode : result . status ,
2026-06-17 04:03:14 +01:00
error : launchError ,
2026-05-20 16:48:20 +01:00
launchMode : "codex-exec" ,
startedAt ,
finishedAt : new Date ( ) . toISOString ( ) ,
close ,
2026-06-17 04:03:14 +01:00
} ) ;
} catch ( error ) {
const message = summarizeError ( error ) ;
const note = ` Codex dispatch failed before completion: ${ message } . Log: ${ logPath } ` ;
const close = runNodeJson ( [
transportCli ,
"close" ,
"--id" , request . id ,
"--status" , "cancelled" ,
"--note" , note ,
] , bifrostRoot ) ;
await dispatchRun ? . fail ( {
threadId : "" ,
turnId : "" ,
resultPath ,
note ,
error : message ,
} ) ;
await writeDispatchResult ( logPath , {
requestId : request . id ,
ok : false ,
error : message ,
launchMode : "codex-exec" ,
startedAt ,
finishedAt : new Date ( ) . toISOString ( ) ,
close ,
} ) ;
throw error ;
}
2026-05-20 16:48:20 +01:00
}
async function runClaimedViaAppServer ( request , repoRoot , promptPath , logPath , options ) {
const model = options . model ? ? process . env . CODEX _MODEL ? ? "gpt-5.4" ;
const reasoningEffort = options [ "reasoning-effort" ] ? ? process . env . CODEX _MODEL _REASONING _EFFORT ? ? "medium" ;
2026-06-17 10:08:16 +01:00
const sandbox = options . sandbox ? ? "workspace-write" ;
2026-05-20 16:48:20 +01:00
const startedAt = new Date ( ) . toISOString ( ) ;
const prompt = await readFile ( promptPath , "utf8" ) ;
2026-06-17 04:03:14 +01:00
const resultPath = resolve ( dirname ( logPath ) , "result.json" ) ;
2026-06-17 09:13:26 +01:00
await mkdir ( dirname ( logPath ) , { recursive : true } ) ;
2026-05-20 16:48:20 +01:00
const client = new CodexAppServerClient ( {
logPath ,
command : resolveCodexCommand ( options ) ,
2026-06-17 09:13:26 +01:00
env : buildBridgeContextEnv ( request , dirname ( logPath ) ) ,
2026-05-20 16:48:20 +01:00
} ) ;
appendLog ( logPath , ` started ${ startedAt } ` ) ;
appendLog ( logPath , ` request ${ request . id } : ${ request . title } ` ) ;
appendLog ( logPath , ` repoRoot ${ repoRoot } ` ) ;
appendLog ( logPath , "launchMode app-server" ) ;
let threadId ;
let turnId ;
2026-06-17 04:03:14 +01:00
const dispatchRun = await beginDispatchRun ( request , {
launchMode : "app-server" ,
workerProcessId : process . pid ,
threadId : "" ,
turnId : "" ,
logPath ,
resultPath ,
note : "Codex app-server launch started." ,
} ) ;
2026-05-20 16:48:20 +01:00
try {
await client . start ( ) ;
await client . request ( "initialize" , {
clientInfo : {
name : "bifrost-dispatcher" ,
title : "Bifrost Dispatcher" ,
version : "0.1.0" ,
} ,
capabilities : {
experimentalApi : true ,
} ,
} ) ;
const threadStart = await client . request ( "thread/start" , {
cwd : repoRoot ,
approvalPolicy : "never" ,
approvalsReviewer : "user" ,
sandbox ,
model ,
personality : "pragmatic" ,
ephemeral : false ,
sessionStartSource : "startup" ,
experimentalRawEvents : false ,
persistExtendedHistory : true ,
} ) ;
threadId = threadStart . thread ? . id ;
if ( ! threadId ) {
throw new Error ( "Codex app-server thread/start returned no thread id." ) ;
}
const turnStart = await client . request ( "turn/start" , {
threadId ,
cwd : repoRoot ,
approvalPolicy : "never" ,
approvalsReviewer : "user" ,
sandboxPolicy : sandboxPolicyFromMode ( sandbox ) ,
model ,
effort : reasoningEffort ,
personality : "pragmatic" ,
input : [
{
type : "text" ,
text : prompt ,
text _elements : [ ] ,
} ,
] ,
} ) ;
turnId = turnStart . turn ? . id ;
if ( ! turnId ) {
throw new Error ( "Codex app-server turn/start returned no turn id." ) ;
}
await writeFile (
resolve ( dirname ( logPath ) , "app-server-dispatch.json" ) ,
` ${ JSON . stringify ( {
requestId : request . id ,
launchMode : "app-server" ,
threadId ,
turnId ,
startedAt ,
} , null , 2 ) } \ n ` ,
"utf8" ,
) ;
if ( options [ "no-discord" ] !== "true" ) {
postDispatchReceipt ( request , { threadId , turnId , startedAt } , options ) ;
}
const completed = await client . waitForTurnCompleted ( threadId , turnId ) ;
const ok = completed . turn ? . status === "completed" ;
const closeStatus = ok ? "completed" : "cancelled" ;
const note = ok
? ` Codex app turn completed for ${ threadId } / ${ turnId } . `
: ` Codex app turn ended as ${ completed . turn ? . status ? ? "unknown" } for ${ threadId } / ${ turnId } . ` ;
const close = runNodeJson ( [
transportCli ,
"close" ,
"--id" , request . id ,
"--status" , closeStatus ,
"--note" , note ,
] , bifrostRoot ) ;
2026-06-17 04:03:14 +01:00
await dispatchRun ? . complete ( {
status : ok ? "Completed" : "Cancelled" ,
threadId ,
turnId ,
resultPath ,
note ,
} ) ;
2026-05-20 16:48:20 +01:00
await writeDispatchResult ( logPath , {
requestId : request . id ,
ok ,
launchMode : "app-server" ,
threadId ,
turnId ,
startedAt ,
finishedAt : new Date ( ) . toISOString ( ) ,
close ,
} ) ;
} catch ( error ) {
2026-06-17 04:03:14 +01:00
const message = summarizeError ( error ) ;
appendLog ( logPath , message ) ;
2026-05-20 16:48:20 +01:00
if ( ! threadId || ! turnId ) {
2026-06-17 04:03:14 +01:00
releaseClaimedRequest ( request . id , ` Visible Codex app turn did not start: ${ message } ` ) ;
2026-05-20 16:48:20 +01:00
} else {
runNodeJson ( [
transportCli ,
"close" ,
"--id" , request . id ,
"--status" , "cancelled" ,
"--note" , ` Codex app dispatch failed after turn start for ${ threadId } / ${ turnId } . ` ,
] , bifrostRoot ) ;
}
2026-06-17 04:03:14 +01:00
await dispatchRun ? . fail ( {
threadId : threadId ? ? "" ,
turnId : turnId ? ? "" ,
resultPath ,
note : "Codex dispatch failed." ,
error : message ,
} ) ;
2026-05-20 16:48:20 +01:00
await writeDispatchResult ( logPath , {
requestId : request . id ,
ok : false ,
launchMode : "app-server" ,
threadId ,
turnId ,
startedAt ,
finishedAt : new Date ( ) . toISOString ( ) ,
2026-06-17 04:03:14 +01:00
error : message ,
2026-05-20 16:48:20 +01:00
} ) ;
process . exitCode = 1 ;
} finally {
client . stop ( ) ;
}
}
function claimNextRequest ( options ) {
const repo = optionalString ( options . repo ) ;
const agent = optionalString ( options . agent ) ;
if ( ! repo || repo === "*" ) {
const queued = runNodeJson ( [
transportCli ,
"list" ,
"--status" , "queued" ,
... optionalArg ( "--store" , options . store ) ,
] , bifrostRoot ) ;
const next = queued [ 0 ] ;
if ( ! next ) {
return null ;
}
return runNodeJson ( [
transportCli ,
"claim" ,
"--repo" , next . targetRepoName ,
... optionalArg ( "--agent" , agent ? ? next . targetAgentIdentity ) ,
"--claimed-by" , options [ "claimed-by" ] ? ? "bifrost-dispatcher" ,
... optionalArg ( "--store" , options . store ) ,
] , bifrostRoot ) ;
}
const args = [
transportCli ,
"claim" ,
"--repo" , repo ,
... optionalArg ( "--agent" , agent ) ,
"--claimed-by" , options [ "claimed-by" ] ? ? "bifrost-dispatcher" ,
... optionalArg ( "--store" , options . store ) ,
] ;
return runNodeJson ( args , bifrostRoot ) ;
}
function resolveRepoRoot ( request , options ) {
if ( options [ "repo-root" ] ) {
return resolveOptionPath ( options [ "repo-root" ] ) ;
}
const projectsRoot = resolveOptionPath ( options [ "projects-root" ] ? ? defaultProjectsRoot ) ;
return resolve ( projectsRoot , request . targetRepoName ) ;
}
function buildCodexPrompt ( request , repoRoot ) {
return ` You are ${ request . targetAgentIdentity ? ? "Codex" } , receiving a Bifrost-dispatched update request.
Target repo : $ { request . targetRepoName }
Target workspace : $ { repoRoot }
Bifrost request id : $ { request . id }
Priority : $ { request . priority }
Work the request in this workspace . Prefer a small coherent change , create or update tests / docs when appropriate , and leave the repo in a reviewable state . If this is only an analysis / proposal request , produce the artifact requested . Bifrost will close the transport request after this Codex turn exits .
Transport policy : do not push to GitHub , open a pull request , or publish external changes unless this specific request explicitly asks for that . A Bifrost - dispatched turn may prepare local changes and report the commit / PR command it would run , but the bridge owns publication policy .
2026-06-17 04:03:14 +01:00
If this request explicitly authorizes a governed crossing and you use \ ` tools/bifrost-bridge.mjs \` , the dispatch runtime already provides bridge provenance for request \` ${ request . id } \` . Preserve it.
2026-05-20 16:48:20 +01:00
$ { request . requestMarkdown }
` ;
}
function postDispatchReceipt ( request , dispatchRecord , options ) {
2026-05-20 22:38:38 +01:00
const channelId = resolveDispatchReceiptChannelId ( options ) ;
const personaName = options [ "persona-name" ] ? ? process . env . BIFROST _DISCORD _PERSONA _NAME ? ? defaultPersonaName ;
const personaAvatarUrl =
optionalString ( options [ "persona-avatar-url" ] ) ? ?
optionalString ( process . env . BIFROST _DISCORD _PERSONA _AVATAR _URL ) ? ?
optionalString ( process . env . DISCORD _PERSONA _AVATAR _URL _BIFROST ) ? ?
defaultPersonaAvatarUrl ;
2026-05-21 00:57:12 +01:00
const content = renderDispatchReceiptContent ( request ) ;
2026-05-20 16:48:20 +01:00
runNodeJson ( [
bridgeCli ,
"discord-post" ,
"--channel-id" , channelId ,
"--persona-name" , personaName ,
"--content" , content ,
2026-06-17 04:03:14 +01:00
"--source-kind" , "bifrost_agent_transport_request" ,
"--source-id" , request . id ,
"--authority-ref" , "bifrost_dispatch_started_receipt" ,
... optionalArg ( "--target-repository-full-name" , request . targetRepositoryFullName ) ,
2026-05-20 22:38:38 +01:00
... optionalArg ( "--persona-avatar-url" , personaAvatarUrl ) ,
2026-06-17 12:06:20 +01:00
... bridgeRecoveryArgs ( options ) ,
2026-05-20 16:48:20 +01:00
] , bifrostRoot ) ;
}
2026-05-20 22:38:38 +01:00
function resolveDispatchReceiptChannelId ( options ) {
return (
optionalString ( options [ "channel-id" ] ) ? ?
optionalString ( process . env . BIFROST _DISCORD _CHANNEL _ID ) ? ?
optionalString ( process . env . DISCORD _BIFROST _CHANNEL _ID ) ? ?
defaultAquariumChannelId
) ;
}
2026-06-17 12:06:20 +01:00
function bridgeRecoveryArgs ( options ) {
2026-07-15 22:16:07 +01:00
return [ ] ;
2026-06-17 12:06:20 +01:00
}
2026-05-21 00:57:12 +01:00
function renderDispatchReceiptContent ( request ) {
const topic = summarizeRequestTopic ( request ) ;
const actor = request . targetAgentIdentity
? ` ${ request . targetAgentIdentity } / ${ request . targetRepoName } `
: request . targetRepoName ;
return [
2026-05-21 19:55:53 +01:00
` Bifrost Codex dispatch started ` ,
2026-05-21 00:57:12 +01:00
` ` ,
` ** ${ request . targetRepoName } : ${ topic } ** ` ,
2026-05-21 19:55:53 +01:00
` Request: \` ${ request . id } \` ` ,
` Target: ${ actor } ` ,
` Status: claimed by dispatcher ` ,
` Codex job: started ` ,
2026-05-21 00:57:12 +01:00
` ` ,
2026-05-21 19:55:53 +01:00
` Codex has started a turn. The claim is now specific enough to inspect, and the work is no longer sitting unclaimed in chat. ` ,
2026-05-21 00:57:12 +01:00
` Keep discussion here if it sharpens this request; Bifrost will carry the result back across the bridge. ` ,
] . join ( "\n" ) ;
}
2026-06-17 09:13:26 +01:00
function buildBridgeContextEnv ( request , runtimeDir ) {
2026-06-17 06:51:08 +01:00
const hooksPath = resolve ( bifrostRoot , "tools" , "git-hooks" ) ;
2026-06-17 07:53:38 +01:00
const gitGatePath = resolve ( bifrostRoot , "tools" , "git-gate" ) ;
2026-06-17 09:13:26 +01:00
const githubConfigDir = resolve ( runtimeDir , "github-gh-config" ) ;
const gitGlobalConfigPath = resolve ( runtimeDir , "github-gitconfig" ) ;
mkdirSync ( githubConfigDir , { recursive : true } ) ;
if ( ! existsSync ( gitGlobalConfigPath ) ) {
writeFileSync ( gitGlobalConfigPath , "" , "utf8" ) ;
}
2026-06-17 04:03:14 +01:00
return {
BIFROST _BRIDGE _SOURCE _KIND : "bifrost_agent_transport_request" ,
BIFROST _BRIDGE _SOURCE _ID : request . id ,
BIFROST _BRIDGE _AUTHORITY _REF : "bifrost_dispatch_execution" ,
2026-06-17 06:51:08 +01:00
BIFROST _ENFORCE _GITHUB _GATE : "true" ,
2026-06-17 08:38:21 +01:00
BIFROST _LOCK _RECOVERY _HATCHES : "true" ,
2026-06-17 07:53:38 +01:00
BIFROST _GIT _EXECUTABLE : resolve ( gitGatePath , "git.cmd" ) ,
BIFROST _GH _EXECUTABLE : resolve ( gitGatePath , "gh.cmd" ) ,
BIFROST _NODE _EXECUTABLE : process . execPath ,
2026-06-17 06:51:08 +01:00
... buildGitHooksConfigEnv ( hooksPath ) ,
2026-06-17 07:53:38 +01:00
... buildPathPrependEnv ( gitGatePath ) ,
2026-06-17 09:13:26 +01:00
GH _CONFIG _DIR : githubConfigDir ,
GIT _CONFIG _GLOBAL : gitGlobalConfigPath ,
GH _TOKEN : "" ,
GITHUB _TOKEN : "" ,
GIT _ASKPASS : "" ,
SSH _ASKPASS : "" ,
SSH _AUTH _SOCK : "" ,
GIT _TERMINAL _PROMPT : "0" ,
GCM _INTERACTIVE : "never" ,
2026-06-17 06:51:08 +01:00
} ;
}
function buildGitHooksConfigEnv ( hooksPath ) {
2026-06-17 09:13:26 +01:00
const entries = [
[ "core.hooksPath" , hooksPath ] ,
[ "credential.helper" , "" ] ,
[ "credential.interactive" , "never" ] ,
] ;
2026-06-17 06:51:08 +01:00
const existingCount = Number . parseInt ( process . env . GIT _CONFIG _COUNT ? ? "" , 10 ) ;
2026-06-17 09:13:26 +01:00
const startIndex = Number . isInteger ( existingCount ) && existingCount >= 0 ? existingCount : 0 ;
const env = {
GIT _CONFIG _COUNT : String ( startIndex + entries . length ) ,
2026-06-17 04:03:14 +01:00
} ;
2026-06-17 09:13:26 +01:00
entries . forEach ( ( [ key , value ] , index ) => {
const slot = startIndex + index ;
env [ ` GIT_CONFIG_KEY_ ${ slot } ` ] = key ;
env [ ` GIT_CONFIG_VALUE_ ${ slot } ` ] = value ;
} ) ;
return env ;
2026-06-17 04:03:14 +01:00
}
2026-06-17 07:53:38 +01:00
function buildPathPrependEnv ( directory ) {
const currentPath = process . env . Path ? ? process . env . PATH ? ? "" ;
const nextPath = currentPath ? ` ${ directory } ; ${ currentPath } ` : directory ;
return {
PATH : nextPath ,
Path : nextPath ,
} ;
}
2026-05-20 16:48:20 +01:00
class CodexAppServerClient {
2026-06-17 04:03:14 +01:00
constructor ( { command , env , logPath } ) {
2026-05-20 16:48:20 +01:00
this . command = command ;
2026-06-17 04:03:14 +01:00
this . env = env ;
2026-05-20 16:48:20 +01:00
this . logPath = logPath ;
this . child = undefined ;
this . nextId = 1 ;
this . buffer = "" ;
this . pending = new Map ( ) ;
this . turnWaiters = [ ] ;
2026-06-17 10:08:16 +01:00
this . completedTurns = new Map ( ) ;
2026-05-20 16:48:20 +01:00
}
async start ( ) {
this . child = spawn ( this . command . exe , [ ... this . command . args , "app-server" , "--listen" , "stdio://" ] , {
cwd : bifrostRoot ,
2026-06-17 04:03:14 +01:00
env : {
... process . env ,
... this . env ,
} ,
2026-05-20 16:48:20 +01:00
stdio : [ "pipe" , "pipe" , "pipe" ] ,
windowsHide : true ,
} ) ;
this . child . stdout . on ( "data" , ( chunk ) => this . readStdout ( chunk . toString ( ) ) ) ;
this . child . stderr . on ( "data" , ( chunk ) => appendLog ( this . logPath , chunk . toString ( ) ) ) ;
this . child . on ( "exit" , ( code , signal ) => {
const error = new Error ( ` codex app-server exited with ${ code ? ? signal ? ? "unknown" } ` ) ;
for ( const waiter of this . pending . values ( ) ) {
waiter . reject ( error ) ;
}
this . pending . clear ( ) ;
for ( const waiter of this . turnWaiters ) {
waiter . reject ( error ) ;
}
this . turnWaiters = [ ] ;
2026-06-17 10:08:16 +01:00
this . completedTurns . clear ( ) ;
2026-05-20 16:48:20 +01:00
} ) ;
this . child . on ( "error" , ( error ) => {
for ( const waiter of this . pending . values ( ) ) {
waiter . reject ( error ) ;
}
this . pending . clear ( ) ;
2026-06-17 10:08:16 +01:00
for ( const waiter of this . turnWaiters ) {
waiter . reject ( error ) ;
}
this . turnWaiters = [ ] ;
this . completedTurns . clear ( ) ;
2026-05-20 16:48:20 +01:00
} ) ;
}
request ( method , params ) {
if ( ! this . child ? . stdin . writable ) {
return Promise . reject ( new Error ( "codex app-server stdin is not writable." ) ) ;
}
const id = this . nextId ;
this . nextId += 1 ;
appendLog ( this . logPath , ` client-request ${ method } # ${ id } ` ) ;
return new Promise ( ( resolvePromise , reject ) => {
const timer = setTimeout ( ( ) => {
this . pending . delete ( id ) ;
reject ( new Error ( ` Timed out waiting for ${ method } # ${ id } ` ) ) ;
} , 60000 ) ;
this . pending . set ( id , {
resolve : ( value ) => {
clearTimeout ( timer ) ;
resolvePromise ( value ) ;
} ,
reject : ( error ) => {
clearTimeout ( timer ) ;
reject ( error ) ;
} ,
} ) ;
2026-06-17 10:08:16 +01:00
this . child . stdin . write ( ` ${ JSON . stringify ( { id , method , params } )} \n ` ) ;
2026-05-20 16:48:20 +01:00
} ) ;
}
waitForTurnCompleted ( threadId , turnId ) {
2026-06-17 10:08:16 +01:00
const key = ` ${ threadId } : ${ turnId } ` ;
const completed = this . completedTurns . get ( key ) ;
if ( completed ) {
this . completedTurns . delete ( key ) ;
return Promise . resolve ( completed ) ;
}
2026-05-20 16:48:20 +01:00
return new Promise ( ( resolvePromise , reject ) => {
this . turnWaiters . push ( { threadId , turnId , resolve : resolvePromise , reject } ) ;
} ) ;
}
readStdout ( text ) {
this . buffer += text ;
while ( true ) {
const newline = this . buffer . indexOf ( "\n" ) ;
if ( newline === - 1 ) {
return ;
}
const line = this . buffer . slice ( 0 , newline ) . trim ( ) ;
this . buffer = this . buffer . slice ( newline + 1 ) ;
if ( line ) {
this . handleMessage ( line ) ;
}
}
}
handleMessage ( line ) {
let message ;
try {
message = JSON . parse ( line ) ;
} catch {
appendLog ( this . logPath , ` app-server non-json stdout: ${ line } ` ) ;
return ;
}
if ( Object . hasOwn ( message , "id" ) && ( Object . hasOwn ( message , "result" ) || Object . hasOwn ( message , "error" ) ) ) {
const waiter = this . pending . get ( message . id ) ;
if ( ! waiter ) {
appendLog ( this . logPath , ` app-server response for unknown id ${ message . id } ` ) ;
return ;
}
this . pending . delete ( message . id ) ;
if ( message . error ) {
waiter . reject ( new Error ( JSON . stringify ( message . error ) ) ) ;
} else {
waiter . resolve ( message . result ) ;
}
return ;
}
appendLog ( this . logPath , ` app-server ${ message . method ? ? "message" } ` ) ;
if ( message . method === "turn/completed" ) {
const params = message . params ? ? { } ;
const completedTurnId = params . turn ? . id ;
2026-06-17 10:08:16 +01:00
const key = ` ${ params . threadId ? ? "" } : ${ completedTurnId ? ? "" } ` ;
2026-05-20 16:48:20 +01:00
const remaining = [ ] ;
2026-06-17 10:08:16 +01:00
let resolved = false ;
2026-05-20 16:48:20 +01:00
for ( const waiter of this . turnWaiters ) {
if ( waiter . threadId === params . threadId && waiter . turnId === completedTurnId ) {
waiter . resolve ( params ) ;
2026-06-17 10:08:16 +01:00
resolved = true ;
2026-05-20 16:48:20 +01:00
} else {
remaining . push ( waiter ) ;
}
}
this . turnWaiters = remaining ;
2026-06-17 10:08:16 +01:00
if ( ! resolved && params . threadId && completedTurnId ) {
this . completedTurns . set ( key , params ) ;
}
2026-05-20 16:48:20 +01:00
}
}
stop ( ) {
this . child ? . kill ( ) ;
}
}
function resolveCodexCommand ( options ) {
const executable = options [ "codex-executable" ] ? ? process . env . CODEX _EXECUTABLE ;
const extraArgs = splitCommandArgs ( options [ "codex-exec-args" ] ? ? process . env . CODEX _EXEC _ARGS ? ? "" ) ;
if ( executable ) {
return { exe : executable , args : extraArgs } ;
}
const codexJs = resolve ( process . env . APPDATA ? ? "" , "npm" , "node_modules" , "@openai" , "codex" , "bin" , "codex.js" ) ;
if ( existsSync ( codexJs ) ) {
return { exe : process . execPath , args : [ codexJs ] } ;
}
return { exe : "codex" , args : [ ] } ;
}
function sandboxPolicyFromMode ( mode ) {
if ( mode === "danger-full-access" ) {
return { type : "dangerFullAccess" } ;
}
if ( mode === "read-only" ) {
2026-06-17 10:08:16 +01:00
return { type : "readOnly" , access : { type : "fullAccess" } , networkAccess : false } ;
2026-05-20 16:48:20 +01:00
}
return {
type : "workspaceWrite" ,
writableRoots : [ ] ,
readOnlyAccess : { type : "fullAccess" } ,
2026-06-17 10:08:16 +01:00
networkAccess : false ,
2026-05-20 16:48:20 +01:00
excludeTmpdirEnvVar : false ,
excludeSlashTmp : false ,
} ;
}
function releaseClaimedRequest ( id , note ) {
runNodeJson ( [
transportCli ,
"release" ,
"--id" , id ,
"--note" , note ,
] , bifrostRoot ) ;
}
async function writeDispatchResult ( logPath , result ) {
await writeFile (
resolve ( dirname ( logPath ) , "result.json" ) ,
` ${ JSON . stringify ( result , null , 2 ) } \n ` ,
"utf8" ,
) ;
}
2026-06-17 04:03:14 +01:00
async function beginDispatchRun ( request , input ) {
2026-07-15 22:16:07 +01:00
void request ;
void input ;
return null ;
2026-06-17 04:03:14 +01:00
}
function summarizeError ( error ) {
if ( error instanceof Error ) {
return error . stack ? ? error . message ;
}
return String ( error ) ;
}
2026-05-20 16:48:20 +01:00
function summarizeRequestTopic ( request ) {
const title = String ( request . title ? ? "" ) . trim ( ) ;
if ( title && ! /^Recent\b/i . test ( title ) ) {
return title ;
}
const text = String ( request . requestMarkdown ? ? "" ) ;
const section = text . match ( /## What Needs To Be Done\s+([\s\S]*?)(?:\n## |\n```|$)/i ) ? . [ 1 ] ? . trim ( ) ;
const firstParagraph = section
? . split ( /\n\s*\n/ )
. map ( ( entry ) => entry . replace ( /\s+/g , " " ) . trim ( ) )
. find ( Boolean ) ;
if ( firstParagraph ) {
return summarizeAction ( firstParagraph ) ;
}
const heading = text . match ( /^#\s+(.+)$/m ) ? . [ 1 ] ? . trim ( ) ;
return truncate ( heading || "routed update request" , 96 ) ;
}
function truncate ( value , maxLength ) {
return value . length > maxLength ? ` ${ value . slice ( 0 , maxLength - 3 ) } ... ` : value ;
}
function summarizeAction ( value ) {
const normalized = value . replace ( /\s+/g , " " ) . trim ( ) ;
const match =
normalized . match ( /^Implement the ([^:]+?) as /i ) ? ?
normalized . match ( /^Update ([^:]+?) from /i ) ? ?
normalized . match ( /^Add (.+?)(?:\.|$)/i ) ;
return truncate ( stripTrailingRepoName ( match ? . [ 1 ] ? ? normalized ) , 96 ) ;
}
function stripTrailingRepoName ( value ) {
return value . replace ( /\s+for\s+[A-Z][A-Za-z0-9._-]+$/g , "" ) ;
}
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 ) ;
}
function appendLog ( path , text ) {
mkdirSync ( dirname ( path ) , { recursive : true } ) ;
writeFileSync ( path , ` ${ text ? ? "" } \n ` , { encoding : "utf8" , flag : "a" } ) ;
}
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 requireOption ( options , name ) {
const value = optionalString ( options [ name ] ) ;
if ( ! value ) {
throw new Error ( ` Missing required option -- ${ name } . ` ) ;
}
return value ;
}
function optionalArg ( flag , value ) {
const normalized = optionalString ( value ) ;
return normalized ? [ flag , normalized ] : [ ] ;
}
function optionalString ( value ) {
return typeof value === "string" && value . trim ( ) . length > 0 ? value . trim ( ) : undefined ;
}
function parseInteger ( value , name ) {
const parsed = Number . parseInt ( value , 10 ) ;
if ( ! Number . isInteger ( parsed ) || parsed < 1 ) {
throw new Error ( ` -- ${ name } must be a positive integer. ` ) ;
}
return parsed ;
}
function splitCommandArgs ( value ) {
if ( typeof value !== "string" || value . trim ( ) . length === 0 ) {
return [ ] ;
}
return value
. split ( "," )
. map ( ( entry ) => entry . trim ( ) )
. filter ( Boolean ) ;
}
function resolveOptionPath ( path ) {
return resolve ( process . cwd ( ) , path ) ;
}
function printJson ( value ) {
process . stdout . write ( ` ${ JSON . stringify ( value , null , 2 ) } \n ` ) ;
}
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 ;
}
}
function printHelp ( ) {
process . stdout . write ( ` Bifrost agent request dispatcher
Commands :
dispatch Claim queued Bifrost requests and launch detached Codex turns
run - claimed Internal worker for one claimed request
Example :
node tools / dispatch - agent - requests . mjs dispatch -- repo AquaSynth -- agent aqua -- max 1
` );
}
main ( ) . catch ( ( error ) => {
process . stderr . write ( ` ${ error instanceof Error ? error . message : String ( error ) } \n ` ) ;
process . exitCode = 1 ;
} ) ;