diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index d7b1dd7e391..58681e9c064 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -35,6 +35,7 @@ export type RequirementBlockedError = InstanceType(agentConfig: Record): Record displayName?: string + source?: string deprecated?: boolean }) { - if (item.options?.displayName && typeof item.options.displayName === "string") { + if (!item.displayName && typeof item.options?.displayName === "string") { item.displayName = item.options.displayName } + if (!item.source && typeof item.options?.source === "string") { + item.source = item.options.source + } + if (item.options) { + delete item.options.displayName + delete item.options.source + } } const locked = new Set(["compaction", "title", "summary"]) @@ -319,6 +331,7 @@ export function patchAgents( { name: string displayName?: string + source?: string description?: string deprecated?: boolean mode: "subagent" | "primary" | "all" @@ -495,7 +508,7 @@ export async function remove(input: { name: string; agent?: AgentInfo; dirs: str if (!input.agent) throw new RemoveError({ name: input.name, message: "agent not found" }) if (input.agent.native) throw new RemoveError({ name: input.name, message: "cannot remove native agent" }) // Prevent removal of organization-managed agents - if (input.agent.options?.source === "organization") + if (input.agent.source === "organization" || input.agent.options?.source === "organization") throw new RemoveError({ name: input.name, message: "cannot remove organization agent — manage it from the cloud dashboard", diff --git a/packages/opencode/src/kilocode/modes-migrator.ts b/packages/opencode/src/kilocode/modes-migrator.ts index 3a01273ff06..ee0abe7dda8 100644 --- a/packages/opencode/src/kilocode/modes-migrator.ts +++ b/packages/opencode/src/kilocode/modes-migrator.ts @@ -111,7 +111,9 @@ export namespace ModesMigrator { description: cfg.description ?? cfg.whenToUse ?? mode.name, prompt: prompt || undefined, permission: convertPermissions(groups), - options: { source: "organization", displayName: mode.name }, + // Typed metadata fields — must NOT live in `options`, which is forwarded to the provider. + displayName: mode.name, + source: "organization", } } diff --git a/packages/opencode/test/kilocode/agent-config-metadata.test.ts b/packages/opencode/test/kilocode/agent-config-metadata.test.ts new file mode 100644 index 00000000000..b2f9194b407 --- /dev/null +++ b/packages/opencode/test/kilocode/agent-config-metadata.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, test } from "bun:test" +import { processConfigItem } from "../../src/kilocode/agent" + +describe("processConfigItem", () => { + test("lifts legacy options-based metadata to typed fields and strips it", () => { + const item: { options: Record; displayName?: string; source?: string } = { + options: { displayName: "Code Reviewer", source: "organization", reasoningEffort: "high" }, + } + processConfigItem(item) + expect(item.displayName).toBe("Code Reviewer") + expect(item.source).toBe("organization") + // metadata removed from options, genuine provider options preserved + expect(item.options).toEqual({ reasoningEffort: "high" }) + }) + + test("does not overwrite metadata already set as typed fields", () => { + const item: { options: Record; displayName?: string; source?: string } = { + displayName: "Typed Name", + source: "organization", + options: { displayName: "Legacy Name", source: "global" }, + } + processConfigItem(item) + expect(item.displayName).toBe("Typed Name") + expect(item.source).toBe("organization") + expect(item.options).toEqual({}) + }) +}) diff --git a/packages/opencode/test/kilocode/modes-migrator.test.ts b/packages/opencode/test/kilocode/modes-migrator.test.ts index 750b205b3b3..6c8b825515b 100644 --- a/packages/opencode/test/kilocode/modes-migrator.test.ts +++ b/packages/opencode/test/kilocode/modes-migrator.test.ts @@ -171,6 +171,30 @@ describe("ModesMigrator", () => { }) }) + describe("convertOrganizationMode", () => { + const mode = { + id: "11111111-1111-1111-1111-111111111111", + organization_id: "org-1", + name: "Code Review custom Agent", + slug: "code-review", + created_by: "user-1", + created_at: "2026-01-01T00:00:00Z", + updated_at: "2026-01-01T00:00:00Z", + config: { + roleDefinition: "You are a reviewer.", + groups: ["read"], + }, + } + + test("carries displayName and source as typed fields, not provider options", () => { + const agent = ModesMigrator.convertOrganizationMode(mode) + expect(agent.displayName).toBe("Code Review custom Agent") + expect(agent.source).toBe("organization") + // The metadata must never live in `options`, which is forwarded to the provider. + expect(agent.options).toBeUndefined() + }) + }) + describe("readModesFile", () => { test("returns empty array for non-existent file", async () => { const modes = await ModesMigrator.readModesFile("/non/existent/path.yaml")