From f025a4ca309ad2f280d94b530ceb70d6539da65e Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 28 Apr 2026 19:03:55 +0900 Subject: [PATCH] feat(keyword-detector): add hyperplan keyword pattern, message and test Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/hooks/keyword-detector/hyperplan.test.ts | 244 ++++++++++++++++++ .../keyword-detector/hyperplan/default.ts | 37 +++ src/hooks/keyword-detector/hyperplan/index.ts | 1 + 3 files changed, 282 insertions(+) create mode 100644 src/hooks/keyword-detector/hyperplan.test.ts create mode 100644 src/hooks/keyword-detector/hyperplan/default.ts create mode 100644 src/hooks/keyword-detector/hyperplan/index.ts diff --git a/src/hooks/keyword-detector/hyperplan.test.ts b/src/hooks/keyword-detector/hyperplan.test.ts new file mode 100644 index 000000000..5110d6498 --- /dev/null +++ b/src/hooks/keyword-detector/hyperplan.test.ts @@ -0,0 +1,244 @@ +import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test" +import type { PluginInput } from "@opencode-ai/plugin" +import { createKeywordDetectorHook } from "./index" +import { setMainSession, _resetForTesting } from "../../features/claude-code-session-state" +import * as sharedModule from "../../shared" +import * as sessionState from "../../features/claude-code-session-state" + +describe("keyword-detector hyperplan keyword", () => { + let logSpy: ReturnType + let getMainSessionSpy: ReturnType + + beforeEach(() => { + _resetForTesting() + logSpy = spyOn(sharedModule, "log").mockImplementation(() => {}) + }) + + afterEach(() => { + logSpy?.mockRestore() + getMainSessionSpy?.mockRestore() + _resetForTesting() + }) + + function createMockPluginInput(options: { toastCalls?: string[] } = {}) { + const toastCalls = options.toastCalls ?? [] + return { + client: { + tui: { + showToast: async (opts: { body: { title: string } }) => { + toastCalls.push(opts.body.title) + }, + }, + }, + } as unknown as PluginInput + } + + test("should inject hyperplan message when user types 'hyperplan'", async () => { + // given - main session typing the full keyword + const sessionID = "hyperplan-full-session" + getMainSessionSpy = spyOn(sessionState, "getMainSessionID").mockReturnValue(sessionID) + const hook = createKeywordDetectorHook(createMockPluginInput()) + const output = { + message: {} as Record, + parts: [{ type: "text", text: "hyperplan refactor the auth module" }], + } + + // when - keyword detection runs + await hook["chat.message"]({ sessionID }, output) + + // then - hyperplan-mode wrapper and skill-loading instruction should be present + const textPart = output.parts.find(p => p.type === "text") + expect(textPart).toBeDefined() + expect(textPart!.text).toContain("") + expect(textPart!.text).toContain('skill(name="hyperplan")') + expect(textPart!.text).toContain("HYPERPLAN MODE ENABLED") + expect(textPart!.text).toContain("refactor the auth module") + expect(textPart!.text).toContain("---") + }) + + test("should inject hyperplan message when user types 'hpp' shorthand", async () => { + // given - main session typing the short keyword + const sessionID = "hyperplan-short-session" + getMainSessionSpy = spyOn(sessionState, "getMainSessionID").mockReturnValue(sessionID) + const hook = createKeywordDetectorHook(createMockPluginInput()) + const output = { + message: {} as Record, + parts: [{ type: "text", text: "hpp how should I structure this feature" }], + } + + // when - keyword detection runs + await hook["chat.message"]({ sessionID }, output) + + // then - hyperplan injection should fire + const textPart = output.parts.find(p => p.type === "text") + expect(textPart).toBeDefined() + expect(textPart!.text).toContain("") + expect(textPart!.text).toContain('skill(name="hyperplan")') + }) + + test("should inject hyperplan message case-insensitively", async () => { + // given - main session typing in mixed case + const sessionID = "hyperplan-case-session" + getMainSessionSpy = spyOn(sessionState, "getMainSessionID").mockReturnValue(sessionID) + const hook = createKeywordDetectorHook(createMockPluginInput()) + const output = { + message: {} as Record, + parts: [{ type: "text", text: "HyperPlan something now" }], + } + + // when - keyword detection runs with mixed case input + await hook["chat.message"]({ sessionID }, output) + + // then - hyperplan should still fire + const textPart = output.parts.find(p => p.type === "text") + expect(textPart).toBeDefined() + expect(textPart!.text).toContain("") + }) + + test("should NOT trigger hyperplan when 'hpp' is a substring of another word", async () => { + // given - text contains 'hpp' only as part of larger string with no word boundary + const sessionID = "hyperplan-substring-session" + getMainSessionSpy = spyOn(sessionState, "getMainSessionID").mockReturnValue(sessionID) + const hook = createKeywordDetectorHook(createMockPluginInput()) + const output = { + message: {} as Record, + parts: [{ type: "text", text: "myhppvar = 1" }], + } + + // when - keyword detection runs + await hook["chat.message"]({ sessionID }, output) + + // then - hyperplan should NOT trigger because 'hpp' lacks word boundaries + const textPart = output.parts.find(p => p.type === "text") + expect(textPart).toBeDefined() + expect(textPart!.text).toBe("myhppvar = 1") + expect(textPart!.text).not.toContain("") + }) + + test("should fire 'Hyperplan Mode Activated' toast when keyword detected", async () => { + // given - main session and toast tracking + const sessionID = "hyperplan-toast-session" + getMainSessionSpy = spyOn(sessionState, "getMainSessionID").mockReturnValue(sessionID) + const toastCalls: string[] = [] + const hook = createKeywordDetectorHook(createMockPluginInput({ toastCalls })) + const output = { + message: {} as Record, + parts: [{ type: "text", text: "hyperplan this task" }], + } + + // when - hyperplan keyword fires + await hook["chat.message"]({ sessionID }, output) + + // then - toast title should be present in tracked calls + expect(toastCalls).toContain("Hyperplan Mode Activated") + }) + + test("should NOT inject hyperplan when disabled_keywords includes 'hyperplan'", async () => { + // given - keyword detector with hyperplan disabled + const sessionID = "hyperplan-disabled-session" + getMainSessionSpy = spyOn(sessionState, "getMainSessionID").mockReturnValue(sessionID) + const toastCalls: string[] = [] + const hook = createKeywordDetectorHook( + createMockPluginInput({ toastCalls }), + undefined, + undefined, + { disabled_keywords: ["hyperplan"] }, + ) + const output = { + message: {} as Record, + parts: [{ type: "text", text: "hyperplan refactor this" }], + } + + // when - hyperplan keyword would normally fire + await hook["chat.message"]({ sessionID }, output) + + // then - neither injection nor toast should occur + const textPart = output.parts.find(p => p.type === "text") + expect(textPart).toBeDefined() + expect(textPart!.text).toBe("hyperplan refactor this") + expect(textPart!.text).not.toContain("") + expect(toastCalls).not.toContain("Hyperplan Mode Activated") + }) + + test("should filter hyperplan keyword in non-main session (only ultrawork allowed there)", async () => { + // given - main session set, different (subagent) session triggers hyperplan + const mainSessionID = "main-hyperplan" + const subagentSessionID = "subagent-hyperplan" + setMainSession(mainSessionID) + const hook = createKeywordDetectorHook(createMockPluginInput()) + const output = { + message: {} as Record, + parts: [{ type: "text", text: "hyperplan please" }], + } + + // when - subagent session triggers hyperplan keyword + await hook["chat.message"]({ sessionID: subagentSessionID }, output) + + // then - hyperplan injection should be skipped in non-main session + const textPart = output.parts.find(p => p.type === "text") + expect(textPart).toBeDefined() + expect(textPart!.text).toBe("hyperplan please") + expect(textPart!.text).not.toContain("") + }) + + test("should skip hyperplan injection when agent is prometheus (planner)", async () => { + // given - hook running with prometheus agent and a prompt that only triggers hyperplan + const sessionID = "hyperplan-prometheus-session" + const hook = createKeywordDetectorHook(createMockPluginInput()) + const output = { + message: {} as Record, + parts: [{ type: "text", text: "hyperplan refactor stuff" }], + } + + // when - hyperplan keyword detected with prometheus agent + await hook["chat.message"]({ sessionID, agent: "prometheus" }, output) + + // then - hyperplan should be filtered out for planner agents + 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")') + expect(textPart!.text).toContain("hyperplan refactor stuff") + }) + + test("should skip hyperplan injection when agent name contains 'planner' token", async () => { + // given - hook running with planner-named agent and a prompt that only triggers hpp + const sessionID = "hyperplan-planner-session" + const hook = createKeywordDetectorHook(createMockPluginInput()) + const output = { + message: {} as Record, + parts: [{ type: "text", text: "hpp build the feature" }], + } + + // when - hpp keyword detected with planner agent + await hook["chat.message"]({ sessionID, agent: "Plan Agent" }, output) + + // then - hyperplan should be filtered out + 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")') + expect(textPart!.text).toContain("hpp build the feature") + }) + + test("should inject hyperplan AND ultrawork together when both keywords present", async () => { + // given - main session typing both keywords in the same message + const sessionID = "hyperplan-combo-session" + getMainSessionSpy = spyOn(sessionState, "getMainSessionID").mockReturnValue(sessionID) + const hook = createKeywordDetectorHook(createMockPluginInput()) + const output = { + message: {} as Record, + parts: [{ type: "text", text: "ultrawork hyperplan ship this feature" }], + } + + // when - both keywords trigger + await hook["chat.message"]({ sessionID }, output) + + // then - both messages should be present + const textPart = output.parts.find(p => p.type === "text") + expect(textPart).toBeDefined() + expect(textPart!.text).toContain("") + expect(textPart!.text).toContain("YOU MUST LEVERAGE ALL AVAILABLE AGENTS") + expect(textPart!.text).toContain("ship this feature") + }) +}) diff --git a/src/hooks/keyword-detector/hyperplan/default.ts b/src/hooks/keyword-detector/hyperplan/default.ts new file mode 100644 index 000000000..792242700 --- /dev/null +++ b/src/hooks/keyword-detector/hyperplan/default.ts @@ -0,0 +1,37 @@ +/** + * Hyperplan keyword detector. + * + * Triggers when the user wants adversarial multi-agent planning via team-mode. + * + * Triggers (case-insensitive, word-bounded): + * - English: hyperplan, hpp + * + * The detector injects a thin wrapper that loads the `hyperplan` skill, which + * carries the full orchestration instructions for the 5-member adversarial team. + */ + +export const HYPERPLAN_PATTERN = /\b(hyperplan|hpp)\b/i + +export const HYPERPLAN_MESSAGE = ` +**MANDATORY**: Say "HYPERPLAN MODE ENABLED!" as your first response, exactly once. + +The user invoked **hyperplan mode** — adversarial multi-agent planning via team-mode. + +LOAD THE HYPERPLAN SKILL IMMEDIATELY: + +\`\`\` +skill(name="hyperplan") +\`\`\` + +After loading, follow the skill's 6-phase workflow EXACTLY: +1. Acknowledge and capture the planning request +2. Spawn the 5-member adversarial team via team_create +3. Round 1 — Independent analysis (each member produces findings) +4. Round 2 — Cross-attack (each member ruthlessly attacks the other 4's findings) +5. Round 3 — Defend, refine, or concede +6. Synthesize defensible insights into a work plan + clean up the team + +Do NOT improvise. Do NOT skip rounds. Be the lead orchestrator and let the adversarial members do the cross-critique. + +If team-mode is unavailable (\`team_*\` tools missing), instruct the user to set \`team_mode.enabled: true\` in \`~/.config/opencode/oh-my-opencode.jsonc\` and restart opencode. +` diff --git a/src/hooks/keyword-detector/hyperplan/index.ts b/src/hooks/keyword-detector/hyperplan/index.ts new file mode 100644 index 000000000..0fe782da7 --- /dev/null +++ b/src/hooks/keyword-detector/hyperplan/index.ts @@ -0,0 +1 @@ +export { HYPERPLAN_PATTERN, HYPERPLAN_MESSAGE } from "./default"