Merge pull request #813 from KNN-07/fix/start-work-ultrawork-plan-confusion
fix(start-work): honor explicit plan name and strip ultrawork keywords
This commit is contained in:
@@ -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
|
||||
<user-request>
|
||||
new-plan
|
||||
</user-request>`,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// #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
|
||||
<user-request>
|
||||
my-feature-plan ultrawork
|
||||
</user-request>`,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// #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
|
||||
<user-request>
|
||||
api-refactor ulw
|
||||
</user-request>`,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// #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
|
||||
<user-request>
|
||||
feature-implementation
|
||||
</user-request>`,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// #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")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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(/<user-request>\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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user