2026-02-26 00:40:01 +09:00
import { statSync } from "node:fs"
2026-02-08 15:01:42 +09:00
import type { PluginInput } from "@opencode-ai/plugin"
import {
readBoulderState ,
writeBoulderState ,
appendSessionId ,
findPrometheusPlans ,
getPlanProgress ,
createBoulderState ,
getPlanName ,
clearBoulderState ,
} from "../../features/boulder-state"
import { log } from "../../shared/logger"
2026-03-31 20:02:00 -07:00
import {
getAgentConfigKey ,
getAgentDisplayName ,
getAgentListDisplayName ,
} from "../../shared/agent-display-names"
import {
getSessionAgent ,
isAgentRegistered ,
updateSessionAgent ,
} from "../../features/claude-code-session-state"
2026-02-26 00:40:01 +09:00
import { detectWorktreePath } from "./worktree-detector"
import { parseUserRequest } from "./parse-user-request"
2026-04-03 21:37:16 +09:00
import { buildStartWorkContextInfo } from "./context-info-builder"
2026-02-08 15:01:42 +09:00
export const HOOK_NAME = "start-work" as const
2026-03-31 20:02:00 -07:00
const START_WORK_TEMPLATE_MARKER = "You are starting a Sisyphus work session."
2026-02-08 15:01:42 +09:00
interface StartWorkHookInput {
sessionID : string
messageID? : string
}
2026-03-31 20:02:00 -07:00
interface StartWorkCommandExecuteBeforeInput {
sessionID : string
command : string
arguments : string
}
2026-02-08 15:01:42 +09:00
interface StartWorkHookOutput {
2026-03-15 19:04:20 -04:00
message? : Record < string , unknown >
2026-02-08 15:01:42 +09:00
parts : Array < { type : string ; text? : string } >
}
2026-03-06 14:20:32 +09:00
function createWorktreeActiveBlock ( worktreePath : string ) : string {
return `
## Worktree Active
2026-02-26 00:40:01 +09:00
2026-03-06 14:20:32 +09:00
**Worktree**: \` ${ worktreePath } \`
2026-02-26 00:40:01 +09:00
2026-04-03 21:37:16 +09:00
**CRITICAL - DO NOT FORGET**: You are working inside a git worktree. ALL operations MUST be performed exclusively within this worktree directory.
2026-03-06 14:20:32 +09:00
- Every file read, write, edit, and git operation MUST target paths under: \` ${ worktreePath } \`
- When delegating tasks to subagents, you MUST include the worktree path in your delegation prompt so they also operate exclusively within the worktree
2026-04-03 21:37:16 +09:00
- NEVER operate on the main repository directory - always use the worktree path above `
2026-03-06 14:20:32 +09:00
}
2026-02-26 00:40:01 +09:00
function resolveWorktreeContext (
explicitWorktreePath : string | null ,
) : { worktreePath : string | undefined ; block : string } {
if ( explicitWorktreePath === null ) {
2026-03-06 14:20:32 +09:00
return { worktreePath : undefined , block : "" }
2026-02-26 00:40:01 +09:00
}
const validatedPath = detectWorktreePath ( explicitWorktreePath )
if ( validatedPath ) {
2026-03-06 14:20:32 +09:00
return { worktreePath : validatedPath , block : createWorktreeActiveBlock ( validatedPath ) }
2026-02-26 00:40:01 +09:00
}
return {
worktreePath : undefined ,
block : ` \ n**Worktree** (needs setup): \` git worktree add ${ explicitWorktreePath } <branch> \` , then add \` "worktree_path" \` to boulder.json ` ,
}
}
2026-02-08 15:01:42 +09:00
export function createStartWorkHook ( ctx : PluginInput ) {
2026-03-31 20:02:00 -07:00
const processStartWork = async (
input : StartWorkHookInput ,
output : StartWorkHookOutput ,
) : Promise < void > = > {
const parts = output . parts
const promptText =
parts
? . filter ( ( p ) = > p . type === "text" && p . text )
. map ( ( p ) = > p . text )
. join ( "\n" )
. trim ( ) || ""
if (
! promptText . includes ( "<session-context>" )
|| ! promptText . includes ( START_WORK_TEMPLATE_MARKER )
) {
return
}
log ( ` [ ${ HOOK_NAME } ] Processing start-work command ` , { sessionID : input.sessionID } )
const currentSessionAgent = getSessionAgent ( input . sessionID )
const currentSessionAgentKey = currentSessionAgent
? getAgentConfigKey ( currentSessionAgent )
: undefined
const activeAgent = currentSessionAgent
&& currentSessionAgentKey
&& currentSessionAgentKey !== "prometheus"
&& currentSessionAgentKey !== "atlas"
? currentSessionAgent
: isAgentRegistered ( "atlas" )
? "atlas"
2026-03-31 18:52:09 -07:00
: "sisyphus"
2026-03-31 20:02:00 -07:00
const activeAgentDisplayName = activeAgent === "atlas"
? getAgentListDisplayName ( activeAgent )
: getAgentDisplayName ( activeAgent )
updateSessionAgent ( input . sessionID , activeAgent )
if ( output . message ) {
output . message [ "agent" ] = activeAgentDisplayName
}
2026-02-08 15:01:42 +09:00
2026-03-31 20:02:00 -07:00
const existingState = readBoulderState ( ctx . directory )
const sessionId = input . sessionID
const timestamp = new Date ( ) . toISOString ( )
2026-02-08 15:01:42 +09:00
2026-03-31 20:02:00 -07:00
const { planName : explicitPlanName , explicitWorktreePath } = parseUserRequest ( promptText )
const { worktreePath , block : worktreeBlock } = resolveWorktreeContext ( explicitWorktreePath )
2026-02-26 00:40:01 +09:00
2026-04-03 21:37:16 +09:00
const contextInfo = buildStartWorkContextInfo ( {
ctx ,
explicitPlanName ,
existingState ,
sessionId ,
timestamp ,
activeAgent ,
worktreePath ,
worktreeBlock ,
} )
2026-03-31 20:02:00 -07:00
const idx = output . parts . findIndex ( ( p ) = > p . type === "text" && p . text )
if ( idx >= 0 && output . parts [ idx ] . text ) {
output . parts [ idx ] . text = output . parts [ idx ] . text
. replace ( /\$SESSION_ID/g , sessionId )
. replace ( /\$TIMESTAMP/g , timestamp )
output . parts [ idx ] . text += ` \ n \ n--- \ n ${ contextInfo } `
}
log ( ` [ ${ HOOK_NAME } ] Context injected ` , {
sessionID : input.sessionID ,
hasExistingState : ! ! existingState ,
worktreePath ,
} )
}
2026-02-08 15:01:42 +09:00
2026-03-31 20:02:00 -07:00
return {
"chat.message" : async ( input : StartWorkHookInput , output : StartWorkHookOutput ) : Promise < void > = > {
await processStartWork ( input , output )
} ,
"command.execute.before" : async (
input : StartWorkCommandExecuteBeforeInput ,
output : StartWorkHookOutput ,
) : Promise < void > = > {
await processStartWork ( input , output )
2026-02-08 15:01:42 +09:00
} ,
}
}