fix(start-work): restore atlas handoff

Keep native /start-work resolvable on Sisyphus, but switch the work session back to Atlas when Atlas is registered. Stamp the outgoing agent with Atlas's actual list-display key so config→start-work execution resolves correctly and still falls back to Sisyphus when Atlas is unavailable.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
YeonGyu-Kim
2026-03-31 20:02:00 -07:00
parent d029bc7621
commit 7f846b2da3
8 changed files with 503 additions and 153 deletions
@@ -59,6 +59,16 @@ describe("loadBuiltinCommands", () => {
//#then //#then
expect(commands.handoff.description).toContain("context summary") expect(commands.handoff.description).toContain("context summary")
}) })
test("should preassign Sisyphus as the native agent for start-work", () => {
//#given - no disabled commands
//#when
const commands = loadBuiltinCommands()
//#then
expect(commands["start-work"].agent).toBe("sisyphus")
})
}) })
describe("loadBuiltinCommands — remove-ai-slops", () => { describe("loadBuiltinCommands — remove-ai-slops", () => {
+1 -1
View File
@@ -58,7 +58,7 @@ ${REFACTOR_TEMPLATE}
}, },
"start-work": { "start-work": {
description: "(builtin) Start Sisyphus work session from Prometheus plan", description: "(builtin) Start Sisyphus work session from Prometheus plan",
agent: "atlas", agent: "sisyphus",
template: `<command-instruction> template: `<command-instruction>
${START_WORK_TEMPLATE} ${START_WORK_TEMPLATE}
</command-instruction> </command-instruction>
+59 -32
View File
@@ -4,6 +4,7 @@ import { join } from "node:path"
import { tmpdir } from "node:os" import { tmpdir } from "node:os"
import { randomUUID } from "node:crypto" import { randomUUID } from "node:crypto"
import { createStartWorkHook } from "./index" import { createStartWorkHook } from "./index"
import { getAgentListDisplayName } from "../../shared/agent-display-names"
import { import {
writeBoulderState, writeBoulderState,
clearBoulderState, clearBoulderState,
@@ -24,6 +25,22 @@ describe("start-work hook", () => {
} as Parameters<typeof createStartWorkHook>[0] } as Parameters<typeof createStartWorkHook>[0]
} }
function createStartWorkPrompt(options?: {
sessionContext?: string
userRequest?: string
}): string {
const sessionContext = options?.sessionContext ?? ""
const userRequest = options?.userRequest ?? ""
return `<command-instruction>
You are starting a Sisyphus work session.
</command-instruction>
<session-context>${sessionContext}</session-context>${userRequest ? `
<user-request>${userRequest}</user-request>` : ""}`
}
beforeEach(() => { beforeEach(() => {
sessionState._resetForTesting() sessionState._resetForTesting()
sessionState.registerAgentName("atlas") sessionState.registerAgentName("atlas")
@@ -65,6 +82,24 @@ describe("start-work hook", () => {
expect(output.parts[0].text).toBe("Just a regular message") expect(output.parts[0].text).toBe("Just a regular message")
}) })
test("should ignore plain session-context blocks without the start-work marker", async () => {
// given
const hook = createStartWorkHook(createMockPluginInput())
const output = {
parts: [{ type: "text", text: "<session-context>Some context here</session-context>" }],
}
// when
await hook["chat.message"](
{ sessionID: "session-123" },
output
)
// then
expect(output.parts[0].text).toBe("<session-context>Some context here</session-context>")
expect(readBoulderState(testDir)).toBeNull()
})
test("should detect start-work command via session-context tag", async () => { test("should detect start-work command via session-context tag", async () => {
// given - hook and start-work message // given - hook and start-work message
const hook = createStartWorkHook(createMockPluginInput()) const hook = createStartWorkHook(createMockPluginInput())
@@ -72,7 +107,7 @@ describe("start-work hook", () => {
parts: [ parts: [
{ {
type: "text", type: "text",
text: "<session-context>Some context here</session-context>", text: createStartWorkPrompt({ sessionContext: "Some context here" }),
}, },
], ],
} }
@@ -102,7 +137,7 @@ describe("start-work hook", () => {
const hook = createStartWorkHook(createMockPluginInput()) const hook = createStartWorkHook(createMockPluginInput())
const output = { const output = {
parts: [{ type: "text", text: "<session-context></session-context>" }], parts: [{ type: "text", text: createStartWorkPrompt() }],
} }
// when // when
@@ -123,7 +158,7 @@ describe("start-work hook", () => {
parts: [ parts: [
{ {
type: "text", type: "text",
text: "<session-context>Session: $SESSION_ID</session-context>", text: createStartWorkPrompt({ sessionContext: "Session: $SESSION_ID" }),
}, },
], ],
} }
@@ -146,7 +181,7 @@ describe("start-work hook", () => {
parts: [ parts: [
{ {
type: "text", type: "text",
text: "<session-context>Time: $TIMESTAMP</session-context>", text: createStartWorkPrompt({ sessionContext: "Time: $TIMESTAMP" }),
}, },
], ],
} }
@@ -177,7 +212,7 @@ describe("start-work hook", () => {
const hook = createStartWorkHook(createMockPluginInput()) const hook = createStartWorkHook(createMockPluginInput())
const output = { const output = {
parts: [{ type: "text", text: "<session-context></session-context>" }], parts: [{ type: "text", text: createStartWorkPrompt() }],
} }
// when // when
@@ -205,7 +240,7 @@ describe("start-work hook", () => {
const hook = createStartWorkHook(createMockPluginInput()) const hook = createStartWorkHook(createMockPluginInput())
const output = { const output = {
parts: [{ type: "text", text: "<session-context></session-context>" }], parts: [{ type: "text", text: createStartWorkPrompt() }],
} }
// when // when
@@ -233,7 +268,7 @@ describe("start-work hook", () => {
const hook = createStartWorkHook(createMockPluginInput()) const hook = createStartWorkHook(createMockPluginInput())
const output = { const output = {
parts: [{ type: "text", text: "<session-context></session-context>" }], parts: [{ type: "text", text: createStartWorkPrompt() }],
} }
// when // when
@@ -274,9 +309,7 @@ describe("start-work hook", () => {
parts: [ parts: [
{ {
type: "text", type: "text",
text: `<session-context> text: createStartWorkPrompt({ userRequest: "new-plan" }),
<user-request>new-plan</user-request>
</session-context>`,
}, },
], ],
} }
@@ -306,9 +339,7 @@ describe("start-work hook", () => {
parts: [ parts: [
{ {
type: "text", type: "text",
text: `<session-context> text: createStartWorkPrompt({ userRequest: "my-feature-plan ultrawork" }),
<user-request>my-feature-plan ultrawork</user-request>
</session-context>`,
}, },
], ],
} }
@@ -337,9 +368,7 @@ describe("start-work hook", () => {
parts: [ parts: [
{ {
type: "text", type: "text",
text: `<session-context> text: createStartWorkPrompt({ userRequest: "api-refactor ulw" }),
<user-request>api-refactor ulw</user-request>
</session-context>`,
}, },
], ],
} }
@@ -368,9 +397,7 @@ describe("start-work hook", () => {
parts: [ parts: [
{ {
type: "text", type: "text",
text: `<session-context> text: createStartWorkPrompt({ userRequest: "feature-implementation" }),
<user-request>feature-implementation</user-request>
</session-context>`,
}, },
], ],
} }
@@ -394,7 +421,7 @@ describe("start-work hook", () => {
const hook = createStartWorkHook(createMockPluginInput()) const hook = createStartWorkHook(createMockPluginInput())
const output = { const output = {
parts: [{ type: "text", text: "<session-context></session-context>" }], parts: [{ type: "text", text: createStartWorkPrompt() }],
} }
// when // when
@@ -408,12 +435,12 @@ describe("start-work hook", () => {
updateSpy.mockRestore() updateSpy.mockRestore()
}) })
test("should stamp the outgoing message with Atlas so follow-up events keep the handoff", async () => { test("should stamp the outgoing message with Atlas list key so follow-up events keep the handoff", async () => {
// given // given
const hook = createStartWorkHook(createMockPluginInput()) const hook = createStartWorkHook(createMockPluginInput())
const output = { const output = {
message: {} as Record<string, unknown>, message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "<session-context></session-context>" }], parts: [{ type: "text", text: createStartWorkPrompt() }],
} }
// when // when
@@ -423,7 +450,7 @@ describe("start-work hook", () => {
) )
// then // then
expect(output.message.agent).toBe("Atlas (Plan Executor)") expect(output.message.agent).toBe(getAgentListDisplayName("atlas"))
}) })
test("should keep the current agent when Atlas is unavailable", async () => { test("should keep the current agent when Atlas is unavailable", async () => {
@@ -435,7 +462,7 @@ describe("start-work hook", () => {
const hook = createStartWorkHook(createMockPluginInput()) const hook = createStartWorkHook(createMockPluginInput())
const output = { const output = {
message: {} as Record<string, unknown>, message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "<session-context></session-context>" }], parts: [{ type: "text", text: createStartWorkPrompt() }],
} }
// when // when
@@ -463,7 +490,7 @@ describe("start-work hook", () => {
const hook = createStartWorkHook(createMockPluginInput()) const hook = createStartWorkHook(createMockPluginInput())
const output = { const output = {
message: {} as Record<string, unknown>, message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "<session-context></session-context>" }], parts: [{ type: "text", text: createStartWorkPrompt() }],
} }
// when // when
@@ -498,7 +525,7 @@ describe("start-work hook", () => {
const hook = createStartWorkHook(createMockPluginInput()) const hook = createStartWorkHook(createMockPluginInput())
const output = { const output = {
message: {} as Record<string, unknown>, message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "<session-context></session-context>" }], parts: [{ type: "text", text: createStartWorkPrompt() }],
} }
// when // when
@@ -532,7 +559,7 @@ describe("start-work hook", () => {
const hook = createStartWorkHook(createMockPluginInput()) const hook = createStartWorkHook(createMockPluginInput())
const output = { const output = {
parts: [{ type: "text", text: "<session-context></session-context>" }], parts: [{ type: "text", text: createStartWorkPrompt() }],
} }
// when // when
@@ -553,7 +580,7 @@ describe("start-work hook", () => {
const hook = createStartWorkHook(createMockPluginInput()) const hook = createStartWorkHook(createMockPluginInput())
const output = { const output = {
parts: [{ type: "text", text: "<session-context>\n<user-request>--worktree /validated/worktree</user-request>\n</session-context>" }], parts: [{ type: "text", text: createStartWorkPrompt({ userRequest: "--worktree /validated/worktree" }) }],
} }
// when // when
@@ -575,7 +602,7 @@ describe("start-work hook", () => {
const hook = createStartWorkHook(createMockPluginInput()) const hook = createStartWorkHook(createMockPluginInput())
const output = { const output = {
parts: [{ type: "text", text: "<session-context>\n<user-request>--worktree /valid/wt</user-request>\n</session-context>" }], parts: [{ type: "text", text: createStartWorkPrompt({ userRequest: "--worktree /valid/wt" }) }],
} }
// when // when
@@ -595,7 +622,7 @@ describe("start-work hook", () => {
const hook = createStartWorkHook(createMockPluginInput()) const hook = createStartWorkHook(createMockPluginInput())
const output = { const output = {
parts: [{ type: "text", text: "<session-context>\n<user-request>--worktree /nonexistent/wt</user-request>\n</session-context>" }], parts: [{ type: "text", text: createStartWorkPrompt({ userRequest: "--worktree /nonexistent/wt" }) }],
} }
// when // when
@@ -624,7 +651,7 @@ describe("start-work hook", () => {
const hook = createStartWorkHook(createMockPluginInput()) const hook = createStartWorkHook(createMockPluginInput())
const output = { const output = {
parts: [{ type: "text", text: "<session-context>\n<user-request>--worktree /new/wt</user-request>\n</session-context>" }], parts: [{ type: "text", text: createStartWorkPrompt({ userRequest: "--worktree /new/wt" }) }],
} }
// when // when
@@ -651,7 +678,7 @@ describe("start-work hook", () => {
const hook = createStartWorkHook(createMockPluginInput()) const hook = createStartWorkHook(createMockPluginInput())
const output = { const output = {
parts: [{ type: "text", text: "<session-context></session-context>" }], parts: [{ type: "text", text: createStartWorkPrompt() }],
} }
// when // when
+50 -9
View File
@@ -11,18 +11,33 @@ import {
clearBoulderState, clearBoulderState,
} from "../../features/boulder-state" } from "../../features/boulder-state"
import { log } from "../../shared/logger" import { log } from "../../shared/logger"
import { getAgentConfigKey, getAgentDisplayName } from "../../shared/agent-display-names" import {
import { getSessionAgent, isAgentRegistered, updateSessionAgent } from "../../features/claude-code-session-state" getAgentConfigKey,
getAgentDisplayName,
getAgentListDisplayName,
} from "../../shared/agent-display-names"
import {
getSessionAgent,
isAgentRegistered,
updateSessionAgent,
} from "../../features/claude-code-session-state"
import { detectWorktreePath } from "./worktree-detector" import { detectWorktreePath } from "./worktree-detector"
import { parseUserRequest } from "./parse-user-request" import { parseUserRequest } from "./parse-user-request"
export const HOOK_NAME = "start-work" as const export const HOOK_NAME = "start-work" as const
const START_WORK_TEMPLATE_MARKER = "You are starting a Sisyphus work session."
interface StartWorkHookInput { interface StartWorkHookInput {
sessionID: string sessionID: string
messageID?: string messageID?: string
} }
interface StartWorkCommandExecuteBeforeInput {
sessionID: string
command: string
arguments: string
}
interface StartWorkHookOutput { interface StartWorkHookOutput {
message?: Record<string, unknown> message?: Record<string, unknown>
parts: Array<{ type: string; text?: string }> parts: Array<{ type: string; text?: string }>
@@ -67,8 +82,10 @@ function resolveWorktreeContext(
} }
export function createStartWorkHook(ctx: PluginInput) { export function createStartWorkHook(ctx: PluginInput) {
return { const processStartWork = async (
"chat.message": async (input: StartWorkHookInput, output: StartWorkHookOutput): Promise<void> => { input: StartWorkHookInput,
output: StartWorkHookOutput,
): Promise<void> => {
const parts = output.parts const parts = output.parts
const promptText = const promptText =
parts parts
@@ -77,16 +94,29 @@ export function createStartWorkHook(ctx: PluginInput) {
.join("\n") .join("\n")
.trim() || "" .trim() || ""
if (!promptText.includes("<session-context>")) return if (
!promptText.includes("<session-context>")
|| !promptText.includes(START_WORK_TEMPLATE_MARKER)
) {
return
}
log(`[${HOOK_NAME}] Processing start-work command`, { sessionID: input.sessionID }) log(`[${HOOK_NAME}] Processing start-work command`, { sessionID: input.sessionID })
const currentSessionAgent = getSessionAgent(input.sessionID) const currentSessionAgent = getSessionAgent(input.sessionID)
const activeAgent = isAgentRegistered("atlas") const currentSessionAgentKey = currentSessionAgent
? "atlas" ? getAgentConfigKey(currentSessionAgent)
: currentSessionAgent && getAgentConfigKey(currentSessionAgent) !== "prometheus" : undefined
const activeAgent = currentSessionAgent
&& currentSessionAgentKey
&& currentSessionAgentKey !== "prometheus"
&& currentSessionAgentKey !== "atlas"
? currentSessionAgent ? currentSessionAgent
: isAgentRegistered("atlas")
? "atlas"
: "sisyphus" : "sisyphus"
const activeAgentDisplayName = getAgentDisplayName(activeAgent) const activeAgentDisplayName = activeAgent === "atlas"
? getAgentListDisplayName(activeAgent)
: getAgentDisplayName(activeAgent)
updateSessionAgent(input.sessionID, activeAgent) updateSessionAgent(input.sessionID, activeAgent)
if (output.message) { if (output.message) {
output.message["agent"] = activeAgentDisplayName output.message["agent"] = activeAgentDisplayName
@@ -282,6 +312,17 @@ ${worktreeBlock}
hasExistingState: !!existingState, hasExistingState: !!existingState,
worktreePath, worktreePath,
}) })
}
return {
"chat.message": async (input: StartWorkHookInput, output: StartWorkHookOutput): Promise<void> => {
await processStartWork(input, output)
},
"command.execute.before": async (
input: StartWorkCommandExecuteBeforeInput,
output: StartWorkHookOutput,
): Promise<void> => {
await processStartWork(input, output)
}, },
} }
} }
+172
View File
@@ -0,0 +1,172 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
import { mkdirSync, rmSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { randomUUID } from "node:crypto"
import { createPluginInterface } from "./plugin-interface"
import { createAutoSlashCommandHook } from "./hooks/auto-slash-command"
import { createStartWorkHook } from "./hooks/start-work"
import { getAgentListDisplayName } from "./shared/agent-display-names"
import { readBoulderState } from "./features/boulder-state"
import {
_resetForTesting,
getSessionAgent,
registerAgentName,
updateSessionAgent,
} from "./features/claude-code-session-state"
describe("createPluginInterface - command.execute.before", () => {
let testDir = ""
beforeEach(() => {
testDir = join(tmpdir(), `plugin-interface-start-work-${randomUUID()}`)
mkdirSync(join(testDir, ".sisyphus", "plans"), { recursive: true })
writeFileSync(join(testDir, ".sisyphus", "plans", "worker-plan.md"), "# Plan\n- [ ] Task 1")
_resetForTesting()
registerAgentName("prometheus")
registerAgentName("sisyphus")
})
afterEach(() => {
_resetForTesting()
rmSync(testDir, { recursive: true, force: true })
})
test("executes start-work side effects for native command execution", async () => {
// given
updateSessionAgent("ses-command-before", "prometheus")
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: [] }),
startWork: createStartWorkHook({
directory: testDir,
client: { tui: { showToast: async () => {} } },
} as never),
} as never,
tools: {},
})
const output = {
parts: [{ type: "text", text: "original" }],
}
// when
await pluginInterface["command.execute.before"]?.(
{
command: "start-work",
sessionID: "ses-command-before",
arguments: "",
},
output as never
)
// then
expect(pluginInterface["command.execute.before"]).toBeDefined()
expect(output.parts[0]?.text).toContain("Auto-Selected Plan")
expect(output.parts[0]?.text).toContain("boulder.json has been created")
expect(getSessionAgent("ses-command-before")).toBe("sisyphus")
expect(readBoulderState(testDir)?.agent).toBe("sisyphus")
})
test("does not run start-work side effects for other native commands with session context", async () => {
// given
updateSessionAgent("ses-handoff", "prometheus")
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: [] }),
startWork: createStartWorkHook({
directory: testDir,
client: { tui: { showToast: async () => {} } },
} as never),
} as never,
tools: {},
})
const output = {
parts: [{ type: "text", text: "original" }],
}
// when
await pluginInterface["command.execute.before"]?.(
{
command: "handoff",
sessionID: "ses-handoff",
arguments: "",
},
output as never
)
// then
expect(output.parts[0]?.text).toContain("HANDOFF CONTEXT")
expect(readBoulderState(testDir)).toBeNull()
expect(getSessionAgent("ses-handoff")).toBe("prometheus")
})
test("switches native start-work to Atlas when Atlas is registered in config", async () => {
// given
registerAgentName("atlas")
updateSessionAgent("ses-command-atlas", "prometheus")
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: [] }),
startWork: createStartWorkHook({
directory: testDir,
client: { tui: { showToast: async () => {} } },
} as never),
} as never,
tools: {},
})
const output = {
message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "/start-work" }],
}
// when
await pluginInterface["chat.message"]?.(
{
sessionID: "ses-command-atlas",
agent: "prometheus",
} as never,
output as never
)
// then
expect(output.message.agent).toBe(getAgentListDisplayName("atlas"))
expect(getSessionAgent("ses-command-atlas")).toBe("atlas")
expect(readBoulderState(testDir)?.agent).toBe("atlas")
})
})
+5
View File
@@ -4,6 +4,7 @@ import type { OhMyOpenCodeConfig } from "./config"
import { createChatParamsHandler } from "./plugin/chat-params" import { createChatParamsHandler } from "./plugin/chat-params"
import { createChatHeadersHandler } from "./plugin/chat-headers" import { createChatHeadersHandler } from "./plugin/chat-headers"
import { createChatMessageHandler } from "./plugin/chat-message" import { createChatMessageHandler } from "./plugin/chat-message"
import { createCommandExecuteBeforeHandler } from "./plugin/command-execute-before"
import { createMessagesTransformHandler } from "./plugin/messages-transform" import { createMessagesTransformHandler } from "./plugin/messages-transform"
import { createSystemTransformHandler } from "./plugin/system-transform" import { createSystemTransformHandler } from "./plugin/system-transform"
import { createEventHandler } from "./plugin/event" import { createEventHandler } from "./plugin/event"
@@ -42,6 +43,10 @@ export function createPluginInterface(args: {
"chat.headers": createChatHeadersHandler({ ctx }), "chat.headers": createChatHeadersHandler({ ctx }),
"command.execute.before": createCommandExecuteBeforeHandler({
hooks,
}),
"chat.message": createChatMessageHandler({ "chat.message": createChatMessageHandler({
ctx, ctx,
pluginConfig, pluginConfig,
+58 -2
View File
@@ -1,7 +1,14 @@
import { afterEach, describe, test, expect } from "bun:test" import { afterEach, beforeEach, describe, test, expect } from "bun:test"
import { mkdirSync, rmSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { randomUUID } from "node:crypto"
import { createChatMessageHandler } from "./chat-message" import { createChatMessageHandler } from "./chat-message"
import { _resetForTesting, setMainSession, subagentSessions } from "../features/claude-code-session-state" import { createAutoSlashCommandHook } from "../hooks/auto-slash-command"
import { createStartWorkHook } from "../hooks/start-work"
import { readBoulderState } from "../features/boulder-state"
import { _resetForTesting, setMainSession, subagentSessions, registerAgentName, updateSessionAgent, getSessionAgent } from "../features/claude-code-session-state"
import { clearSessionModel, getSessionModel, setSessionModel } from "../shared/session-model-state" import { clearSessionModel, getSessionModel, setSessionModel } from "../shared/session-model-state"
type ChatMessagePart = { type: string; text?: string; [key: string]: unknown } type ChatMessagePart = { type: string; text?: string; [key: string]: unknown }
@@ -39,6 +46,55 @@ afterEach(() => {
clearSessionModel("subagent-session") clearSessionModel("subagent-session")
}) })
describe("createChatMessageHandler - /start-work integration", () => {
let testDir = ""
let originalWorkingDirectory = ""
beforeEach(() => {
testDir = join(tmpdir(), `chat-message-start-work-${randomUUID()}`)
originalWorkingDirectory = process.cwd()
mkdirSync(join(testDir, ".sisyphus", "plans"), { recursive: true })
writeFileSync(join(testDir, ".sisyphus", "plans", "worker-plan.md"), "# Plan\n- [ ] Task 1")
process.chdir(testDir)
_resetForTesting()
registerAgentName("prometheus")
registerAgentName("sisyphus")
})
afterEach(() => {
process.chdir(originalWorkingDirectory)
rmSync(testDir, { recursive: true, force: true })
})
test("falls back to Sisyphus through the full chat.message slash-command path when Atlas is unavailable", async () => {
// given
updateSessionAgent("test-session", "prometheus")
const args = createMockHandlerArgs()
args.hooks.autoSlashCommand = createAutoSlashCommandHook({ skills: [] })
args.hooks.startWork = createStartWorkHook({
directory: testDir,
client: { tui: { showToast: async () => {} } },
} as never)
const handler = createChatMessageHandler(args)
const input = createMockInput("prometheus")
const output: ChatMessageHandlerOutput = {
message: {},
parts: [{ type: "text", text: "/start-work" }],
}
// when
await handler(input, output)
// then
expect(output.message["agent"]).toBe("Sisyphus (Ultraworker)")
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")
expect(getSessionAgent("test-session")).toBe("sisyphus")
expect(readBoulderState(testDir)?.agent).toBe("sisyphus")
})
})
function createMockInput(agent?: string, model?: { providerID: string; modelID: string }) { function createMockInput(agent?: string, model?: { providerID: string; modelID: string }) {
return { return {
sessionID: "test-session", sessionID: "test-session",
+39
View File
@@ -0,0 +1,39 @@
import type { CreatedHooks } from "../create-hooks"
type CommandExecuteBeforeInput = {
command: string
sessionID: string
arguments: string
}
type CommandExecuteBeforeOutput = {
parts: Array<{ type: string; text?: string; [key: string]: unknown }>
}
function hasPartsOutput(value: unknown): value is CommandExecuteBeforeOutput {
if (typeof value !== "object" || value === null) return false
const record = value as Record<string, unknown>
const parts = record["parts"]
return Array.isArray(parts)
}
export function createCommandExecuteBeforeHandler(args: {
hooks: CreatedHooks
}): (
input: CommandExecuteBeforeInput,
output: CommandExecuteBeforeOutput,
) => Promise<void> {
const { hooks } = args
return async (input, output): Promise<void> => {
await hooks.autoSlashCommand?.["command.execute.before"]?.(input, output)
if (
hooks.startWork
&& input.command.toLowerCase() === "start-work"
&& hasPartsOutput(output)
) {
await hooks.startWork["command.execute.before"]?.(input, output)
}
}
}