diff --git a/assets/oh-my-opencode.schema.json b/assets/oh-my-opencode.schema.json
index 7ecbd40c..a57bc2e0 100644
--- a/assets/oh-my-opencode.schema.json
+++ b/assets/oh-my-opencode.schema.json
@@ -76,6 +76,7 @@
"edit-error-recovery",
"delegate-task-retry",
"prometheus-md-only",
+ "sisyphus-junior-notepad",
"start-work",
"atlas"
]
diff --git a/src/agents/sisyphus-junior.ts b/src/agents/sisyphus-junior.ts
index a9f592dd..3dc0d28b 100644
--- a/src/agents/sisyphus-junior.ts
+++ b/src/agents/sisyphus-junior.ts
@@ -20,32 +20,6 @@ ALLOWED: call_omo_agent - You CAN spawn explore/librarian agents for research.
You work ALONE for implementation. No delegation of implementation tasks.
-
-## Notepad Location (for recording learnings)
-NOTEPAD PATH: .sisyphus/notepads/{plan-name}/
-- learnings.md: Record patterns, conventions, successful approaches
-- issues.md: Record problems, blockers, gotchas encountered
-- decisions.md: Record architectural choices and rationales
-- problems.md: Record unresolved issues, technical debt
-
-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
-
-CRITICAL RULE: NEVER MODIFY THE PLAN FILE
-
-The plan file (.sisyphus/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
-- You MUST NOT mark checkboxes as complete in the plan
-- Only the Orchestrator manages the plan file
-
-VIOLATION = IMMEDIATE FAILURE. The Orchestrator tracks plan state.
-
-
TODO OBSESSION (NON-NEGOTIABLE):
- 2+ steps → todowrite FIRST, atomic breakdown
diff --git a/src/config/schema.ts b/src/config/schema.ts
index 48bdb916..126355b7 100644
--- a/src/config/schema.ts
+++ b/src/config/schema.ts
@@ -83,6 +83,7 @@ export const HookNameSchema = z.enum([
"edit-error-recovery",
"delegate-task-retry",
"prometheus-md-only",
+ "sisyphus-junior-notepad",
"start-work",
"atlas",
])
diff --git a/src/hooks/atlas/index.ts b/src/hooks/atlas/index.ts
index 8cd91cf8..bd20168b 100644
--- a/src/hooks/atlas/index.ts
+++ b/src/hooks/atlas/index.ts
@@ -11,6 +11,7 @@ import { getMainSessionID, subagentSessions } from "../../features/claude-code-s
import { findNearestMessageWithFields, MESSAGE_STORAGE } from "../../features/hook-message-injector"
import { log } from "../../shared/logger"
import { createSystemDirective, SYSTEM_DIRECTIVE_PREFIX, SystemDirectiveTypes } from "../../shared/system-directive"
+import { isCallerOrchestrator, getMessageDir } from "../../shared/session-utils"
import type { BackgroundManager } from "../../features/background-agent"
export const HOOK_NAME = "atlas"
@@ -380,28 +381,6 @@ interface ToolExecuteAfterOutput {
metadata: Record
}
-function getMessageDir(sessionID: string): string | null {
- if (!existsSync(MESSAGE_STORAGE)) return null
-
- const directPath = join(MESSAGE_STORAGE, sessionID)
- if (existsSync(directPath)) return directPath
-
- for (const dir of readdirSync(MESSAGE_STORAGE)) {
- const sessionPath = join(MESSAGE_STORAGE, dir, sessionID)
- if (existsSync(sessionPath)) return sessionPath
- }
-
- return null
-}
-
-function isCallerOrchestrator(sessionID?: string): boolean {
- if (!sessionID) return false
- const messageDir = getMessageDir(sessionID)
- if (!messageDir) return false
- const nearest = findNearestMessageWithFields(messageDir)
- return nearest?.agent?.toLowerCase() === "atlas"
- }
-
interface SessionState {
lastEventWasAbortError?: boolean
lastContinuationInjectedAt?: number
@@ -672,7 +651,7 @@ export function createAtlasHook(
if (input.tool === "delegate_task") {
const prompt = output.args.prompt as string | undefined
if (prompt && !prompt.includes(SYSTEM_DIRECTIVE_PREFIX)) {
- output.args.prompt = prompt + `\n${SINGLE_TASK_DIRECTIVE}`
+ output.args.prompt = `${SINGLE_TASK_DIRECTIVE}\n` + prompt
log(`[${HOOK_NAME}] Injected single-task directive to delegate_task`, {
sessionID: input.sessionID,
})
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
index d781f0df..dd38cc38 100644
--- a/src/hooks/index.ts
+++ b/src/hooks/index.ts
@@ -26,6 +26,7 @@ export { createRalphLoopHook, type RalphLoopHook } from "./ralph-loop";
export { createAutoSlashCommandHook } from "./auto-slash-command";
export { createEditErrorRecoveryHook } from "./edit-error-recovery";
export { createPrometheusMdOnlyHook } from "./prometheus-md-only";
+export { createSisyphusJuniorNotepadHook } from "./sisyphus-junior-notepad";
export { createTaskResumeInfoHook } from "./task-resume-info";
export { createStartWorkHook } from "./start-work";
export { createAtlasHook } from "./atlas";
diff --git a/src/hooks/prometheus-md-only/index.ts b/src/hooks/prometheus-md-only/index.ts
index 645c0541..2487a1cc 100644
--- a/src/hooks/prometheus-md-only/index.ts
+++ b/src/hooks/prometheus-md-only/index.ts
@@ -89,10 +89,10 @@ export function createPrometheusMdOnlyHook(ctx: PluginInput) {
const toolName = input.tool
// Inject read-only warning for task tools called by Prometheus
- if (TASK_TOOLS.includes(toolName)) {
- const prompt = output.args.prompt as string | undefined
- if (prompt && !prompt.includes(SYSTEM_DIRECTIVE_PREFIX)) {
- output.args.prompt = prompt + PLANNING_CONSULT_WARNING
+ if (TASK_TOOLS.includes(toolName)) {
+ const prompt = output.args.prompt as string | undefined
+ if (prompt && !prompt.includes(SYSTEM_DIRECTIVE_PREFIX)) {
+ output.args.prompt = PLANNING_CONSULT_WARNING + prompt
log(`[${HOOK_NAME}] Injected read-only planning warning to ${toolName}`, {
sessionID: input.sessionID,
tool: toolName,
diff --git a/src/hooks/sisyphus-junior-notepad/constants.ts b/src/hooks/sisyphus-junior-notepad/constants.ts
new file mode 100644
index 00000000..2abf733c
--- /dev/null
+++ b/src/hooks/sisyphus-junior-notepad/constants.ts
@@ -0,0 +1,29 @@
+export const HOOK_NAME = "sisyphus-junior-notepad"
+
+export const NOTEPAD_DIRECTIVE = `
+
+## Notepad Location (for recording learnings)
+NOTEPAD PATH: .sisyphus/notepads/{plan-name}/
+- learnings.md: Record patterns, conventions, successful approaches
+- issues.md: Record problems, blockers, gotchas encountered
+- decisions.md: Record architectural choices and rationales
+- problems.md: Record unresolved issues, technical debt
+
+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
+
+CRITICAL RULE: NEVER MODIFY THE PLAN FILE
+
+The plan file (.sisyphus/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
+- You MUST NOT mark checkboxes as complete in the plan
+- Only the Orchestrator manages the plan file
+
+VIOLATION = IMMEDIATE FAILURE. The Orchestrator tracks plan state.
+
+`
diff --git a/src/hooks/sisyphus-junior-notepad/index.ts b/src/hooks/sisyphus-junior-notepad/index.ts
new file mode 100644
index 00000000..588df568
--- /dev/null
+++ b/src/hooks/sisyphus-junior-notepad/index.ts
@@ -0,0 +1,45 @@
+import type { PluginInput } from "@opencode-ai/plugin"
+import { isCallerOrchestrator } from "../../shared/session-utils"
+import { SYSTEM_DIRECTIVE_PREFIX } from "../../shared/system-directive"
+import { log } from "../../shared/logger"
+import { HOOK_NAME, NOTEPAD_DIRECTIVE } from "./constants"
+
+export * from "./constants"
+
+export function createSisyphusJuniorNotepadHook(ctx: PluginInput) {
+ return {
+ "tool.execute.before": async (
+ input: { tool: string; sessionID: string; callID: string },
+ output: { args: Record; message?: string }
+ ): Promise => {
+ // 1. Check if tool is delegate_task
+ if (input.tool !== "delegate_task") {
+ return
+ }
+
+ // 2. Check if caller is Atlas (orchestrator)
+ if (!isCallerOrchestrator(input.sessionID)) {
+ return
+ }
+
+ // 3. Get prompt from output.args
+ const prompt = output.args.prompt as string | undefined
+ if (!prompt) {
+ return
+ }
+
+ // 4. Check for double injection
+ if (prompt.includes(SYSTEM_DIRECTIVE_PREFIX)) {
+ return
+ }
+
+ // 5. Prepend directive
+ output.args.prompt = NOTEPAD_DIRECTIVE + prompt
+
+ // 6. Log injection
+ log(`[${HOOK_NAME}] Injected notepad directive to delegate_task`, {
+ sessionID: input.sessionID,
+ })
+ },
+ }
+}
diff --git a/src/index.ts b/src/index.ts
index d3375c42..3210602d 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -31,6 +31,7 @@ import {
createStartWorkHook,
createAtlasHook,
createPrometheusMdOnlyHook,
+ createSisyphusJuniorNotepadHook,
createQuestionLabelTruncatorHook,
} from "./hooks";
import {
@@ -204,6 +205,10 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
? createPrometheusMdOnlyHook(ctx)
: null;
+ const sisyphusJuniorNotepad = isHookEnabled("sisyphus-junior-notepad")
+ ? createSisyphusJuniorNotepadHook(ctx)
+ : null;
+
const questionLabelTruncator = createQuestionLabelTruncatorHook();
const taskResumeInfo = createTaskResumeInfoHook();
@@ -495,6 +500,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
await directoryReadmeInjector?.["tool.execute.before"]?.(input, output);
await rulesInjector?.["tool.execute.before"]?.(input, output);
await prometheusMdOnly?.["tool.execute.before"]?.(input, output);
+ await sisyphusJuniorNotepad?.["tool.execute.before"]?.(input, output);
await atlasHook?.["tool.execute.before"]?.(input, output);
if (input.tool === "task") {
diff --git a/src/shared/index.ts b/src/shared/index.ts
index 759fcd40..6c681a01 100644
--- a/src/shared/index.ts
+++ b/src/shared/index.ts
@@ -29,3 +29,4 @@ export * from "./model-requirements"
export * from "./model-resolver"
export * from "./model-availability"
export * from "./case-insensitive"
+export * from "./session-utils"
diff --git a/src/shared/session-utils.ts b/src/shared/session-utils.ts
new file mode 100644
index 00000000..eb983974
--- /dev/null
+++ b/src/shared/session-utils.ts
@@ -0,0 +1,27 @@
+import * as path from "node:path"
+import * as os from "node:os"
+import { existsSync, readdirSync } from "node:fs"
+import { join } from "node:path"
+import { findNearestMessageWithFields, MESSAGE_STORAGE } from "../features/hook-message-injector"
+
+export function getMessageDir(sessionID: string): string | null {
+ if (!existsSync(MESSAGE_STORAGE)) return null
+
+ const directPath = join(MESSAGE_STORAGE, sessionID)
+ if (existsSync(directPath)) return directPath
+
+ for (const dir of readdirSync(MESSAGE_STORAGE)) {
+ const sessionPath = join(MESSAGE_STORAGE, dir, sessionID)
+ if (existsSync(sessionPath)) return sessionPath
+ }
+
+ return null
+}
+
+export function isCallerOrchestrator(sessionID?: string): boolean {
+ if (!sessionID) return false
+ const messageDir = getMessageDir(sessionID)
+ if (!messageDir) return false
+ const nearest = findNearestMessageWithFields(messageDir)
+ return nearest?.agent?.toLowerCase() === "atlas"
+}