Merge pull request #4190 from herjarsa/feat/default-mode
feat(default-mode): auto-activate ultrawork and ralph loop without commands
This commit is contained in:
@@ -13,6 +13,7 @@ export type {
|
||||
SisyphusAgentConfig,
|
||||
ExperimentalConfig,
|
||||
DynamicContextPruningConfig,
|
||||
DefaultModeConfig,
|
||||
RalphLoopConfig,
|
||||
TmuxConfig,
|
||||
TmuxLayout,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<typeof DefaultModeConfigSchema>
|
||||
@@ -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(),
|
||||
})
|
||||
|
||||
@@ -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<string>()
|
||||
|
||||
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<RalphLoopHook, "startLoop">,
|
||||
config?: KeywordDetectorConfig,
|
||||
defaultMode?: DefaultModeConfig,
|
||||
) {
|
||||
const disabledKeywords = config?.disabled_keywords
|
||||
function getRuntimeVariant(input: { variant?: string }, message: Record<string, unknown>): 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"
|
||||
|
||||
@@ -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>{{PROMISE}}</promise>
|
||||
- Do not stop until the task is truly done
|
||||
|
||||
Original task:
|
||||
Continue. Output <promise>{{PROMISE}}</promise> when done.
|
||||
{{PROMPT}}`
|
||||
|
||||
const ULTRAWORK_VERIFICATION_PROMPT = `${SYSTEM_DIRECTIVE_PREFIX} - ULTRAWORK LOOP VERIFICATION {{ITERATION}}/{{MAX}}]
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -61,6 +61,7 @@ export function createTransformHooks(args: {
|
||||
contextCollector,
|
||||
ralphLoop ?? undefined,
|
||||
pluginConfig.keyword_detector,
|
||||
pluginConfig.default_mode,
|
||||
),
|
||||
{ enabled: safeHookEnabled },
|
||||
)
|
||||
|
||||
@@ -1,6 +1,32 @@
|
||||
export function createSystemTransformHandler(): (
|
||||
import type { DefaultModeConfig } from "../config/schema/default-mode"
|
||||
|
||||
const ULTRAWORK_MODE_TAG = "<ultrawork-mode>"
|
||||
|
||||
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<void> {
|
||||
return async (): Promise<void> => {}
|
||||
return async (input, output): Promise<void> => {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user