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-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 } >
}
function findPlanByName ( plans : string [ ] , requestedName : string ) : string | null {
const lowerName = requestedName . toLowerCase ( )
2026-02-26 00:40:01 +09:00
const exactMatch = plans . find ( ( p ) = > getPlanName ( p ) . toLowerCase ( ) === lowerName )
2026-02-08 15:01:42 +09:00
if ( exactMatch ) return exactMatch
2026-02-26 00:40:01 +09:00
const partialMatch = plans . find ( ( p ) = > getPlanName ( p ) . toLowerCase ( ) . includes ( lowerName ) )
2026-02-08 15:01:42 +09:00
return partialMatch || null
}
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-03-06 14:20:32 +09:00
**CRITICAL — DO NOT FORGET**: You are working inside a git worktree. ALL operations MUST be performed exclusively within this worktree directory.
- 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
- NEVER operate on the main repository directory — always use the worktree path above `
}
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-03-31 20:02:00 -07:00
let contextInfo = ""
2026-02-26 00:40:01 +09:00
2026-03-31 20:02:00 -07:00
if ( explicitPlanName ) {
log ( ` [ ${ HOOK_NAME } ] Explicit plan name requested: ${ explicitPlanName } ` , { sessionID : input.sessionID } )
2026-02-26 00:40:01 +09:00
2026-03-31 20:02:00 -07:00
const allPlans = findPrometheusPlans ( ctx . directory )
const matchedPlan = findPlanByName ( allPlans , explicitPlanName )
2026-02-26 00:40:01 +09:00
2026-03-31 20:02:00 -07:00
if ( matchedPlan ) {
const progress = getPlanProgress ( matchedPlan )
2026-02-26 00:40:01 +09:00
2026-03-31 20:02:00 -07:00
if ( progress . isComplete ) {
contextInfo = `
2026-02-08 15:01:42 +09:00
## Plan Already Complete
The requested plan " ${ getPlanName ( matchedPlan ) } " has been completed.
All ${ progress . total } tasks are done. Create a new plan with: /plan "your task" `
2026-03-31 20:02:00 -07:00
} else {
if ( existingState ) clearBoulderState ( ctx . directory )
const newState = createBoulderState ( matchedPlan , sessionId , activeAgent , worktreePath )
writeBoulderState ( ctx . directory , newState )
2026-02-26 00:40:01 +09:00
2026-03-31 20:02:00 -07:00
contextInfo = `
2026-02-08 15:01:42 +09:00
## Auto-Selected Plan
**Plan**: ${ getPlanName ( matchedPlan ) }
**Path**: ${ matchedPlan }
**Progress**: ${ progress . completed } / ${ progress . total } tasks
**Session ID**: ${ sessionId }
**Started**: ${ timestamp }
2026-02-26 00:40:01 +09:00
${ worktreeBlock }
2026-02-08 15:01:42 +09:00
boulder.json has been created. Read the plan and begin execution. `
2026-03-31 20:02:00 -07:00
}
} else {
const incompletePlans = allPlans . filter ( ( p ) = > ! getPlanProgress ( p ) . isComplete )
if ( incompletePlans . length > 0 ) {
const planList = incompletePlans
. map ( ( p , i ) = > {
const prog = getPlanProgress ( p )
return ` ${ i + 1 } . [ ${ getPlanName ( p ) } ] - Progress: ${ prog . completed } / ${ prog . total } `
} )
. join ( "\n" )
contextInfo = `
2026-02-08 15:01:42 +09:00
## Plan Not Found
Could not find a plan matching " ${ explicitPlanName } ".
Available incomplete plans:
${ planList }
Ask the user which plan to work on. `
2026-03-31 20:02:00 -07:00
} else {
contextInfo = `
2026-02-08 15:01:42 +09:00
## Plan Not Found
Could not find a plan matching " ${ explicitPlanName } ".
No incomplete plans available. Create a new plan with: /plan "your task" `
}
2026-03-31 20:02:00 -07:00
}
} else if ( existingState ) {
const progress = getPlanProgress ( existingState . active_plan )
if ( ! progress . isComplete ) {
const effectiveWorktree = worktreePath ? ? existingState . worktree_path
const sessionAlreadyTracked = existingState . session_ids . includes ( sessionId )
const updatedSessions = sessionAlreadyTracked
? existingState . session_ids
: [ . . . existingState . session_ids , sessionId ]
const shouldRewriteState = existingState . agent !== activeAgent || worktreePath !== undefined
if ( shouldRewriteState ) {
writeBoulderState ( ctx . directory , {
. . . existingState ,
agent : activeAgent ,
. . . ( worktreePath !== undefined ? { worktree_path : worktreePath } : { } ) ,
session_ids : updatedSessions ,
} )
} else if ( ! sessionAlreadyTracked ) {
appendSessionId ( ctx . directory , sessionId )
}
2026-02-26 00:40:01 +09:00
2026-03-31 20:02:00 -07:00
const worktreeDisplay = effectiveWorktree ? createWorktreeActiveBlock ( effectiveWorktree ) : worktreeBlock
2026-02-26 00:40:01 +09:00
2026-03-31 20:02:00 -07:00
contextInfo = `
2026-02-08 15:01:42 +09:00
## Active Work Session Found
**Status**: RESUMING existing work
**Plan**: ${ existingState . plan_name }
**Path**: ${ existingState . active_plan }
**Progress**: ${ progress . completed } / ${ progress . total } tasks completed
**Sessions**: ${ existingState . session_ids . length + 1 } (current session appended)
**Started**: ${ existingState . started_at }
2026-02-26 00:40:01 +09:00
${ worktreeDisplay }
2026-02-08 15:01:42 +09:00
The current session ( ${ sessionId } ) has been added to session_ids.
Read the plan file and continue from the first unchecked task. `
2026-03-31 20:02:00 -07:00
} else {
contextInfo = `
2026-02-08 15:01:42 +09:00
## Previous Work Complete
The previous plan ( ${ existingState . plan_name } ) has been completed.
Looking for new plans... `
}
2026-03-31 20:02:00 -07:00
}
2026-02-08 15:01:42 +09:00
2026-03-31 20:02:00 -07:00
if (
( ! existingState && ! explicitPlanName ) ||
( existingState && ! explicitPlanName && getPlanProgress ( existingState . active_plan ) . isComplete )
) {
const plans = findPrometheusPlans ( ctx . directory )
const incompletePlans = plans . filter ( ( p ) = > ! getPlanProgress ( p ) . isComplete )
2026-02-26 00:40:01 +09:00
2026-03-31 20:02:00 -07:00
if ( plans . length === 0 ) {
contextInfo += `
2026-02-08 15:01:42 +09:00
## No Plans Found
No Prometheus plan files found at .sisyphus/plans/
Use Prometheus to create a work plan first: /plan "your task" `
2026-03-31 20:02:00 -07:00
} else if ( incompletePlans . length === 0 ) {
contextInfo += `
2026-02-08 15:01:42 +09:00
## All Plans Complete
All ${ plans . length } plan(s) are complete. Create a new plan with: /plan "your task" `
2026-03-31 20:02:00 -07:00
} else if ( incompletePlans . length === 1 ) {
const planPath = incompletePlans [ 0 ]
const progress = getPlanProgress ( planPath )
const newState = createBoulderState ( planPath , sessionId , activeAgent , worktreePath )
writeBoulderState ( ctx . directory , newState )
2026-02-08 15:01:42 +09:00
2026-03-31 20:02:00 -07:00
contextInfo += `
2026-02-08 15:01:42 +09:00
## Auto-Selected Plan
**Plan**: ${ getPlanName ( planPath ) }
**Path**: ${ planPath }
**Progress**: ${ progress . completed } / ${ progress . total } tasks
**Session ID**: ${ sessionId }
**Started**: ${ timestamp }
2026-02-26 00:40:01 +09:00
${ worktreeBlock }
2026-02-08 15:01:42 +09:00
boulder.json has been created. Read the plan and begin execution. `
2026-03-31 20:02:00 -07:00
} else {
const planList = incompletePlans
. map ( ( p , i ) = > {
const progress = getPlanProgress ( p )
const modified = new Date ( statSync ( p ) . mtimeMs ) . toISOString ( )
return ` ${ i + 1 } . [ ${ getPlanName ( p ) } ] - Modified: ${ modified } - Progress: ${ progress . completed } / ${ progress . total } `
} )
. join ( "\n" )
2026-02-08 15:01:42 +09:00
2026-03-31 20:02:00 -07:00
contextInfo += `
2026-02-08 15:01:42 +09:00
<system-reminder>
## Multiple Plans Found
Current Time: ${ timestamp }
Session ID: ${ sessionId }
${ planList }
Ask the user which plan to work on. Present the options above and wait for their response.
2026-02-26 00:40:01 +09:00
${ worktreeBlock }
2026-02-08 15:01:42 +09:00
</system-reminder> `
}
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
} ,
}
}