fix(start-work): use Atlas list key in command config
This commit is contained in:
@@ -348,6 +348,22 @@ describe("createAutoSlashCommandHook", () => {
|
||||
expect(output.parts[0].text).toContain("/ralph-loop Command")
|
||||
})
|
||||
|
||||
it("should inject template for known builtin commands like ulw-loop", async () => {
|
||||
//#given
|
||||
const hook = createAutoSlashCommandHook()
|
||||
const input = createCommandInput("ulw-loop", '"Ship feature" --strategy=continue')
|
||||
const output = createCommandOutput("original")
|
||||
|
||||
//#when
|
||||
await hook["command.execute.before"](input, output)
|
||||
|
||||
//#then
|
||||
expect(output.parts[0].text).toContain("<auto-slash-command>")
|
||||
expect(output.parts[0].text).toContain("/ulw-loop Command")
|
||||
expect(output.parts[0].text).toContain("<user-task>")
|
||||
expect(output.parts[0].text).toContain('"Ship feature" --strategy=continue')
|
||||
})
|
||||
|
||||
it("should pass command arguments correctly", async () => {
|
||||
//#given
|
||||
const hook = createAutoSlashCommandHook()
|
||||
|
||||
@@ -5,6 +5,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 { getAgentListDisplayName } from "../shared/agent-display-names";
|
||||
|
||||
function createPluginComponents(): PluginComponents {
|
||||
return {
|
||||
@@ -95,4 +96,29 @@ describe("applyCommandConfig", () => {
|
||||
expect(commandConfig["agents-project-skill"]?.description).toContain("Agents project skill");
|
||||
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 () => {
|
||||
// given
|
||||
loadBuiltinCommandsSpy.mockReturnValue({
|
||||
"start-work": {
|
||||
name: "start-work",
|
||||
description: "(builtin) Start work",
|
||||
template: "template",
|
||||
agent: "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(getAgentListDisplayName("atlas"));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { OhMyOpenCodeConfig } from "../config";
|
||||
import { getAgentDisplayName } from "../shared/agent-display-names";
|
||||
import { getAgentListDisplayName } from "../shared/agent-display-names";
|
||||
import {
|
||||
loadUserCommands,
|
||||
loadProjectCommands,
|
||||
@@ -97,7 +97,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 = getAgentListDisplayName(cmd.agent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,3 +170,90 @@ describe("createPluginInterface - command.execute.before", () => {
|
||||
expect(readBoulderState(testDir)?.agent).toBe("atlas")
|
||||
})
|
||||
})
|
||||
|
||||
describe("createPluginInterface - ulw-loop native command smoke", () => {
|
||||
let testDir = ""
|
||||
|
||||
beforeEach(() => {
|
||||
testDir = join(tmpdir(), `plugin-interface-ulw-loop-${randomUUID()}`)
|
||||
mkdirSync(testDir, { recursive: true })
|
||||
_resetForTesting()
|
||||
registerAgentName("sisyphus")
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
_resetForTesting()
|
||||
rmSync(testDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test("starts the ultrawork loop from the native command flow with parsed arguments intact", async () => {
|
||||
// given
|
||||
const startLoopCalls: Array<{
|
||||
sessionID: string
|
||||
prompt: string
|
||||
options: Record<string, unknown>
|
||||
}> = []
|
||||
const pluginInterface = createPluginInterface({
|
||||
ctx: {
|
||||
directory: testDir,
|
||||
client: { tui: { showToast: async () => {} } },
|
||||
} as never,
|
||||
pluginConfig: {} as never,
|
||||
firstMessageVariantGate: {
|
||||
shouldOverride: () => false,
|
||||
markApplied: () => {},
|
||||
markSessionCreated: () => {},
|
||||
clear: () => {},
|
||||
},
|
||||
managers: {} as never,
|
||||
hooks: {
|
||||
autoSlashCommand: createAutoSlashCommandHook({ skills: [] }),
|
||||
ralphLoop: {
|
||||
startLoop: (sessionID: string, prompt: string, options?: Record<string, unknown>) => {
|
||||
startLoopCalls.push({ sessionID, prompt, options: options ?? {} })
|
||||
return true
|
||||
},
|
||||
cancelLoop: () => true,
|
||||
getState: () => null,
|
||||
},
|
||||
} as never,
|
||||
tools: {},
|
||||
})
|
||||
const output = {
|
||||
message: {} as Record<string, unknown>,
|
||||
parts: [{ type: "text", text: "original" }],
|
||||
}
|
||||
|
||||
// when
|
||||
await pluginInterface["command.execute.before"]?.(
|
||||
{
|
||||
command: "ulw-loop",
|
||||
sessionID: "ses-ulw-native",
|
||||
arguments: '"Ship feature" --strategy=continue',
|
||||
},
|
||||
output as never,
|
||||
)
|
||||
await pluginInterface["chat.message"]?.(
|
||||
{
|
||||
sessionID: "ses-ulw-native",
|
||||
agent: "sisyphus",
|
||||
} as never,
|
||||
output as never,
|
||||
)
|
||||
|
||||
// then
|
||||
expect(output.parts[0]?.text).toContain("/ulw-loop Command")
|
||||
expect(startLoopCalls).toEqual([
|
||||
{
|
||||
sessionID: "ses-ulw-native",
|
||||
prompt: "Ship feature",
|
||||
options: {
|
||||
ultrawork: true,
|
||||
maxIterations: undefined,
|
||||
completionPromise: undefined,
|
||||
strategy: "continue",
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -95,6 +95,49 @@ describe("createChatMessageHandler - /start-work integration", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("createChatMessageHandler - /ulw-loop raw slash fallback", () => {
|
||||
test("starts ultrawork loop when /ulw-loop arrives through chat.message without native command expansion", async () => {
|
||||
// given
|
||||
const startLoopCalls: Array<{
|
||||
sessionID: string
|
||||
prompt: string
|
||||
options: Record<string, unknown>
|
||||
}> = []
|
||||
const args = createMockHandlerArgs()
|
||||
args.hooks.autoSlashCommand = createAutoSlashCommandHook({ skills: [] })
|
||||
args.hooks.ralphLoop = {
|
||||
startLoop: (sessionID: string, prompt: string, options?: Record<string, unknown>) => {
|
||||
startLoopCalls.push({ sessionID, prompt, options: options ?? {} })
|
||||
return true
|
||||
},
|
||||
cancelLoop: () => true,
|
||||
}
|
||||
const handler = createChatMessageHandler(args)
|
||||
const input = createMockInput("sisyphus")
|
||||
const output: ChatMessageHandlerOutput = {
|
||||
message: {},
|
||||
parts: [{ type: "text", text: '/ulw-loop "Ship feature" --strategy=continue' }],
|
||||
}
|
||||
|
||||
// when
|
||||
await handler(input, output)
|
||||
|
||||
// then
|
||||
expect(startLoopCalls).toEqual([
|
||||
{
|
||||
sessionID: "test-session",
|
||||
prompt: "Ship feature",
|
||||
options: {
|
||||
ultrawork: true,
|
||||
maxIterations: undefined,
|
||||
completionPromise: undefined,
|
||||
strategy: "continue",
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
function createMockInput(agent?: string, model?: { providerID: string; modelID: string }) {
|
||||
return {
|
||||
sessionID: "test-session",
|
||||
|
||||
@@ -25,6 +25,10 @@ type StartWorkHookOutput = { parts: Array<{ type: string; text?: string }> }
|
||||
|
||||
type SessionModelOverride = { providerID: string; modelID: string }
|
||||
|
||||
type RawLoopCommand =
|
||||
| { command: "ralph-loop" | "ulw-loop"; args: string }
|
||||
| { command: "cancel-ralph"; args: "" }
|
||||
|
||||
function isStartWorkHookOutput(value: unknown): value is StartWorkHookOutput {
|
||||
if (typeof value !== "object" || value === null) return false
|
||||
const record = value as Record<string, unknown>
|
||||
@@ -84,6 +88,33 @@ function getStoredMainSessionModel(
|
||||
return getSessionModel(input.sessionID)
|
||||
}
|
||||
|
||||
function parseRawLoopSlashCommand(promptText: string): RawLoopCommand | null {
|
||||
const trimmed = promptText.trim()
|
||||
|
||||
if (!trimmed.startsWith("/")) {
|
||||
return null
|
||||
}
|
||||
|
||||
const cancelMatch = trimmed.match(/^\/cancel-ralph(?:\s+.*)?$/i)
|
||||
if (cancelMatch) {
|
||||
return { command: "cancel-ralph", args: "" }
|
||||
}
|
||||
|
||||
const loopMatch = trimmed.match(/^\/(ralph-loop|ulw-loop)\s*([\s\S]*)$/i)
|
||||
if (!loopMatch) {
|
||||
return null
|
||||
}
|
||||
|
||||
const command = loopMatch[1]?.toLowerCase()
|
||||
const args = loopMatch[2]?.trim() ?? ""
|
||||
|
||||
if (command === "ralph-loop" || command === "ulw-loop") {
|
||||
return { command, args }
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function createChatMessageHandler(args: {
|
||||
ctx: PluginContext
|
||||
pluginConfig: OhMyOpenCodeConfig
|
||||
@@ -201,19 +232,24 @@ export function createChatMessageHandler(args: {
|
||||
const isCancelRalphTemplate = promptText.includes(
|
||||
"Cancel the currently active Ralph Loop",
|
||||
)
|
||||
const rawLoopCommand =
|
||||
!isRalphLoopTemplate && !isUlwLoopTemplate && !isCancelRalphTemplate
|
||||
? parseRawLoopSlashCommand(promptText)
|
||||
: null
|
||||
|
||||
if (isRalphLoopTemplate || isUlwLoopTemplate) {
|
||||
if (isRalphLoopTemplate || isUlwLoopTemplate || rawLoopCommand?.command === "ralph-loop" || rawLoopCommand?.command === "ulw-loop") {
|
||||
const taskMatch = promptText.match(/<user-task>\s*([\s\S]*?)\s*<\/user-task>/i)
|
||||
const rawTask = taskMatch?.[1]?.trim() || ""
|
||||
const rawTask = taskMatch?.[1]?.trim() || rawLoopCommand?.args || ""
|
||||
const parsedArguments = parseRalphLoopArguments(rawTask)
|
||||
const ultrawork = isUlwLoopTemplate || rawLoopCommand?.command === "ulw-loop"
|
||||
|
||||
hooks.ralphLoop.startLoop(input.sessionID, parsedArguments.prompt, {
|
||||
ultrawork: isUlwLoopTemplate,
|
||||
ultrawork,
|
||||
maxIterations: parsedArguments.maxIterations,
|
||||
completionPromise: parsedArguments.completionPromise,
|
||||
strategy: parsedArguments.strategy,
|
||||
})
|
||||
} else if (isCancelRalphTemplate) {
|
||||
} else if (isCancelRalphTemplate || rawLoopCommand?.command === "cancel-ralph") {
|
||||
hooks.ralphLoop.cancelLoop(input.sessionID)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user