feat(agents): add centralized GPT apply_patch permission guard

Extract hardcoded GPT apply_patch permission logic into a reusable module
to ensure consistent behavior across all agents. This prevents GPT models
from using the unreliable apply_patch tool while allowing other models.

- Add gpt-apply-patch-guard.ts with GPT_APPLY_PATCH_GUIDANCE and getGptApplyPatchPermission
- Update Hephaestus agent to use centralized permission logic
- Update Sisyphus-Junior agent to use centralized permission logic
- Update all GPT prompt builders to reference shared guidance constant

🤖 Generated with assistance of OhMyOpenCode
This commit is contained in:
YeonGyu-Kim
2026-04-10 10:47:27 +09:00
parent 498d021dc2
commit 2083cb0710
18 changed files with 87 additions and 41 deletions
+7
View File
@@ -0,0 +1,7 @@
import { isGptModel } from "./types"
export const GPT_APPLY_PATCH_GUIDANCE = "Use the `edit` and `write` tools for file changes. Do not use `apply_patch` on GPT models - it is unreliable here and can hang during verification."
export function getGptApplyPatchPermission(model: string): Record<string, "deny"> {
return isGptModel(model) ? { apply_patch: "deny" as const } : {}
}
+3 -2
View File
@@ -1,6 +1,6 @@
import type { AgentConfig } from "@opencode-ai/sdk"; import type { AgentConfig } from "@opencode-ai/sdk";
import type { AgentMode, AgentPromptMetadata } from "../types"; import type { AgentMode, AgentPromptMetadata } from "../types";
import { isGptModel, isGpt5_4Model, isGpt5_3CodexModel } from "../types"; import { isGpt5_4Model, isGpt5_3CodexModel } from "../types";
import type { import type {
AvailableAgent, AvailableAgent,
AvailableTool, AvailableTool,
@@ -8,6 +8,7 @@ import type {
AvailableCategory, AvailableCategory,
} from "../dynamic-agent-prompt-builder"; } from "../dynamic-agent-prompt-builder";
import { categorizeTools, buildAgentIdentitySection } from "../dynamic-agent-prompt-builder"; import { categorizeTools, buildAgentIdentitySection } from "../dynamic-agent-prompt-builder";
import { getGptApplyPatchPermission } from "../gpt-apply-patch-guard";
import { buildHephaestusPrompt as buildGptPrompt } from "./gpt"; import { buildHephaestusPrompt as buildGptPrompt } from "./gpt";
import { buildHephaestusPrompt as buildGpt53CodexPrompt } from "./gpt-5-3-codex"; import { buildHephaestusPrompt as buildGpt53CodexPrompt } from "./gpt-5-3-codex";
@@ -125,7 +126,7 @@ export function createHephaestusAgent(
permission: { permission: {
question: "allow", question: "allow",
call_omo_agent: "deny", call_omo_agent: "deny",
...(isGptModel(model) ? { apply_patch: "deny" as const } : {}), ...getGptApplyPatchPermission(model),
} as AgentConfig["permission"], } as AgentConfig["permission"],
reasoningEffort: "medium", reasoningEffort: "medium",
}; };
+2 -1
View File
@@ -1,4 +1,5 @@
/** GPT-5.3 Codex optimized Hephaestus prompt */ /** GPT-5.3 Codex optimized Hephaestus prompt */
import { GPT_APPLY_PATCH_GUIDANCE } from "../gpt-apply-patch-guard";
import type { AgentConfig } from "@opencode-ai/sdk"; import type { AgentConfig } from "@opencode-ai/sdk";
import type { AgentMode } from "../types"; import type { AgentMode } from "../types";
import type { import type {
@@ -448,7 +449,7 @@ ${oracleSection}
1. SEARCH existing codebase for similar patterns/styles 1. SEARCH existing codebase for similar patterns/styles
2. Match naming, indentation, import styles, error handling conventions 2. Match naming, indentation, import styles, error handling conventions
3. Default to ASCII. Add comments only for non-obvious blocks 3. Default to ASCII. Add comments only for non-obvious blocks
4. Use the \`edit\` and \`write\` tools for file changes. Do not use \`apply_patch\` on GPT models - it is unreliable here and can hang during verification. 4. ${GPT_APPLY_PATCH_GUIDANCE}
### After Implementation (MANDATORY - DO NOT SKIP) ### After Implementation (MANDATORY - DO NOT SKIP)
+2 -1
View File
@@ -21,6 +21,7 @@
* 9. <communication> - Output format, tone guidance * 9. <communication> - Output format, tone guidance
*/ */
import { GPT_APPLY_PATCH_GUIDANCE } from "../gpt-apply-patch-guard";
import type { import type {
AvailableAgent, AvailableAgent,
AvailableTool, AvailableTool,
@@ -252,7 +253,7 @@ ${antiPatterns}
1. **Explore**: Fire 2-5 explore/librarian agents in parallel + direct tool reads. Goal: complete understanding, not just enough context. 1. **Explore**: Fire 2-5 explore/librarian agents in parallel + direct tool reads. Goal: complete understanding, not just enough context.
2. **Plan**: List files to modify, specific changes, dependencies, complexity estimate. 2. **Plan**: List files to modify, specific changes, dependencies, complexity estimate.
3. **Decide**: Trivial (<10 lines, single file) -> self. Complex (multi-file, >100 lines) -> delegate. 3. **Decide**: Trivial (<10 lines, single file) -> self. Complex (multi-file, >100 lines) -> delegate.
4. **Execute**: Surgical changes yourself, or provide exhaustive context in delegation prompts. Match existing patterns. Minimal diff. Search the codebase for similar patterns before writing code. Default to ASCII. Add comments only for non-obvious blocks. Use the \`edit\` and \`write\` tools for file changes. Do not use \`apply_patch\` on GPT models - it is unreliable here and can hang during verification. 4. **Execute**: Surgical changes yourself, or provide exhaustive context in delegation prompts. Match existing patterns. Minimal diff. Search the codebase for similar patterns before writing code. Default to ASCII. Add comments only for non-obvious blocks. ${GPT_APPLY_PATCH_GUIDANCE}
5. **Verify**: \`lsp_diagnostics\` on all modified files (zero errors) -> run related tests (\`foo.ts\` -> \`foo.test.ts\`) -> typecheck -> build if applicable (exit 0). Fix only issues your changes caused. 5. **Verify**: \`lsp_diagnostics\` on all modified files (zero errors) -> run related tests (\`foo.ts\` -> \`foo.test.ts\`) -> typecheck -> build if applicable (exit 0). Fix only issues your changes caused.
If verification fails, return to step 1 with a materially different approach. After three attempts: stop, revert to last working state, document what you tried, consult Oracle. If Oracle cannot resolve, ask the user. If verification fails, return to step 1 with a materially different approach. After three attempts: stop, revert to last working state, document what you tried, consult Oracle. If Oracle cannot resolve, ask the user.
+2 -1
View File
@@ -1,5 +1,6 @@
/** Generic GPT Hephaestus prompt - fallback for GPT models without a model-specific variant */ /** Generic GPT Hephaestus prompt - fallback for GPT models without a model-specific variant */
import { GPT_APPLY_PATCH_GUIDANCE } from "../gpt-apply-patch-guard"
import type { import type {
AvailableAgent, AvailableAgent,
AvailableTool, AvailableTool,
@@ -311,7 +312,7 @@ ${oracleSection}
1. SEARCH existing codebase for similar patterns/styles 1. SEARCH existing codebase for similar patterns/styles
2. Match naming, indentation, import styles, error handling conventions 2. Match naming, indentation, import styles, error handling conventions
3. Default to ASCII. Add comments only for non-obvious blocks 3. Default to ASCII. Add comments only for non-obvious blocks
4. Use the \`edit\` and \`write\` tools for file changes. Do not use \`apply_patch\` on GPT models - it is unreliable here and can hang during verification. 4. ${GPT_APPLY_PATCH_GUIDANCE}
### After Implementation (MANDATORY - DO NOT SKIP) ### After Implementation (MANDATORY - DO NOT SKIP)
+7 -2
View File
@@ -18,6 +18,7 @@ import {
createAgentToolRestrictions, createAgentToolRestrictions,
type PermissionValue, type PermissionValue,
} from "../../shared/permission-compat" } from "../../shared/permission-compat"
import { getGptApplyPatchPermission } from "../gpt-apply-patch-guard"
import { buildDefaultSisyphusJuniorPrompt } from "./default" import { buildDefaultSisyphusJuniorPrompt } from "./default"
import { buildGptSisyphusJuniorPrompt } from "./gpt" import { buildGptSisyphusJuniorPrompt } from "./gpt"
@@ -103,7 +104,11 @@ export function createSisyphusJuniorAgentWithOverrides(
merged[tool] = "deny" merged[tool] = "deny"
} }
merged.call_omo_agent = "allow" merged.call_omo_agent = "allow"
const toolsConfig = { permission: { ...merged, ...basePermission } } const toolsConfig = { permission: { ...merged, ...basePermission } as Record<string, PermissionValue> }
const permission: Record<string, PermissionValue> = {
...toolsConfig.permission,
...getGptApplyPatchPermission(model),
}
const base: AgentConfig = { const base: AgentConfig = {
description: override?.description ?? description: override?.description ??
@@ -114,7 +119,7 @@ export function createSisyphusJuniorAgentWithOverrides(
maxTokens: 64000, maxTokens: 64000,
prompt, prompt,
color: override?.color ?? "#20B2AA", color: override?.color ?? "#20B2AA",
...toolsConfig, permission,
} }
if (override?.top_p !== undefined) { if (override?.top_p !== undefined) {
+2 -1
View File
@@ -8,6 +8,7 @@
import { resolvePromptAppend } from "../builtin-agents/resolve-file-uri" import { resolvePromptAppend } from "../builtin-agents/resolve-file-uri"
import { buildAntiDuplicationSection } from "../dynamic-agent-prompt-builder" import { buildAntiDuplicationSection } from "../dynamic-agent-prompt-builder"
import { GPT_APPLY_PATCH_GUIDANCE } from "../gpt-apply-patch-guard"
export function buildGpt53CodexSisyphusJuniorPrompt( export function buildGpt53CodexSisyphusJuniorPrompt(
useTaskSystem: boolean, useTaskSystem: boolean,
@@ -92,7 +93,7 @@ Style:
1. SEARCH existing codebase for similar patterns/styles 1. SEARCH existing codebase for similar patterns/styles
2. Match naming, indentation, import styles, error handling conventions 2. Match naming, indentation, import styles, error handling conventions
3. Default to ASCII. Add comments only for non-obvious blocks 3. Default to ASCII. Add comments only for non-obvious blocks
4. Use the \`edit\` and \`write\` tools for file changes. Do not use \`apply_patch\` on GPT models - it is unreliable here and can hang during verification. 4. ${GPT_APPLY_PATCH_GUIDANCE}
### After Implementation (MANDATORY - DO NOT SKIP) ### After Implementation (MANDATORY - DO NOT SKIP)
+2 -1
View File
@@ -11,6 +11,7 @@
import { resolvePromptAppend } from "../builtin-agents/resolve-file-uri"; import { resolvePromptAppend } from "../builtin-agents/resolve-file-uri";
import { buildAntiDuplicationSection } from "../dynamic-agent-prompt-builder"; import { buildAntiDuplicationSection } from "../dynamic-agent-prompt-builder";
import { GPT_APPLY_PATCH_GUIDANCE } from "../gpt-apply-patch-guard";
export function buildGpt54SisyphusJuniorPrompt( export function buildGpt54SisyphusJuniorPrompt(
useTaskSystem: boolean, useTaskSystem: boolean,
@@ -96,7 +97,7 @@ Style:
1. SEARCH existing codebase for similar patterns/styles 1. SEARCH existing codebase for similar patterns/styles
2. Match naming, indentation, import styles, error handling conventions 2. Match naming, indentation, import styles, error handling conventions
3. Default to ASCII. Add comments only for non-obvious blocks 3. Default to ASCII. Add comments only for non-obvious blocks
4. Use the \`edit\` and \`write\` tools for file changes. Do not use \`apply_patch\` on GPT models - it is unreliable here and can hang during verification. 4. ${GPT_APPLY_PATCH_GUIDANCE}
5. Do not chain bash commands with separators - each command should be a separate tool call 5. Do not chain bash commands with separators - each command should be a separate tool call
### After Implementation (MANDATORY - DO NOT SKIP) ### After Implementation (MANDATORY - DO NOT SKIP)
+2 -1
View File
@@ -9,6 +9,7 @@
import { resolvePromptAppend } from "../builtin-agents/resolve-file-uri" import { resolvePromptAppend } from "../builtin-agents/resolve-file-uri"
import { buildAntiDuplicationSection } from "../dynamic-agent-prompt-builder" import { buildAntiDuplicationSection } from "../dynamic-agent-prompt-builder"
import { GPT_APPLY_PATCH_GUIDANCE } from "../gpt-apply-patch-guard"
export function buildGptSisyphusJuniorPrompt( export function buildGptSisyphusJuniorPrompt(
useTaskSystem: boolean, useTaskSystem: boolean,
@@ -93,7 +94,7 @@ Style:
1. SEARCH existing codebase for similar patterns/styles 1. SEARCH existing codebase for similar patterns/styles
2. Match naming, indentation, import styles, error handling conventions 2. Match naming, indentation, import styles, error handling conventions
3. Default to ASCII. Add comments only for non-obvious blocks 3. Default to ASCII. Add comments only for non-obvious blocks
4. Use the \`edit\` and \`write\` tools for file changes. Do not use \`apply_patch\` on GPT models - it is unreliable here and can hang during verification. 4. ${GPT_APPLY_PATCH_GUIDANCE}
### After Implementation (MANDATORY - DO NOT SKIP) ### After Implementation (MANDATORY - DO NOT SKIP)
+3 -2
View File
@@ -11,6 +11,7 @@ import {
} from "./sisyphus/gemini"; } from "./sisyphus/gemini";
import { buildGpt54SisyphusPrompt } from "./sisyphus/gpt-5-4"; import { buildGpt54SisyphusPrompt } from "./sisyphus/gpt-5-4";
import { buildTaskManagementSection } from "./sisyphus/default"; import { buildTaskManagementSection } from "./sisyphus/default";
import { getGptApplyPatchPermission } from "./gpt-apply-patch-guard";
const MODE: AgentMode = "primary"; const MODE: AgentMode = "primary";
export const SISYPHUS_PROMPT_METADATA: AgentPromptMetadata = { export const SISYPHUS_PROMPT_METADATA: AgentPromptMetadata = {
@@ -499,7 +500,7 @@ export function createSisyphusAgent(
permission: { permission: {
question: "allow", question: "allow",
call_omo_agent: "deny", call_omo_agent: "deny",
apply_patch: "deny", ...getGptApplyPatchPermission(model),
} as AgentConfig["permission"], } as AgentConfig["permission"],
reasoningEffort: "medium", reasoningEffort: "medium",
}; };
@@ -539,7 +540,7 @@ export function createSisyphusAgent(
const permission = { const permission = {
question: "allow", question: "allow",
call_omo_agent: "deny", call_omo_agent: "deny",
...(isGptModel(model) ? { apply_patch: "deny" as const } : {}), ...getGptApplyPatchPermission(model),
} as AgentConfig["permission"]; } as AgentConfig["permission"];
const base = { const base = {
description: description:
+2 -1
View File
@@ -21,6 +21,7 @@
* 8. <style> - Tone (prose) + output contract + progress updates * 8. <style> - Tone (prose) + output contract + progress updates
*/ */
import { GPT_APPLY_PATCH_GUIDANCE } from "../gpt-apply-patch-guard";
import type { import type {
AvailableAgent, AvailableAgent,
AvailableTool, AvailableTool,
@@ -310,7 +311,7 @@ Every implementation task follows this cycle. No exceptions.
Skills: if ANY available skill's domain overlaps with the task, load it NOW via \`skill\` tool and include it in \`load_skills\`. When the connection is even remotely plausible, load the skill - the cost of loading an irrelevant skill is near zero, the cost of missing a relevant one is high. Skills: if ANY available skill's domain overlaps with the task, load it NOW via \`skill\` tool and include it in \`load_skills\`. When the connection is even remotely plausible, load the skill - the cost of loading an irrelevant skill is near zero, the cost of missing a relevant one is high.
4. EXECUTE_OR_SUPERVISE - 4. EXECUTE_OR_SUPERVISE -
If self: surgical changes, match existing patterns, minimal diff. Never suppress type errors. Never commit unless asked. Bugfix rule: fix minimally, never refactor while fixing. Use the \`edit\` and \`write\` tools for file changes. Do not use \`apply_patch\` on GPT models - it is unreliable here and can hang during verification. If self: surgical changes, match existing patterns, minimal diff. Never suppress type errors. Never commit unless asked. Bugfix rule: fix minimally, never refactor while fixing. ${GPT_APPLY_PATCH_GUIDANCE}
If delegated: exhaustive 6-section prompt per \`<delegation>\` protocol. Session continuity for follow-ups. If delegated: exhaustive 6-section prompt per \`<delegation>\` protocol. Session continuity for follow-ups.
5. VERIFY - 5. VERIFY -
@@ -1,16 +1,10 @@
import { existsSync, readFileSync } from "node:fs" import { existsSync, readFileSync } from "node:fs"
import { homedir } from "node:os" import { homedir } from "node:os"
import { join } from "node:path" import { join } from "node:path"
import { parseJsonc } from "../../../shared" import { getOpenCodeCacheDir, parseJsonc } from "../../../shared"
import type { AvailableModelsInfo } from "./model-resolution-types" import type { AvailableModelsInfo } from "./model-resolution-types"
function getOpenCodeCacheDir(): string { function getUserConfigDir(): string {
const xdgCache = process.env.XDG_CACHE_HOME
if (xdgCache) return join(xdgCache, "opencode")
return join(homedir(), ".cache", "opencode")
}
function getOpenCodeConfigDir(): string {
const xdgConfig = process.env.XDG_CONFIG_HOME const xdgConfig = process.env.XDG_CONFIG_HOME
if (xdgConfig) return join(xdgConfig, "opencode") if (xdgConfig) return join(xdgConfig, "opencode")
return join(homedir(), ".config", "opencode") return join(homedir(), ".config", "opencode")
@@ -24,7 +18,7 @@ function getOpenCodeConfigDir(): string {
* warnings in doctor. * warnings in doctor.
*/ */
function loadCustomProviderNames(): string[] { function loadCustomProviderNames(): string[] {
const configDir = getOpenCodeConfigDir() const configDir = getUserConfigDir()
const candidatePaths = [ const candidatePaths = [
join(configDir, "opencode.json"), join(configDir, "opencode.json"),
join(configDir, "opencode.jsonc"), join(configDir, "opencode.jsonc"),
+7 -7
View File
@@ -69,10 +69,10 @@ export function createManagers(args: {
pluginConfig.background_task, pluginConfig.background_task,
{ {
tmuxConfig, tmuxConfig,
onSubagentSessionCreated: async (event: SubagentSessionCreatedEvent) => { onSubagentSessionCreated: async (event: SubagentSessionCreatedEvent) => {
log("[index] onSubagentSessionCreated callback received", { log("[create-managers] onSubagentSessionCreated callback received", {
sessionID: event.sessionID, sessionID: event.sessionID,
parentID: event.parentID, parentID: event.parentID,
title: event.title, title: event.title,
}) })
@@ -88,7 +88,7 @@ export function createManagers(args: {
}) })
if (pluginConfig.openclaw) { if (pluginConfig.openclaw) {
await openclawRuntimeDispatch.dispatchOpenClawEvent({ await openclawRuntimeDispatch.dispatchOpenClawEvent({
config: pluginConfig.openclaw, config: pluginConfig.openclaw,
rawEvent: "session.created", rawEvent: "session.created",
context: { context: {
@@ -99,11 +99,11 @@ export function createManagers(args: {
}) })
} }
log("[index] onSubagentSessionCreated callback completed") log("[create-managers] onSubagentSessionCreated callback completed")
}, },
onShutdown: async () => { onShutdown: async () => {
await tmuxSessionManager.cleanup().catch((error) => { await tmuxSessionManager.cleanup().catch((error) => {
log("[index] tmux cleanup error during shutdown:", error) log("[create-managers] tmux cleanup error during shutdown:", error)
}) })
}, },
enableParentSessionNotifications: backgroundNotificationHookEnabled, enableParentSessionNotifications: backgroundNotificationHookEnabled,
@@ -6,13 +6,17 @@ import type { PackageJson } from "../types"
import { INSTALLED_PACKAGE_JSON_CANDIDATES } from "../constants" import { INSTALLED_PACKAGE_JSON_CANDIDATES } from "../constants"
import { findPackageJsonUp } from "./package-json-locator" import { findPackageJsonUp } from "./package-json-locator"
function readPackageVersion(packageJsonPath: string): string | null {
const content = fs.readFileSync(packageJsonPath, "utf-8")
const pkg = JSON.parse(content) as PackageJson
return pkg.version ?? null
}
export function getCachedVersion(): string | null { export function getCachedVersion(): string | null {
for (const candidate of INSTALLED_PACKAGE_JSON_CANDIDATES) { for (const candidate of INSTALLED_PACKAGE_JSON_CANDIDATES) {
try { try {
if (fs.existsSync(candidate)) { if (fs.existsSync(candidate)) {
const content = fs.readFileSync(candidate, "utf-8") return readPackageVersion(candidate)
const pkg = JSON.parse(content) as PackageJson
if (pkg.version) return pkg.version
} }
} catch { } catch {
// ignore; try next candidate // ignore; try next candidate
@@ -23,9 +27,7 @@ export function getCachedVersion(): string | null {
const currentDir = path.dirname(fileURLToPath(import.meta.url)) const currentDir = path.dirname(fileURLToPath(import.meta.url))
const pkgPath = findPackageJsonUp(currentDir) const pkgPath = findPackageJsonUp(currentDir)
if (pkgPath) { if (pkgPath) {
const content = fs.readFileSync(pkgPath, "utf-8") return readPackageVersion(pkgPath)
const pkg = JSON.parse(content) as PackageJson
if (pkg.version) return pkg.version
} }
} catch (err) { } catch (err) {
log("[auto-update-checker] Failed to resolve version from current directory:", err) log("[auto-update-checker] Failed to resolve version from current directory:", err)
@@ -35,9 +37,7 @@ export function getCachedVersion(): string | null {
const execDir = path.dirname(fs.realpathSync(process.execPath)) const execDir = path.dirname(fs.realpathSync(process.execPath))
const pkgPath = findPackageJsonUp(execDir) const pkgPath = findPackageJsonUp(execDir)
if (pkgPath) { if (pkgPath) {
const content = fs.readFileSync(pkgPath, "utf-8") return readPackageVersion(pkgPath)
const pkg = JSON.parse(content) as PackageJson
if (pkg.version) return pkg.version
} }
} catch (err) { } catch (err) {
log("[auto-update-checker] Failed to resolve version from execPath:", err) log("[auto-update-checker] Failed to resolve version from execPath:", err)
+4 -1
View File
@@ -94,7 +94,10 @@ export async function pollDiscordReplies(
headers: { Authorization: `Bot ${replyListener.discordBotToken}` }, headers: { Authorization: `Bot ${replyListener.discordBotToken}` },
}, },
) )
} catch { } catch (error) {
logReplyListenerMessage(
`WARN: Failed to acknowledge Discord message ${message.id}: ${error instanceof Error ? error.message : String(error)}`,
)
} }
} else { } else {
state.errors += 1 state.errors += 1
+1
View File
@@ -15,6 +15,7 @@ import {
updateSessionAgent, updateSessionAgent,
} from "./features/claude-code-session-state" } from "./features/claude-code-session-state"
describe("createPluginInterface - command.execute.before", () => { describe("createPluginInterface - command.execute.before", () => {
let testDir = "" let testDir = ""
+2 -1
View File
@@ -6,6 +6,7 @@ import { getAgentConfigKey } from "../shared/agent-display-names"
import { getSessionModel, setSessionModel } from "../shared/session-model-state" import { getSessionModel, setSessionModel } from "../shared/session-model-state"
import { getMainSessionID, setSessionAgent, subagentSessions } from "../features/claude-code-session-state" import { getMainSessionID, setSessionAgent, subagentSessions } from "../features/claude-code-session-state"
import { applyUltraworkModelOverrideOnMessage } from "./ultrawork-model-override" import { applyUltraworkModelOverrideOnMessage } from "./ultrawork-model-override"
import { NATIVE_LOOP_TRIGGERED_FLAG } from "./command-execute-before"
import { parseRalphLoopArguments } from "../hooks/ralph-loop/command-arguments" import { parseRalphLoopArguments } from "../hooks/ralph-loop/command-arguments"
import type { CreatedHooks } from "../create-hooks" import type { CreatedHooks } from "../create-hooks"
@@ -223,7 +224,7 @@ export function createChatMessageHandler(args: {
.catch(() => {}) .catch(() => {})
} }
if (hooks.ralphLoop) { if (hooks.ralphLoop && output.message[NATIVE_LOOP_TRIGGERED_FLAG] !== true) {
const parts = output.parts const parts = output.parts
const promptText = const promptText =
parts parts
+27 -1
View File
@@ -1,4 +1,5 @@
import type { CreatedHooks } from "../create-hooks" import type { CreatedHooks } from "../create-hooks"
import { parseRalphLoopArguments } from "../hooks/ralph-loop/command-arguments"
type CommandExecuteBeforeInput = { type CommandExecuteBeforeInput = {
command: string command: string
@@ -8,8 +9,11 @@ type CommandExecuteBeforeInput = {
type CommandExecuteBeforeOutput = { type CommandExecuteBeforeOutput = {
parts: Array<{ type: string; text?: string; [key: string]: unknown }> parts: Array<{ type: string; text?: string; [key: string]: unknown }>
message?: Record<string, unknown>
} }
const NATIVE_LOOP_TRIGGERED_FLAG = "__omoNativeLoopTriggered"
function hasPartsOutput(value: unknown): value is CommandExecuteBeforeOutput { function hasPartsOutput(value: unknown): value is CommandExecuteBeforeOutput {
if (typeof value !== "object" || value === null) return false if (typeof value !== "object" || value === null) return false
const record = value as Record<string, unknown> const record = value as Record<string, unknown>
@@ -28,12 +32,34 @@ export function createCommandExecuteBeforeHandler(args: {
return async (input, output): Promise<void> => { return async (input, output): Promise<void> => {
await hooks.autoSlashCommand?.["command.execute.before"]?.(input, output) await hooks.autoSlashCommand?.["command.execute.before"]?.(input, output)
const normalizedCommand = input.command.toLowerCase()
const sessionID = input.sessionID
if (hooks.ralphLoop && sessionID) {
if (normalizedCommand === "ralph-loop" || normalizedCommand === "ulw-loop") {
const parsedArguments = parseRalphLoopArguments(input.arguments || "")
hooks.ralphLoop.startLoop(sessionID, parsedArguments.prompt, {
ultrawork: normalizedCommand === "ulw-loop",
maxIterations: parsedArguments.maxIterations,
completionPromise: parsedArguments.completionPromise,
strategy: parsedArguments.strategy,
})
output.message ??= {}
output.message[NATIVE_LOOP_TRIGGERED_FLAG] = true
} else if (normalizedCommand === "cancel-ralph") {
hooks.ralphLoop.cancelLoop(sessionID)
output.message ??= {}
output.message[NATIVE_LOOP_TRIGGERED_FLAG] = true
}
}
if ( if (
hooks.startWork hooks.startWork
&& input.command.toLowerCase() === "start-work" && normalizedCommand === "start-work"
&& hasPartsOutput(output) && hasPartsOutput(output)
) { ) {
await hooks.startWork["command.execute.before"]?.(input, output) await hooks.startWork["command.execute.before"]?.(input, output)
} }
} }
} }
export { NATIVE_LOOP_TRIGGERED_FLAG }