From 2d65896bbdb99e88874e3929d28ae9a4fce6fb3f Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 11 Apr 2026 23:35:57 +0900 Subject: [PATCH 01/10] refactor(shared): simplify normalize SDK null guards Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/shared/normalize-sdk-response.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shared/normalize-sdk-response.ts b/src/shared/normalize-sdk-response.ts index 080cc992c..5398d69ab 100644 --- a/src/shared/normalize-sdk-response.ts +++ b/src/shared/normalize-sdk-response.ts @@ -7,7 +7,7 @@ export function normalizeSDKResponse( fallback: TData, options?: NormalizeSDKResponseOptions, ): TData { - if (response === null || response === undefined) { + if (response == null) { return fallback } @@ -17,7 +17,7 @@ export function normalizeSDKResponse( if (typeof response === "object" && "data" in response) { const data = (response as { data?: unknown }).data - if (data !== null && data !== undefined) { + if (data != null) { return data as TData } From 79a475d60ffbce67b9063326258c9c1274eadb56 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 11 Apr 2026 23:36:14 +0900 Subject: [PATCH 02/10] refactor(background-agent): simplify loop detector null guard Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/features/background-agent/loop-detector.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/background-agent/loop-detector.ts b/src/features/background-agent/loop-detector.ts index afcbe4abb..ecfcf23f3 100644 --- a/src/features/background-agent/loop-detector.ts +++ b/src/features/background-agent/loop-detector.ts @@ -62,7 +62,7 @@ export function recordToolCall( } function sortObject(obj: unknown): unknown { - if (obj === null || obj === undefined) return obj + if (obj == null) return obj if (typeof obj !== "object") return obj if (Array.isArray(obj)) return obj.map(sortObject) From cd8352c10808f6bc31e8e42da5b875b436eb27ab Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 11 Apr 2026 23:36:24 +0900 Subject: [PATCH 03/10] refactor(claude-code-mcp-loader): simplify env expansion null guard Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/features/claude-code-mcp-loader/env-expander.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/claude-code-mcp-loader/env-expander.ts b/src/features/claude-code-mcp-loader/env-expander.ts index ad3264f96..96e18ed37 100644 --- a/src/features/claude-code-mcp-loader/env-expander.ts +++ b/src/features/claude-code-mcp-loader/env-expander.ts @@ -35,7 +35,7 @@ export function expandEnvVars(value: string, options: ExpandEnvVarsOptions = {}) } export function expandEnvVarsInObject(obj: T, options: ExpandEnvVarsOptions = {}): T { - if (obj === null || obj === undefined) return obj + if (obj == null) return obj if (typeof obj === "string") return expandEnvVars(obj, options) as T if (Array.isArray(obj)) { return obj.map((item) => expandEnvVarsInObject(item, options)) as T From 50df6f0d3eb00d29d3051c10eeebd0b25d3f043d Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 11 Apr 2026 23:36:35 +0900 Subject: [PATCH 04/10] test(claude-code-plugin-loader): cover plugin path nullish resolution Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../plugin-path-resolver.test.ts | 55 +++++++++++++++++++ .../plugin-path-resolver.ts | 2 +- 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 src/features/claude-code-plugin-loader/plugin-path-resolver.test.ts diff --git a/src/features/claude-code-plugin-loader/plugin-path-resolver.test.ts b/src/features/claude-code-plugin-loader/plugin-path-resolver.test.ts new file mode 100644 index 000000000..3461b1f01 --- /dev/null +++ b/src/features/claude-code-plugin-loader/plugin-path-resolver.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, test } from "bun:test" + +import { resolvePluginPath, resolvePluginPaths } from "./plugin-path-resolver" + +describe("resolvePluginPath", () => { + test("#given a plugin root placeholder #when resolving the path #then it replaces the placeholder", () => { + // given + const path = "${CLAUDE_PLUGIN_ROOT}/dist/index.js" + + // when + const result = resolvePluginPath(path, "/tmp/plugin-root") + + // then + expect(result).toBe("/tmp/plugin-root/dist/index.js") + }) +}) + +describe("resolvePluginPaths", () => { + test("#given a nested object #when resolving paths #then it rewrites every nested string path", () => { + // given + const value = { + command: "node", + args: ["${CLAUDE_PLUGIN_ROOT}/server.js"], + nested: { + config: "${CLAUDE_PLUGIN_ROOT}/config.json", + }, + } + + // when + const result = resolvePluginPaths(value, "/tmp/plugin-root") + + // then + expect(result).toEqual({ + command: "node", + args: ["/tmp/plugin-root/server.js"], + nested: { + config: "/tmp/plugin-root/config.json", + }, + }) + }) + + test("#given nullish input #when resolving paths #then it returns the same nullish value", () => { + // given + const nullValue = null + const undefinedValue = undefined + + // when + const nullResult = resolvePluginPaths(nullValue, "/tmp/plugin-root") + const undefinedResult = resolvePluginPaths(undefinedValue, "/tmp/plugin-root") + + // then + expect(nullResult).toBeNull() + expect(undefinedResult).toBeUndefined() + }) +}) diff --git a/src/features/claude-code-plugin-loader/plugin-path-resolver.ts b/src/features/claude-code-plugin-loader/plugin-path-resolver.ts index c8806aa58..0027d8fe9 100644 --- a/src/features/claude-code-plugin-loader/plugin-path-resolver.ts +++ b/src/features/claude-code-plugin-loader/plugin-path-resolver.ts @@ -5,7 +5,7 @@ export function resolvePluginPath(path: string, pluginRoot: string): string { } export function resolvePluginPaths(obj: T, pluginRoot: string): T { - if (obj === null || obj === undefined) return obj + if (obj == null) return obj if (typeof obj === "string") { return resolvePluginPath(obj, pluginRoot) as T } From 668bc8e83d7d5047a680b800a8f9010c4bdcb5c5 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 11 Apr 2026 23:36:46 +0900 Subject: [PATCH 05/10] refactor(config-manager): simplify config parsing guards Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../add-plugin-to-opencode-config.ts | 6 +-- .../parse-opencode-config-file.test.ts | 49 +++++++++++++++++++ .../parse-opencode-config-file.ts | 2 +- 3 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 src/cli/config-manager/parse-opencode-config-file.test.ts diff --git a/src/cli/config-manager/add-plugin-to-opencode-config.ts b/src/cli/config-manager/add-plugin-to-opencode-config.ts index 23c398873..8cb7d0838 100644 --- a/src/cli/config-manager/add-plugin-to-opencode-config.ts +++ b/src/cli/config-manager/add-plugin-to-opencode-config.ts @@ -79,11 +79,7 @@ export async function addPluginToOpenCodeConfig(currentVersion: string): Promise const normalizedPlugins = [...otherPlugins] - if (canonicalEntries.length > 0 || legacyEntries.length > 0) { - normalizedPlugins.push(pluginEntry) - } else { - normalizedPlugins.push(pluginEntry) - } + normalizedPlugins.push(pluginEntry) config.plugin = normalizedPlugins diff --git a/src/cli/config-manager/parse-opencode-config-file.test.ts b/src/cli/config-manager/parse-opencode-config-file.test.ts new file mode 100644 index 000000000..f8351d8c2 --- /dev/null +++ b/src/cli/config-manager/parse-opencode-config-file.test.ts @@ -0,0 +1,49 @@ +import { afterEach, describe, expect, test } from "bun:test" +import { mkdtempSync, rmSync, writeFileSync } from "node:fs" +import { tmpdir } from "node:os" +import { join } from "node:path" + +import { parseOpenCodeConfigFileWithError } from "./parse-opencode-config-file" + +describe("parseOpenCodeConfigFileWithError", () => { + const tempDirectories: string[] = [] + + afterEach(() => { + for (const directory of tempDirectories.splice(0)) { + rmSync(directory, { recursive: true, force: true }) + } + }) + + test("#given a valid object config #when parsing the file #then it returns the parsed config", () => { + // given + const directory = mkdtempSync(join(tmpdir(), "omo-parse-config-")) + tempDirectories.push(directory) + const filePath = join(directory, "opencode.json") + writeFileSync(filePath, '{"plugin": ["oh-my-openagent"]}\n', "utf-8") + + // when + const result = parseOpenCodeConfigFileWithError(filePath) + + // then + expect(result).toEqual({ + config: { plugin: ["oh-my-openagent"] }, + }) + }) + + test("#given a null config payload #when parsing the file #then it returns a null parse error", () => { + // given + const directory = mkdtempSync(join(tmpdir(), "omo-parse-config-")) + tempDirectories.push(directory) + const filePath = join(directory, "opencode.json") + writeFileSync(filePath, "null\n", "utf-8") + + // when + const result = parseOpenCodeConfigFileWithError(filePath) + + // then + expect(result).toEqual({ + config: null, + error: `Config file parsed to null/undefined: ${filePath}. Ensure it contains valid JSON.`, + }) + }) +}) diff --git a/src/cli/config-manager/parse-opencode-config-file.ts b/src/cli/config-manager/parse-opencode-config-file.ts index 3e399d847..ed1f25526 100644 --- a/src/cli/config-manager/parse-opencode-config-file.ts +++ b/src/cli/config-manager/parse-opencode-config-file.ts @@ -30,7 +30,7 @@ export function parseOpenCodeConfigFileWithError(path: string): ParseConfigResul const config = parseJsonc(content) - if (config === null || config === undefined) { + if (config == null) { return { config: null, error: `Config file parsed to null/undefined: ${path}. Ensure it contains valid JSON.` } } From a0d5131ee1d3bd189cb5c35d12bbd1f15be3ec23 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 11 Apr 2026 23:36:55 +0900 Subject: [PATCH 06/10] refactor(plugin): remove dead chat params code Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/plugin/chat-params.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/plugin/chat-params.ts b/src/plugin/chat-params.ts index b28f6a420..41e4a0200 100644 --- a/src/plugin/chat-params.ts +++ b/src/plugin/chat-params.ts @@ -1,4 +1,3 @@ -import { normalizeSDKResponse } from "../shared/normalize-sdk-response" import { getSessionPromptParams } from "../shared/session-prompt-params-state" import { getModelCapabilities, resolveCompatibleModelSettings } from "../shared" @@ -58,8 +57,6 @@ function buildChatParamsInput(raw: unknown): ChatParamsHookInput | null { ? model.id : undefined const providerId = provider.id - const variant = message.variant - if (typeof providerID !== "string") return null if (typeof modelID !== "string") return null if (typeof providerId !== "string") return null @@ -71,7 +68,6 @@ function buildChatParamsInput(raw: unknown): ChatParamsHookInput | null { provider: { id: providerId }, message, rawMessage: message, - ...(typeof variant === "string" ? {} : {}), } } From 6713b30cc1973c190444084b31a5187e1f7f2371 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 11 Apr 2026 23:37:05 +0900 Subject: [PATCH 07/10] refactor(hooks): drop no-op directory injector callbacks Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/hooks/directory-agents-injector/hook.ts | 17 +++++------------ src/hooks/directory-readme-injector/hook.ts | 17 +++++------------ 2 files changed, 10 insertions(+), 24 deletions(-) diff --git a/src/hooks/directory-agents-injector/hook.ts b/src/hooks/directory-agents-injector/hook.ts index fba64cc7b..c1f62208f 100644 --- a/src/hooks/directory-agents-injector/hook.ts +++ b/src/hooks/directory-agents-injector/hook.ts @@ -16,8 +16,10 @@ interface ToolExecuteOutput { metadata: unknown; } -interface ToolExecuteBeforeOutput { - args: unknown; +interface DirectoryAgentsInjectorHook { + "tool.execute.before"?: (input: ToolExecuteInput, output: { args: unknown }) => Promise; + "tool.execute.after": (input: ToolExecuteInput, output: ToolExecuteOutput) => Promise; + event: (input: EventInput) => Promise; } interface EventInput { @@ -30,7 +32,7 @@ interface EventInput { export function createDirectoryAgentsInjectorHook( ctx: PluginInput, modelCacheState?: { anthropicContext1MEnabled: boolean }, -) { +): DirectoryAgentsInjectorHook { const sessionCaches = new Map>(); const truncator = createDynamicTruncator(ctx, modelCacheState); @@ -50,14 +52,6 @@ export function createDirectoryAgentsInjectorHook( } }; - const toolExecuteBefore = async ( - input: ToolExecuteInput, - output: ToolExecuteBeforeOutput, - ): Promise => { - void input; - void output; - }; - const eventHandler = async ({ event }: EventInput) => { const props = event.properties as Record | undefined; @@ -80,7 +74,6 @@ export function createDirectoryAgentsInjectorHook( }; return { - "tool.execute.before": toolExecuteBefore, "tool.execute.after": toolExecuteAfter, event: eventHandler, }; diff --git a/src/hooks/directory-readme-injector/hook.ts b/src/hooks/directory-readme-injector/hook.ts index d621c7f27..0fdab1858 100644 --- a/src/hooks/directory-readme-injector/hook.ts +++ b/src/hooks/directory-readme-injector/hook.ts @@ -16,8 +16,10 @@ interface ToolExecuteOutput { metadata: unknown; } -interface ToolExecuteBeforeOutput { - args: unknown; +interface DirectoryReadmeInjectorHook { + "tool.execute.before"?: (input: ToolExecuteInput, output: { args: unknown }) => Promise; + "tool.execute.after": (input: ToolExecuteInput, output: ToolExecuteOutput) => Promise; + event: (input: EventInput) => Promise; } interface EventInput { @@ -30,7 +32,7 @@ interface EventInput { export function createDirectoryReadmeInjectorHook( ctx: PluginInput, modelCacheState?: { anthropicContext1MEnabled: boolean }, -) { +): DirectoryReadmeInjectorHook { const sessionCaches = new Map>(); const truncator = createDynamicTruncator(ctx, modelCacheState); @@ -50,14 +52,6 @@ export function createDirectoryReadmeInjectorHook( } }; - const toolExecuteBefore = async ( - input: ToolExecuteInput, - output: ToolExecuteBeforeOutput, - ): Promise => { - void input; - void output; - }; - const eventHandler = async ({ event }: EventInput) => { const props = event.properties as Record | undefined; @@ -80,7 +74,6 @@ export function createDirectoryReadmeInjectorHook( }; return { - "tool.execute.before": toolExecuteBefore, "tool.execute.after": toolExecuteAfter, event: eventHandler, }; From 21cad26c089219c14e4bd03819fe27a019edeae3 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 11 Apr 2026 23:37:17 +0900 Subject: [PATCH 08/10] docs(config): remove redundant schema default comments Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/config/schema/dynamic-context-pruning.ts | 2 -- src/config/schema/ralph-loop.ts | 2 -- src/config/schema/start-work.ts | 1 - 3 files changed, 5 deletions(-) diff --git a/src/config/schema/dynamic-context-pruning.ts b/src/config/schema/dynamic-context-pruning.ts index 1d99c95c2..e4c07ec98 100644 --- a/src/config/schema/dynamic-context-pruning.ts +++ b/src/config/schema/dynamic-context-pruning.ts @@ -1,9 +1,7 @@ import { z } from "zod" export const DynamicContextPruningConfigSchema = z.object({ - /** Enable dynamic context pruning (default: false) */ enabled: z.boolean().default(false), - /** Notification level: off, minimal, or detailed (default: detailed) */ notification: z.enum(["off", "minimal", "detailed"]).default("detailed"), /** Turn protection - prevent pruning recent tool outputs */ turn_protection: z diff --git a/src/config/schema/ralph-loop.ts b/src/config/schema/ralph-loop.ts index 23770f056..ba061d287 100644 --- a/src/config/schema/ralph-loop.ts +++ b/src/config/schema/ralph-loop.ts @@ -1,9 +1,7 @@ import { z } from "zod" export const RalphLoopConfigSchema = z.object({ - /** Enable ralph loop functionality (default: false - opt-in feature) */ enabled: z.boolean().default(false), - /** Default max iterations if not specified in command (default: 100) */ default_max_iterations: z.number().min(1).max(1000).default(100), /** Custom state file directory relative to project root (default: .opencode/) */ state_dir: z.string().optional(), diff --git a/src/config/schema/start-work.ts b/src/config/schema/start-work.ts index 7daae0c3d..59ae9be45 100644 --- a/src/config/schema/start-work.ts +++ b/src/config/schema/start-work.ts @@ -1,7 +1,6 @@ import { z } from "zod" export const StartWorkConfigSchema = z.object({ - /** Enable auto-commit after each atomic task completion (default: true) */ auto_commit: z.boolean().default(true), }) From 4de02094ab1a35f873ec7857f364fe67f50b7f23 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 12 Apr 2026 00:42:25 +0900 Subject: [PATCH 09/10] test(background-agent): lock nullish loop detector behavior Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../background-agent/loop-detector.test.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/features/background-agent/loop-detector.test.ts b/src/features/background-agent/loop-detector.test.ts index f3f85d2c1..a35307818 100644 --- a/src/features/background-agent/loop-detector.test.ts +++ b/src/features/background-agent/loop-detector.test.ts @@ -112,6 +112,20 @@ describe("loop-detector", () => { expect(result).toBe("read") }) + test("#given nullish inputs #when signatures are created #then null and undefined behave the same", () => { + // given + const undefinedInput = undefined + const nullInput = null + + // when + const undefinedResult = createToolCallSignature("read", undefinedInput) + const nullResult = createToolCallSignature("read", nullInput) + + // then + expect(undefinedResult).toBe("read") + expect(nullResult).toBe(undefinedResult) + }) + test("#given tool with empty object input #when signature created #then returns bare tool name", () => { const result = createToolCallSignature("read", {}) @@ -259,5 +273,24 @@ describe("loop-detector", () => { expect(result).toEqual({ triggered: false }) }) }) + + describe("#given nullish tool inputs", () => { + test("#when recorded #then null and undefined produce the same unknown-input window", () => { + // given + const settings = resolveCircuitBreakerSettings() + + // when + const undefinedWindow = recordToolCall(undefined, "read", settings, undefined) + const nullWindow = recordToolCall(undefined, "read", settings, null) + + // then + expect(undefinedWindow).toEqual(nullWindow) + expect(undefinedWindow).toEqual({ + lastSignature: "read::__unknown-input__", + consecutiveCount: 1, + threshold: settings.consecutiveThreshold, + }) + }) + }) }) }) From 141798efed411f874b126ce8afff411b3e6f3a0b Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 12 Apr 2026 00:42:52 +0900 Subject: [PATCH 10/10] refactor(background-agent): standardize loop detector null guards Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/features/background-agent/loop-detector.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/background-agent/loop-detector.ts b/src/features/background-agent/loop-detector.ts index ecfcf23f3..6120a168c 100644 --- a/src/features/background-agent/loop-detector.ts +++ b/src/features/background-agent/loop-detector.ts @@ -36,7 +36,7 @@ export function recordToolCall( settings: CircuitBreakerSettings, toolInput?: Record | null ): ToolCallWindow { - if (toolInput === undefined || toolInput === null) { + if (toolInput == null) { return { lastSignature: `${toolName}::__unknown-input__`, consecutiveCount: 1, @@ -78,7 +78,7 @@ export function createToolCallSignature( toolName: string, toolInput?: Record | null ): string { - if (toolInput === undefined || toolInput === null) { + if (toolInput == null) { return toolName } if (Object.keys(toolInput).length === 0) {