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:
commit
5e44996746
@ -236,5 +236,148 @@ describe("start-work hook", () => {
|
|||||||
expect(output.parts[0].text).toContain("Ask the user")
|
expect(output.parts[0].text).toContain("Ask the user")
|
||||||
expect(output.parts[0].text).not.toContain("Which plan would you like to work on?")
|
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,
|
getPlanProgress,
|
||||||
createBoulderState,
|
createBoulderState,
|
||||||
getPlanName,
|
getPlanName,
|
||||||
|
clearBoulderState,
|
||||||
} from "../../features/boulder-state"
|
} from "../../features/boulder-state"
|
||||||
import { log } from "../../shared/logger"
|
import { log } from "../../shared/logger"
|
||||||
|
|
||||||
export const HOOK_NAME = "start-work"
|
export const HOOK_NAME = "start-work"
|
||||||
|
|
||||||
|
const KEYWORD_PATTERN = /\b(ultrawork|ulw)\b/gi
|
||||||
|
|
||||||
interface StartWorkHookInput {
|
interface StartWorkHookInput {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
messageID?: string
|
messageID?: string
|
||||||
@ -21,6 +24,27 @@ interface StartWorkHookOutput {
|
|||||||
parts: Array<{ type: string; text?: string }>
|
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) {
|
export function createStartWorkHook(ctx: PluginInput) {
|
||||||
return {
|
return {
|
||||||
"chat.message": async (
|
"chat.message": async (
|
||||||
@ -52,7 +76,69 @@ export function createStartWorkHook(ctx: PluginInput) {
|
|||||||
|
|
||||||
let contextInfo = ""
|
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
|
||||||
|
|
||||||
|
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) {
|
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)
|
const progress = getPlanProgress(existingState.active_plan)
|
||||||
|
|
||||||
if (!progress.isComplete) {
|
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 plans = findPrometheusPlans(ctx.directory)
|
||||||
const incompletePlans = plans.filter(p => !getPlanProgress(p).isComplete)
|
const incompletePlans = plans.filter(p => !getPlanProgress(p).isComplete)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user