diff --git a/src/hooks/start-work/context-info-builder.ts b/src/hooks/start-work/context-info-builder.ts index 2fe074429..ecbe1d37a 100644 --- a/src/hooks/start-work/context-info-builder.ts +++ b/src/hooks/start-work/context-info-builder.ts @@ -23,7 +23,7 @@ function normalizePlanLookupValue(value: string): string { .replace(/^["'`]+|["'`]+$/g, "") .toLowerCase() .replace(/[\s_]+/g, "-") - .replace(/[^a-z0-9-]+/g, "-") + .replace(/[^\p{L}\p{N}-]+/gu, "-") .replace(/-+/g, "-") .replace(/^-+|-+$/g, "") } diff --git a/src/hooks/start-work/index.test.ts b/src/hooks/start-work/index.test.ts index c2a4fb09a..63f37f06d 100644 --- a/src/hooks/start-work/index.test.ts +++ b/src/hooks/start-work/index.test.ts @@ -443,6 +443,122 @@ You are starting a Sisyphus work session. expect(output.parts[0].text).toContain("my-feature-plan") expect(output.parts[0].text).toContain("Auto-Selected Plan") }) + + test("should match Korean plan names after Unicode-aware normalization", async () => { + // given + const plansDir = join(testDir, ".sisyphus", "plans") + mkdirSync(plansDir, { recursive: true }) + + const planPath = join(plansDir, "결제-플로우.md") + writeFileSync(planPath, "# 결제 플로우\n- [ ] 작업 1") + + const hook = createStartWorkHook(createMockPluginInput()) + const output = { + parts: [ + { + type: "text", + text: createStartWorkPrompt({ userRequest: "결제 플로우" }), + }, + ], + } + + // when + await hook["chat.message"]( + { sessionID: "session-korean-plan" }, + output, + ) + + // then + expect(output.parts[0].text).toContain("결제-플로우") + expect(output.parts[0].text).toContain("Auto-Selected Plan") + }) + + test("should match Japanese plan names after Unicode-aware normalization", async () => { + // given + const plansDir = join(testDir, ".sisyphus", "plans") + mkdirSync(plansDir, { recursive: true }) + + const planPath = join(plansDir, "支払い-フロー.md") + writeFileSync(planPath, "# 支払い フロー\n- [ ] タスク 1") + + const hook = createStartWorkHook(createMockPluginInput()) + const output = { + parts: [ + { + type: "text", + text: createStartWorkPrompt({ userRequest: "支払い フロー" }), + }, + ], + } + + // when + await hook["chat.message"]( + { sessionID: "session-japanese-plan" }, + output, + ) + + // then + expect(output.parts[0].text).toContain("支払い-フロー") + expect(output.parts[0].text).toContain("Auto-Selected Plan") + }) + + test("should keep ASCII plan name matching behavior unchanged", async () => { + // given + const plansDir = join(testDir, ".sisyphus", "plans") + mkdirSync(plansDir, { recursive: true }) + + const planPath = join(plansDir, "checkout-flow.md") + writeFileSync(planPath, "# Checkout Flow\n- [ ] Task 1") + + const hook = createStartWorkHook(createMockPluginInput()) + const output = { + parts: [ + { + type: "text", + text: createStartWorkPrompt({ userRequest: "checkout flow" }), + }, + ], + } + + // when + await hook["chat.message"]( + { sessionID: "session-ascii-plan" }, + output, + ) + + // then + expect(output.parts[0].text).toContain("checkout-flow") + expect(output.parts[0].text).toContain("Auto-Selected Plan") + }) + + test("should match mixed ASCII and non-ASCII plan names", async () => { + // given + const plansDir = join(testDir, ".sisyphus", "plans") + mkdirSync(plansDir, { recursive: true }) + + const planPath = join(plansDir, "v2-결제-flow.md") + writeFileSync(planPath, "# v2 결제 flow\n- [ ] Task 1") + + const hook = createStartWorkHook(createMockPluginInput()) + const output = { + parts: [ + { + type: "text", + text: createStartWorkPrompt({ userRequest: "v2 결제 flow" }), + }, + ], + } + + // when + await hook["chat.message"]( + { sessionID: "session-mixed-plan" }, + output, + ) + + // then + expect(output.parts[0].text).toContain("v2-결제-flow") + expect(output.parts[0].text).toContain("Auto-Selected Plan") + }) }) describe("session agent management", () => {