From e5463e2db7f22e9e22565e057a0c8ffb2c359aa4 Mon Sep 17 00:00:00 2001 From: herjarsa <204746071+herjarsa@users.noreply.github.com> Date: Tue, 19 May 2026 15:35:21 +0200 Subject: [PATCH 1/4] feat(default-mode): auto-activate ultrawork and ralph loop without commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add new `default_mode` config section with two boolean fields: - `ultrawork`: Auto-inject ultrawork mode prompt on main session start without requiring the "ultrawork"/"ulw" keyword. Wired through the keyword-detector hook — injects once per session, respects existing guards (non-OMO agents, planner agents, subagent sessions). - `ralph_loop`: Auto-start ralph loop on first main session message without requiring /ralph-loop or /ulw-loop commands. When ultrawork is also enabled, the loop starts in ultrawork mode. Usage: ```jsonc { "default_mode": { "ultrawork": true, // Always get ultrawork prompt on start "ralph_loop": true // Auto-start ralph loop } } ``` Files: 7 modified/added, ~65 LOC added. --- src/config/index.ts | 1 + src/config/schema.ts | 1 + src/config/schema/default-mode.ts | 18 ++++++++ src/config/schema/oh-my-opencode-config.ts | 3 ++ src/hooks/keyword-detector/hook.ts | 48 ++++++++++++++++++---- src/plugin/chat-message.ts | 19 +++++++++ src/plugin/hooks/create-transform-hooks.ts | 1 + 7 files changed, 83 insertions(+), 8 deletions(-) create mode 100644 src/config/schema/default-mode.ts diff --git a/src/config/index.ts b/src/config/index.ts index c1572d5e4..cfdc11db8 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -13,6 +13,7 @@ export type { SisyphusAgentConfig, ExperimentalConfig, DynamicContextPruningConfig, + DefaultModeConfig, RalphLoopConfig, TmuxConfig, TmuxLayout, diff --git a/src/config/schema.ts b/src/config/schema.ts index 86ad7ecad..4c5473c90 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -7,6 +7,7 @@ export * from "./schema/categories" export * from "./schema/claude-code" export * from "./schema/comment-checker" export * from "./schema/commands" +export * from "./schema/default-mode" export * from "./schema/dynamic-context-pruning" export * from "./schema/experimental" export * from "./schema/fallback-models" diff --git a/src/config/schema/default-mode.ts b/src/config/schema/default-mode.ts new file mode 100644 index 000000000..4c96b8196 --- /dev/null +++ b/src/config/schema/default-mode.ts @@ -0,0 +1,18 @@ +import { z } from "zod" + +export const DefaultModeConfigSchema = z.object({ + /** + * Automatically inject ultrawork mode prompt on main session start + * without requiring "ultrawork"/"ulw" keyword in the message. + * The ultrawork mode system prompt is injected once per session. + */ + ultrawork: z.boolean().default(false), + /** + * Automatically start ralph loop on main session start + * without requiring /ralph-loop or /ulw-loop commands. + * When ultrawork is also enabled, the loop starts in ultrawork mode. + */ + ralph_loop: z.boolean().default(false), +}) + +export type DefaultModeConfig = z.infer diff --git a/src/config/schema/oh-my-opencode-config.ts b/src/config/schema/oh-my-opencode-config.ts index 197948bca..3ae85430a 100644 --- a/src/config/schema/oh-my-opencode-config.ts +++ b/src/config/schema/oh-my-opencode-config.ts @@ -10,6 +10,7 @@ import { CategoriesConfigSchema } from "./categories" import { ClaudeCodeConfigSchema } from "./claude-code" import { CommentCheckerConfigSchema } from "./comment-checker" import { BuiltinCommandNameSchema } from "./commands" +import { DefaultModeConfigSchema } from "./default-mode" import { ExperimentalConfigSchema } from "./experimental" import { GitMasterConfigSchema } from "./git-master" import { KeywordDetectorConfigSchema } from "./keyword-detector" @@ -81,6 +82,8 @@ export const OhMyOpenCodeConfigSchema = z.object({ tmux: TmuxConfigSchema.optional(), sisyphus: SisyphusConfigSchema.optional(), start_work: StartWorkConfigSchema.optional(), + /** Default mode auto-activation settings (ultrawork, ralph loop) */ + default_mode: DefaultModeConfigSchema.optional(), /** Migration history to prevent re-applying migrations (e.g., model version upgrades) */ _migrations: z.array(z.string()).optional(), }) diff --git a/src/hooks/keyword-detector/hook.ts b/src/hooks/keyword-detector/hook.ts index 83494c4fd..1376ded90 100644 --- a/src/hooks/keyword-detector/hook.ts +++ b/src/hooks/keyword-detector/hook.ts @@ -1,4 +1,5 @@ import type { PluginInput } from "@opencode-ai/plugin" +import type { DefaultModeConfig } from "../../config/schema/default-mode" import type { KeywordDetectorConfig } from "../../config/schema/keyword-detector" import { getMainSessionID, @@ -16,10 +17,12 @@ import { removeSystemReminders, } from "../../shared/system-directive" import type { RalphLoopHook } from "../ralph-loop" -import { isNonOmoAgent, isPlannerAgent } from "./constants" +import { getUltraworkMessage, isNonOmoAgent, isPlannerAgent } from "./constants" import type { DetectedKeyword } from "./detector" import { detectKeywordsWithType, extractPromptText, looksLikeSlashCommand } from "./detector" +const defaultModeUltraworkInjectedSessions = new Set() + function suppressComboStandalones(detected: DetectedKeyword[]): DetectedKeyword[] { const hasCombo = detected.some((k) => k.type === "hyperplan-ultrawork") if (!hasCombo) return detected @@ -31,6 +34,7 @@ export function createKeywordDetectorHook( _collector?: ContextCollector, _ralphLoop?: Pick, config?: KeywordDetectorConfig, + defaultMode?: DefaultModeConfig, ) { const disabledKeywords = config?.disabled_keywords function getRuntimeVariant(input: { variant?: string }, message: Record): string | undefined { @@ -74,13 +78,11 @@ export function createKeywordDetectorHook( const currentAgent = getSessionAgent(input.sessionID) ?? input.agent - // Skip all keyword injection for non-OMO agents (e.g., OpenCode-Builder, Plan) if (isNonOmoAgent(currentAgent)) { log(`[keyword-detector] Skipping keyword injection for non-OMO agent`, { sessionID: input.sessionID, agent: currentAgent }) return } - // Remove system-reminder content to prevent automated system messages from triggering mode keywords const cleanText = removeSystemReminders(promptText) const modelID = input.model?.modelID let detectedKeywords = detectKeywordsWithType(cleanText, currentAgent, modelID, disabledKeywords) @@ -96,19 +98,49 @@ export function createKeywordDetectorHook( } } - if (detectedKeywords.length === 0) { - return - } - const isBackgroundTaskSession = subagentSessions.has(input.sessionID) if (isBackgroundTaskSession) { - log(`[keyword-detector] Skipping keyword injection for background task session`, { sessionID: input.sessionID }) + if (detectedKeywords.length > 0) { + log(`[keyword-detector] Skipping keyword injection for background task session`, { sessionID: input.sessionID }) + } return } const mainSessionID = getMainSessionID() const isNonMainSession = mainSessionID && input.sessionID !== mainSessionID + if (detectedKeywords.length === 0) { + if (defaultMode?.ultrawork && !isNonMainSession && !defaultModeUltraworkInjectedSessions.has(input.sessionID)) { + defaultModeUltraworkInjectedSessions.add(input.sessionID) + + const ultraworkMessage = getUltraworkMessage(currentAgent, modelID) + const textPartIndex = output.parts.findIndex(isRealUserTextPart) + if (textPartIndex >= 0) { + const originalText = output.parts[textPartIndex].text ?? "" + output.parts[textPartIndex].text = `${ultraworkMessage}\n\n---\n\n${originalText}` + } + + log(`[keyword-detector] Default ultrawork mode auto-activated`, { sessionID: input.sessionID }) + + ctx.client.tui + .showToast({ + body: { + title: "Ultrawork Mode Activated", + message: "Default ultrawork mode enabled. All agents at your disposal.", + variant: "success" as const, + duration: 3000, + }, + }) + .catch((err) => + log(`[keyword-detector] Failed to show toast`, { + error: err, + sessionID: input.sessionID, + }) + ) + } + return + } + if (isNonMainSession) { detectedKeywords = detectedKeywords.filter( (k) => k.type === "ultrawork" || k.type === "hyperplan-ultrawork" diff --git a/src/plugin/chat-message.ts b/src/plugin/chat-message.ts index ca8577d85..b32c8656e 100644 --- a/src/plugin/chat-message.ts +++ b/src/plugin/chat-message.ts @@ -304,6 +304,25 @@ export function createChatMessageHandler(args: { } else if (isCancelRalphTemplate || rawLoopCommand?.command === "cancel-ralph") { hooks.ralphLoop.cancelLoop(input.sessionID) } + + if ( + !isRalphLoopTemplate + && !isUlwLoopTemplate + && !isCancelRalphTemplate + && !rawLoopCommand + && isFirstMessage + && pluginConfig.default_mode?.ralph_loop + ) { + const loopPrompt = promptText + const ultrawork = pluginConfig.default_mode?.ultrawork ?? false + hooks.ralphLoop.startLoop(input.sessionID, loopPrompt, { + ultrawork, + }) + log("[chat-message] Default ralph loop auto-started", { + sessionID: input.sessionID, + ultrawork, + }) + } } await applyUltraworkModelOverrideOnMessage( diff --git a/src/plugin/hooks/create-transform-hooks.ts b/src/plugin/hooks/create-transform-hooks.ts index 36fb4c4d9..fe71aefdb 100644 --- a/src/plugin/hooks/create-transform-hooks.ts +++ b/src/plugin/hooks/create-transform-hooks.ts @@ -61,6 +61,7 @@ export function createTransformHooks(args: { contextCollector, ralphLoop ?? undefined, pluginConfig.keyword_detector, + pluginConfig.default_mode, ), { enabled: safeHookEnabled }, ) From 2f1380f52d2a54f857d4f635f5333c9d58625b76 Mon Sep 17 00:00:00 2001 From: herjarsa <204746071+herjarsa@users.noreply.github.com> Date: Tue, 19 May 2026 15:50:02 +0200 Subject: [PATCH 2/4] refactor(default-mode): inject ultrawork via system prompt instead of visible text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When default_mode.ultrawork is enabled, inject the ultrawork behavioral instructions into the system prompt (experimental.chat.system.transform) instead of prepending them to the user's visible chat message. - system-transform.ts: No-op handler now injects ultrawork message into output.system[] when defaultMode.ultrawork is active. Checks for existing tag to avoid re-injection after compaction. - keyword-detector/hook.ts: Removes the visible text injection (output.parts[].text prepend) for default_mode.ultrawork path. Only shows the toast as the user-facing indicator. - plugin-interface.ts: Wires getUltraworkMessage from keyword-detector into createSystemTransformHandler. Result: Ultrawork mode activates silently via system prompt — the chat stays clean, and only a toast shows 'Ultrawork Mode Active'. --- src/hooks/keyword-detector/hook.ts | 11 ++--------- src/plugin-interface.ts | 6 +++++- src/plugin/system-transform.ts | 23 +++++++++++++++++++++-- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/hooks/keyword-detector/hook.ts b/src/hooks/keyword-detector/hook.ts index 1376ded90..65051021e 100644 --- a/src/hooks/keyword-detector/hook.ts +++ b/src/hooks/keyword-detector/hook.ts @@ -17,7 +17,7 @@ import { removeSystemReminders, } from "../../shared/system-directive" import type { RalphLoopHook } from "../ralph-loop" -import { getUltraworkMessage, isNonOmoAgent, isPlannerAgent } from "./constants" +import { isNonOmoAgent, isPlannerAgent } from "./constants" import type { DetectedKeyword } from "./detector" import { detectKeywordsWithType, extractPromptText, looksLikeSlashCommand } from "./detector" @@ -113,14 +113,7 @@ export function createKeywordDetectorHook( if (defaultMode?.ultrawork && !isNonMainSession && !defaultModeUltraworkInjectedSessions.has(input.sessionID)) { defaultModeUltraworkInjectedSessions.add(input.sessionID) - const ultraworkMessage = getUltraworkMessage(currentAgent, modelID) - const textPartIndex = output.parts.findIndex(isRealUserTextPart) - if (textPartIndex >= 0) { - const originalText = output.parts[textPartIndex].text ?? "" - output.parts[textPartIndex].text = `${ultraworkMessage}\n\n---\n\n${originalText}` - } - - log(`[keyword-detector] Default ultrawork mode auto-activated`, { sessionID: input.sessionID }) + log(`[keyword-detector] Default ultrawork mode auto-activated (injected via system prompt)`, { sessionID: input.sessionID }) ctx.client.tui .showToast({ diff --git a/src/plugin-interface.ts b/src/plugin-interface.ts index 5bcc0c364..79543c086 100644 --- a/src/plugin-interface.ts +++ b/src/plugin-interface.ts @@ -7,6 +7,7 @@ import { createChatMessageHandler } from "./plugin/chat-message" import { createCommandExecuteBeforeHandler } from "./plugin/command-execute-before" import { createMessagesTransformHandler } from "./plugin/messages-transform" import { createSystemTransformHandler } from "./plugin/system-transform" +import { getUltraworkMessage } from "./hooks/keyword-detector/ultrawork" import { createEventHandler } from "./plugin/event" import { createToolExecuteAfterHandler } from "./plugin/tool-execute-after" import { createToolExecuteBeforeHandler } from "./plugin/tool-execute-before" @@ -58,7 +59,10 @@ export function createPluginInterface(args: { hooks, }), - "experimental.chat.system.transform": createSystemTransformHandler(), + "experimental.chat.system.transform": createSystemTransformHandler( + pluginConfig.default_mode, + getUltraworkMessage, + ), config: managers.configHandler, diff --git a/src/plugin/system-transform.ts b/src/plugin/system-transform.ts index d6a04820c..39356a380 100644 --- a/src/plugin/system-transform.ts +++ b/src/plugin/system-transform.ts @@ -1,6 +1,25 @@ -export function createSystemTransformHandler(): ( +import type { DefaultModeConfig } from "../config/schema/default-mode" + +const ULTRAWORK_MODE_TAG = "" + +export function createSystemTransformHandler( + defaultMode?: DefaultModeConfig, + getUltraworkMessage?: (agentName?: string, modelID?: string) => string, +): ( input: { sessionID?: string; model: { id: string; providerID: string; [key: string]: unknown } }, output: { system: string[] }, ) => Promise { - return async (): Promise => {} + return async (input, output): Promise => { + if (!defaultMode?.ultrawork || !getUltraworkMessage) return + + // Avoid re-injecting if the ultrawork prompt is already in the system prompt + // (e.g. after compaction the system prompt is rebuilt and this hook fires again) + if (output.system.some((part) => part.includes(ULTRAWORK_MODE_TAG))) return + + const modelID = input.model?.id + const ultraworkMessage = getUltraworkMessage("sisyphus", modelID) + if (!ultraworkMessage) return + + output.system.push(ultraworkMessage) + } } From dc2e082ac9a0135199401a486c79bfc61b2e1376 Mon Sep 17 00:00:00 2001 From: herjarsa <204746071+herjarsa@users.noreply.github.com> Date: Tue, 19 May 2026 18:53:24 +0200 Subject: [PATCH 3/4] fix(default-mode): skip ultrawork system prompt when ralph_loop is also enabled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When both default_mode.ultrawork and default_mode.ralph_loop are true, the ralph loop handles ultrawork behavior via its continuation mechanism (prepends 'ultrawork' to each iteration prompt). Injecting the ultrawork system prompt on top would be redundant — the continuation prompt re-triggers keyword detection, which defeats the purpose of invisible system prompt injection. The ultrawork flag still controls loop behavior (500 max iterations + Oracle verification gate) via chat-message.ts startLoop({ ultrawork }). --- src/plugin/system-transform.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/plugin/system-transform.ts b/src/plugin/system-transform.ts index 39356a380..cb1e86afd 100644 --- a/src/plugin/system-transform.ts +++ b/src/plugin/system-transform.ts @@ -12,6 +12,13 @@ export function createSystemTransformHandler( return async (input, output): Promise => { if (!defaultMode?.ultrawork || !getUltraworkMessage) return + // When ralph_loop is also enabled, skip system prompt injection: the loop's + // own continuation mechanism handles ultrawork re-injection on each iteration. + // Injecting both would be redundant — the continuation prompt prepends + // "ultrawork" and re-triggers keyword detection, defeating invisibility. + // The `ultrawork` flag still controls loop behavior (500 iters + Oracle gate). + if (defaultMode?.ralph_loop) return + // Avoid re-injecting if the ultrawork prompt is already in the system prompt // (e.g. after compaction the system prompt is rebuilt and this hook fires again) if (output.system.some((part) => part.includes(ULTRAWORK_MODE_TAG))) return From 3cb6785d743a3d5001c10fb06acb7b720f464be1 Mon Sep 17 00:00:00 2001 From: herjarsa <204746071+herjarsa@users.noreply.github.com> Date: Tue, 19 May 2026 20:12:47 +0200 Subject: [PATCH 4/4] fix(ralph-loop): minimal continuation prompt for default_mode Reduce the ralph loop continuation prompt from ~15 lines to ~3 lines: - Keep system directive prefix for proper identification - Keep the essential 'output DONE' instruction - Keep the original task prompt (needed context) - Remove verbose boilerplate about reviewing progress and not stopping --- src/hooks/ralph-loop/continuation-prompt-builder.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/hooks/ralph-loop/continuation-prompt-builder.ts b/src/hooks/ralph-loop/continuation-prompt-builder.ts index e709caa23..edf1c07c5 100644 --- a/src/hooks/ralph-loop/continuation-prompt-builder.ts +++ b/src/hooks/ralph-loop/continuation-prompt-builder.ts @@ -6,16 +6,7 @@ function getMaxIterationsLabel(state: RalphLoopState): string { } const CONTINUATION_PROMPT = `${SYSTEM_DIRECTIVE_PREFIX} - RALPH LOOP {{ITERATION}}/{{MAX}}] - -Your previous attempt did not output the completion promise. Continue working on the task. - -IMPORTANT: -- Review your progress so far -- Continue from where you left off -- When FULLY complete, output: {{PROMISE}} -- Do not stop until the task is truly done - -Original task: +Continue. Output {{PROMISE}} when done. {{PROMPT}}` const ULTRAWORK_VERIFICATION_PROMPT = `${SYSTEM_DIRECTIVE_PREFIX} - ULTRAWORK LOOP VERIFICATION {{ITERATION}}/{{MAX}}]