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-02-16 22:11:35 +09:00
import { 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
interface StartWorkHookInput {
sessionID : string
messageID? : string
}
interface StartWorkHookOutput {
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 ) {
return {
2026-02-26 00:40:01 +09:00
"chat.message" : async ( input : StartWorkHookInput , output : StartWorkHookOutput ) : Promise < void > = > {
2026-02-08 15:01:42 +09:00
const parts = output . parts
2026-02-26 00:40:01 +09:00
const promptText =
parts
? . filter ( ( p ) = > p . type === "text" && p . text )
. map ( ( p ) = > p . text )
. join ( "\n" )
. trim ( ) || ""
2026-02-08 15:01:42 +09:00
2026-02-26 00:40:01 +09:00
if ( ! promptText . includes ( "<session-context>" ) ) return
2026-02-08 15:01:42 +09:00
2026-02-26 00:40:01 +09:00
log ( ` [ ${ HOOK_NAME } ] Processing start-work command ` , { sessionID : input.sessionID } )
updateSessionAgent ( input . sessionID , "atlas" )
2026-02-08 15:01:42 +09:00
const existingState = readBoulderState ( ctx . directory )
const sessionId = input . sessionID
const timestamp = new Date ( ) . toISOString ( )
2026-02-26 00:40:01 +09:00
const { planName : explicitPlanName , explicitWorktreePath } = parseUserRequest ( promptText )
const { worktreePath , block : worktreeBlock } = resolveWorktreeContext ( explicitWorktreePath )
2026-02-08 15:01:42 +09:00
let contextInfo = ""
2026-02-26 00:40:01 +09:00
2026-02-08 15:01:42 +09:00
if ( explicitPlanName ) {
2026-02-26 00:40:01 +09:00
log ( ` [ ${ HOOK_NAME } ] Explicit plan name requested: ${ explicitPlanName } ` , { sessionID : input.sessionID } )
2026-02-08 15:01:42 +09:00
const allPlans = findPrometheusPlans ( ctx . directory )
const matchedPlan = findPlanByName ( allPlans , explicitPlanName )
2026-02-26 00:40:01 +09:00
2026-02-08 15:01:42 +09:00
if ( matchedPlan ) {
const progress = getPlanProgress ( matchedPlan )
2026-02-26 00:40:01 +09:00
2026-02-08 15:01:42 +09:00
if ( progress . isComplete ) {
contextInfo = `
## 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" `
} else {
2026-02-26 00:40:01 +09:00
if ( existingState ) clearBoulderState ( ctx . directory )
const newState = createBoulderState ( matchedPlan , sessionId , "atlas" , worktreePath )
2026-02-08 15:01:42 +09:00
writeBoulderState ( ctx . directory , newState )
2026-02-26 00:40:01 +09:00
2026-02-08 15:01:42 +09:00
contextInfo = `
## 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. `
}
} else {
2026-02-26 00:40:01 +09:00
const incompletePlans = allPlans . filter ( ( p ) = > ! getPlanProgress ( p ) . isComplete )
2026-02-08 15:01:42 +09:00
if ( incompletePlans . length > 0 ) {
2026-02-26 00:40:01 +09:00
const planList = incompletePlans
. map ( ( p , i ) = > {
const prog = getPlanProgress ( p )
return ` ${ i + 1 } . [ ${ getPlanName ( p ) } ] - Progress: ${ prog . completed } / ${ prog . total } `
} )
. join ( "\n" )
2026-02-08 15:01:42 +09:00
contextInfo = `
## Plan Not Found
Could not find a plan matching " ${ explicitPlanName } ".
Available incomplete plans:
${ planList }
Ask the user which plan to work on. `
} else {
contextInfo = `
## Plan Not Found
Could not find a plan matching " ${ explicitPlanName } ".
No incomplete plans available. Create a new plan with: /plan "your task" `
}
}
} else if ( existingState ) {
const progress = getPlanProgress ( existingState . active_plan )
2026-02-26 00:40:01 +09:00
2026-02-08 15:01:42 +09:00
if ( ! progress . isComplete ) {
2026-02-26 00:40:01 +09:00
const effectiveWorktree = worktreePath ? ? existingState . worktree_path
if ( worktreePath !== undefined ) {
const updatedSessions = existingState . session_ids . includes ( sessionId )
? existingState . session_ids
: [ . . . existingState . session_ids , sessionId ]
writeBoulderState ( ctx . directory , {
. . . existingState ,
worktree_path : worktreePath ,
session_ids : updatedSessions ,
} )
} else {
appendSessionId ( ctx . directory , sessionId )
}
2026-03-06 14:20:32 +09:00
const worktreeDisplay = effectiveWorktree ? createWorktreeActiveBlock ( effectiveWorktree ) : worktreeBlock
2026-02-26 00:40:01 +09:00
2026-02-08 15:01:42 +09:00
contextInfo = `
## 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. `
} else {
contextInfo = `
## Previous Work Complete
The previous plan ( ${ existingState . plan_name } ) has been completed.
Looking for new plans... `
}
}
2026-02-26 00:40:01 +09:00
if (
( ! existingState && ! explicitPlanName ) ||
( existingState && ! explicitPlanName && getPlanProgress ( existingState . active_plan ) . isComplete )
) {
2026-02-08 15:01:42 +09:00
const plans = findPrometheusPlans ( ctx . directory )
2026-02-26 00:40:01 +09:00
const incompletePlans = plans . filter ( ( p ) = > ! getPlanProgress ( p ) . isComplete )
2026-02-08 15:01:42 +09:00
if ( plans . length === 0 ) {
contextInfo += `
## No Plans Found
No Prometheus plan files found at .sisyphus/plans/
Use Prometheus to create a work plan first: /plan "your task" `
} else if ( incompletePlans . length === 0 ) {
contextInfo += `
## All Plans Complete
All ${ plans . length } plan(s) are complete. Create a new plan with: /plan "your task" `
} else if ( incompletePlans . length === 1 ) {
const planPath = incompletePlans [ 0 ]
const progress = getPlanProgress ( planPath )
2026-02-26 00:40:01 +09:00
const newState = createBoulderState ( planPath , sessionId , "atlas" , worktreePath )
2026-02-08 15:01:42 +09:00
writeBoulderState ( ctx . directory , newState )
contextInfo += `
## 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. `
} else {
2026-02-26 00:40:01 +09:00
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
contextInfo += `
<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> `
}
}
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 )
2026-02-26 00:40:01 +09:00
2026-02-08 15:01:42 +09:00
output . parts [ idx ] . text += ` \ n \ n--- \ n ${ contextInfo } `
}
log ( ` [ ${ HOOK_NAME } ] Context injected ` , {
sessionID : input.sessionID ,
hasExistingState : ! ! existingState ,
2026-02-26 00:40:01 +09:00
worktreePath ,
2026-02-08 15:01:42 +09:00
} )
} ,
}
}