feat(workspace): point planning guardrails at omo
This commit is contained in:
@@ -113,7 +113,7 @@ describe("Atlas final-wave approval gate regressions", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
testDirectory = join(tmpdir(), `atlas-final-wave-regression-${randomUUID()}`)
|
||||
mkdirSync(join(testDirectory, ".sisyphus"), { recursive: true })
|
||||
mkdirSync(join(testDirectory, ".omo"), { recursive: true })
|
||||
clearBoulderState(testDirectory)
|
||||
})
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ describe("Atlas final verification approval gate", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
testDirectory = join(tmpdir(), `atlas-final-wave-test-${randomUUID()}`)
|
||||
mkdirSync(join(testDirectory, ".sisyphus"), { recursive: true })
|
||||
mkdirSync(join(testDirectory, ".omo"), { recursive: true })
|
||||
clearBoulderState(testDirectory)
|
||||
})
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ type MockAtlasInput = Parameters<typeof createAtlasHook>[0] & {
|
||||
|
||||
describe("atlas hook", () => {
|
||||
let TEST_DIR: string
|
||||
let SISYPHUS_DIR: string
|
||||
let OMO_DIR: string
|
||||
|
||||
function createMockPluginInput(overrides?: {
|
||||
promptMock?: ReturnType<typeof mock>
|
||||
@@ -81,12 +81,12 @@ describe("atlas hook", () => {
|
||||
registerAgentName("atlas")
|
||||
registerAgentName("sisyphus")
|
||||
TEST_DIR = join(tmpdir(), `atlas-test-${randomUUID()}`)
|
||||
SISYPHUS_DIR = join(TEST_DIR, ".sisyphus")
|
||||
OMO_DIR = join(TEST_DIR, ".omo")
|
||||
if (!existsSync(TEST_DIR)) {
|
||||
mkdirSync(TEST_DIR, { recursive: true })
|
||||
}
|
||||
if (!existsSync(SISYPHUS_DIR)) {
|
||||
mkdirSync(SISYPHUS_DIR, { recursive: true })
|
||||
if (!existsSync(OMO_DIR)) {
|
||||
mkdirSync(OMO_DIR, { recursive: true })
|
||||
}
|
||||
clearBoulderState(TEST_DIR)
|
||||
callerAgentBySession.clear()
|
||||
@@ -1082,7 +1082,7 @@ session_id: ses_untrusted_999
|
||||
cleanupMessageStorage(ORCHESTRATOR_SESSION)
|
||||
})
|
||||
|
||||
test("should append delegation reminder when orchestrator writes outside .sisyphus/", async () => {
|
||||
test("should append delegation reminder when orchestrator writes outside .omo/", async () => {
|
||||
// given
|
||||
const hook = createTestAtlasHook(createMockPluginInput())
|
||||
const output = {
|
||||
@@ -1103,7 +1103,7 @@ session_id: ses_untrusted_999
|
||||
expect(output.output).toContain("task")
|
||||
})
|
||||
|
||||
test("should append delegation reminder when orchestrator edits outside .sisyphus/", async () => {
|
||||
test("should append delegation reminder when orchestrator edits outside .omo/", async () => {
|
||||
// given
|
||||
const hook = createTestAtlasHook(createMockPluginInput())
|
||||
const output = {
|
||||
@@ -1122,14 +1122,14 @@ session_id: ses_untrusted_999
|
||||
expect(output.output).toContain("DELEGATION REQUIRED")
|
||||
})
|
||||
|
||||
test("should NOT append reminder when orchestrator writes inside .sisyphus/", async () => {
|
||||
test("should NOT append reminder when orchestrator writes inside .omo/", async () => {
|
||||
// given
|
||||
const hook = createTestAtlasHook(createMockPluginInput())
|
||||
const originalOutput = "File written successfully"
|
||||
const output = {
|
||||
title: "Write",
|
||||
output: originalOutput,
|
||||
metadata: { filePath: "/project/.sisyphus/plans/work-plan.md" },
|
||||
metadata: { filePath: "/project/.omo/plans/work-plan.md" },
|
||||
}
|
||||
|
||||
// when
|
||||
@@ -1143,7 +1143,7 @@ session_id: ses_untrusted_999
|
||||
expect(output.output).not.toContain("DELEGATION REQUIRED")
|
||||
})
|
||||
|
||||
test("should NOT append reminder when non-orchestrator writes outside .sisyphus/", async () => {
|
||||
test("should NOT append reminder when non-orchestrator writes outside .omo/", async () => {
|
||||
// given
|
||||
const nonOrchestratorSession = "non-orchestrator-session"
|
||||
setupMessageStorage(nonOrchestratorSession, "sisyphus-junior")
|
||||
@@ -1210,14 +1210,14 @@ session_id: ses_untrusted_999
|
||||
})
|
||||
|
||||
describe("cross-platform path validation (Windows support)", () => {
|
||||
test("should NOT append reminder when orchestrator writes inside .sisyphus\\ (Windows backslash)", async () => {
|
||||
test("should NOT append reminder when orchestrator writes inside .omo\\ (Windows backslash)", async () => {
|
||||
// given
|
||||
const hook = createTestAtlasHook(createMockPluginInput())
|
||||
const originalOutput = "File written successfully"
|
||||
const output = {
|
||||
title: "Write",
|
||||
output: originalOutput,
|
||||
metadata: { filePath: ".sisyphus\\plans\\work-plan.md" },
|
||||
metadata: { filePath: ".omo\\plans\\work-plan.md" },
|
||||
}
|
||||
|
||||
// when
|
||||
@@ -1231,14 +1231,14 @@ session_id: ses_untrusted_999
|
||||
expect(output.output).not.toContain("DELEGATION REQUIRED")
|
||||
})
|
||||
|
||||
test("should NOT append reminder when orchestrator writes inside .sisyphus with mixed separators", async () => {
|
||||
test("should NOT append reminder when orchestrator writes inside .omo with mixed separators", async () => {
|
||||
// given
|
||||
const hook = createTestAtlasHook(createMockPluginInput())
|
||||
const originalOutput = "File written successfully"
|
||||
const output = {
|
||||
title: "Write",
|
||||
output: originalOutput,
|
||||
metadata: { filePath: ".sisyphus\\plans/work-plan.md" },
|
||||
metadata: { filePath: ".omo\\plans/work-plan.md" },
|
||||
}
|
||||
|
||||
// when
|
||||
@@ -1252,14 +1252,14 @@ session_id: ses_untrusted_999
|
||||
expect(output.output).not.toContain("DELEGATION REQUIRED")
|
||||
})
|
||||
|
||||
test("should NOT append reminder for absolute Windows path inside .sisyphus\\", async () => {
|
||||
test("should NOT append reminder for absolute Windows path inside .omo\\", async () => {
|
||||
// given
|
||||
const hook = createTestAtlasHook(createMockPluginInput())
|
||||
const originalOutput = "File written successfully"
|
||||
const output = {
|
||||
title: "Write",
|
||||
output: originalOutput,
|
||||
metadata: { filePath: "C:\\Users\\test\\project\\.sisyphus\\plans\\x.md" },
|
||||
metadata: { filePath: "C:\\Users\\test\\project\\.omo\\plans\\x.md" },
|
||||
}
|
||||
|
||||
// when
|
||||
@@ -1273,7 +1273,7 @@ session_id: ses_untrusted_999
|
||||
expect(output.output).not.toContain("DELEGATION REQUIRED")
|
||||
})
|
||||
|
||||
test("should append reminder for Windows path outside .sisyphus\\", async () => {
|
||||
test("should append reminder for Windows path outside .omo\\", async () => {
|
||||
// given
|
||||
const hook = createTestAtlasHook(createMockPluginInput())
|
||||
const output = {
|
||||
@@ -1553,11 +1553,11 @@ session_id: ses_untrusted_999
|
||||
|
||||
test("should inject completion nudge when mirrored worktree plan is complete even if the main repo plan is stale", async () => {
|
||||
// given
|
||||
const mainPlanPath = join(TEST_DIR, ".sisyphus", "plans", "worktree-complete-plan.md")
|
||||
const mainPlanPath = join(TEST_DIR, ".omo", "plans", "worktree-complete-plan.md")
|
||||
const worktreeDir = join(tmpdir(), `atlas-worktree-${randomUUID()}`)
|
||||
const worktreePlanPath = join(worktreeDir, ".sisyphus", "plans", "worktree-complete-plan.md")
|
||||
mkdirSync(join(TEST_DIR, ".sisyphus", "plans"), { recursive: true })
|
||||
mkdirSync(join(worktreeDir, ".sisyphus", "plans"), { recursive: true })
|
||||
const worktreePlanPath = join(worktreeDir, ".omo", "plans", "worktree-complete-plan.md")
|
||||
mkdirSync(join(TEST_DIR, ".omo", "plans"), { recursive: true })
|
||||
mkdirSync(join(worktreeDir, ".omo", "plans"), { recursive: true })
|
||||
writeFileSync(mainPlanPath, "# Plan\n- [ ] Main repo task\n")
|
||||
writeFileSync(worktreePlanPath, "# Plan\n- [x] Worktree task\n")
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Cross-platform check if a path is inside .omo/ directory.
|
||||
* Handles both forward slashes (Unix) and backslashes (Windows).
|
||||
* Uses path segment matching instead of substring matching.
|
||||
*/
|
||||
export function isOmoPath(filePath: string): boolean {
|
||||
return /\.omo[/\\]/.test(filePath)
|
||||
}
|
||||
@@ -99,9 +99,9 @@ describe("resolveActiveBoulderSession", () => {
|
||||
|
||||
test("returns complete progress when a mirrored worktree plan is complete", async () => {
|
||||
// given
|
||||
const mainPlanPath = join(testDirectory, ".sisyphus", "plans", "worktree-plan.md")
|
||||
const mainPlanPath = join(testDirectory, ".omo", "plans", "worktree-plan.md")
|
||||
const worktreeDirectory = join(tmpdir(), `resolve-active-boulder-worktree-${randomUUID()}`)
|
||||
const worktreePlanPath = join(worktreeDirectory, ".sisyphus", "plans", "worktree-plan.md")
|
||||
const worktreePlanPath = join(worktreeDirectory, ".omo", "plans", "worktree-plan.md")
|
||||
mkdirSync(dirname(mainPlanPath), { recursive: true })
|
||||
mkdirSync(dirname(worktreePlanPath), { recursive: true })
|
||||
writeFileSync(mainPlanPath, "# Plan\n- [ ] Main repo task\n", "utf-8")
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* Cross-platform check if a path is inside .sisyphus/ directory.
|
||||
* Handles both forward slashes (Unix) and backslashes (Windows).
|
||||
* Uses path segment matching (not substring) to avoid false positives like "not-sisyphus/file.txt"
|
||||
*/
|
||||
export function isSisyphusPath(filePath: string): boolean {
|
||||
return /\.sisyphus[/\\]/.test(filePath)
|
||||
}
|
||||
@@ -29,7 +29,7 @@ You have an active work plan with incomplete tasks. Continue working.
|
||||
RULES:
|
||||
- **FIRST**: Read the plan file NOW. If the last completed task is still unchecked, mark it \`- [x]\` IMMEDIATELY before anything else
|
||||
- Proceed without asking for permission
|
||||
- Use the notepad at .sisyphus/notepads/{PLAN_NAME}/ to record learnings
|
||||
- Use the notepad at .omo/notepads/{PLAN_NAME}/ to record learnings
|
||||
- Do not stop until all tasks are complete
|
||||
- If blocked, document the blocker and move to the next task`
|
||||
|
||||
@@ -203,7 +203,7 @@ task(
|
||||
\`\`\`
|
||||
|
||||
Allowed direct operations:
|
||||
- \`.sisyphus/\` files (plans, notepads)
|
||||
- \`.omo/\` files (plans, notepads)
|
||||
- Reading any file (verification)
|
||||
- Running commands (verification)
|
||||
|
||||
|
||||
@@ -227,7 +227,7 @@ describe("createToolExecuteAfterHandler task timers", () => {
|
||||
it("ends task timer when plan checkbox flips to checked via edit tool", async () => {
|
||||
// given
|
||||
const parentSessionID = "ses_parent_3"
|
||||
const planDirectory = join(testDirectory, ".sisyphus", "plans")
|
||||
const planDirectory = join(testDirectory, ".omo", "plans")
|
||||
mkdirSync(planDirectory, { recursive: true })
|
||||
const planPath = join(planDirectory, "task-timer-edit-plan.md")
|
||||
writeFileSync(planPath, "# Plan\n\n## TODOs\n- [ ] 1. Implement auth flow\n", "utf-8")
|
||||
|
||||
@@ -19,7 +19,7 @@ import { collectGitDiffStats, formatFileChanges } from "../../shared/git-worktre
|
||||
import { shouldPauseForFinalWaveApproval } from "./final-wave-approval-gate"
|
||||
import { HOOK_NAME } from "./hook-name"
|
||||
import { DIRECT_WORK_REMINDER } from "./system-reminder-templates"
|
||||
import { isSisyphusPath } from "./sisyphus-path"
|
||||
import { isOmoPath } from "./omo-path"
|
||||
import { resolvePreferredSessionId, resolveTaskContext } from "./task-context"
|
||||
import { extractSessionIdFromMetadata, extractSessionIdFromOutput, validateSubagentSessionId } from "./subagent-session-id"
|
||||
import {
|
||||
@@ -175,7 +175,7 @@ export function createToolExecuteAfterHandler(input: {
|
||||
}
|
||||
}
|
||||
|
||||
if (filePath && !isSisyphusPath(filePath)) {
|
||||
if (filePath && !isOmoPath(filePath)) {
|
||||
toolOutput.output = (toolOutput.output || "") + DIRECT_WORK_REMINDER
|
||||
log(`[${HOOK_NAME}] Direct work reminder appended`, {
|
||||
sessionID: toolInput.sessionID,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { resolve } from "node:path"
|
||||
import { getWorkForSession, readBoulderState, readCurrentTopLevelTask, resolveBoulderPlanPath, resolveBoulderPlanPathForWork } from "../../features/boulder-state"
|
||||
import { HOOK_NAME } from "./hook-name"
|
||||
import { ORCHESTRATOR_DELEGATION_REQUIRED, SINGLE_TASK_DIRECTIVE } from "./system-reminder-templates"
|
||||
import { isSisyphusPath } from "./sisyphus-path"
|
||||
import { isOmoPath } from "./omo-path"
|
||||
import type { PendingTaskRef, TrackedTopLevelTaskRef } from "./types"
|
||||
import { isWriteOrEditToolName } from "./write-edit-tool-policy"
|
||||
|
||||
@@ -108,7 +108,7 @@ export function createToolExecuteBeforeHandler(input: {
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSisyphusPath(filePath)) {
|
||||
if (!isOmoPath(filePath)) {
|
||||
const warning = ORCHESTRATOR_DELEGATION_REQUIRED.replace("$FILE_PATH", filePath)
|
||||
toolOutput.message = (toolOutput.message || "") + warning
|
||||
log(`[${HOOK_NAME}] Injected delegation warning for direct file modification`, {
|
||||
|
||||
@@ -26,7 +26,7 @@ describe("buildCompletionGate", () => {
|
||||
|
||||
then("gate interpolates the plan name path", () => {
|
||||
expect(gate).toContain(planName)
|
||||
expect(gate).toContain(`.sisyphus/plans/${planName}.md`)
|
||||
expect(gate).toContain(`.omo/plans/${planName}.md`)
|
||||
})
|
||||
|
||||
then("gate includes Edit instructions", () => {
|
||||
|
||||
@@ -15,13 +15,13 @@ export function buildCompletionGate(planName: string, sessionId: string): string
|
||||
|
||||
Your completion will NOT be recorded until you complete ALL of the following:
|
||||
|
||||
1. **Edit** the plan file \`.sisyphus/plans/${planName}.md\`:
|
||||
1. **Edit** the plan file \`.omo/plans/${planName}.md\`:
|
||||
- Change \`- [ ]\` to \`- [x]\` for the completed task
|
||||
- Use \`Edit\` tool to modify the checkbox
|
||||
|
||||
2. **Read** the plan file AGAIN:
|
||||
\`\`\`
|
||||
Read(".sisyphus/plans/${planName}.md")
|
||||
Read(".omo/plans/${planName}.md")
|
||||
\`\`\`
|
||||
- Verify the checkbox count changed (more \`- [x]\` than before)
|
||||
|
||||
@@ -88,7 +88,7 @@ ${includeCompletionGate ? `${buildCompletionGate(planName, sessionId)}
|
||||
|
||||
The subagent was instructed to record findings in notepad files. Read them NOW:
|
||||
\`\`\`
|
||||
Glob(".sisyphus/notepads/${planName}/*.md")
|
||||
Glob(".omo/notepads/${planName}/*.md")
|
||||
\`\`\`
|
||||
Then \`Read\` each file found - especially:
|
||||
- **learnings.md**: Patterns, conventions, successful approaches discovered
|
||||
@@ -104,7 +104,7 @@ Then \`Read\` each file found - especially:
|
||||
|
||||
Do NOT rely on cached progress. Read the plan file NOW:
|
||||
\`\`\`
|
||||
Read(".sisyphus/plans/${planName}.md")
|
||||
Read(".omo/plans/${planName}.md")
|
||||
\`\`\`
|
||||
Count exactly: how many \`- [ ]\` remain? How many \`- [x]\` completed?
|
||||
This is YOUR ground truth. Use it to decide what comes next.
|
||||
@@ -143,7 +143,7 @@ The last Final Verification Wave result just passed.
|
||||
This is the ONLY point where approval-style user interaction is required.
|
||||
|
||||
1. Read \
|
||||
\`.sisyphus/plans/${planName}.md\` again and confirm every remaining unchecked **top-level** task belongs to F1-F4.
|
||||
\`.omo/plans/${planName}.md\` again and confirm every remaining unchecked **top-level** task belongs to F1-F4.
|
||||
Ignore nested checkboxes under Acceptance Criteria, Evidence, or Final Checklist sections.
|
||||
2. Consolidate the F1-F4 verdicts into a short summary for the user.
|
||||
3. Tell the user all final reviewers approved.
|
||||
|
||||
@@ -11,19 +11,19 @@ You ARE the planner. You ARE NOT an implementer. You DO NOT write code. You DO N
|
||||
**TOOL RESTRICTIONS (SYSTEM-ENFORCED):**
|
||||
| Tool | Allowed | Blocked |
|
||||
|------|---------|---------|
|
||||
| Write/Edit | \`.sisyphus/**/*.md\` ONLY | Everything else |
|
||||
| Write/Edit | \`.omo/**/*.md\` ONLY | Everything else |
|
||||
| Read | All files | - |
|
||||
| Bash | Research commands only | Implementation commands |
|
||||
| task | explore, librarian | - |
|
||||
|
||||
**IF YOU TRY TO WRITE/EDIT OUTSIDE \`.sisyphus/\`:**
|
||||
**IF YOU TRY TO WRITE/EDIT OUTSIDE \`.omo/\`:**
|
||||
- System will BLOCK your action
|
||||
- You will receive an error
|
||||
- DO NOT retry - you are not supposed to implement
|
||||
|
||||
**YOUR ONLY WRITABLE PATHS:**
|
||||
- \`.sisyphus/plans/*.md\` - Final work plans
|
||||
- \`.sisyphus/drafts/*.md\` - Working drafts during interview
|
||||
- \`.omo/plans/*.md\` - Final work plans
|
||||
- \`.omo/drafts/*.md\` - Working drafts during interview
|
||||
|
||||
**WHEN USER ASKS YOU TO IMPLEMENT:**
|
||||
REFUSE. Say: "I'm a planner. I create work plans, not implementations. Run \`/start-work\` after I finish planning."
|
||||
|
||||
@@ -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) │
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const HOOK_NAME = "ralph-loop"
|
||||
export const DEFAULT_STATE_FILE = ".sisyphus/ralph-loop.local.md"
|
||||
export const DEFAULT_STATE_FILE = ".omo/ralph-loop.local.md"
|
||||
export const COMPLETION_TAG_PATTERN = /<promise>(.*?)<\/promise>/is
|
||||
export const DEFAULT_MAX_ITERATIONS = 100
|
||||
export const ULTRAWORK_MAX_ITERATIONS = 500
|
||||
|
||||
@@ -3,7 +3,7 @@ export const HOOK_NAME = "sisyphus-junior-notepad"
|
||||
export const NOTEPAD_DIRECTIVE = `
|
||||
<Work_Context>
|
||||
## Notepad Location (for recording learnings)
|
||||
NOTEPAD PATH: .sisyphus/notepads/{plan-name}/
|
||||
NOTEPAD PATH: .omo/notepads/{plan-name}/
|
||||
- learnings.md: Record patterns, conventions, successful approaches
|
||||
- issues.md: Record problems, blockers, gotchas encountered
|
||||
- decisions.md: Record architectural choices and rationales
|
||||
@@ -13,11 +13,11 @@ You SHOULD append findings to notepad files after completing work.
|
||||
IMPORTANT: Always APPEND to notepad files - never overwrite or use Edit tool.
|
||||
|
||||
## Plan Location (READ ONLY)
|
||||
PLAN PATH: .sisyphus/plans/{plan-name}.md
|
||||
PLAN PATH: .omo/plans/{plan-name}.md
|
||||
|
||||
CRITICAL RULE: NEVER MODIFY THE PLAN FILE
|
||||
|
||||
The plan file (.sisyphus/plans/*.md) is SACRED and READ-ONLY.
|
||||
The plan file (.omo/plans/*.md) is SACRED and READ-ONLY.
|
||||
- You may READ the plan to understand tasks
|
||||
- You may READ checkbox items to know what to do
|
||||
- You MUST NOT edit, modify, or update the plan file
|
||||
|
||||
@@ -244,8 +244,8 @@ describe("createWriteExistingFileGuardHook", () => {
|
||||
).rejects.toThrow(BLOCK_MESSAGE)
|
||||
})
|
||||
|
||||
test("#given existing file under .sisyphus #when write executes #then always allows", async () => {
|
||||
const existingFile = createFile(".sisyphus/plans/plan.txt")
|
||||
test("#given existing file under .omo #when write executes #then always allows", async () => {
|
||||
const existingFile = createFile(".omo/plans/plan.txt")
|
||||
|
||||
await expect(
|
||||
invoke({
|
||||
|
||||
@@ -149,9 +149,9 @@ export async function handleWriteExistingFileGuardToolExecuteBefore(params: {
|
||||
return
|
||||
}
|
||||
|
||||
const isSisyphusPath = canonicalPath.includes("/.sisyphus/")
|
||||
if (isSisyphusPath) {
|
||||
log("[write-existing-file-guard] Allowing .sisyphus/** overwrite", {
|
||||
const isOmoPath = canonicalPath.includes("/.omo/")
|
||||
if (isOmoPath) {
|
||||
log("[write-existing-file-guard] Allowing .omo/** overwrite", {
|
||||
sessionID: input.sessionID,
|
||||
filePath,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user