Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/cli/config-manager/generate-omo-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ describe("generateOmoConfig - model fallback system", () => {

//#then
expect((result.agents as Record<string, { model: string }>).librarian.model).toBe("zai-coding-plan/glm-4.7")
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("anthropic/claude-opus-4-6")
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("anthropic/claude-opus-4.6")
})

test("uses native OpenAI models when only ChatGPT available", () => {
Expand Down Expand Up @@ -126,7 +126,7 @@ describe("generateOmoConfig - model fallback system", () => {
}>

//#then
expect(agents.sisyphus.model).toBe("anthropic/claude-opus-4-6")
expect(agents.sisyphus.model).toBe("anthropic/claude-opus-4.6")
expect(agents.sisyphus.fallback_models).toEqual([
{
model: "openai/gpt-5.4",
Expand All @@ -136,7 +136,7 @@ describe("generateOmoConfig - model fallback system", () => {
expect(categories.deep.model).toBe("openai/gpt-5.4")
expect(categories.deep.fallback_models).toEqual([
{
model: "anthropic/claude-opus-4-6",
model: "anthropic/claude-opus-4.6",
variant: "max",
},
])
Expand Down
12 changes: 6 additions & 6 deletions src/plugin-handlers/agent-config-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import type { OhMyOpenCodeConfig } from "../config"
import * as agentLoader from "../features/claude-code-agent-loader"
import * as skillLoader from "../features/opencode-skill-loader"
import type { LoadedSkill } from "../features/opencode-skill-loader"
import { getAgentListDisplayName, getAgentRuntimeName } from "../shared/agent-display-names"
import { getAgentDisplayName, getAgentRuntimeName } from "../shared/agent-display-names"
import { applyAgentConfig } from "./agent-config-handler"
import type { PluginComponents } from "./plugin-components-loader"

const BUILTIN_SISYPHUS_DISPLAY_NAME = getAgentListDisplayName("sisyphus")
const BUILTIN_SISYPHUS_JUNIOR_DISPLAY_NAME = getAgentListDisplayName("sisyphus-junior")
const BUILTIN_MULTIMODAL_LOOKER_DISPLAY_NAME = getAgentListDisplayName("multimodal-looker")
const BUILTIN_SISYPHUS_DISPLAY_NAME = getAgentDisplayName("sisyphus")
const BUILTIN_SISYPHUS_JUNIOR_DISPLAY_NAME = getAgentDisplayName("sisyphus-junior")
const BUILTIN_MULTIMODAL_LOOKER_DISPLAY_NAME = getAgentDisplayName("multimodal-looker")

function createPluginComponents(): PluginComponents {
return {
Expand Down Expand Up @@ -175,8 +175,8 @@ describe("applyAgentConfig builtin override protection", () => {
})

// then every registered agent key must be HTTP-header-safe (no parentheses)
// Parentheses in agent names cause HTTP header validation errors in
// x-opencode-agent-name and prevent the agents from showing in the OpenCode UI.
// Agent keys can flow into HTTP header values in some plugin paths.
// Parentheses and ZWSP characters violate RFC 7230 header value rules.
for (const key of Object.keys(result)) {
expect(key).not.toMatch(/[()]/)
}
Expand Down
6 changes: 3 additions & 3 deletions src/plugin-handlers/agent-config-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createBuiltinAgents } from "../agents";
import { createSisyphusJuniorAgentWithOverrides } from "../agents/sisyphus-junior";
import type { OhMyOpenCodeConfig } from "../config";
import { isTaskSystemEnabled, log, migrateAgentConfig } from "../shared";
import { getAgentRuntimeName } from "../shared/agent-display-names";
import { getAgentDisplayName, getAgentRuntimeName } from "../shared/agent-display-names";
import { AGENT_NAME_MAP } from "../shared/migration";
import { registerAgentName } from "../features/claude-code-session-state";
import {
Expand Down Expand Up @@ -159,10 +159,10 @@ export async function applyAgentConfig(params: {
if (isSisyphusEnabled && builtinAgents.sisyphus) {
if (configuredDefaultAgent) {
(params.config as { default_agent?: string }).default_agent =
getAgentRuntimeName(configuredDefaultAgent);
getAgentDisplayName(configuredDefaultAgent);
} else {
(params.config as { default_agent?: string }).default_agent =
getAgentRuntimeName("sisyphus");
getAgentDisplayName("sisyphus");
}

// Assembly order: Sisyphus -> Hephaestus -> Prometheus -> Atlas
Expand Down
99 changes: 75 additions & 24 deletions src/plugin-handlers/agent-key-remapper.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,59 @@
import { describe, it, expect } from "bun:test"
import { remapAgentKeysToDisplayNames } from "./agent-key-remapper"
import { getAgentDisplayName, getAgentListDisplayName, getAgentRuntimeName } from "../shared/agent-display-names"
import { getAgentDisplayName, getAgentRuntimeName } from "../shared/agent-display-names"

const ZWSP_REGEX = /[\u200B\u200C\u200D\uFEFF]/

describe("remapAgentKeysToDisplayNames", () => {
it("object keys must not contain ZWSP characters (RFC 7230)", () => {
// given all core agents with ZWSP-based ordering
const agents = {
sisyphus: { prompt: "test" },
hephaestus: { prompt: "test" },
prometheus: { prompt: "test" },
atlas: { prompt: "test" },
}

// when remapping
const result = remapAgentKeysToDisplayNames(agents)

// then NO object key should contain ZWSP (RFC 7230 compliance)
for (const key of Object.keys(result)) {
expect(key).not.toMatch(ZWSP_REGEX)
}
})

it("name field MUST contain ZWSP for core agents (OpenCode sort ordering)", () => {
// given core agents
const agents = {
sisyphus: { prompt: "test" },
hephaestus: { prompt: "test" },
prometheus: { prompt: "test" },
atlas: { prompt: "test" },
}

// when remapping
const result = remapAgentKeysToDisplayNames(agents)

// then name fields MUST have ZWSP prefixes for sort ordering
const sisyphusConfig = result[getAgentDisplayName("sisyphus")] as Record<string, unknown>
const hephaestusConfig = result[getAgentDisplayName("hephaestus")] as Record<string, unknown>
const prometheusConfig = result[getAgentDisplayName("prometheus")] as Record<string, unknown>
const atlasConfig = result[getAgentDisplayName("atlas")] as Record<string, unknown>

expect(sisyphusConfig.name).toMatch(ZWSP_REGEX)
expect(hephaestusConfig.name).toMatch(ZWSP_REGEX)
expect(prometheusConfig.name).toMatch(ZWSP_REGEX)
expect(atlasConfig.name).toMatch(ZWSP_REGEX)

// And they should be the runtime names (with ZWSP)
expect(sisyphusConfig.name).toBe(getAgentRuntimeName("sisyphus"))
expect(hephaestusConfig.name).toBe(getAgentRuntimeName("hephaestus"))
expect(prometheusConfig.name).toBe(getAgentRuntimeName("prometheus"))
expect(atlasConfig.name).toBe(getAgentRuntimeName("atlas"))
})


it("remaps known agent keys to display names", () => {
// given agents with lowercase keys
const agents = {
Expand All @@ -14,7 +65,7 @@ describe("remapAgentKeysToDisplayNames", () => {
const result = remapAgentKeysToDisplayNames(agents)

// then known agents get display name keys only
expect(result[getAgentListDisplayName("sisyphus")]).toBeDefined()
expect(result[getAgentDisplayName("sisyphus")]).toBeDefined()
expect(result["oracle"]).toBeDefined()
expect(result["sisyphus"]).toBeUndefined()
})
Expand Down Expand Up @@ -49,13 +100,13 @@ describe("remapAgentKeysToDisplayNames", () => {
const result = remapAgentKeysToDisplayNames(agents)

// then all get display name keys
expect(result[getAgentListDisplayName("sisyphus")]).toBeDefined()
expect(result[getAgentDisplayName("sisyphus")]).toBeDefined()
expect(result["sisyphus"]).toBeUndefined()
expect(result[getAgentListDisplayName("hephaestus")]).toBeDefined()
expect(result[getAgentDisplayName("hephaestus")]).toBeDefined()
expect(result["hephaestus"]).toBeUndefined()
expect(result[getAgentListDisplayName("prometheus")]).toBeDefined()
expect(result[getAgentDisplayName("prometheus")]).toBeDefined()
expect(result["prometheus"]).toBeUndefined()
expect(result[getAgentListDisplayName("atlas")]).toBeDefined()
expect(result[getAgentDisplayName("atlas")]).toBeDefined()
expect(result["atlas"]).toBeUndefined()
expect(result[getAgentDisplayName("athena")]).toBeDefined()
expect(result["athena"]).toBeUndefined()
Expand All @@ -77,8 +128,8 @@ describe("remapAgentKeysToDisplayNames", () => {
const result = remapAgentKeysToDisplayNames(agents)

// then only display key is emitted
expect(Object.keys(result)).toEqual([getAgentListDisplayName("sisyphus")])
expect(result[getAgentListDisplayName("sisyphus")]).toBeDefined()
expect(Object.keys(result)).toEqual([getAgentDisplayName("sisyphus")])
expect(result[getAgentDisplayName("sisyphus")]).toBeDefined()
expect(result["sisyphus"]).toBeUndefined()
})

Expand All @@ -96,10 +147,10 @@ describe("remapAgentKeysToDisplayNames", () => {

// then
expect(remappedNames).toEqual([
getAgentListDisplayName("atlas"),
getAgentListDisplayName("prometheus"),
getAgentListDisplayName("hephaestus"),
getAgentListDisplayName("sisyphus"),
getAgentDisplayName("atlas"),
getAgentDisplayName("prometheus"),
getAgentDisplayName("hephaestus"),
getAgentDisplayName("sisyphus"),
])
})

Expand All @@ -118,27 +169,27 @@ describe("remapAgentKeysToDisplayNames", () => {

// then keys and names both use the same runtime-facing list names
expect(Object.keys(result).slice(0, 4)).toEqual([
getAgentListDisplayName("sisyphus"),
getAgentListDisplayName("hephaestus"),
getAgentListDisplayName("prometheus"),
getAgentListDisplayName("atlas"),
getAgentDisplayName("sisyphus"),
getAgentDisplayName("hephaestus"),
getAgentDisplayName("prometheus"),
getAgentDisplayName("atlas"),
])
expect(result[getAgentListDisplayName("sisyphus")]).toEqual({
expect(result[getAgentDisplayName("sisyphus")]).toEqual({
name: getAgentRuntimeName("sisyphus"),
prompt: "test",
mode: "primary",
})
expect(result[getAgentListDisplayName("hephaestus")]).toEqual({
expect(result[getAgentDisplayName("hephaestus")]).toEqual({
name: getAgentRuntimeName("hephaestus"),
prompt: "test",
mode: "primary",
})
expect(result[getAgentListDisplayName("prometheus")]).toEqual({
expect(result[getAgentDisplayName("prometheus")]).toEqual({
name: getAgentRuntimeName("prometheus"),
prompt: "test",
mode: "all",
})
expect(result[getAgentListDisplayName("atlas")]).toEqual({
expect(result[getAgentDisplayName("atlas")]).toEqual({
name: getAgentRuntimeName("atlas"),
prompt: "test",
mode: "primary",
Expand All @@ -159,22 +210,22 @@ describe("remapAgentKeysToDisplayNames", () => {
const result = remapAgentKeysToDisplayNames(agents)

// then runtime-facing names stay aligned even when builtin configs omit name
expect(result[getAgentListDisplayName("sisyphus")]).toEqual({
expect(result[getAgentDisplayName("sisyphus")]).toEqual({
name: getAgentRuntimeName("sisyphus"),
prompt: "test",
mode: "primary",
})
expect(result[getAgentListDisplayName("hephaestus")]).toEqual({
expect(result[getAgentDisplayName("hephaestus")]).toEqual({
name: getAgentRuntimeName("hephaestus"),
prompt: "test",
mode: "primary",
})
expect(result[getAgentListDisplayName("prometheus")]).toEqual({
expect(result[getAgentDisplayName("prometheus")]).toEqual({
name: getAgentRuntimeName("prometheus"),
prompt: "test",
mode: "all",
})
expect(result[getAgentListDisplayName("atlas")]).toEqual({
expect(result[getAgentDisplayName("atlas")]).toEqual({
name: getAgentRuntimeName("atlas"),
prompt: "test",
mode: "primary",
Expand Down
4 changes: 2 additions & 2 deletions src/plugin-handlers/agent-key-remapper.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getAgentListDisplayName, getAgentRuntimeName } from "../shared/agent-display-names"
import { getAgentDisplayName, getAgentRuntimeName } from "../shared/agent-display-names"

function rewriteAgentNameForListDisplay(
key: string,
Expand All @@ -21,7 +21,7 @@ export function remapAgentKeysToDisplayNames(
const result: Record<string, unknown> = {}

for (const [key, value] of Object.entries(agents)) {
const displayName = getAgentListDisplayName(key)
const displayName = getAgentDisplayName(key)
if (displayName && displayName !== key) {
result[displayName] = rewriteAgentNameForListDisplay(key, value)
// Regression guard: do not also assign result[key].
Expand Down
12 changes: 6 additions & 6 deletions src/plugin-handlers/agent-priority-order.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
reorderAgentsByPriority,
CANONICAL_CORE_AGENT_ORDER,
} from "./agent-priority-order"
import { getAgentDisplayName, getAgentListDisplayName } from "../shared/agent-display-names"
import { getAgentDisplayName } from "../shared/agent-display-names"

describe("agent-priority-order", () => {
describe("CANONICAL_CORE_AGENT_ORDER", () => {
Expand Down Expand Up @@ -35,11 +35,11 @@ describe("agent-priority-order", () => {
})

describe("reorderAgentsByPriority", () => {
// given: display names for all core agents
const sisyphus = getAgentListDisplayName("sisyphus")
const hephaestus = getAgentListDisplayName("hephaestus")
const prometheus = getAgentListDisplayName("prometheus")
const atlas = getAgentListDisplayName("atlas")
// given: display names for all core agents (no ZWSP in keys)
const sisyphus = getAgentDisplayName("sisyphus")
const hephaestus = getAgentDisplayName("hephaestus")
const prometheus = getAgentDisplayName("prometheus")
const atlas = getAgentDisplayName("atlas")
const oracle = getAgentDisplayName("oracle")
const librarian = getAgentDisplayName("librarian")
const explore = getAgentDisplayName("explore")
Expand Down
4 changes: 2 additions & 2 deletions src/plugin-handlers/agent-priority-order.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getAgentListDisplayName } from "../shared/agent-display-names"
import { getAgentDisplayName } from "../shared/agent-display-names"

/**
* CRITICAL: This is the ONLY source of truth for core agent ordering.
Expand All @@ -25,7 +25,7 @@ const CORE_AGENT_ORDER: ReadonlyArray<{
order: number
}> = CANONICAL_CORE_AGENT_ORDER.map((configKey, index) => ({
configKey,
displayName: getAgentListDisplayName(configKey),
displayName: getAgentDisplayName(configKey),
order: index + 1,
}))

Expand Down
13 changes: 5 additions & 8 deletions src/plugin-handlers/command-config-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@ import * as skillLoader from "../features/opencode-skill-loader";
import type { OhMyOpenCodeConfig } from "../config";
import type { PluginComponents } from "./plugin-components-loader";
import { applyCommandConfig } from "./command-config-handler";
import {
getAgentDisplayName,
getAgentListDisplayName,
} from "../shared/agent-display-names";
import { getAgentDisplayName } from "../shared/agent-display-names";

function createPluginComponents(): PluginComponents {
return {
Expand Down Expand Up @@ -108,7 +105,7 @@ describe("applyCommandConfig", () => {
expect(commandConfig["agents-global-skill"]?.description).toContain("Agents global skill");
});

test("normalizes Atlas command agents to the runtime list name used by opencode command routing", async () => {
test("normalizes Atlas command agents to the display name for HTTP-safe routing", async () => {
// given
loadBuiltinCommandsSpy.mockReturnValue({
"start-work": {
Expand All @@ -130,10 +127,10 @@ describe("applyCommandConfig", () => {

// then
const commandConfig = config.command as Record<string, { agent?: string }>;
expect(commandConfig["start-work"]?.agent).toBe(getAgentListDisplayName("atlas"));
expect(commandConfig["start-work"]?.agent).toBe(getAgentDisplayName("atlas"));
});

test("normalizes legacy display-name command agents to the runtime list name", async () => {
test("normalizes legacy display-name command agents to the display name", async () => {
// given
loadBuiltinCommandsSpy.mockReturnValue({
"start-work": {
Expand All @@ -155,6 +152,6 @@ describe("applyCommandConfig", () => {

// then
const commandConfig = config.command as Record<string, { agent?: string }>;
expect(commandConfig["start-work"]?.agent).toBe(getAgentListDisplayName("atlas"));
expect(commandConfig["start-work"]?.agent).toBe(getAgentDisplayName("atlas"));
});
});
4 changes: 2 additions & 2 deletions src/plugin-handlers/command-config-handler.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { OhMyOpenCodeConfig } from "../config";
import {
getAgentConfigKey,
getAgentListDisplayName,
getAgentDisplayName,
} from "../shared/agent-display-names";
import {
loadUserCommands,
Expand Down Expand Up @@ -99,7 +99,7 @@ export async function applyCommandConfig(params: {
function remapCommandAgentFields(commands: Record<string, Record<string, unknown>>): void {
for (const cmd of Object.values(commands)) {
if (cmd?.agent && typeof cmd.agent === "string") {
cmd.agent = getAgentListDisplayName(getAgentConfigKey(cmd.agent));
cmd.agent = getAgentDisplayName(getAgentConfigKey(cmd.agent));
}
}
}
Loading
Loading