feat(workspace): point planning guardrails at omo

This commit is contained in:
YeonGyu-Kim
2026-05-16 17:41:35 +09:00
parent a86221b1a9
commit 36e373cdbb
48 changed files with 244 additions and 244 deletions
+4 -4
View File
@@ -7,7 +7,7 @@ export const PROMETHEUS_AGENT = "prometheus"
export const ALLOWED_EXTENSIONS = [".md"]
export const ALLOWED_PATH_PREFIX = ".sisyphus"
export const ALLOWED_PATH_PREFIX = ".omo"
export const BLOCKED_TOOLS = ["Write", "Edit", "write", "edit"]
@@ -17,7 +17,7 @@ export const PLANNING_CONSULT_WARNING = `
${createSystemDirective(SystemDirectiveTypes.PROMETHEUS_READ_ONLY)}
You are being invoked by ${getAgentDisplayName("prometheus")}, a planning agent restricted to .sisyphus/*.md plan files only.
You are being invoked by ${getAgentDisplayName("prometheus")}, a planning agent restricted to .omo/*.md plan files only.
**CRITICAL CONSTRAINTS:**
- DO NOT modify any files (no Write, Edit, or any file mutations)
@@ -48,13 +48,13 @@ ${createSystemDirective(SystemDirectiveTypes.PROMETHEUS_READ_ONLY)}
│ 1 │ INTERVIEW: Full consultation with user │
│ │ - Gather ALL requirements │
│ │ - Clarify ambiguities │
│ │ - Record decisions to .sisyphus/drafts/ │
│ │ - Record decisions to .omo/drafts/ │
├──────┼──────────────────────────────────────────────────────────────┤
│ 2 │ METIS CONSULTATION: Pre-generation gap analysis │
│ │ - task(agent="Metis - Plan Consultant", ...) │
│ │ - Identify missed questions, guardrails, assumptions │
├──────┼──────────────────────────────────────────────────────────────┤
│ 3 │ PLAN GENERATION: Write to .sisyphus/plans/*.md │
│ 3 │ PLAN GENERATION: Write to .omo/plans/*.md │
│ │ <- YOU ARE HERE │
├──────┼──────────────────────────────────────────────────────────────┤
│ 4 │ MOMUS REVIEW (if high accuracy requested) │
+4 -4
View File
@@ -47,21 +47,21 @@ export function createPrometheusMdOnlyHook(ctx: PluginInput) {
}
if (!isAllowedFile(filePath, ctx.directory)) {
log(`[${HOOK_NAME}] Blocked: Prometheus can only write to .sisyphus/*.md`, {
log(`[${HOOK_NAME}] Blocked: Prometheus can only write to .omo/*.md`, {
sessionID: input.sessionID,
tool: toolName,
filePath,
agent: agentName,
})
throw new Error(
`[${HOOK_NAME}] Prometheus is a planning agent. File operations restricted to .sisyphus/*.md plan files only. Use task() to delegate implementation. ` +
`[${HOOK_NAME}] Prometheus is a planning agent. File operations restricted to .omo/*.md plan files only. Use task() to delegate implementation. ` +
`Attempted to modify: ${filePath}. ` +
`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\\")) {
if (normalizedPath.includes(".omo/plans/") || normalizedPath.includes(".omo\\plans\\")) {
log(`[${HOOK_NAME}] Injecting workflow reminder for plan write`, {
sessionID: input.sessionID,
tool: toolName,
@@ -71,7 +71,7 @@ export function createPrometheusMdOnlyHook(ctx: PluginInput) {
output.message = (output.message || "") + PROMETHEUS_WORKFLOW_REMINDER
}
log(`[${HOOK_NAME}] Allowed: .sisyphus/*.md write permitted`, {
log(`[${HOOK_NAME}] Allowed: .omo/*.md write permitted`, {
sessionID: input.sessionID,
tool: toolName,
filePath,
+39 -39
View File
@@ -89,7 +89,7 @@ describe("prometheus-md-only", () => {
//#when //#then
await expect(
hook["tool.execute.before"](input, output)
).rejects.toThrow("File operations restricted to .sisyphus/*.md plan files only")
).rejects.toThrow("File operations restricted to .omo/*.md plan files only")
})
test("should enforce md-only restriction for Prometheus display name Plan Builder", async () => {
@@ -108,7 +108,7 @@ describe("prometheus-md-only", () => {
//#when //#then
await expect(
hook["tool.execute.before"](input, output)
).rejects.toThrow("File operations restricted to .sisyphus/*.md plan files only")
).rejects.toThrow("File operations restricted to .omo/*.md plan files only")
})
test("should enforce md-only restriction for Prometheus display name Planner", async () => {
@@ -127,7 +127,7 @@ describe("prometheus-md-only", () => {
//#when //#then
await expect(
hook["tool.execute.before"](input, output)
).rejects.toThrow("File operations restricted to .sisyphus/*.md plan files only")
).rejects.toThrow("File operations restricted to .omo/*.md plan files only")
})
test("should enforce md-only restriction for uppercase PROMETHEUS", async () => {
@@ -146,7 +146,7 @@ describe("prometheus-md-only", () => {
//#when //#then
await expect(
hook["tool.execute.before"](input, output)
).rejects.toThrow("File operations restricted to .sisyphus/*.md plan files only")
).rejects.toThrow("File operations restricted to .omo/*.md plan files only")
})
test("should not enforce restriction for non-Prometheus agent", async () => {
@@ -208,10 +208,10 @@ describe("prometheus-md-only", () => {
// when / #then
await expect(
hook["tool.execute.before"](input, output)
).rejects.toThrow("File operations restricted to .sisyphus/*.md plan files only")
).rejects.toThrow("File operations restricted to .omo/*.md plan files only")
})
test("should allow Prometheus to write .md files inside .sisyphus/", async () => {
test("should allow Prometheus to write .md files inside .omo/", async () => {
// given
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
const input = {
@@ -220,7 +220,7 @@ describe("prometheus-md-only", () => {
callID: "call-1",
}
const output = {
args: { filePath: "/tmp/test/.sisyphus/plans/work-plan.md" },
args: { filePath: "/tmp/test/.omo/plans/work-plan.md" },
}
// when / #then
@@ -229,7 +229,7 @@ describe("prometheus-md-only", () => {
).resolves.toBeUndefined()
})
test("should inject workflow reminder when Prometheus writes to .sisyphus/plans/", async () => {
test("should inject workflow reminder when Prometheus writes to .omo/plans/", async () => {
// given
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
const input = {
@@ -238,7 +238,7 @@ describe("prometheus-md-only", () => {
callID: "call-1",
}
const output: { args: Record<string, unknown>; message?: string } = {
args: { filePath: "/tmp/test/.sisyphus/plans/work-plan.md" },
args: { filePath: "/tmp/test/.omo/plans/work-plan.md" },
}
// when
@@ -251,7 +251,7 @@ describe("prometheus-md-only", () => {
expect(output.message).toContain("MOMUS REVIEW")
})
test("should NOT inject workflow reminder for .sisyphus/drafts/", async () => {
test("should NOT inject workflow reminder for .omo/drafts/", async () => {
// given
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
const input = {
@@ -260,7 +260,7 @@ describe("prometheus-md-only", () => {
callID: "call-1",
}
const output: { args: Record<string, unknown>; message?: string } = {
args: { filePath: "/tmp/test/.sisyphus/drafts/notes.md" },
args: { filePath: "/tmp/test/.omo/drafts/notes.md" },
}
// when
@@ -270,7 +270,7 @@ describe("prometheus-md-only", () => {
expect(output.message).toBeUndefined()
})
test("should block Prometheus from writing .md files outside .sisyphus/", async () => {
test("should block Prometheus from writing .md files outside .omo/", async () => {
// given
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
const input = {
@@ -285,7 +285,7 @@ describe("prometheus-md-only", () => {
// when / #then
await expect(
hook["tool.execute.before"](input, output)
).rejects.toThrow("File operations restricted to .sisyphus/*.md plan files only")
).rejects.toThrow("File operations restricted to .omo/*.md plan files only")
})
test("should block Edit tool for non-.md files", async () => {
@@ -303,7 +303,7 @@ describe("prometheus-md-only", () => {
// when / #then
await expect(
hook["tool.execute.before"](input, output)
).rejects.toThrow("File operations restricted to .sisyphus/*.md plan files only")
).rejects.toThrow("File operations restricted to .omo/*.md plan files only")
})
test("should allow bash commands from Prometheus", async () => {
@@ -487,10 +487,10 @@ describe("prometheus-md-only", () => {
describe("boulder state priority over message files (fixes #927)", () => {
const BOULDER_DIR = join(tmpdir(), `boulder-test-${randomUUID()}`)
const BOULDER_FILE = join(BOULDER_DIR, ".sisyphus", "boulder.json")
const BOULDER_FILE = join(BOULDER_DIR, ".omo", "boulder.json")
beforeEach(() => {
mkdirSync(join(BOULDER_DIR, ".sisyphus"), { recursive: true })
mkdirSync(join(BOULDER_DIR, ".omo"), { recursive: true })
})
afterEach(() => {
@@ -562,7 +562,7 @@ describe("prometheus-md-only", () => {
// when / then - should block because boulder says prometheus
await expect(
hook["tool.execute.before"](input, output)
).rejects.toThrow("File operations restricted to .sisyphus/*.md plan files only")
).rejects.toThrow("File operations restricted to .omo/*.md plan files only")
})
test("should fall back to message files when session not in boulder", async () => {
@@ -595,7 +595,7 @@ describe("prometheus-md-only", () => {
// when / then - should block because falls back to message files (prometheus)
await expect(
hook["tool.execute.before"](input, output)
).rejects.toThrow("File operations restricted to .sisyphus/*.md plan files only")
).rejects.toThrow("File operations restricted to .omo/*.md plan files only")
})
})
@@ -624,7 +624,7 @@ describe("prometheus-md-only", () => {
setupMessageStorage(TEST_SESSION_ID, "prometheus")
})
test("should allow Windows-style backslash paths under .sisyphus/", async () => {
test("should allow Windows-style backslash paths under .omo/", async () => {
// given
setupMessageStorage(TEST_SESSION_ID, "prometheus")
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
@@ -634,7 +634,7 @@ describe("prometheus-md-only", () => {
callID: "call-1",
}
const output = {
args: { filePath: ".sisyphus\\plans\\work-plan.md" },
args: { filePath: ".omo\\plans\\work-plan.md" },
}
// when / #then
@@ -643,7 +643,7 @@ describe("prometheus-md-only", () => {
).resolves.toBeUndefined()
})
test("should allow mixed separator paths under .sisyphus/", async () => {
test("should allow mixed separator paths under .omo/", async () => {
// given
setupMessageStorage(TEST_SESSION_ID, "prometheus")
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
@@ -653,7 +653,7 @@ describe("prometheus-md-only", () => {
callID: "call-1",
}
const output = {
args: { filePath: ".sisyphus\\plans/work-plan.MD" },
args: { filePath: ".omo\\plans/work-plan.MD" },
}
// when / #then
@@ -672,7 +672,7 @@ describe("prometheus-md-only", () => {
callID: "call-1",
}
const output = {
args: { filePath: ".sisyphus/plans/work-plan.MD" },
args: { filePath: ".omo/plans/work-plan.MD" },
}
// when / #then
@@ -681,7 +681,7 @@ describe("prometheus-md-only", () => {
).resolves.toBeUndefined()
})
test("should block paths outside workspace root even if containing .sisyphus", async () => {
test("should block paths outside workspace root even if containing .omo", async () => {
// given
setupMessageStorage(TEST_SESSION_ID, "prometheus")
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
@@ -691,16 +691,16 @@ describe("prometheus-md-only", () => {
callID: "call-1",
}
const output = {
args: { filePath: "/other/project/.sisyphus/plans/x.md" },
args: { filePath: "/other/project/.omo/plans/x.md" },
}
// when / #then
await expect(
hook["tool.execute.before"](input, output)
).rejects.toThrow("File operations restricted to .sisyphus/*.md plan files only")
).rejects.toThrow("File operations restricted to .omo/*.md plan files only")
})
test("should allow nested .sisyphus directories (ctx.directory may be parent)", async () => {
test("should allow nested .omo 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())
@@ -710,10 +710,10 @@ describe("prometheus-md-only", () => {
callID: "call-1",
}
const output = {
args: { filePath: "src/.sisyphus/plans/x.md" },
args: { filePath: "src/.omo/plans/x.md" },
}
// when / #then - should allow because .sisyphus is in path
// when / #then - should allow because .omo is in path
await expect(
hook["tool.execute.before"](input, output)
).resolves.toBeUndefined()
@@ -729,16 +729,16 @@ describe("prometheus-md-only", () => {
callID: "call-1",
}
const output = {
args: { filePath: ".sisyphus/../secrets.md" },
args: { filePath: ".omo/../secrets.md" },
}
// when / #then
await expect(
hook["tool.execute.before"](input, output)
).rejects.toThrow("File operations restricted to .sisyphus/*.md plan files only")
).rejects.toThrow("File operations restricted to .omo/*.md plan files only")
})
test("should allow case-insensitive .SISYPHUS directory", async () => {
test("should allow case-insensitive .OMO directory", async () => {
// given
setupMessageStorage(TEST_SESSION_ID, "prometheus")
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
@@ -748,7 +748,7 @@ describe("prometheus-md-only", () => {
callID: "call-1",
}
const output = {
args: { filePath: ".SISYPHUS/plans/work-plan.md" },
args: { filePath: ".OMO/plans/work-plan.md" },
}
// when / #then
@@ -757,9 +757,9 @@ describe("prometheus-md-only", () => {
).resolves.toBeUndefined()
})
test("should allow nested project path with .sisyphus (Windows real-world case)", async () => {
test("should allow nested project path with .omo (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
// User reported: xauusd-dxy-plan\.omo\drafts\supabase-email-templates.md
setupMessageStorage(TEST_SESSION_ID, "prometheus")
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
const input = {
@@ -768,7 +768,7 @@ describe("prometheus-md-only", () => {
callID: "call-1",
}
const output = {
args: { filePath: "xauusd-dxy-plan\\.sisyphus\\drafts\\supabase-email-templates.md" },
args: { filePath: "xauusd-dxy-plan\\.omo\\drafts\\supabase-email-templates.md" },
}
// when / #then
@@ -787,7 +787,7 @@ describe("prometheus-md-only", () => {
callID: "call-1",
}
const output = {
args: { filePath: "my-project/.sisyphus\\plans/task.md" },
args: { filePath: "my-project/.omo\\plans/task.md" },
}
// when / #then
@@ -796,7 +796,7 @@ describe("prometheus-md-only", () => {
).resolves.toBeUndefined()
})
test("should block nested project path without .sisyphus", async () => {
test("should block nested project path without .omo", async () => {
// given
setupMessageStorage(TEST_SESSION_ID, "prometheus")
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
@@ -812,7 +812,7 @@ describe("prometheus-md-only", () => {
// when / #then
await expect(
hook["tool.execute.before"](input, output)
).rejects.toThrow("File operations restricted to .sisyphus/*.md plan files only")
).rejects.toThrow("File operations restricted to .omo/*.md plan files only")
})
})
})
+6 -6
View File
@@ -5,11 +5,11 @@ import { ALLOWED_EXTENSIONS } from "./constants"
/**
* Cross-platform path validator for Prometheus file writes.
* Uses path.resolve/relative instead of string matching to handle:
* - Windows backslashes (e.g., .sisyphus\\plans\\x.md)
* - Mixed separators (e.g., .sisyphus\\plans/x.md)
* - Windows backslashes (e.g., .omo\\plans\\x.md)
* - Mixed separators (e.g., .omo\\plans/x.md)
* - Case-insensitive directory/extension matching
* - Workspace confinement (blocks paths outside root or via traversal)
* - Nested project paths (e.g., parent/.sisyphus/... when ctx.directory is parent)
* - Nested project paths (e.g., parent/.omo/... when ctx.directory is parent)
*/
export function isAllowedFile(filePath: string, workspaceRoot: string): boolean {
// 1. Resolve to absolute path
@@ -23,9 +23,9 @@ export function isAllowedFile(filePath: string, workspaceRoot: string): boolean
return false
}
// 4. Check if .sisyphus/ or .sisyphus\ exists anywhere in the path (case-insensitive)
// This handles both direct paths (.sisyphus/x.md) and nested paths (project/.sisyphus/x.md)
if (!/\.sisyphus[/\\]/i.test(rel)) {
// 4. Check if .omo/ or .omo\ exists anywhere in the path (case-insensitive)
// This handles both direct paths (.omo/x.md) and nested paths (project/.omo/x.md)
if (!/\.omo[/\\]/i.test(rel)) {
return false
}