feat(keyword-detector): add hyperplan-ultrawork combo + tuple refactor

Adjacent "hpp ulw" or "ulw hpp" (8 form combinations: short/long, both
orders) triggers a fused mode that suppresses the standalone ultrawork
and hyperplan banners and toasts in favor of one combo banner. The
combo banner explicitly preserves hyperplan's mandatory adversarial
workflow contract (do NOT improvise, do NOT skip rounds) instead of
silently downgrading it.

Suppression runs as a named pipeline step (suppressComboStandalones)
immediately after detection and before all consumers (planner filter,
session filters, toasts, message injection), so standalone toast checks
naturally see the already-suppressed list. Combo is allowed in non-main
sessions like ultrawork, filtered for planner agents like both
standalones, and blocked in subagent sessions via the existing gate.

disabled_keywords uses the intersection rule: disabling either
"ultrawork" or "hyperplan" also disables the combo, so no flavored
content leaks via the combo embedding when either base keyword is
disabled.

Includes a same-PR refactor of KEYWORD_DETECTORS from {pattern, message}
to {type, pattern, message} tuple shape, dropping the parallel hardcoded
types array in detector.ts that previously coupled type assignment to
registry index. Future detector additions can no longer silently corrupt
DetectedKeyword.type via reorder or insertion.

10 behavioral contract tests in hyperplan-ultrawork.test.ts cover both
trigger orders, non-adjacent rejection, suppression of injection and
toast, intersection-rule disable behavior, session/agent policy, and
ultrawork variant routing through the combo. The pre-existing combined
"ultrawork hyperplan" assertion in hyperplan.test.ts is removed in
favor of the new file.

Plan distilled from a hyperplan adversarial review (5 members,
3 rounds: skeptic, validator, researcher, architect, creative).
This commit is contained in:
YeonGyu-Kim
2026-04-29 17:36:18 +09:00
parent 8473e1cc32
commit 5069e5e345
6 changed files with 323 additions and 27 deletions

View File

