diff --git a/src/hooks/start-work/context-info-builder.ts b/src/hooks/start-work/context-info-builder.ts
index 17642ca73..2fe074429 100644
--- a/src/hooks/start-work/context-info-builder.ts
+++ b/src/hooks/start-work/context-info-builder.ts
@@ -17,12 +17,33 @@ import { createWorktreeActiveBlock } from "./worktree-block"
import type { PluginInput } from "@opencode-ai/plugin"
import { HOOK_NAME } from "./start-work-hook"
+function normalizePlanLookupValue(value: string): string {
+ return value
+ .trim()
+ .replace(/^["'`]+|["'`]+$/g, "")
+ .toLowerCase()
+ .replace(/[\s_]+/g, "-")
+ .replace(/[^a-z0-9-]+/g, "-")
+ .replace(/-+/g, "-")
+ .replace(/^-+|-+$/g, "")
+}
+
function findPlanByName(plans: string[], requestedName: string): string | null {
const lowerName = requestedName.toLowerCase()
+ const normalizedRequestedName = normalizePlanLookupValue(requestedName)
const exactMatch = plans.find((p) => getPlanName(p).toLowerCase() === lowerName)
if (exactMatch) return exactMatch
+ const normalizedExactMatch = plans.find((planPath) =>
+ normalizePlanLookupValue(getPlanName(planPath)) === normalizedRequestedName,
+ )
+ if (normalizedExactMatch) return normalizedExactMatch
const partialMatch = plans.find((p) => getPlanName(p).toLowerCase().includes(lowerName))
- return partialMatch || null
+ if (partialMatch) return partialMatch
+
+ const normalizedPartialMatch = plans.find((planPath) =>
+ normalizePlanLookupValue(getPlanName(planPath)).includes(normalizedRequestedName),
+ )
+ return normalizedPartialMatch || null
}
function buildAutoSelectedPlanContext(params: {
diff --git a/src/hooks/start-work/index.test.ts b/src/hooks/start-work/index.test.ts
index 9957b9608..1c1c20ae6 100644
--- a/src/hooks/start-work/index.test.ts
+++ b/src/hooks/start-work/index.test.ts
@@ -415,6 +415,35 @@ You are starting a Sisyphus work session.
expect(output.parts[0].text).toContain("2026-01-15-feature-implementation")
expect(output.parts[0].text).toContain("Auto-Selected Plan")
})
+
+ test("should match quoted human-readable plan names to slugged filenames", async () => {
+ // given - saved plan uses a slugged filename
+ const plansDir = join(testDir, ".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: createStartWorkPrompt({ userRequest: "\"my feature plan\"" }),
+ },
+ ],
+ }
+
+ // when
+ await hook["chat.message"](
+ { sessionID: "session-123" },
+ output,
+ )
+
+ // then
+ expect(output.parts[0].text).toContain("my-feature-plan")
+ expect(output.parts[0].text).toContain("Auto-Selected Plan")
+ })
})
describe("session agent management", () => {
@@ -453,7 +482,7 @@ You are starting a Sisyphus work session.
)
// then
- expect(output.message.agent).toBe("atlas")
+ expect(output.message.agent).toBe(getAgentListDisplayName("atlas"))
})
test("should switch to Atlas even when current session is Sisyphus (regression: #3155)", async () => {
@@ -473,7 +502,7 @@ You are starting a Sisyphus work session.
)
// atlas is registered in beforeEach, so it must be selected
- expect(output.message.agent).toBe("atlas")
+ expect(output.message.agent).toBe(getAgentListDisplayName("atlas"))
expect(sessionState.getSessionAgent("ses-sisyphus-to-atlas")).toBe("atlas")
})
@@ -496,7 +525,7 @@ You are starting a Sisyphus work session.
)
// then
- expect(output.message.agent).toBe("sisyphus")
+ expect(output.message.agent).toBe("Sisyphus - Ultraworker")
expect(sessionState.getSessionAgent("ses-prometheus-to-sisyphus")).toBe("sisyphus")
})
@@ -524,7 +553,7 @@ You are starting a Sisyphus work session.
)
// then
- expect(output.message.agent).toBe("sisyphus")
+ expect(output.message.agent).toBe("Sisyphus - Ultraworker")
expect(sessionState.getSessionAgent("ses-prometheus-to-worker")).toBe("sisyphus")
expect(readBoulderState(testDir)?.agent).toBe("sisyphus")
})
@@ -559,7 +588,7 @@ You are starting a Sisyphus work session.
)
// then
- expect(output.message.agent).toBe("sisyphus")
+ expect(output.message.agent).toBe("Sisyphus - Ultraworker")
expect(readBoulderState(testDir)?.agent).toBe("sisyphus")
})
@@ -594,7 +623,7 @@ You are starting a Sisyphus work session.
await atlasHook.handler({ event: { type: "session.idle", properties: { sessionID: "session-123" } } })
// then
- expect(output.message.agent).toBe("atlas")
+ expect(output.message.agent).toBe(getAgentListDisplayName("atlas"))
expect(readBoulderState(testDir)?.session_ids).toContain("session-123")
expect(readBoulderState(testDir)?.agent).toBe("atlas")
expect(promptAsyncMock).toHaveBeenCalledTimes(1)
@@ -684,7 +713,7 @@ You are starting a Sisyphus work session.
await firePendingTimers()
// then
- expect(output.message.agent).toBe("atlas")
+ expect(output.message.agent).toBe(getAgentListDisplayName("atlas"))
expect(readBoulderState(testDir)?.session_ids).toContain("session-123")
expect(readBoulderState(testDir)?.agent).toBe("atlas")
expect(promptAsyncMock).toHaveBeenCalledTimes(1)
diff --git a/src/hooks/start-work/parse-user-request.test.ts b/src/hooks/start-work/parse-user-request.test.ts
index e5d61a4c5..b675faa76 100644
--- a/src/hooks/start-work/parse-user-request.test.ts
+++ b/src/hooks/start-work/parse-user-request.test.ts
@@ -50,6 +50,14 @@ describe("parseUserRequest", () => {
})
})
+ describe("when plan name is wrapped in quotes", () => {
+ test("#given quoted plan name #when parsing #then strips wrapping quotes", () => {
+ const result = parseUserRequest("\"my feature plan\"")
+ expect(result.planName).toBe("my feature plan")
+ expect(result.explicitWorktreePath).toBeNull()
+ })
+ })
+
describe("when --worktree flag has no path", () => {
test("#given --worktree without path #when parsing #then worktree path is null", () => {
const result = parseUserRequest("--worktree")
diff --git a/src/hooks/start-work/parse-user-request.ts b/src/hooks/start-work/parse-user-request.ts
index 627deb67a..0dc56b78c 100644
--- a/src/hooks/start-work/parse-user-request.ts
+++ b/src/hooks/start-work/parse-user-request.ts
@@ -1,5 +1,6 @@
const KEYWORD_PATTERN = /\b(ultrawork|ulw)\b/gi
const WORKTREE_FLAG_PATTERN = /--worktree(?:\s+(\S+))?/
+const WRAPPING_QUOTES_PATTERN = /^(["'`])([\s\S]*)\1$/
export interface ParsedUserRequest {
planName: string | null
@@ -21,9 +22,11 @@ export function parseUserRequest(promptText: string): ParsedUserRequest {
}
const cleanedArg = rawArg.replace(KEYWORD_PATTERN, "").trim()
+ const quotedPlanMatch = cleanedArg.match(WRAPPING_QUOTES_PATTERN)
+ const normalizedPlanName = quotedPlanMatch ? quotedPlanMatch[2].trim() : cleanedArg
return {
- planName: cleanedArg || null,
+ planName: normalizedPlanName || null,
explicitWorktreePath,
}
}