Add optional enabled_expansions field to keyword_detector config schema.
When set, acts as an allowlist - only those expansion types fire.
Empty array disables all expansions. Absent field keeps all enabled (backward-compatible).
Also supports coexistence with disabled_keywords denylist for fine-grained control.
- src/config/schema/keyword-detector.ts: add enabled_expansions field
- src/hooks/keyword-detector/detector.ts: apply allowlist filter in detectKeywordsWithType
- src/hooks/keyword-detector/hook.ts: pass enabled_expansions from config
- src/hooks/keyword-detector/index.test.ts: add 4 tests for enabled_expansions behavior
- assets/oh-my-opencode.schema.json: regenerate schema
When user types /hyperplan something, two pipelines race for the message:
1. keyword-detector hook (chat.message): the regex \b(hyperplan|hpp)\b
matches /hyperplan because \b is satisfied by the / boundary, so the
hook prepends <hyperplan-mode> to the text part.
2. auto-slash-command hook (chat.message, runs immediately after):
detectSlashCommand() checks `text.trimStart().startsWith("/")`. After
keyword-detector's prepend, the part now starts with <hyperplan-mode>
and the slash check fails, so the builtin command template
(with $ARGUMENTS substituted) is never injected.
The visible symptom: /hyperplan refactor X never runs the actual
HYPERPLAN_TEMPLATE - the user sees only the keyword-detector wrapper, which
is similar but not identical, and the slash command's $ARGUMENTS payload
is silently lost.
Fix: at the top of the keyword-detector hook, after isSystemDirective() but
before any keyword scan, bail out if the prompt text starts with a slash
command (^\s*\/[a-zA-Z][\w-]*\b). Slash commands are explicit invocations
and own their own mode-injection path; the keyword detector must not race
them. Free-form mentions like "hyperplan: refactor X" still trigger
keyword detection - only the leading-slash form is suppressed.
Lock the contract with three regression tests:
- /hyperplan refactor ... must NOT inject <hyperplan-mode>
- /hpp investigate ... must NOT inject (shorthand slash command)
- "hyperplan: refactor src/auth/handler.ts" still injects (free-form)
Generated with assistance of [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode).
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).
Adds keyword_detector.disabled_keywords config so users can opt out of
specific keyword detectors individually without disabling the entire
keyword-detector hook. Allowed values: 'ultrawork', 'search', 'analyze',
'team'. Default empty/missing -> all four detectors active (no behavior
change for existing configs).
Motivation: an audit revealed search and analyze patterns trigger on
~30-60% of normal conversational user messages (e.g. 'how to', 'why is',
'show me', '왜', '어떻게'). The disable list is the immediate kill switch
while the patterns themselves are tightened in a separate PR.
Schema follows the existing per-feature config block convention shared
by team_mode, ralph_loop, runtime_fallback, and comment_checker. The
KeywordType enum (z.enum) lives next to the config schema and is
re-imported by the detector to keep the union type in lockstep with the
schema.
Threading:
pluginConfig.keyword_detector
-> create-transform-hooks.ts (factory wiring)
-> createKeywordDetectorHook(config)
-> detectKeywordsWithType(text, agent, model, disabledKeywords)
-> Set-based filter at the source-of-truth detector
Adds 8 regression tests covering per-keyword disable, multi-keyword
disable, partial disable (one keyword off, another still firing),
ultrawork toast suppression, undefined config, and empty array.
Detects user invocations of team-mode work across English and Korean
('team mode', '팀 모드', '팀으로') and injects a concise English directive
instructing the LLM to orchestrate via team_* tools (team_create ->
team_task_create + team_send_message), forbidding delegate_task
substitution and fallbacks.
The Korean variants use a Hangul-syllable negative lookbehind (가-힣) so
that '스팀으로 게임 켜줘' does not falsely match '팀으로' and '스팀모드' does
not falsely match '팀모드'.
Follows the existing folder pattern (mode/default.ts + mode/index.ts)
shared by ultrawork/, search/, and analyze/. The hook orchestration in
hook.ts handles the new keyword type generically through the shared
KEYWORD_DETECTORS array, so existing guards (non-OMO agent skip,
non-main session filter, system-reminder strip, code-block strip) all
apply automatically.
Adds 7 regression tests covering English/Korean trigger forms, the
Hangul-prefix false-positive guard, the bare-'team' negative case, and
non-main-session filtering.
* refactor(keyword-detector): split constants into domain-specific modules
* feat(shared): add requiresAnyModel and isAnyFallbackModelAvailable
* feat(config): add hephaestus to agent schemas
* feat(agents): add Hephaestus autonomous deep worker
* feat(cli): update model-fallback for hephaestus support
* feat(plugin): add hephaestus to config handler with ordering
* test(delegate-task): update tests for hephaestus agent
* docs: update AGENTS.md files for hephaestus
* docs: add hephaestus to READMEs
* chore: regenerate config schema
* fix(delegate-task): bypass requiresModel check when user provides explicit config
* docs(hephaestus): add 4-part context structure for explore/librarian prompts
* docs: fix review comments from cubic (non-breaking changes)
- Move Hephaestus from Primary Agents to Subagents (uses own fallback chain)
- Fix Hephaestus fallback chain documentation (claude-opus-4-5 → gemini-3-pro)
- Add settings.local.json to claude-code-hooks config sources
- Fix delegate_task parameters in ultrawork prompt (agent→subagent_type, background→run_in_background, add load_skills)
- Update line counts in AGENTS.md (index.ts: 788, manager.ts: 1440)
* docs: fix additional documentation inconsistencies from oracle review
- Fix delegate_task parameters in Background Agents example (docs/features.md)
- Fix Hephaestus fallback chain in root AGENTS.md to match model-requirements.ts
* docs: clarify Hephaestus has no fallback (requires gpt-5.2-codex only)
Hephaestus uses requiresModel constraint - it only activates when gpt-5.2-codex
is available. The fallback chain in code is unreachable, so documentation
should not mention fallbacks.
* fix(hephaestus): remove unreachable fallback chain entries
Hephaestus has requiresModel: gpt-5.2-codex which means the agent only
activates when that specific model is available. The fallback entries
(claude-opus-4-5, gemini-3-pro) were unreachable and misleading.
---------
Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
Restructure ultrawork message generation to support agent-specific instructions.
- Extract ultrawork message components into modular constants
- Add getUltraworkMessage(agentName) function that adapts instructions based on agent type
- Support planner-specific vs default agent execution patterns
- Pass agentName parameter through detector.ts for message resolution
🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
* feat(keyword-detector): show toast notification when ultrawork mode is activated
When users trigger ultrawork mode (via 'ultrawork' or 'ulw' keywords), a toast
notification now appears to confirm the mode is active. The notification is
shown once per session to avoid spamming.
Changes:
- Add detectKeywordsWithType() to identify which keyword type triggered
- Show 'Ultrawork Mode Activated' toast with success variant
- Track notified sessions to prevent duplicate toasts
Closes#392
* fix(keyword-detector): fix index bug in detectKeywordsWithType and add error logging
- Fix P1: detectKeywordsWithType now maps before filtering to preserve
original KEYWORD_DETECTORS indices. Previously, the index used was from
the filtered array, causing incorrect type assignment (e.g., 'search'
match would incorrectly return 'ultrawork' type).
- Fix P2: Replace silent .catch(() => {}) with proper error logging using
the log function for easier debugging when toast notifications fail.
---------
Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>