diff --git a/src/cli/doctor/checks/tools-gh.test.ts b/src/cli/doctor/checks/tools-gh.test.ts
new file mode 100644
index 000000000..46eec87e5
--- /dev/null
+++ b/src/cli/doctor/checks/tools-gh.test.ts
@@ -0,0 +1,35 @@
+///
+
+import { afterEach, describe, expect, it, mock } from "bun:test"
+
+const originalWhich = Bun.which
+
+afterEach(() => {
+ Bun.which = originalWhich
+ mock.restore()
+})
+
+describe("getGhCliInfo", () => {
+ it("falls back to gh --version when Bun.which cannot find gh", async () => {
+ // given
+ Bun.which = mock(() => null)
+ mock.module("../spawn-with-timeout", () => ({
+ spawnWithTimeout: mock((command: string[]) => {
+ if (command.join(" ") === "gh --version") {
+ return Promise.resolve({ stdout: "gh version 2.82.1\n", stderr: "", exitCode: 0, timedOut: false })
+ }
+
+ return Promise.resolve({ stdout: "", stderr: "not logged in", exitCode: 1, timedOut: false })
+ }),
+ }))
+ const { getGhCliInfo } = await import("./tools-gh")
+
+ // when
+ const info = await getGhCliInfo()
+
+ // then
+ expect(info.installed).toBe(true)
+ expect(info.version).toBe("2.82.1")
+ expect(info.path).toBe(null)
+ })
+})
diff --git a/src/cli/doctor/checks/tools-gh.ts b/src/cli/doctor/checks/tools-gh.ts
index 71a539d1e..6839a71fc 100644
--- a/src/cli/doctor/checks/tools-gh.ts
+++ b/src/cli/doctor/checks/tools-gh.ts
@@ -80,6 +80,20 @@ async function getGhAuthStatus(): Promise<{
export async function getGhCliInfo(): Promise {
const binaryStatus = await checkBinaryExists("gh")
if (!binaryStatus.exists) {
+ const version = await getGhVersion()
+ if (version) {
+ const authStatus = await getGhAuthStatus()
+ return {
+ installed: true,
+ version,
+ path: null,
+ authenticated: authStatus.authenticated,
+ username: authStatus.username,
+ scopes: authStatus.scopes,
+ error: authStatus.error,
+ }
+ }
+
return {
installed: false,
version: null,
diff --git a/src/plugin/normalize-tool-arg-schemas.test.ts b/src/plugin/normalize-tool-arg-schemas.test.ts
index 27f148995..8a9247dda 100644
--- a/src/plugin/normalize-tool-arg-schemas.test.ts
+++ b/src/plugin/normalize-tool-arg-schemas.test.ts
@@ -6,7 +6,7 @@ import { tmpdir } from "node:os"
import { dirname, join } from "node:path"
import { pathToFileURL } from "node:url"
import { tool } from "@opencode-ai/plugin"
-import { normalizeToolArgSchemas } from "./normalize-tool-arg-schemas"
+import { normalizeToolArgSchemas, sanitizeJsonSchema } from "./normalize-tool-arg-schemas"
const tempDirectories: string[] = []
@@ -95,3 +95,36 @@ describe("normalizeToolArgSchemas", () => {
expect(afterQuery?.examples).toEqual(["issue 2314"])
})
})
+
+describe("sanitizeJsonSchema", () => {
+ it("rewrites bare $ref values to $defs JSON pointers", () => {
+ // given
+ const schema = {
+ type: "object",
+ properties: {
+ new_encoding: { $ref: "Encoding" },
+ existing_pointer: { $ref: "#/$defs/AlreadyValid" },
+ },
+ $defs: {
+ Encoding: { type: "string" },
+ AlreadyValid: { type: "string" },
+ },
+ }
+
+ // when
+ const sanitized = sanitizeJsonSchema(schema)
+
+ // then
+ expect(sanitized).toEqual({
+ type: "object",
+ properties: {
+ new_encoding: { $ref: "#/$defs/Encoding" },
+ existing_pointer: { $ref: "#/$defs/AlreadyValid" },
+ },
+ $defs: {
+ Encoding: { type: "string" },
+ AlreadyValid: { type: "string" },
+ },
+ })
+ })
+})
diff --git a/src/plugin/normalize-tool-arg-schemas.ts b/src/plugin/normalize-tool-arg-schemas.ts
index 0f626b546..52813bc64 100644
--- a/src/plugin/normalize-tool-arg-schemas.ts
+++ b/src/plugin/normalize-tool-arg-schemas.ts
@@ -47,6 +47,14 @@ function isRecord(value: unknown): value is Record {
return typeof value === "object" && value !== null && !Array.isArray(value)
}
+function normalizeJsonSchemaRef(value: string): string {
+ if (value.startsWith("#") || value.includes(":") || value.startsWith("/")) {
+ return value
+ }
+
+ return `#/$defs/${value}`
+}
+
export function sanitizeJsonSchema(value: unknown, depth = 0, isPropertyName = false): unknown {
if (Array.isArray(value)) {
return value.map((item) => sanitizeJsonSchema(item, depth + 1, false))
@@ -67,6 +75,11 @@ export function sanitizeJsonSchema(value: unknown, depth = 0, isPropertyName = f
continue
}
+ if (!isPropertyName && key === "$ref" && typeof nestedValue === "string") {
+ sanitized[key] = normalizeJsonSchemaRef(nestedValue)
+ continue
+ }
+
const childIsPropertyName = key === "properties" && !isPropertyName
sanitized[key] = sanitizeJsonSchema(nestedValue, depth + 1, childIsPropertyName)
}
diff --git a/src/tools/call-omo-agent/tools.test.ts b/src/tools/call-omo-agent/tools.test.ts
index 23d459353..bad411339 100644
--- a/src/tools/call-omo-agent/tools.test.ts
+++ b/src/tools/call-omo-agent/tools.test.ts
@@ -136,6 +136,19 @@ describe("createCallOmoAgent", () => {
})
describe("dynamic custom agent resolution", () => {
+ test("should reject missing subagent_type without throwing", async () => {
+ const mockCtx = createMockCtx(DEFAULT_AGENTS)
+ const toolDef = createCallOmoAgent(mockCtx, mockBackgroundManager, [])
+ const executeFunc = toolDef.execute as Function
+
+ const result = await executeFunc(
+ { description: "Test", prompt: "Fix bug", run_in_background: true },
+ toolCtx
+ )
+
+ expect(result).toContain("subagent_type is required")
+ })
+
test("should accept a custom agent returned by client.app.agents()", async () => {
const agents = [...DEFAULT_AGENTS, { name: "bug-fixer", mode: "subagent" }]
const mockCtx = createMockCtx(agents)
diff --git a/src/tools/call-omo-agent/tools.ts b/src/tools/call-omo-agent/tools.ts
index 51ea8730c..6e315cd86 100644
--- a/src/tools/call-omo-agent/tools.ts
+++ b/src/tools/call-omo-agent/tools.ts
@@ -140,6 +140,10 @@ export function createCallOmoAgent(
`[call_omo_agent] Starting with agent: ${args.subagent_type}, background: ${args.run_in_background}`,
);
+ if (typeof args.subagent_type !== "string" || args.subagent_type.trim() === "") {
+ return "Error: subagent_type is required."
+ }
+
const callableAgents = await resolveCallableAgents(ctx.client);
// Strip ZWSP and case-insensitive agent validation - allows "Explore", "EXPLORE", "explore" etc.