import { statSync } from "node:fs" 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" import { updateSessionAgent } from "../../features/claude-code-session-state" import { detectWorktreePath } from "./worktree-detector" import { parseUserRequest } from "./parse-user-request" 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() const exactMatch = plans.find((p) => getPlanName(p).toLowerCase() === lowerName) if (exactMatch) return exactMatch const partialMatch = plans.find((p) => getPlanName(p).toLowerCase().includes(lowerName)) return partialMatch || null } function createWorktreeActiveBlock(worktreePath: string): string { return ` ## Worktree Active **Worktree**: \`${worktreePath}\` **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` } function resolveWorktreeContext( explicitWorktreePath: string | null, ): { worktreePath: string | undefined; block: string } { if (explicitWorktreePath === null) { return { worktreePath: undefined, block: "" } } const validatedPath = detectWorktreePath(explicitWorktreePath) if (validatedPath) { return { worktreePath: validatedPath, block: createWorktreeActiveBlock(validatedPath) } } return { worktreePath: undefined, block: `\n**Worktree** (needs setup): \`git worktree add ${explicitWorktreePath} \`, then add \`"worktree_path"\` to boulder.json`, } } export function createStartWorkHook(ctx: PluginInput) { return { "chat.message": async (input: StartWorkHookInput, output: StartWorkHookOutput): Promise => { const parts = output.parts const promptText = parts ?.filter((p) => p.type === "text" && p.text) .map((p) => p.text) .join("\n") .trim() || "" if (!promptText.includes("")) return log(`[${HOOK_NAME}] Processing start-work command`, { sessionID: input.sessionID }) updateSessionAgent(input.sessionID, "atlas") const existingState = readBoulderState(ctx.directory) const sessionId = input.sessionID const timestamp = new Date().toISOString() const { planName: explicitPlanName, explicitWorktreePath } = parseUserRequest(promptText) const { worktreePath, block: worktreeBlock } = resolveWorktreeContext(explicitWorktreePath) let contextInfo = "" if (explicitPlanName) { log(`[${HOOK_NAME}] Explicit plan name requested: ${explicitPlanName}`, { sessionID: input.sessionID }) const allPlans = findPrometheusPlans(ctx.directory) const matchedPlan = findPlanByName(allPlans, explicitPlanName) if (matchedPlan) { const progress = getPlanProgress(matchedPlan) 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 { if (existingState) clearBoulderState(ctx.directory) const newState = createBoulderState(matchedPlan, sessionId, "atlas", worktreePath) writeBoulderState(ctx.directory, newState) contextInfo = ` ## Auto-Selected Plan **Plan**: ${getPlanName(matchedPlan)} **Path**: ${matchedPlan} **Progress**: ${progress.completed}/${progress.total} tasks **Session ID**: ${sessionId} **Started**: ${timestamp} ${worktreeBlock} boulder.json has been created. Read the plan and begin execution.` } } 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 = ` ## 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) if (!progress.isComplete) { 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) } const worktreeDisplay = effectiveWorktree ? createWorktreeActiveBlock(effectiveWorktree) : worktreeBlock 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} ${worktreeDisplay} 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...` } } if ( (!existingState && !explicitPlanName) || (existingState && !explicitPlanName && getPlanProgress(existingState.active_plan).isComplete) ) { const plans = findPrometheusPlans(ctx.directory) const incompletePlans = plans.filter((p) => !getPlanProgress(p).isComplete) 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) const newState = createBoulderState(planPath, sessionId, "atlas", worktreePath) writeBoulderState(ctx.directory, newState) contextInfo += ` ## Auto-Selected Plan **Plan**: ${getPlanName(planPath)} **Path**: ${planPath} **Progress**: ${progress.completed}/${progress.total} tasks **Session ID**: ${sessionId} **Started**: ${timestamp} ${worktreeBlock} boulder.json has been created. Read the plan and begin execution.` } 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") contextInfo += ` ## 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. ${worktreeBlock} ` } } 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, }) }, } }