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 {
|
|
|
|
|
getAgentDisplayName,
|
|
|
|
|
getAgentListDisplayName,
|
2026-04-08 13:01:51 +09:00
|
|
|
stripAgentListSortPrefix,
|
2026-03-31 20:02:00 -07:00
|
|
|
} from "../../shared/agent-display-names"
|
|
|
|
|
import {
|
|
|
|
|
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-04-03 23:06:35 +09:00
|
|
|
import { createWorktreeActiveBlock } from "./worktree-block"
|
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-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 })
|
2026-04-06 19:55:04 +09:00
|
|
|
const activeAgent = isAgentRegistered("atlas")
|
|
|
|
|
? "atlas"
|
|
|
|
|
: "sisyphus"
|
2026-04-07 19:05:06 +09:00
|
|
|
const activeAgentDisplayName = activeAgent === "atlas"
|
|
|
|
|
? getAgentListDisplayName(activeAgent)
|
|
|
|
|
: getAgentDisplayName(activeAgent)
|
2026-03-31 20:02:00 -07:00
|
|
|
updateSessionAgent(input.sessionID, activeAgent)
|
|
|
|
|
if (output.message) {
|
2026-04-08 13:01:51 +09:00
|
|
|
output.message["agent"] = stripAgentListSortPrefix(activeAgentDisplayName)
|
2026-03-31 20:02:00 -07:00
|
|
|
}
|
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
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|