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..65051021e 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, @@ -20,6 +21,8 @@ import { 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,42 @@ 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) + + log(`[keyword-detector] Default ultrawork mode auto-activated (injected via system prompt)`, { 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/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}}] diff --git a/src/plugin-interface.ts b/src/plugin-interface.ts index 9aa982808..c33db45dc 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 { createToolDefinitionHandler } from "./plugin/tool-definition" import { createToolExecuteAfterHandler } from "./plugin/tool-execute-after" @@ -59,7 +60,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/chat-message.ts b/src/plugin/chat-message.ts index 494530cf5..abd5f23d7 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 }, ) diff --git a/src/plugin/system-transform.ts b/src/plugin/system-transform.ts index d6a04820c..cb1e86afd 100644 --- a/src/plugin/system-transform.ts +++ b/src/plugin/system-transform.ts @@ -1,6 +1,32 @@ -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 + + // 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 + + const modelID = input.model?.id + const ultraworkMessage = getUltraworkMessage("sisyphus", modelID) + if (!ultraworkMessage) return + + output.system.push(ultraworkMessage) + } }