Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/hooks/auto-slash-command/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ export async function executeSlashCommand(parsed: ParsedSlashCommand, options?:
if (!command) {
return {
success: false,
error: `Command "/${parsed.command}" not found. Use the slashcommand tool to list available commands.`,
error: parsed.command.includes(":") ? `Marketplace plugin commands like "/${parsed.command}" are not supported. Use .claude/commands/ for custom commands.` : `Command "/${parsed.command}" not found. Use the slashcommand tool to list available commands.`,
}
}

Expand Down
5 changes: 5 additions & 0 deletions src/hooks/prometheus-md-only/agent-matcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { PROMETHEUS_AGENT } from "./constants"

export function isPrometheusAgent(agentName: string | undefined): boolean {
return agentName?.toLowerCase().includes(PROMETHEUS_AGENT) ?? false
}
5 changes: 3 additions & 2 deletions src/hooks/prometheus-md-only/hook.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { PluginInput } from "@opencode-ai/plugin"
import { HOOK_NAME, PROMETHEUS_AGENT, BLOCKED_TOOLS, PLANNING_CONSULT_WARNING, PROMETHEUS_WORKFLOW_REMINDER } from "./constants"
import { HOOK_NAME, BLOCKED_TOOLS, PLANNING_CONSULT_WARNING, PROMETHEUS_WORKFLOW_REMINDER } from "./constants"
import { log } from "../../shared/logger"
import { SYSTEM_DIRECTIVE_PREFIX } from "../../shared/system-directive"
import { getAgentDisplayName } from "../../shared/agent-display-names"
import { getAgentFromSession } from "./agent-resolution"
import { isPrometheusAgent } from "./agent-matcher"
import { isAllowedFile } from "./path-policy"

const TASK_TOOLS = ["task", "call_omo_agent"]
Expand All @@ -16,7 +17,7 @@ export function createPrometheusMdOnlyHook(ctx: PluginInput) {
): Promise<void> => {
const agentName = getAgentFromSession(input.sessionID, ctx.directory)

if (agentName !== PROMETHEUS_AGENT) {
if (!isPrometheusAgent(agentName)) {
return
}

Expand Down
120 changes: 118 additions & 2 deletions src/hooks/prometheus-md-only/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ describe("prometheus-md-only", () => {
} as never
}

function setupMessageStorage(sessionID: string, agent: string): void {
function setupMessageStorage(sessionID: string, agent: string | undefined): void {
testMessageDir = join(MESSAGE_STORAGE, sessionID)
mkdirSync(testMessageDir, { recursive: true })
const messageContent = {
agent,
...(agent ? { agent } : {}),
model: { providerID: "test", modelID: "test-model" },
}
writeFileSync(
Expand All @@ -55,6 +55,122 @@ describe("prometheus-md-only", () => {
rmSync(TEST_STORAGE_ROOT, { recursive: true, force: true })
})

describe("agent name matching", () => {
test("should enforce md-only restriction for exact prometheus agent name", async () => {
//#given
setupMessageStorage(TEST_SESSION_ID, "prometheus")
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
const input = {
tool: "Write",
sessionID: TEST_SESSION_ID,
callID: "call-1",
}
const output = {
args: { filePath: "/path/to/file.ts" },
}

//#when //#then
await expect(
hook["tool.execute.before"](input, output)
).rejects.toThrow("can only write/edit .md files")
})

test("should enforce md-only restriction for Prometheus display name Plan Builder", async () => {
//#given
setupMessageStorage(TEST_SESSION_ID, "Prometheus (Plan Builder)")
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
const input = {
tool: "Write",
sessionID: TEST_SESSION_ID,
callID: "call-1",
}
const output = {
args: { filePath: "/path/to/file.ts" },
}

//#when //#then
await expect(
hook["tool.execute.before"](input, output)
).rejects.toThrow("can only write/edit .md files")
})

test("should enforce md-only restriction for Prometheus display name Planner", async () => {
//#given
setupMessageStorage(TEST_SESSION_ID, "Prometheus (Planner)")
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
const input = {
tool: "Write",
sessionID: TEST_SESSION_ID,
callID: "call-1",
}
const output = {
args: { filePath: "/path/to/file.ts" },
}

//#when //#then
await expect(
hook["tool.execute.before"](input, output)
).rejects.toThrow("can only write/edit .md files")
})

test("should enforce md-only restriction for uppercase PROMETHEUS", async () => {
//#given
setupMessageStorage(TEST_SESSION_ID, "PROMETHEUS")
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
const input = {
tool: "Write",
sessionID: TEST_SESSION_ID,
callID: "call-1",
}
const output = {
args: { filePath: "/path/to/file.ts" },
}

//#when //#then
await expect(
hook["tool.execute.before"](input, output)
).rejects.toThrow("can only write/edit .md files")
})

test("should not enforce restriction for non-Prometheus agent", async () => {
//#given
setupMessageStorage(TEST_SESSION_ID, "sisyphus")
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
const input = {
tool: "Write",
sessionID: TEST_SESSION_ID,
callID: "call-1",
}
const output = {
args: { filePath: "/path/to/file.ts" },
}

//#when //#then
await expect(
hook["tool.execute.before"](input, output)
).resolves.toBeUndefined()
})

test("should not enforce restriction when agent name is undefined", async () => {
//#given
setupMessageStorage(TEST_SESSION_ID, undefined)
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
const input = {
tool: "Write",
sessionID: TEST_SESSION_ID,
callID: "call-1",
}
const output = {
args: { filePath: "/path/to/file.ts" },
}

//#when //#then
await expect(
hook["tool.execute.before"](input, output)
).resolves.toBeUndefined()
})
})

describe("with Prometheus agent in message storage", () => {
beforeEach(() => {
setupMessageStorage(TEST_SESSION_ID, "prometheus")
Expand Down
4 changes: 3 additions & 1 deletion src/tools/slashcommand/slashcommand-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ export function createSlashcommandTool(options: SlashcommandToolOptions = {}): T
return `No exact match for "/${commandName}". Did you mean: ${matchList}?\n\n${formatCommandList(allItems)}`
}

return `Command or skill "/${commandName}" not found.\n\n${formatCommandList(allItems)}\n\nTry a different name.`
return commandName.includes(":")
? `Marketplace plugin commands like "/${commandName}" are not supported. Use .claude/commands/ for custom commands.\n\n${formatCommandList(allItems)}`
: `Command or skill "/${commandName}" not found.\n\n${formatCommandList(allItems)}\n\nTry a different name.`
},
})
}
Expand Down