fix(keyword-detector): stop hyperplan firing on '.hpp' C++ header paths (fixes #4215)
The hyperplan trigger \b(hyperplan|hpp)\b/i matched 'hpp' inside common C++ header references like 'check interface.hpp' or 'open buffer.hpp'. The leading '.' is a non-word character, so \b is already satisfied and the false positive fires the hyperplan-mode prompt on routine code questions.
Split the alternation so 'hpp' additionally requires that the preceding character is neither a word character nor a '.'. This preserves every existing trigger ('hpp do this', '/hpp ...', mid-sentence usage, mixed case) while rejecting filename uses of the .hpp extension. The longer 'hyperplan' keyword keeps the original \b boundary semantics.
Reproduction (added regression tests):
- 'please help to check interface.hpp' must NOT fire
- 'open src/include/audio/buffer.hpp and fix the leak' must NOT fire
All 14 cases in hyperplan.test.ts pass (12 existing + 2 new), broader keyword-detector suite stays green (92 pass), typecheck clean.
This commit is contained in:
@@ -122,6 +122,46 @@ describe("keyword-detector hyperplan keyword", () => {
|
|||||||
expect(textPart!.text).not.toContain("<hyperplan-mode>")
|
expect(textPart!.text).not.toContain("<hyperplan-mode>")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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<string, unknown>,
|
||||||
|
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("<hyperplan-mode>")
|
||||||
|
})
|
||||||
|
|
||||||
|
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<string, unknown>,
|
||||||
|
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("<hyperplan-mode>")
|
||||||
|
expect(textPart!.text).not.toContain('skill(name="hyperplan")')
|
||||||
|
})
|
||||||
|
|
||||||
test("should fire 'Hyperplan Mode Activated' toast when keyword detected", async () => {
|
test("should fire 'Hyperplan Mode Activated' toast when keyword detected", async () => {
|
||||||
// given - main session and toast tracking
|
// given - main session and toast tracking
|
||||||
const sessionID = "hyperplan-toast-session"
|
const sessionID = "hyperplan-toast-session"
|
||||||
|
|||||||
@@ -8,9 +8,14 @@
|
|||||||
*
|
*
|
||||||
* The detector injects a thin wrapper that loads the `hyperplan` skill, which
|
* The detector injects a thin wrapper that loads the `hyperplan` skill, which
|
||||||
* carries the full orchestration instructions for the 5-member adversarial team.
|
* 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|(?<![\w.])hpp\b/i
|
||||||
|
|
||||||
export const HYPERPLAN_MESSAGE = `<hyperplan-mode>
|
export const HYPERPLAN_MESSAGE = `<hyperplan-mode>
|
||||||
**MANDATORY**: Say "HYPERPLAN MODE ENABLED!" as your first response, exactly once.
|
**MANDATORY**: Say "HYPERPLAN MODE ENABLED!" as your first response, exactly once.
|
||||||
|
|||||||
Reference in New Issue
Block a user