2026-02-08 16:25:25 +09:00
import type { PluginContext } from "./types"
2026-03-06 22:37:41 +09:00
import { randomUUID } from "node:crypto"
2026-02-18 19:26:20 +01:00
import type { BackgroundManager } from "../features/background-agent"
2026-02-08 16:25:25 +09:00
import { getMainSessionID } from "../features/claude-code-session-state"
import { clearBoulderState } from "../features/boulder-state"
import { log } from "../shared"
2026-02-18 19:26:20 +01:00
import { stripInvisibleAgentCharacters , getAgentConfigKey } from "../shared/agent-display-names"
2026-02-13 12:02:40 +09:00
import { resolveSessionAgent } from "./session-agent-resolver"
2026-02-21 02:27:57 +09:00
import { parseRalphLoopArguments } from "../hooks/ralph-loop/command-arguments"
2026-03-06 22:00:14 +09:00
import { ULTRAWORK_VERIFICATION_PROMISE } from "../hooks/ralph-loop/constants"
2026-03-06 22:37:41 +09:00
import { readState , writeState } from "../hooks/ralph-loop/storage"
2026-02-08 16:25:25 +09:00
import type { CreatedHooks } from "../create-hooks"
2026-02-19 02:21:33 +01:00
import { COUNCIL_MEMBER_KEY_PREFIX } from "../agents/builtin-agents/council-member-agents"
2026-02-08 16:25:25 +09:00
2026-04-01 17:19:45 -07:00
function getLoopCommandArguments ( args : Record < string , unknown > , command : "ralph-loop" | "ulw-loop" ) : string {
const rawUserMessage = typeof args . user_message === "string" ? args . user_message . trim ( ) : ""
if ( rawUserMessage ) {
return rawUserMessage
}
const rawName = typeof args . name === "string" ? args . name : ""
return rawName . replace ( new RegExp ( ` ^/?( ${ command } ) \\ s* ` , "i" ) , "" )
}
2026-02-08 16:25:25 +09:00
export function createToolExecuteBeforeHandler ( args : {
ctx : PluginContext
hooks : CreatedHooks
2026-02-18 19:26:20 +01:00
backgroundManager? : Pick < BackgroundManager , "getTasksByParentSession" >
2026-02-08 16:25:25 +09:00
} ) : (
input : { tool : string ; sessionID : string ; callID : string } ,
output : { args : Record < string , unknown > } ,
) = > Promise < void > {
2026-02-18 19:26:20 +01:00
const { ctx , hooks , backgroundManager } = args
function hasPendingCouncilMembers ( sessionID : string ) : boolean {
if ( ! backgroundManager ) {
return false
}
const tasks = backgroundManager . getTasksByParentSession ( sessionID )
return tasks . some ( ( task ) = >
2026-02-20 16:10:08 +01:00
task . agent . startsWith ( COUNCIL_MEMBER_KEY_PREFIX ) &&
2026-02-18 19:26:20 +01:00
( task . status === "pending" || task . status === "running" )
)
}
2026-02-08 16:25:25 +09:00
2026-03-18 17:45:59 +09:00
function buildUltraworkOracleVerificationPrompt ( prompt : string , originalTask : string , verificationAttemptId : string ) : string {
const verificationPrompt = [
"You are verifying the active ULTRAWORK loop result for this session." ,
"" ,
"Original task:" ,
originalTask ,
"" ,
"Review the work skeptically and critically." ,
"Assume it may be incomplete, misleading, or subtly broken until the evidence proves otherwise." ,
"Look for missing scope, weak verification, process violations, hidden regressions, and any reason the task should NOT be considered complete." ,
"" ,
` If the work is fully complete, end your response with <promise> ${ ULTRAWORK_VERIFICATION_PROMISE } </promise>. ` ,
"If the work is not complete, explain the blocking issues clearly and DO NOT emit that promise." ,
"" ,
` <ulw_verification_attempt_id> ${ verificationAttemptId } </ulw_verification_attempt_id> ` ,
] . join ( "\n" )
return ` ${ prompt ? ` ${ prompt } \ n \ n ` : "" } ${ verificationPrompt } `
}
2026-02-08 16:25:25 +09:00
return async ( input , output ) : Promise < void > = > {
2026-03-24 19:17:05 +09:00
if ( input . tool . toLowerCase ( ) === "bash" && typeof output . args . command === "string" ) {
if ( output . args . command . includes ( "\x00" ) ) {
output . args . command = output . args . command . replace ( /\x00/g , "" )
log ( "[tool-execute-before] Stripped null bytes from bash command" , {
sessionID : input.sessionID ,
callID : input.callID ,
} )
}
2026-02-18 19:26:20 +01:00
}
const toolNameLower = input . tool ? . toLowerCase ( )
if ( toolNameLower === "question" || toolNameLower === "askuserquestion" || toolNameLower === "ask_user_question" || toolNameLower === "switch_agent" ) {
const sessionAgent = await resolveSessionAgent ( ctx . client , input . sessionID )
const sessionAgentKey = sessionAgent ? getAgentConfigKey ( sessionAgent ) : undefined
if ( sessionAgentKey === "athena" && hasPendingCouncilMembers ( input . sessionID ) ) {
throw new Error (
"Council members are still running. Wait for all launched members to finish and collect their outputs before asking next-step questions or switching agents."
)
}
2026-03-24 19:17:05 +09:00
}
2026-02-08 16:25:25 +09:00
await hooks . writeExistingFileGuard ? . [ "tool.execute.before" ] ? . ( input , output )
await hooks . questionLabelTruncator ? . [ "tool.execute.before" ] ? . ( input , output )
await hooks . claudeCodeHooks ? . [ "tool.execute.before" ] ? . ( input , output )
await hooks . nonInteractiveEnv ? . [ "tool.execute.before" ] ? . ( input , output )
2026-03-23 20:15:20 +09:00
await hooks . bashFileReadGuard ? . [ "tool.execute.before" ] ? . ( input , output )
2026-02-08 16:25:25 +09:00
await hooks . commentChecker ? . [ "tool.execute.before" ] ? . ( input , output )
await hooks . directoryAgentsInjector ? . [ "tool.execute.before" ] ? . ( input , output )
await hooks . directoryReadmeInjector ? . [ "tool.execute.before" ] ? . ( input , output )
await hooks . rulesInjector ? . [ "tool.execute.before" ] ? . ( input , output )
await hooks . tasksTodowriteDisabler ? . [ "tool.execute.before" ] ? . ( input , output )
2026-03-24 23:58:53 +09:00
await hooks . webfetchRedirectGuard ? . [ "tool.execute.before" ] ? . ( input , output )
2026-02-08 16:25:25 +09:00
await hooks . prometheusMdOnly ? . [ "tool.execute.before" ] ? . ( input , output )
await hooks . sisyphusJuniorNotepad ? . [ "tool.execute.before" ] ? . ( input , output )
await hooks . atlasHook ? . [ "tool.execute.before" ] ? . ( input , output )
2026-02-21 15:49:23 -07:00
const normalizedToolName = input . tool . toLowerCase ( )
if (
normalizedToolName === "question"
|| normalizedToolName === "ask_user_question"
|| normalizedToolName === "askuserquestion"
) {
2026-02-21 16:42:23 -07:00
const sessionID = input . sessionID || getMainSessionID ( )
2026-02-21 15:49:23 -07:00
await hooks . sessionNotification ? . ( {
event : {
type : "tool.execute.before" ,
properties : {
2026-02-21 16:42:23 -07:00
sessionID ,
2026-02-21 15:49:23 -07:00
tool : input.tool ,
args : output.args ,
} ,
} ,
} )
}
2026-02-08 16:25:25 +09:00
if ( input . tool === "task" ) {
const argsObject = output . args
const category = typeof argsObject . category === "string" ? argsObject.category : undefined
const subagentType = typeof argsObject . subagent_type === "string" ? argsObject.subagent_type : undefined
2026-02-13 12:02:40 +09:00
const sessionId = typeof argsObject . session_id === "string" ? argsObject.session_id : undefined
if ( category ) {
2026-02-08 16:25:25 +09:00
argsObject . subagent_type = "sisyphus-junior"
2026-02-13 12:02:40 +09:00
} else if ( ! subagentType && sessionId ) {
const resolvedAgent = await resolveSessionAgent ( ctx . client , sessionId )
argsObject . subagent_type = resolvedAgent ? ? "continue"
2026-02-08 16:25:25 +09:00
}
2026-03-06 22:00:14 +09:00
const normalizedSubagentType =
2026-04-15 10:46:41 +09:00
typeof argsObject . subagent_type === "string" ? stripInvisibleAgentCharacters ( argsObject . subagent_type ) : undefined
2026-03-06 22:00:14 +09:00
const prompt = typeof argsObject . prompt === "string" ? argsObject . prompt : ""
const loopState = typeof ctx . directory === "string" ? readState ( ctx . directory ) : null
const shouldInjectOracleVerification =
normalizedSubagentType === "oracle"
&& loopState ? . active === true
&& loopState . ultrawork === true
&& loopState . verification_pending === true
&& loopState . session_id === input . sessionID
if ( shouldInjectOracleVerification ) {
2026-03-06 22:37:41 +09:00
const verificationAttemptId = randomUUID ( )
2026-03-17 16:20:27 +09:00
log ( "[tool-execute-before] Injecting ULW oracle verification attempt" , {
sessionID : input.sessionID ,
callID : input.callID ,
verificationAttemptId ,
loopSessionID : loopState.session_id ,
} )
2026-03-06 22:37:41 +09:00
writeState ( ctx . directory , {
. . . loopState ,
verification_attempt_id : verificationAttemptId ,
verification_session_id : undefined ,
} )
2026-03-06 22:00:14 +09:00
argsObject . run_in_background = false
2026-03-18 17:45:59 +09:00
argsObject . prompt = buildUltraworkOracleVerificationPrompt (
prompt ,
loopState . prompt ,
verificationAttemptId ,
)
2026-03-06 22:00:14 +09:00
}
2026-02-08 16:25:25 +09:00
}
2026-02-18 18:40:10 +08:00
if ( hooks . ralphLoop && input . tool === "skill" ) {
const rawName = typeof output . args . name === "string" ? output.args.name : undefined
const command = rawName ? . replace ( /^\// , "" ) . toLowerCase ( )
2026-02-08 16:25:25 +09:00
const sessionID = input . sessionID || getMainSessionID ( )
if ( command === "ralph-loop" && sessionID ) {
2026-04-01 17:19:45 -07:00
const rawArgs = getLoopCommandArguments ( output . args , "ralph-loop" )
2026-02-21 02:27:57 +09:00
const parsedArguments = parseRalphLoopArguments ( rawArgs )
2026-02-08 16:25:25 +09:00
2026-02-21 02:27:57 +09:00
hooks . ralphLoop . startLoop ( sessionID , parsedArguments . prompt , {
maxIterations : parsedArguments.maxIterations ,
completionPromise : parsedArguments.completionPromise ,
strategy : parsedArguments.strategy ,
2026-02-08 16:25:25 +09:00
} )
} else if ( command === "cancel-ralph" && sessionID ) {
hooks . ralphLoop . cancelLoop ( sessionID )
} else if ( command === "ulw-loop" && sessionID ) {
2026-04-01 17:19:45 -07:00
const rawArgs = getLoopCommandArguments ( output . args , "ulw-loop" )
2026-02-21 02:27:57 +09:00
const parsedArguments = parseRalphLoopArguments ( rawArgs )
2026-02-08 16:25:25 +09:00
2026-02-21 02:27:57 +09:00
hooks . ralphLoop . startLoop ( sessionID , parsedArguments . prompt , {
2026-02-08 16:25:25 +09:00
ultrawork : true ,
2026-02-21 02:27:57 +09:00
maxIterations : parsedArguments.maxIterations ,
completionPromise : parsedArguments.completionPromise ,
strategy : parsedArguments.strategy ,
2026-02-08 16:25:25 +09:00
} )
}
}
2026-02-18 18:40:10 +08:00
if ( input . tool === "skill" ) {
const rawName = typeof output . args . name === "string" ? output.args.name : undefined
const command = rawName ? . replace ( /^\// , "" ) . toLowerCase ( )
2026-02-08 16:25:25 +09:00
const sessionID = input . sessionID || getMainSessionID ( )
if ( command === "stop-continuation" && sessionID ) {
hooks . stopContinuationGuard ? . stop ( sessionID )
hooks . todoContinuationEnforcer ? . cancelAllCountdowns ( )
hooks . ralphLoop ? . cancelLoop ( sessionID )
clearBoulderState ( ctx . directory )
log ( "[stop-continuation] All continuation mechanisms stopped" , {
sessionID ,
} )
}
2026-04-09 21:36:10 +09:00
// Clear stop state when user explicitly resumes work via work-starting commands.
// This ensures /stop-continuation persists until the user intentionally restarts.
const workStartingCommands = [ "start-work" , "ralph-loop" , "ulw-loop" ]
if ( workStartingCommands . includes ( command ? ? "" ) && sessionID ) {
if ( hooks . stopContinuationGuard ? . isStopped ( sessionID ) ) {
hooks . stopContinuationGuard . clear ( sessionID )
log ( "[stop-continuation] Stop state cleared by work-starting command" , {
sessionID ,
command ,
} )
}
}
2026-02-08 16:25:25 +09:00
}
}
}