@@ -1,6 +1,6 @@
import { z } from "zod"
export const KeywordTypeSchema = z.enum(["ultrawork", "search", "analyze", "team", "hyperplan"])
export const KeywordTypeSchema = z.enum(["ultrawork", "search", "analyze", "team", "hyperplan", "hyperplan-ultrawork"])
export type KeywordType = z.infer<typeof KeywordTypeSchema>
export const KeywordDetectorConfigSchema = z.object({

View File

@@ -7,26 +7,45 @@ export { ANALYZE_PATTERN, ANALYZE_MESSAGE } from "./analyze"
export { TEAM_PATTERN, TEAM_MESSAGE } from "./team"
export { HYPERPLAN_PATTERN, HYPERPLAN_MESSAGE } from "./hyperplan"
import type { KeywordType } from "../../config/schema/keyword-detector"
import { getUltraworkMessage } from "./ultrawork"
import { SEARCH_PATTERN, SEARCH_MESSAGE } from "./search"
import { TEAM_PATTERN, TEAM_MESSAGE } from "./team"
import { HYPERPLAN_PATTERN, HYPERPLAN_MESSAGE } from "./hyperplan"
// Hyperplan-ultrawork combo: strict adjacency, both word orders
export const HYPERPLAN_ULTRAWORK_PATTERN =
/\b(?:hpp|hyperplan)\s+(?:ulw|ultrawork)\b|\b(?:ulw|ultrawork)\s+(?:hpp|hyperplan)\b/i
const HYPERPLAN_ULTRAWORK_BANNER = `<hyperplan-ultrawork-mode>
**MANDATORY**: Say "HYPERPLAN ULTRAWORK MODE ENABLED!" exactly once as your first response. Do NOT say the standalone "ULTRAWORK MODE ENABLED!" or "HYPERPLAN MODE ENABLED!" banners.
Apply the ultrawork protocol below as your execution framework. You MUST ALSO load the hyperplan skill immediately via \`skill(name="hyperplan")\` and follow its full adversarial workflow — do NOT improvise, do NOT skip rounds, do NOT write the plan yourself.
</hyperplan-ultrawork-mode>`
export function getHyperplanUltraworkMessage(agentName?: string, modelID?: string): string {
return `${HYPERPLAN_ULTRAWORK_BANNER}\n\n${getUltraworkMessage(agentName, modelID)}`
}
export type KeywordDetector = {
type: KeywordType
pattern: RegExp
message: string | ((agentName?: string, modelID?: string) => string)
}
export const KEYWORD_DETECTORS: KeywordDetector[] = [
{
type: "ultrawork",
pattern: /\b(ultrawork|ulw)\b/i,
message: getUltraworkMessage,
},
{
type: "search",
pattern: SEARCH_PATTERN,
message: SEARCH_MESSAGE,
},
{
type: "analyze",
pattern:
/\b(analyze|analyse|investigate|examine|research|study|deep[\s-]?dive|inspect|audit|evaluate|assess|review|diagnose|scrutinize|dissect|debug|comprehend|interpret|breakdown|understand)\b|why\s+is|how\s+does|how\s+to|분석|조사|파악|연구|검토|진단|이해|설명|원인|이유|뜯어봐|따져봐|평가|해석|디버깅|디버그|어떻게|왜|살펴|分析|調査|解析|検討|研究|診断|理解|説明|検証|精査|究明|デバッグ|なぜ|どう|仕組み|调查|检查|剖析|深入|诊断|解释|调试|为什么|原理|搞清楚|弄明白|phân tích|điều tra|nghiên cứu|kiểm tra|xem xét|chẩn đoán|giải thích|tìm hiểu|gỡ lỗi|tại sao/i,
message: `[analyze-mode]
@@ -46,11 +65,18 @@ MANDATORY delegate_task params: ALWAYS include load_skills=[] and run_in_backgro
Example: delegate_task(subagent_type="explore", prompt="...", run_in_background=true, load_skills=[])`,
},
{
type: "team",
pattern: TEAM_PATTERN,
message: TEAM_MESSAGE,
},
{
type: "hyperplan",
pattern: HYPERPLAN_PATTERN,
message: HYPERPLAN_MESSAGE,
},
{
type: "hyperplan-ultrawork",
pattern: HYPERPLAN_ULTRAWORK_PATTERN,
message: getHyperplanUltraworkMessage,
},
]

View File

@@ -40,11 +40,14 @@ export function detectKeywordsWithType(
disabledKeywords?: ReadonlyArray<KeywordType>,
): DetectedKeyword[] {
const textWithoutCode = removeCodeBlocks(text)
const types: Array<KeywordType> = ["ultrawork", "search", "analyze", "team", "hyperplan"]
const disabled = new Set<KeywordType>(disabledKeywords ?? [])
return KEYWORD_DETECTORS.map(({ pattern, message }, index) => ({
// Intersection rule: combo requires BOTH base keywords enabled
if (disabled.has("ultrawork") || disabled.has("hyperplan")) {
disabled.add("hyperplan-ultrawork")
}
return KEYWORD_DETECTORS.map(({ type, pattern, message }) => ({
matches: pattern.test(textWithoutCode),
type: types[index],
type,
message: resolveMessage(message, agentName, modelID),
}))
.filter((result) => result.matches && !disabled.has(result.type))

View File

@@ -1,5 +1,6 @@
import type { PluginInput } from "@opencode-ai/plugin"
import type { KeywordDetectorConfig } from "../../config/schema/keyword-detector"
import type { DetectedKeyword } from "./detector"
import { detectKeywordsWithType, extractPromptText } from "./detector"
import { isPlannerAgent, isNonOmoAgent } from "./constants"
import { log } from "../../shared"
@@ -15,6 +16,12 @@ import {
import type { ContextCollector } from "../../features/context-injector"
import type { RalphLoopHook } from "../ralph-loop"
function suppressComboStandalones(detected: DetectedKeyword[]): DetectedKeyword[] {
const hasCombo = detected.some((k) => k.type === "hyperplan-ultrawork")
if (!hasCombo) return detected
return detected.filter((k) => k.type !== "ultrawork" && k.type !== "hyperplan")
}
export function createKeywordDetectorHook(
ctx: PluginInput,
_collector?: ContextCollector,
@@ -63,10 +70,13 @@ export function createKeywordDetectorHook(
const cleanText = removeSystemReminders(promptText)
const modelID = input.model?.modelID
let detectedKeywords = detectKeywordsWithType(cleanText, currentAgent, modelID, disabledKeywords)
detectedKeywords = suppressComboStandalones(detectedKeywords)
if (isPlannerAgent(currentAgent)) {
const preFilterCount = detectedKeywords.length
detectedKeywords = detectedKeywords.filter((k) => k.type !== "ultrawork" && k.type !== "hyperplan")
detectedKeywords = detectedKeywords.filter(
(k) => k.type !== "ultrawork" && k.type !== "hyperplan" && k.type !== "hyperplan-ultrawork"
)
if (preFilterCount > detectedKeywords.length) {
log(`[keyword-detector] Filtered ultrawork/hyperplan keywords for planner agent`, { sessionID: input.sessionID, agent: currentAgent })
}
@@ -86,7 +96,9 @@ export function createKeywordDetectorHook(
const isNonMainSession = mainSessionID && input.sessionID !== mainSessionID
if (isNonMainSession) {
detectedKeywords = detectedKeywords.filter((k) => k.type === "ultrawork")
detectedKeywords = detectedKeywords.filter(
(k) => k.type === "ultrawork" || k.type === "hyperplan-ultrawork"
)
if (detectedKeywords.length === 0) {
log(`[keyword-detector] Skipping non-ultrawork keywords in non-main session`, {
sessionID: input.sessionID,
@@ -149,6 +161,21 @@ export function createKeywordDetectorHook(
)
}
const hasHyperplanUltrawork = detectedKeywords.some((k) => k.type === "hyperplan-ultrawork")
if (hasHyperplanUltrawork) {
log(`[keyword-detector] Hyperplan Ultrawork mode activated`, { sessionID: input.sessionID })
ctx.client.tui
.showToast({
body: {
title: "Hyperplan Ultrawork Mode Activated",
message: "Ultrawork execution with adversarial hyperplan workflow.",
variant: "success" as const,
duration: 3000,
},
})
.catch((err) => log(`[keyword-detector] Failed to show toast`, { error: err, sessionID: input.sessionID }))
}
const textPartIndex = output.parts.findIndex((p) => p.type === "text" && p.text !== undefined)
if (textPartIndex === -1) {
log(`[keyword-detector] No text part found, skipping injection`, { sessionID: input.sessionID })

View File

@@ -0,0 +1,261 @@
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-ultrawork combo", () => {
let logSpy: ReturnType<typeof spyOn>
let getMainSessionSpy: ReturnType<typeof spyOn>
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 combo message when user types 'hpp ulw' (forward order)", async () => {
// given - main session with adjacent forward-order combo keywords
const sessionID = "combo-forward-session"
getMainSessionSpy = spyOn(sessionState, "getMainSessionID").mockReturnValue(sessionID)
const hook = createKeywordDetectorHook(createMockPluginInput())
const output = {
message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "hpp ulw refactor the auth module" }],
}
// when - keyword detection runs
await hook["chat.message"]({ sessionID }, output)
// then - combo banner and embedded ultrawork content both present
const textPart = output.parts.find(p => p.type === "text")
expect(textPart).toBeDefined()
expect(textPart!.text).toContain("<hyperplan-ultrawork-mode>")
expect(textPart!.text).toContain("<ultrawork-mode>")
expect(textPart!.text).toContain("refactor the auth module")
})
test("should inject combo message when user types 'ulw hpp' (reverse order)", async () => {
// given - main session with adjacent reverse-order combo keywords
const sessionID = "combo-reverse-session"
getMainSessionSpy = spyOn(sessionState, "getMainSessionID").mockReturnValue(sessionID)
const hook = createKeywordDetectorHook(createMockPluginInput())
const output = {
message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "ulw hpp ship this feature" }],
}
// when - keyword detection runs
await hook["chat.message"]({ sessionID }, output)
// then - combo fires identically regardless of word order
const textPart = output.parts.find(p => p.type === "text")
expect(textPart).toBeDefined()
expect(textPart!.text).toContain("<hyperplan-ultrawork-mode>")
expect(textPart!.text).toContain("<ultrawork-mode>")
expect(textPart!.text).toContain("ship this feature")
})
test("should NOT trigger combo on non-adjacent 'hpp do ulw' but fire both standalones instead", async () => {
// given - keywords separated by another word block adjacency requirement
const sessionID = "combo-non-adjacent-session"
getMainSessionSpy = spyOn(sessionState, "getMainSessionID").mockReturnValue(sessionID)
const hook = createKeywordDetectorHook(createMockPluginInput())
const output = {
message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "hpp do ulw stuff" }],
}
// when - keyword detection runs
await hook["chat.message"]({ sessionID }, output)
// then - combo absent, both standalone banners injected separately
const textPart = output.parts.find(p => p.type === "text")
expect(textPart).toBeDefined()
expect(textPart!.text).not.toContain("<hyperplan-ultrawork-mode>")
expect(textPart!.text).toContain("<hyperplan-mode>")
expect(textPart!.text).toContain("<ultrawork-mode>")
})
test("should suppress standalone messages when combo fires (only ONE banner injected)", async () => {
// given - combo keywords that would also match standalone patterns
const sessionID = "combo-suppress-session"
getMainSessionSpy = spyOn(sessionState, "getMainSessionID").mockReturnValue(sessionID)
const hook = createKeywordDetectorHook(createMockPluginInput())
const output = {
message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "hpp ulw build" }],
}
// when - keyword detection runs
await hook["chat.message"]({ sessionID }, output)
// then - only combo banner present, standalone hyperplan suppressed, ultrawork content appears once via embed
const textPart = output.parts.find(p => p.type === "text")
expect(textPart).toBeDefined()
expect(textPart!.text).toContain("<hyperplan-ultrawork-mode>")
expect(textPart!.text).not.toContain("<hyperplan-mode>")
const ultraworkMatches = textPart!.text!.match(/<ultrawork-mode>/g) ?? []
expect(ultraworkMatches).toHaveLength(1)
})
test("should fire combo toast and suppress standalone toasts", async () => {
// given - combo keywords with toast tracking
const sessionID = "combo-toast-session"
getMainSessionSpy = spyOn(sessionState, "getMainSessionID").mockReturnValue(sessionID)
const toastCalls: string[] = []
const hook = createKeywordDetectorHook(createMockPluginInput({ toastCalls }))
const output = {
message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "hpp ulw do it" }],
}
// when - combo fires
await hook["chat.message"]({ sessionID }, output)
// then - only combo toast title is shown, standalone toasts suppressed
expect(toastCalls).toContain("Hyperplan Ultrawork Mode Activated")
expect(toastCalls).not.toContain("Ultrawork Mode Activated")
expect(toastCalls).not.toContain("Hyperplan Mode Activated")
})
test("should disable combo only when disabled_keywords includes 'hyperplan-ultrawork' (standalones still fire)", async () => {
// given - combo keyword disabled but standalones remain enabled
const sessionID = "combo-disabled-session"
getMainSessionSpy = spyOn(sessionState, "getMainSessionID").mockReturnValue(sessionID)
const hook = createKeywordDetectorHook(
createMockPluginInput(),
undefined,
undefined,
{ disabled_keywords: ["hyperplan-ultrawork"] },
)
const output = {
message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "hpp ulw work it" }],
}
// when - keyword detection runs with combo disabled
await hook["chat.message"]({ sessionID }, output)
// then - combo absent, both individual standalones still match and inject
const textPart = output.parts.find(p => p.type === "text")
expect(textPart).toBeDefined()
expect(textPart!.text).not.toContain("<hyperplan-ultrawork-mode>")
expect(textPart!.text).toContain("<hyperplan-mode>")
expect(textPart!.text).toContain("<ultrawork-mode>")
})
test("should block combo via intersection rule when disabled_keywords includes 'ultrawork'", async () => {
// given - ultrawork standalone disabled, intersection rule cascades to combo
const sessionID = "combo-intersection-session"
getMainSessionSpy = spyOn(sessionState, "getMainSessionID").mockReturnValue(sessionID)
const toastCalls: string[] = []
const hook = createKeywordDetectorHook(
createMockPluginInput({ toastCalls }),
undefined,
undefined,
{ disabled_keywords: ["ultrawork"] },
)
const output = {
message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "hpp ulw plan stuff" }],
}
// when - combo would match but is blocked via intersection
await hook["chat.message"]({ sessionID }, output)
// then - no combo, no ultrawork content leaks; standalone hyperplan still fires
const textPart = output.parts.find(p => p.type === "text")
expect(textPart).toBeDefined()
expect(textPart!.text).not.toContain("<hyperplan-ultrawork-mode>")
expect(textPart!.text).not.toContain("<ultrawork-mode>")
expect(textPart!.text).toContain("<hyperplan-mode>")
expect(toastCalls).not.toContain("Hyperplan Ultrawork Mode Activated")
expect(toastCalls).not.toContain("Ultrawork Mode Activated")
})
test("should allow combo in non-main session (passes through like standalone ultrawork)", async () => {
// given - main session set, different (subagent) session triggers combo
const mainSessionID = "main-combo"
const subagentSessionID = "subagent-combo"
setMainSession(mainSessionID)
const hook = createKeywordDetectorHook(createMockPluginInput())
const output = {
message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "hpp ulw run this" }],
}
// when - subagent session triggers combo
await hook["chat.message"]({ sessionID: subagentSessionID }, output)
// then - combo banner reaches non-main session (whitelisted alongside standalone ultrawork)
const textPart = output.parts.find(p => p.type === "text")
expect(textPart).toBeDefined()
expect(textPart!.text).toContain("<hyperplan-ultrawork-mode>")
expect(textPart!.text).toContain("<ultrawork-mode>")
expect(textPart!.text).toContain("run this")
})
test("should filter combo when agent is prometheus (planner)", async () => {
// given - planner agent receives a combo prompt
const sessionID = "combo-prometheus-session"
const hook = createKeywordDetectorHook(createMockPluginInput())
const output = {
message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "hpp ulw plan stuff" }],
}
// when - planner-agent path filters all execution-mode keywords
await hook["chat.message"]({ sessionID, agent: "prometheus" }, output)
// then - text untouched: combo, ultrawork, and hyperplan all filtered for planner
const textPart = output.parts.find(p => p.type === "text")
expect(textPart).toBeDefined()
expect(textPart!.text).toBe("hpp ulw plan stuff")
expect(textPart!.text).not.toContain("<hyperplan-ultrawork-mode>")
expect(textPart!.text).not.toContain("<ultrawork-mode>")
expect(textPart!.text).not.toContain("<hyperplan-mode>")
})
test("should reuse ultrawork variant: combo with GPT model embeds GPT ultrawork content", async () => {
// given - GPT-5.4 model selects the GPT ultrawork variant inside the combo banner
const sessionID = "combo-gpt-variant-session"
getMainSessionSpy = spyOn(sessionState, "getMainSessionID").mockReturnValue(sessionID)
const hook = createKeywordDetectorHook(createMockPluginInput())
const output = {
message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "hpp ulw build feature" }],
}
// when - combo fires with GPT model resolved
await hook["chat.message"](
{ sessionID, agent: "sisyphus", model: { providerID: "openai", modelID: "gpt-5.4" } },
output,
)
// then - combo banner present and GPT-variant ultrawork content embedded (output_verbosity_spec is GPT-only)
const textPart = output.parts.find(p => p.type === "text")
expect(textPart).toBeDefined()
expect(textPart!.text).toContain("<hyperplan-ultrawork-mode>")
expect(textPart!.text).toContain("<output_verbosity_spec>")
})
})

View File

@@ -220,25 +220,4 @@ describe("keyword-detector hyperplan keyword", () => {
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<string, unknown>,
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("<hyperplan-mode>")
expect(textPart!.text).toContain("YOU MUST LEVERAGE ALL AVAILABLE AGENTS")
expect(textPart!.text).toContain("ship this feature")
})
})