feat(default-mode): auto-activate ultrawork and ralph loop without commands
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.
This commit is contained in:
@@ -13,6 +13,7 @@ export type {
|
|||||||
SisyphusAgentConfig,
|
SisyphusAgentConfig,
|
||||||
ExperimentalConfig,
|
ExperimentalConfig,
|
||||||
DynamicContextPruningConfig,
|
DynamicContextPruningConfig,
|
||||||
|
DefaultModeConfig,
|
||||||
RalphLoopConfig,
|
RalphLoopConfig,
|
||||||
TmuxConfig,
|
TmuxConfig,
|
||||||
TmuxLayout,
|
TmuxLayout,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export * from "./schema/categories"
|
|||||||
export * from "./schema/claude-code"
|
export * from "./schema/claude-code"
|
||||||
export * from "./schema/comment-checker"
|
export * from "./schema/comment-checker"
|
||||||
export * from "./schema/commands"
|
export * from "./schema/commands"
|
||||||
|
export * from "./schema/default-mode"
|
||||||
export * from "./schema/dynamic-context-pruning"
|
export * from "./schema/dynamic-context-pruning"
|
||||||
export * from "./schema/experimental"
|
export * from "./schema/experimental"
|
||||||
export * from "./schema/fallback-models"
|
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 { ClaudeCodeConfigSchema } from "./claude-code"
|
||||||
import { CommentCheckerConfigSchema } from "./comment-checker"
|
import { CommentCheckerConfigSchema } from "./comment-checker"
|
||||||
import { BuiltinCommandNameSchema } from "./commands"
|
import { BuiltinCommandNameSchema } from "./commands"
|
||||||
|
import { DefaultModeConfigSchema } from "./default-mode"
|
||||||
import { ExperimentalConfigSchema } from "./experimental"
|
import { ExperimentalConfigSchema } from "./experimental"
|
||||||
import { GitMasterConfigSchema } from "./git-master"
|
import { GitMasterConfigSchema } from "./git-master"
|
||||||
import { KeywordDetectorConfigSchema } from "./keyword-detector"
|
import { KeywordDetectorConfigSchema } from "./keyword-detector"
|
||||||
@@ -81,6 +82,8 @@ export const OhMyOpenCodeConfigSchema = z.object({
|
|||||||
tmux: TmuxConfigSchema.optional(),
|
tmux: TmuxConfigSchema.optional(),
|
||||||
sisyphus: SisyphusConfigSchema.optional(),
|
sisyphus: SisyphusConfigSchema.optional(),
|
||||||
start_work: StartWorkConfigSchema.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) */
|
/** Migration history to prevent re-applying migrations (e.g., model version upgrades) */
|
||||||
_migrations: z.array(z.string()).optional(),
|
_migrations: z.array(z.string()).optional(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { PluginInput } from "@opencode-ai/plugin"
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
|
import type { DefaultModeConfig } from "../../config/schema/default-mode"
|
||||||
import type { KeywordDetectorConfig } from "../../config/schema/keyword-detector"
|
import type { KeywordDetectorConfig } from "../../config/schema/keyword-detector"
|
||||||
import {
|
import {
|
||||||
getMainSessionID,
|
getMainSessionID,
|
||||||
@@ -16,10 +17,12 @@ import {
|
|||||||
removeSystemReminders,
|
removeSystemReminders,
|
||||||
} from "../../shared/system-directive"
|
} from "../../shared/system-directive"
|
||||||
import type { RalphLoopHook } from "../ralph-loop"
|
import type { RalphLoopHook } from "../ralph-loop"
|
||||||
import { isNonOmoAgent, isPlannerAgent } from "./constants"
|
import { getUltraworkMessage, isNonOmoAgent, isPlannerAgent } from "./constants"
|
||||||
import type { DetectedKeyword } from "./detector"
|
import type { DetectedKeyword } from "./detector"
|
||||||
import { detectKeywordsWithType, extractPromptText, looksLikeSlashCommand } from "./detector"
|
import { detectKeywordsWithType, extractPromptText, looksLikeSlashCommand } from "./detector"
|
||||||
|
|
||||||
|
const defaultModeUltraworkInjectedSessions = new Set<string>()
|
||||||
|
|
||||||
function suppressComboStandalones(detected: DetectedKeyword[]): DetectedKeyword[] {
|
function suppressComboStandalones(detected: DetectedKeyword[]): DetectedKeyword[] {
|
||||||
const hasCombo = detected.some((k) => k.type === "hyperplan-ultrawork")
|
const hasCombo = detected.some((k) => k.type === "hyperplan-ultrawork")
|
||||||
if (!hasCombo) return detected
|
if (!hasCombo) return detected
|
||||||
@@ -31,6 +34,7 @@ export function createKeywordDetectorHook(
|
|||||||
_collector?: ContextCollector,
|
_collector?: ContextCollector,
|
||||||
_ralphLoop?: Pick<RalphLoopHook, "startLoop">,
|
_ralphLoop?: Pick<RalphLoopHook, "startLoop">,
|
||||||
config?: KeywordDetectorConfig,
|
config?: KeywordDetectorConfig,
|
||||||
|
defaultMode?: DefaultModeConfig,
|
||||||
) {
|
) {
|
||||||
const disabledKeywords = config?.disabled_keywords
|
const disabledKeywords = config?.disabled_keywords
|
||||||
function getRuntimeVariant(input: { variant?: string }, message: Record<string, unknown>): string | undefined {
|
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
|
const currentAgent = getSessionAgent(input.sessionID) ?? input.agent
|
||||||
|
|
||||||
// Skip all keyword injection for non-OMO agents (e.g., OpenCode-Builder, Plan)
|
|
||||||
if (isNonOmoAgent(currentAgent)) {
|
if (isNonOmoAgent(currentAgent)) {
|
||||||
log(`[keyword-detector] Skipping keyword injection for non-OMO agent`, { sessionID: input.sessionID, agent: currentAgent })
|
log(`[keyword-detector] Skipping keyword injection for non-OMO agent`, { sessionID: input.sessionID, agent: currentAgent })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove system-reminder content to prevent automated system messages from triggering mode keywords
|
|
||||||
const cleanText = removeSystemReminders(promptText)
|
const cleanText = removeSystemReminders(promptText)
|
||||||
const modelID = input.model?.modelID
|
const modelID = input.model?.modelID
|
||||||
let detectedKeywords = detectKeywordsWithType(cleanText, currentAgent, modelID, disabledKeywords)
|
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)
|
const isBackgroundTaskSession = subagentSessions.has(input.sessionID)
|
||||||
if (isBackgroundTaskSession) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const mainSessionID = getMainSessionID()
|
const mainSessionID = getMainSessionID()
|
||||||
const isNonMainSession = mainSessionID && input.sessionID !== mainSessionID
|
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) {
|
if (isNonMainSession) {
|
||||||
detectedKeywords = detectedKeywords.filter(
|
detectedKeywords = detectedKeywords.filter(
|
||||||
(k) => k.type === "ultrawork" || k.type === "hyperplan-ultrawork"
|
(k) => k.type === "ultrawork" || k.type === "hyperplan-ultrawork"
|
||||||
|
|||||||
@@ -304,6 +304,25 @@ export function createChatMessageHandler(args: {
|
|||||||
} else if (isCancelRalphTemplate || rawLoopCommand?.command === "cancel-ralph") {
|
} else if (isCancelRalphTemplate || rawLoopCommand?.command === "cancel-ralph") {
|
||||||
hooks.ralphLoop.cancelLoop(input.sessionID)
|
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(
|
await applyUltraworkModelOverrideOnMessage(
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export function createTransformHooks(args: {
|
|||||||
contextCollector,
|
contextCollector,
|
||||||
ralphLoop ?? undefined,
|
ralphLoop ?? undefined,
|
||||||
pluginConfig.keyword_detector,
|
pluginConfig.keyword_detector,
|
||||||
|
pluginConfig.default_mode,
|
||||||
),
|
),
|
||||||
{ enabled: safeHookEnabled },
|
{ enabled: safeHookEnabled },
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user