diff --git a/src/hooks/keyword-detector/hyperplan.test.ts b/src/hooks/keyword-detector/hyperplan.test.ts index 8565bccdb..167b119b6 100644 --- a/src/hooks/keyword-detector/hyperplan.test.ts +++ b/src/hooks/keyword-detector/hyperplan.test.ts @@ -122,6 +122,46 @@ describe("keyword-detector hyperplan keyword", () => { expect(textPart!.text).not.toContain("") }) + test("should NOT trigger hyperplan when 'hpp' is the extension of a C++ header path", async () => { + // given - text references a .hpp file, which is extremely common in C++ codebases + const sessionID = "hyperplan-hpp-extension-session" + getMainSessionSpy = spyOn(sessionState, "getMainSessionID").mockReturnValue(sessionID) + const hook = createKeywordDetectorHook(createMockPluginInput()) + const output = { + message: {} as Record, + parts: [{ type: "text", text: "please help to check interface.hpp" }], + } + + // when - keyword detection runs on a message that only contains 'hpp' as a file extension + await hook["chat.message"]({ sessionID }, output) + + // then - hyperplan must NOT fire just because '.hpp' appears as a file extension + const textPart = output.parts.find(p => p.type === "text") + expect(textPart).toBeDefined() + expect(textPart!.text).toBe("please help to check interface.hpp") + expect(textPart!.text).not.toContain("") + }) + + test("should NOT trigger hyperplan when path-like '.hpp' appears in a deeper file path", async () => { + // given - text contains a longer path ending in .hpp (free of other trigger words like 'review') + const sessionID = "hyperplan-hpp-path-session" + getMainSessionSpy = spyOn(sessionState, "getMainSessionID").mockReturnValue(sessionID) + const hook = createKeywordDetectorHook(createMockPluginInput()) + const output = { + message: {} as Record, + parts: [{ type: "text", text: "open src/include/audio/buffer.hpp and fix the leak" }], + } + + // when - keyword detection runs + await hook["chat.message"]({ sessionID }, output) + + // then - hyperplan must not fire (the trailing '.hpp' is a header extension, not the trigger) + const textPart = output.parts.find(p => p.type === "text") + expect(textPart).toBeDefined() + expect(textPart!.text).not.toContain("") + expect(textPart!.text).not.toContain('skill(name="hyperplan")') + }) + test("should fire 'Hyperplan Mode Activated' toast when keyword detected", async () => { // given - main session and toast tracking const sessionID = "hyperplan-toast-session" diff --git a/src/hooks/keyword-detector/hyperplan/default.ts b/src/hooks/keyword-detector/hyperplan/default.ts index 1a38b75b3..cf27e087a 100644 --- a/src/hooks/keyword-detector/hyperplan/default.ts +++ b/src/hooks/keyword-detector/hyperplan/default.ts @@ -8,9 +8,14 @@ * * The detector injects a thin wrapper that loads the `hyperplan` skill, which * carries the full orchestration instructions for the 5-member adversarial team. + * + * The `hpp` shorthand uses an extra negative-lookbehind so that the very common + * C++ header-file extension `.hpp` (e.g. `interface.hpp`, `src/buffer.hpp`) + * does NOT falsely trigger hyperplan mode. A leading `.` would otherwise + * satisfy `\b` because the dot is a non-word character. See issue #4215. */ -export const HYPERPLAN_PATTERN = /\b(hyperplan|hpp)\b/i +export const HYPERPLAN_PATTERN = /\bhyperplan\b|(? **MANDATORY**: Say "HYPERPLAN MODE ENABLED!" as your first response, exactly once.