diff --git a/src/hooks/start-work/index.test.ts b/src/hooks/start-work/index.test.ts
index 31f73fdfb..64f09e151 100644
--- a/src/hooks/start-work/index.test.ts
+++ b/src/hooks/start-work/index.test.ts
@@ -236,5 +236,148 @@ describe("start-work hook", () => {
expect(output.parts[0].text).toContain("Ask the user")
expect(output.parts[0].text).not.toContain("Which plan would you like to work on?")
})
+
+ test("should select explicitly specified plan name from user-request, ignoring existing boulder state", async () => {
+ // #given - existing boulder state pointing to old plan
+ const plansDir = join(TEST_DIR, ".sisyphus", "plans")
+ mkdirSync(plansDir, { recursive: true })
+
+ // Old plan (in boulder state)
+ const oldPlanPath = join(plansDir, "old-plan.md")
+ writeFileSync(oldPlanPath, "# Old Plan\n- [ ] Old Task 1")
+
+ // New plan (user wants this one)
+ const newPlanPath = join(plansDir, "new-plan.md")
+ writeFileSync(newPlanPath, "# New Plan\n- [ ] New Task 1")
+
+ // Set up stale boulder state pointing to old plan
+ const staleState: BoulderState = {
+ active_plan: oldPlanPath,
+ started_at: "2026-01-01T10:00:00Z",
+ session_ids: ["old-session"],
+ plan_name: "old-plan",
+ }
+ writeBoulderState(TEST_DIR, staleState)
+
+ const hook = createStartWorkHook(createMockPluginInput())
+ const output = {
+ parts: [
+ {
+ type: "text",
+ text: `Start Sisyphus work session
+
+new-plan
+`,
+ },
+ ],
+ }
+
+ // #when - user explicitly specifies new-plan
+ await hook["chat.message"](
+ { sessionID: "session-123" },
+ output
+ )
+
+ // #then - should select new-plan, NOT resume old-plan
+ expect(output.parts[0].text).toContain("new-plan")
+ expect(output.parts[0].text).not.toContain("RESUMING")
+ expect(output.parts[0].text).not.toContain("old-plan")
+ })
+
+ test("should strip ultrawork/ulw keywords from plan name argument", async () => {
+ // #given - plan with ultrawork keyword in user-request
+ const plansDir = join(TEST_DIR, ".sisyphus", "plans")
+ mkdirSync(plansDir, { recursive: true })
+
+ const planPath = join(plansDir, "my-feature-plan.md")
+ writeFileSync(planPath, "# My Feature Plan\n- [ ] Task 1")
+
+ const hook = createStartWorkHook(createMockPluginInput())
+ const output = {
+ parts: [
+ {
+ type: "text",
+ text: `Start Sisyphus work session
+
+my-feature-plan ultrawork
+`,
+ },
+ ],
+ }
+
+ // #when - user specifies plan with ultrawork keyword
+ await hook["chat.message"](
+ { sessionID: "session-123" },
+ output
+ )
+
+ // #then - should find plan without ultrawork suffix
+ expect(output.parts[0].text).toContain("my-feature-plan")
+ expect(output.parts[0].text).toContain("Auto-Selected Plan")
+ })
+
+ test("should strip ulw keyword from plan name argument", async () => {
+ // #given - plan with ulw keyword in user-request
+ const plansDir = join(TEST_DIR, ".sisyphus", "plans")
+ mkdirSync(plansDir, { recursive: true })
+
+ const planPath = join(plansDir, "api-refactor.md")
+ writeFileSync(planPath, "# API Refactor\n- [ ] Task 1")
+
+ const hook = createStartWorkHook(createMockPluginInput())
+ const output = {
+ parts: [
+ {
+ type: "text",
+ text: `Start Sisyphus work session
+
+api-refactor ulw
+`,
+ },
+ ],
+ }
+
+ // #when
+ await hook["chat.message"](
+ { sessionID: "session-123" },
+ output
+ )
+
+ // #then - should find plan without ulw suffix
+ expect(output.parts[0].text).toContain("api-refactor")
+ expect(output.parts[0].text).toContain("Auto-Selected Plan")
+ })
+
+ test("should match plan by partial name", async () => {
+ // #given - user specifies partial plan name
+ const plansDir = join(TEST_DIR, ".sisyphus", "plans")
+ mkdirSync(plansDir, { recursive: true })
+
+ const planPath = join(plansDir, "2026-01-15-feature-implementation.md")
+ writeFileSync(planPath, "# Feature Implementation\n- [ ] Task 1")
+
+ const hook = createStartWorkHook(createMockPluginInput())
+ const output = {
+ parts: [
+ {
+ type: "text",
+ text: `Start Sisyphus work session
+
+feature-implementation
+`,
+ },
+ ],
+ }
+
+ // #when
+ await hook["chat.message"](
+ { sessionID: "session-123" },
+ output
+ )
+
+ // #then - should find plan by partial match
+ expect(output.parts[0].text).toContain("2026-01-15-feature-implementation")
+ expect(output.parts[0].text).toContain("Auto-Selected Plan")
+ })
})
})
diff --git a/src/hooks/start-work/index.ts b/src/hooks/start-work/index.ts
index d7a8c6922..0ba3768c8 100644
--- a/src/hooks/start-work/index.ts
+++ b/src/hooks/start-work/index.ts
@@ -7,11 +7,14 @@ import {
getPlanProgress,
createBoulderState,
getPlanName,
+ clearBoulderState,
} from "../../features/boulder-state"
import { log } from "../../shared/logger"
export const HOOK_NAME = "start-work"
+const KEYWORD_PATTERN = /\b(ultrawork|ulw)\b/gi
+
interface StartWorkHookInput {
sessionID: string
messageID?: string
@@ -21,6 +24,27 @@ interface StartWorkHookOutput {
parts: Array<{ type: string; text?: string }>
}
+function extractUserRequestPlanName(promptText: string): string | null {
+ const userRequestMatch = promptText.match(/\s*([\s\S]*?)\s*<\/user-request>/i)
+ if (!userRequestMatch) return null
+
+ const rawArg = userRequestMatch[1].trim()
+ if (!rawArg) return null
+
+ const cleanedArg = rawArg.replace(KEYWORD_PATTERN, "").trim()
+ return cleanedArg || null
+}
+
+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
+}
+
export function createStartWorkHook(ctx: PluginInput) {
return {
"chat.message": async (
@@ -51,8 +75,70 @@ export function createStartWorkHook(ctx: PluginInput) {
const timestamp = new Date().toISOString()
let contextInfo = ""
+
+ const explicitPlanName = extractUserRequestPlanName(promptText)
+
+ 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
- if (existingState) {
+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)
+ writeBoulderState(ctx.directory, newState)
+
+ contextInfo = `
+## Auto-Selected Plan
+
+**Plan**: ${getPlanName(matchedPlan)}
+**Path**: ${matchedPlan}
+**Progress**: ${progress.completed}/${progress.total} tasks
+**Session ID**: ${sessionId}
+**Started**: ${timestamp}
+
+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) {
@@ -78,7 +164,7 @@ Looking for new plans...`
}
}
- if (!existingState || getPlanProgress(existingState.active_plan).isComplete) {
+ if ((!existingState && !explicitPlanName) || (existingState && !explicitPlanName && getPlanProgress(existingState.active_plan).isComplete)) {
const plans = findPrometheusPlans(ctx.directory)
const incompletePlans = plans.filter(p => !getPlanProgress(p).isComplete)