From 90292db4c4fed3adb7ad5195190801f0785e75fd Mon Sep 17 00:00:00 2001 From: justsisyphus Date: Fri, 23 Jan 2026 20:49:17 +0900 Subject: [PATCH] refactor(prometheus-hook): use lowercase config key --- src/hooks/prometheus-md-only/constants.ts | 5 +- src/hooks/prometheus-md-only/index.test.ts | 340 +++++++++++---------- src/hooks/prometheus-md-only/index.ts | 29 +- 3 files changed, 193 insertions(+), 181 deletions(-) diff --git a/src/hooks/prometheus-md-only/constants.ts b/src/hooks/prometheus-md-only/constants.ts index e63c51206..e9b05482f 100644 --- a/src/hooks/prometheus-md-only/constants.ts +++ b/src/hooks/prometheus-md-only/constants.ts @@ -1,8 +1,9 @@ import { createSystemDirective, SystemDirectiveTypes } from "../../shared/system-directive" +import { getAgentDisplayName } from "../../shared/agent-display-names" export const HOOK_NAME = "prometheus-md-only" -export const PROMETHEUS_AGENTS = ["Prometheus (Planner)"] +export const PROMETHEUS_AGENTS = ["prometheus"] export const ALLOWED_EXTENSIONS = [".md"] @@ -16,7 +17,7 @@ export const PLANNING_CONSULT_WARNING = ` ${createSystemDirective(SystemDirectiveTypes.PROMETHEUS_READ_ONLY)} -You are being invoked by Prometheus (Planner), a READ-ONLY planning agent. +You are being invoked by ${getAgentDisplayName("prometheus")}, a READ-ONLY planning agent. **CRITICAL CONSTRAINTS:** - DO NOT modify any files (no Write, Edit, or any file mutations) diff --git a/src/hooks/prometheus-md-only/index.test.ts b/src/hooks/prometheus-md-only/index.test.ts index 94723046b..d6c2cbb3e 100644 --- a/src/hooks/prometheus-md-only/index.test.ts +++ b/src/hooks/prometheus-md-only/index.test.ts @@ -41,10 +41,10 @@ describe("prometheus-md-only", () => { } }) - describe("with Prometheus agent in message storage", () => { - beforeEach(() => { - setupMessageStorage(TEST_SESSION_ID, "Prometheus (Planner)") - }) + describe("with Prometheus agent in message storage", () => { + beforeEach(() => { + setupMessageStorage(TEST_SESSION_ID, "prometheus") + }) test("should block Prometheus from writing non-.md files", async () => { // #given @@ -345,185 +345,195 @@ describe("prometheus-md-only", () => { setupMessageStorage(TEST_SESSION_ID, "Prometheus (Planner)") }) - test("should allow Windows-style backslash paths under .sisyphus/", async () => { - // #given - const hook = createPrometheusMdOnlyHook(createMockPluginInput()) - const input = { - tool: "Write", - sessionID: TEST_SESSION_ID, - callID: "call-1", - } - const output = { - args: { filePath: ".sisyphus\\plans\\work-plan.md" }, - } + test("should allow Windows-style backslash paths under .sisyphus/", 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: ".sisyphus\\plans\\work-plan.md" }, + } - // #when / #then - await expect( - hook["tool.execute.before"](input, output) - ).resolves.toBeUndefined() - }) + // #when / #then + await expect( + hook["tool.execute.before"](input, output) + ).resolves.toBeUndefined() + }) - test("should allow mixed separator paths under .sisyphus/", async () => { - // #given - const hook = createPrometheusMdOnlyHook(createMockPluginInput()) - const input = { - tool: "Write", - sessionID: TEST_SESSION_ID, - callID: "call-1", - } - const output = { - args: { filePath: ".sisyphus\\plans/work-plan.MD" }, - } + test("should allow mixed separator paths under .sisyphus/", 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: ".sisyphus\\plans/work-plan.MD" }, + } - // #when / #then - await expect( - hook["tool.execute.before"](input, output) - ).resolves.toBeUndefined() - }) + // #when / #then + await expect( + hook["tool.execute.before"](input, output) + ).resolves.toBeUndefined() + }) - test("should allow uppercase .MD extension", async () => { - // #given - const hook = createPrometheusMdOnlyHook(createMockPluginInput()) - const input = { - tool: "Write", - sessionID: TEST_SESSION_ID, - callID: "call-1", - } - const output = { - args: { filePath: ".sisyphus/plans/work-plan.MD" }, - } + test("should allow uppercase .MD extension", 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: ".sisyphus/plans/work-plan.MD" }, + } - // #when / #then - await expect( - hook["tool.execute.before"](input, output) - ).resolves.toBeUndefined() - }) + // #when / #then + await expect( + hook["tool.execute.before"](input, output) + ).resolves.toBeUndefined() + }) - test("should block paths outside workspace root even if containing .sisyphus", async () => { - // #given - const hook = createPrometheusMdOnlyHook(createMockPluginInput()) - const input = { - tool: "Write", - sessionID: TEST_SESSION_ID, - callID: "call-1", - } - const output = { - args: { filePath: "/other/project/.sisyphus/plans/x.md" }, - } + test("should block paths outside workspace root even if containing .sisyphus", 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: "/other/project/.sisyphus/plans/x.md" }, + } - // #when / #then - await expect( - hook["tool.execute.before"](input, output) - ).rejects.toThrow("can only write/edit .md files inside .sisyphus/") - }) + // #when / #then + await expect( + hook["tool.execute.before"](input, output) + ).rejects.toThrow("can only write/edit .md files inside .sisyphus/") + }) - test("should allow nested .sisyphus directories (ctx.directory may be parent)", async () => { - // #given - when ctx.directory is parent of actual project, path includes project name - const hook = createPrometheusMdOnlyHook(createMockPluginInput()) - const input = { - tool: "Write", - sessionID: TEST_SESSION_ID, - callID: "call-1", - } - const output = { - args: { filePath: "src/.sisyphus/plans/x.md" }, - } + test("should allow nested .sisyphus directories (ctx.directory may be parent)", async () => { + // #given - when ctx.directory is parent of actual project, path includes project name + setupMessageStorage(TEST_SESSION_ID, "prometheus") + const hook = createPrometheusMdOnlyHook(createMockPluginInput()) + const input = { + tool: "Write", + sessionID: TEST_SESSION_ID, + callID: "call-1", + } + const output = { + args: { filePath: "src/.sisyphus/plans/x.md" }, + } - // #when / #then - should allow because .sisyphus is in path - await expect( - hook["tool.execute.before"](input, output) - ).resolves.toBeUndefined() - }) + // #when / #then - should allow because .sisyphus is in path + await expect( + hook["tool.execute.before"](input, output) + ).resolves.toBeUndefined() + }) - test("should block path traversal attempts", async () => { - // #given - const hook = createPrometheusMdOnlyHook(createMockPluginInput()) - const input = { - tool: "Write", - sessionID: TEST_SESSION_ID, - callID: "call-1", - } - const output = { - args: { filePath: ".sisyphus/../secrets.md" }, - } + test("should block path traversal attempts", 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: ".sisyphus/../secrets.md" }, + } - // #when / #then - await expect( - hook["tool.execute.before"](input, output) - ).rejects.toThrow("can only write/edit .md files inside .sisyphus/") - }) + // #when / #then + await expect( + hook["tool.execute.before"](input, output) + ).rejects.toThrow("can only write/edit .md files inside .sisyphus/") + }) - test("should allow case-insensitive .SISYPHUS directory", async () => { - // #given - const hook = createPrometheusMdOnlyHook(createMockPluginInput()) - const input = { - tool: "Write", - sessionID: TEST_SESSION_ID, - callID: "call-1", - } - const output = { - args: { filePath: ".SISYPHUS/plans/work-plan.md" }, - } + test("should allow case-insensitive .SISYPHUS directory", 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: ".SISYPHUS/plans/work-plan.md" }, + } - // #when / #then - await expect( - hook["tool.execute.before"](input, output) - ).resolves.toBeUndefined() - }) + // #when / #then + await expect( + hook["tool.execute.before"](input, output) + ).resolves.toBeUndefined() + }) - test("should allow nested project path with .sisyphus (Windows real-world case)", async () => { - // #given - simulates when ctx.directory is parent of actual project - // User reported: xauusd-dxy-plan\.sisyphus\drafts\supabase-email-templates.md - const hook = createPrometheusMdOnlyHook(createMockPluginInput()) - const input = { - tool: "Write", - sessionID: TEST_SESSION_ID, - callID: "call-1", - } - const output = { - args: { filePath: "xauusd-dxy-plan\\.sisyphus\\drafts\\supabase-email-templates.md" }, - } + test("should allow nested project path with .sisyphus (Windows real-world case)", async () => { + // #given - simulates when ctx.directory is parent of actual project + // User reported: xauusd-dxy-plan\.sisyphus\drafts\supabase-email-templates.md + setupMessageStorage(TEST_SESSION_ID, "prometheus") + const hook = createPrometheusMdOnlyHook(createMockPluginInput()) + const input = { + tool: "Write", + sessionID: TEST_SESSION_ID, + callID: "call-1", + } + const output = { + args: { filePath: "xauusd-dxy-plan\\.sisyphus\\drafts\\supabase-email-templates.md" }, + } - // #when / #then - await expect( - hook["tool.execute.before"](input, output) - ).resolves.toBeUndefined() - }) + // #when / #then + await expect( + hook["tool.execute.before"](input, output) + ).resolves.toBeUndefined() + }) - test("should allow nested project path with mixed separators", async () => { - // #given - const hook = createPrometheusMdOnlyHook(createMockPluginInput()) - const input = { - tool: "Write", - sessionID: TEST_SESSION_ID, - callID: "call-1", - } - const output = { - args: { filePath: "my-project/.sisyphus\\plans/task.md" }, - } + test("should allow nested project path with mixed separators", 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: "my-project/.sisyphus\\plans/task.md" }, + } - // #when / #then - await expect( - hook["tool.execute.before"](input, output) - ).resolves.toBeUndefined() - }) + // #when / #then + await expect( + hook["tool.execute.before"](input, output) + ).resolves.toBeUndefined() + }) - test("should block nested project path without .sisyphus", async () => { - // #given - const hook = createPrometheusMdOnlyHook(createMockPluginInput()) - const input = { - tool: "Write", - sessionID: TEST_SESSION_ID, - callID: "call-1", - } - const output = { - args: { filePath: "my-project\\src\\code.ts" }, - } + test("should block nested project path without .sisyphus", 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: "my-project\\src\\code.ts" }, + } - // #when / #then - await expect( - hook["tool.execute.before"](input, output) - ).rejects.toThrow("can only write/edit .md files") - }) + // #when / #then + await expect( + hook["tool.execute.before"](input, output) + ).rejects.toThrow("can only write/edit .md files") + }) }) }) diff --git a/src/hooks/prometheus-md-only/index.ts b/src/hooks/prometheus-md-only/index.ts index f91e20c20..645c0541f 100644 --- a/src/hooks/prometheus-md-only/index.ts +++ b/src/hooks/prometheus-md-only/index.ts @@ -6,6 +6,7 @@ import { findNearestMessageWithFields, findFirstMessageWithAgent, MESSAGE_STORAG import { getSessionAgent } from "../../features/claude-code-session-state" import { log } from "../../shared/logger" import { SYSTEM_DIRECTIVE_PREFIX } from "../../shared/system-directive" +import { getAgentDisplayName } from "../../shared/agent-display-names" export * from "./constants" @@ -110,20 +111,20 @@ export function createPrometheusMdOnlyHook(ctx: PluginInput) { return } - if (!isAllowedFile(filePath, ctx.directory)) { - log(`[${HOOK_NAME}] Blocked: Prometheus can only write to .sisyphus/*.md`, { - sessionID: input.sessionID, - tool: toolName, - filePath, - agent: agentName, - }) - throw new Error( - `[${HOOK_NAME}] Prometheus (Planner) can only write/edit .md files inside .sisyphus/ directory. ` + - `Attempted to modify: ${filePath}. ` + - `Prometheus is a READ-ONLY planner. Use /start-work to execute the plan. ` + - `APOLOGIZE TO THE USER, REMIND OF YOUR PLAN WRITING PROCESSES, TELL USER WHAT YOU WILL GOING TO DO AS THE PROCESS, WRITE THE PLAN` - ) - } + if (!isAllowedFile(filePath, ctx.directory)) { + log(`[${HOOK_NAME}] Blocked: Prometheus can only write to .sisyphus/*.md`, { + sessionID: input.sessionID, + tool: toolName, + filePath, + agent: agentName, + }) + throw new Error( + `[${HOOK_NAME}] ${getAgentDisplayName("prometheus")} can only write/edit .md files inside .sisyphus/ directory. ` + + `Attempted to modify: ${filePath}. ` + + `${getAgentDisplayName("prometheus")} is a READ-ONLY planner. Use /start-work to execute the plan. ` + + `APOLOGIZE TO THE USER, REMIND OF YOUR PLAN WRITING PROCESSES, TELL USER WHAT YOU WILL GOING TO DO AS THE PROCESS, WRITE THE PLAN` + ) + } const normalizedPath = filePath.toLowerCase().replace(/\\/g, "/") if (normalizedPath.includes(".sisyphus/plans/") || normalizedPath.includes(".sisyphus\\plans\\")) {