a3f9eb1337
/start-work should leave plan mode even when Atlas is unavailable. This prevents Prometheus from being persisted into boulder state and keeping resumed work sessions in md-only mode. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
288 lines
9.9 KiB
TypeScript
288 lines
9.9 KiB
TypeScript
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 { getAgentConfigKey, getAgentDisplayName } from "../../shared/agent-display-names"
|
|
import { getSessionAgent, isAgentRegistered, 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 {
|
|
message?: Record<string, unknown>
|
|
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} <branch>\`, then add \`"worktree_path"\` to boulder.json`,
|
|
}
|
|
}
|
|
|
|
export function createStartWorkHook(ctx: PluginInput) {
|
|
return {
|
|
"chat.message": 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>")) return
|
|
|
|
log(`[${HOOK_NAME}] Processing start-work command`, { sessionID: input.sessionID })
|
|
const currentSessionAgent = getSessionAgent(input.sessionID)
|
|
const activeAgent = isAgentRegistered("atlas")
|
|
? "atlas"
|
|
: currentSessionAgent && getAgentConfigKey(currentSessionAgent) !== "prometheus"
|
|
? currentSessionAgent
|
|
: "sisyphus"
|
|
const activeAgentDisplayName = getAgentDisplayName(activeAgent)
|
|
updateSessionAgent(input.sessionID, activeAgent)
|
|
if (output.message) {
|
|
output.message["agent"] = activeAgentDisplayName
|
|
}
|
|
|
|
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, activeAgent, 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
|
|
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)
|
|
}
|
|
|
|
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, activeAgent, 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 += `
|
|
|
|
<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.
|
|
${worktreeBlock}
|
|
</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)
|
|
|
|
output.parts[idx].text += `\n\n---\n${contextInfo}`
|
|
}
|
|
|
|
log(`[${HOOK_NAME}] Context injected`, {
|
|
sessionID: input.sessionID,
|
|
hasExistingState: !!existingState,
|
|
worktreePath,
|
|
})
|
|
},
|
|
}
|
|
}
|