fix(start-work): keep native command agents on config keys

This commit is contained in:
YeonGyu-Kim
2026-04-08 15:55:58 +09:00
parent aa1a18c5d8
commit cd95172e42
6 changed files with 42 additions and 26 deletions
+9 -10
View File
@@ -7,7 +7,6 @@ import { tmpdir } from "node:os"
import { randomUUID } from "node:crypto"
import { createStartWorkHook } from "./index"
import { createAtlasHook } from "../atlas"
import { getAgentDisplayName, getAgentListDisplayName } from "../../shared/agent-display-names"
import {
writeBoulderState,
clearBoulderState,
@@ -467,7 +466,7 @@ You are starting a Sisyphus work session.
updateSpy.mockRestore()
})
test("should stamp the outgoing message with Atlas list key so follow-up events keep the handoff", async () => {
test("should stamp the outgoing message with Atlas config key so OpenCode can resolve the agent", async () => {
// given
const hook = createStartWorkHook(createMockPluginInput())
const output = {
@@ -481,8 +480,8 @@ You are starting a Sisyphus work session.
output
)
// then
expect(output.message.agent).toBe(getAgentDisplayName("atlas"))
// then - config key, not display name (matches no-sisyphus-gpt / boulder-continuation-injector convention)
expect(output.message.agent).toBe("atlas")
})
test("should switch to Atlas even when current session is Sisyphus (regression: #3155)", async () => {
@@ -502,7 +501,7 @@ You are starting a Sisyphus work session.
)
// atlas is registered in beforeEach, so it must be selected
expect(output.message.agent).toBe(getAgentDisplayName("atlas"))
expect(output.message.agent).toBe("atlas")
expect(sessionState.getSessionAgent("ses-sisyphus-to-atlas")).toBe("atlas")
})
@@ -525,7 +524,7 @@ You are starting a Sisyphus work session.
)
// then
expect(output.message.agent).toBe("Sisyphus - Ultraworker")
expect(output.message.agent).toBe("sisyphus")
expect(sessionState.getSessionAgent("ses-prometheus-to-sisyphus")).toBe("sisyphus")
})
@@ -553,7 +552,7 @@ You are starting a Sisyphus work session.
)
// then
expect(output.message.agent).toBe("Sisyphus - Ultraworker")
expect(output.message.agent).toBe("sisyphus")
expect(sessionState.getSessionAgent("ses-prometheus-to-worker")).toBe("sisyphus")
expect(readBoulderState(testDir)?.agent).toBe("sisyphus")
})
@@ -588,7 +587,7 @@ You are starting a Sisyphus work session.
)
// then
expect(output.message.agent).toBe("Sisyphus - Ultraworker")
expect(output.message.agent).toBe("sisyphus")
expect(readBoulderState(testDir)?.agent).toBe("sisyphus")
})
@@ -623,7 +622,7 @@ You are starting a Sisyphus work session.
await atlasHook.handler({ event: { type: "session.idle", properties: { sessionID: "session-123" } } })
// then
expect(output.message.agent).toBe(getAgentDisplayName("atlas"))
expect(output.message.agent).toBe("atlas")
expect(readBoulderState(testDir)?.session_ids).toContain("session-123")
expect(readBoulderState(testDir)?.agent).toBe("atlas")
expect(promptAsyncMock).toHaveBeenCalledTimes(1)
@@ -713,7 +712,7 @@ You are starting a Sisyphus work session.
await firePendingTimers()
// then
expect(output.message.agent).toBe(getAgentDisplayName("atlas"))
expect(output.message.agent).toBe("atlas")
expect(readBoulderState(testDir)?.session_ids).toContain("session-123")
expect(readBoulderState(testDir)?.agent).toBe("atlas")
expect(promptAsyncMock).toHaveBeenCalledTimes(1)
+1 -9
View File
@@ -11,11 +11,6 @@ import {
clearBoulderState,
} from "../../features/boulder-state"
import { log } from "../../shared/logger"
import {
getAgentDisplayName,
getAgentListDisplayName,
stripAgentListSortPrefix,
} from "../../shared/agent-display-names"
import {
isAgentRegistered,
updateSessionAgent,
@@ -86,12 +81,9 @@ export function createStartWorkHook(ctx: PluginInput) {
const activeAgent = isAgentRegistered("atlas")
? "atlas"
: "sisyphus"
const activeAgentDisplayName = activeAgent === "atlas"
? getAgentListDisplayName(activeAgent)
: getAgentDisplayName(activeAgent)
updateSessionAgent(input.sessionID, activeAgent)
if (output.message) {
output.message["agent"] = stripAgentListSortPrefix(activeAgentDisplayName)
output.message["agent"] = activeAgent
}
const existingState = readBoulderState(ctx.directory)
@@ -97,7 +97,7 @@ describe("applyCommandConfig", () => {
expect(commandConfig["agents-global-skill"]?.description).toContain("Agents global skill");
});
test("remaps Atlas command agents to the list display name used by runtime agent lookup", async () => {
test("normalizes Atlas command agents to the config key OpenCode expects for native routing", async () => {
// given
loadBuiltinCommandsSpy.mockReturnValue({
"start-work": {
@@ -119,6 +119,31 @@ describe("applyCommandConfig", () => {
// then
const commandConfig = config.command as Record<string, { agent?: string }>;
expect(commandConfig["start-work"]?.agent).toBe(getAgentDisplayName("atlas"));
expect(commandConfig["start-work"]?.agent).toBe("atlas");
});
test("normalizes legacy display-name command agents back to config keys", async () => {
// given
loadBuiltinCommandsSpy.mockReturnValue({
"start-work": {
name: "start-work",
description: "(builtin) Start work",
template: "template",
agent: getAgentDisplayName("atlas"),
},
});
const config: Record<string, unknown> = { command: {} };
// when
await applyCommandConfig({
config,
pluginConfig: createPluginConfig(),
ctx: { directory: "/tmp" },
pluginComponents: createPluginComponents(),
});
// then
const commandConfig = config.command as Record<string, { agent?: string }>;
expect(commandConfig["start-work"]?.agent).toBe("atlas");
});
});
@@ -1,5 +1,5 @@
import type { OhMyOpenCodeConfig } from "../config";
import { getAgentDisplayName } from "../shared/agent-display-names";
import { getAgentConfigKey } from "../shared/agent-display-names";
import {
loadUserCommands,
loadProjectCommands,
@@ -96,7 +96,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 = getAgentDisplayName(cmd.agent);
cmd.agent = getAgentConfigKey(cmd.agent);
}
}
}
+1 -1
View File
@@ -165,7 +165,7 @@ describe("createPluginInterface - command.execute.before", () => {
)
// then
expect(output.message.agent).toBe("Atlas - Plan Executor")
expect(output.message.agent).toBe("atlas")
expect(getSessionAgent("ses-command-atlas")).toBe("atlas")
expect(readBoulderState(testDir)?.agent).toBe("atlas")
})
+2 -2
View File
@@ -87,7 +87,7 @@ describe("createChatMessageHandler - /start-work integration", () => {
await handler(input, output)
// then
expect(output.message["agent"]).toBe("Sisyphus - Ultraworker")
expect(output.message["agent"]).toBe("sisyphus")
expect(output.parts[0].text).toContain("<auto-slash-command>")
expect(output.parts[0].text).toContain("Auto-Selected Plan")
expect(output.parts[0].text).toContain("boulder.json has been created")
@@ -116,7 +116,7 @@ describe("createChatMessageHandler - /start-work integration", () => {
await handler(input, output)
// then
expect(output.message["agent"]).toBe("Sisyphus - Ultraworker")
expect(output.message["agent"]).toBe("sisyphus")
expect(output.parts[0].text).toContain("<auto-slash-command>")
expect(output.parts[0].text).toContain("Auto-Selected Plan")
expect(output.parts[0].text).toContain("my-feature-plan")