2026-05-29 12:38:22 +09:00
import { parseUlwLoopSteeringDirective , steerUlwLoop } from "./steering.js" ;
2026-05-25 22:24:37 +09:00
export interface UserPromptSubmitPayload {
readonly cwd : string ;
readonly hook_event_name : "UserPromptSubmit" ;
readonly model? : string ;
readonly permission_mode? : string ;
readonly prompt : string ;
readonly session_id : string ;
readonly transcript_path? : string ;
readonly turn_id? : string ;
}
2026-05-28 14:19:14 +09:00
export interface PreToolUsePayload {
readonly cwd : string ;
readonly hook_event_name : "PreToolUse" ;
readonly model : string ;
readonly permission_mode : string ;
readonly session_id : string ;
readonly tool_input : unknown ;
readonly tool_name : string ;
readonly tool_use_id : string ;
readonly transcript_path : string | null ;
readonly turn_id : string ;
}
interface PreToolUseHookOutput {
readonly hookSpecificOutput : {
readonly hookEventName : "PreToolUse" ;
readonly permissionDecision : "deny" ;
readonly permissionDecisionReason : string ;
readonly additionalContext : string ;
} ;
}
const CREATE_GOAL_TOOL_NAME = "create_goal" ;
const GOAL_BUDGET_WARNING =
2026-05-29 12:38:22 +09:00
"Do not set token_budget on create_goal. Omit the budget field so the goal stays unlimited; ultrawork and ulw-loop runs must always use unlimited goals." ;
2026-05-28 14:19:14 +09:00
2026-05-25 22:24:37 +09:00
export function parseUserPromptSubmitPayload ( raw : string ) : UserPromptSubmitPayload | null {
if ( raw . trim ( ) . length === 0 ) return null ;
try {
const parsed : unknown = JSON . parse ( raw ) ;
return isUserPromptSubmitPayload ( parsed ) ? parsed : null ;
} catch ( error ) {
if ( error instanceof SyntaxError ) return null ;
return null ;
}
}
2026-05-28 14:19:14 +09:00
export function parsePreToolUsePayload ( raw : string ) : PreToolUsePayload | null {
if ( raw . trim ( ) . length === 0 ) return null ;
try {
const parsed : unknown = JSON . parse ( raw ) ;
return isPreToolUsePayload ( parsed ) ? parsed : null ;
} catch ( error ) {
if ( error instanceof SyntaxError ) return null ;
return null ;
}
}
2026-05-29 12:38:22 +09:00
export async function applyUserPromptUlwLoopSteering ( payload : UserPromptSubmitPayload ) : Promise < string > {
2026-05-25 22:24:37 +09:00
try {
if ( payload . hook_event_name !== "UserPromptSubmit" ) return "" ;
2026-05-29 12:38:22 +09:00
const proposal = parseUlwLoopSteeringDirective ( payload . prompt ) ;
2026-05-25 22:24:37 +09:00
if ( proposal === null ) return "" ;
2026-05-29 12:38:22 +09:00
const result = await steerUlwLoop ( payload . cwd , proposal ) ;
2026-05-25 22:24:37 +09:00
if ( ! result . accepted ) return "" ;
return JSON . stringify ( {
status : "accepted" ,
kind : result.audit.kind ,
source : result.audit.source ,
deduped : result.deduped ,
} ) ;
} catch ( error ) {
if ( error instanceof Error ) return "" ;
return "" ;
}
}
2026-05-28 14:19:14 +09:00
export function applyPreToolUseGoalBudgetGuard ( payload : PreToolUsePayload ) : string {
if ( payload . hook_event_name !== "PreToolUse" ) return "" ;
if ( payload . tool_name !== CREATE_GOAL_TOOL_NAME ) return "" ;
if ( ! hasGoalBudgetInput ( payload . tool_input ) ) return "" ;
const output : PreToolUseHookOutput = {
hookSpecificOutput : {
hookEventName : "PreToolUse" ,
permissionDecision : "deny" ,
permissionDecisionReason : GOAL_BUDGET_WARNING ,
additionalContext : GOAL_BUDGET_WARNING ,
} ,
} ;
return ` ${ JSON . stringify ( output ) } \ n ` ;
}
2026-05-29 12:38:22 +09:00
export async function runUlwLoopHookCli ( stdin : NodeJS.ReadableStream , stdout : NodeJS.WritableStream ) : Promise < void > {
2026-05-25 22:24:37 +09:00
try {
const payload = parseUserPromptSubmitPayload ( await readAll ( stdin ) ) ;
if ( payload === null ) return ;
2026-05-29 12:38:22 +09:00
const output = await applyUserPromptUlwLoopSteering ( payload ) ;
2026-05-25 22:24:37 +09:00
if ( output . length > 0 ) stdout . write ( output ) ;
} catch ( error ) {
if ( error instanceof Error ) return ;
return ;
}
}
2026-05-28 14:19:14 +09:00
export async function runPreToolUseGoalBudgetGuardCli (
stdin : NodeJS.ReadableStream ,
stdout : NodeJS.WritableStream ,
) : Promise < void > {
try {
const payload = parsePreToolUsePayload ( await readAll ( stdin ) ) ;
if ( payload === null ) return ;
const output = applyPreToolUseGoalBudgetGuard ( payload ) ;
if ( output . length > 0 ) stdout . write ( output ) ;
} catch ( error ) {
if ( error instanceof Error ) return ;
return ;
}
}
2026-05-25 22:24:37 +09:00
function isUserPromptSubmitPayload ( value : unknown ) : value is UserPromptSubmitPayload {
if ( ! isRecord ( value ) ) return false ;
return (
value [ "hook_event_name" ] === "UserPromptSubmit" &&
typeof value [ "cwd" ] === "string" &&
typeof value [ "prompt" ] === "string" &&
typeof value [ "session_id" ] === "string" &&
[ "model" , "permission_mode" , "transcript_path" , "turn_id" ] . every ( ( key ) = > optionalString ( value [ key ] ) )
) ;
}
2026-05-28 14:19:14 +09:00
function isPreToolUsePayload ( value : unknown ) : value is PreToolUsePayload {
if ( ! isRecord ( value ) ) return false ;
return (
value [ "hook_event_name" ] === "PreToolUse" &&
typeof value [ "cwd" ] === "string" &&
typeof value [ "model" ] === "string" &&
typeof value [ "permission_mode" ] === "string" &&
typeof value [ "session_id" ] === "string" &&
typeof value [ "tool_name" ] === "string" &&
typeof value [ "tool_use_id" ] === "string" &&
( value [ "transcript_path" ] === null || typeof value [ "transcript_path" ] === "string" ) &&
typeof value [ "turn_id" ] === "string" &&
Object . hasOwn ( value , "tool_input" )
) ;
}
function hasGoalBudgetInput ( value : unknown ) : boolean {
return isRecord ( value ) && ( Object . hasOwn ( value , "token_budget" ) || Object . hasOwn ( value , "tokenBudget" ) ) ;
}
2026-05-25 22:24:37 +09:00
function isRecord ( value : unknown ) : value is Record < string , unknown > {
return typeof value === "object" && value !== null && ! Array . isArray ( value ) ;
}
function optionalString ( value : unknown ) : boolean {
return value === undefined || typeof value === "string" ;
}
function readAll ( stdin : NodeJS.ReadableStream ) : Promise < string > {
return new Promise ( ( resolve , reject ) = > {
let data = "" ;
stdin . setEncoding ( "utf8" ) ;
stdin . on ( "data" , ( chunk : unknown ) = > {
data += chunk instanceof Buffer ? chunk . toString ( ) : String ( chunk ) ;
} ) ;
stdin . once ( "error" , reject ) ;
stdin . once ( "end" , ( ) = > resolve ( data ) ) ;
} ) ;
}