diff --git a/src/hooks/compaction-context-injector/index.ts b/src/hooks/compaction-context-injector/index.ts index 1df79c4ab..ee262ab7b 100644 --- a/src/hooks/compaction-context-injector/index.ts +++ b/src/hooks/compaction-context-injector/index.ts @@ -1,5 +1,6 @@ import { injectHookMessage } from "../../features/hook-message-injector" import { log } from "../../shared/logger" +import { createSystemDirective, SystemDirectiveTypes } from "../../shared/system-directive" export interface SummarizeContext { sessionID: string @@ -9,7 +10,7 @@ export interface SummarizeContext { directory: string } -const SUMMARIZE_CONTEXT_PROMPT = `[COMPACTION CONTEXT INJECTION] +const SUMMARIZE_CONTEXT_PROMPT = `${createSystemDirective(SystemDirectiveTypes.COMPACTION_CONTEXT)} When summarizing this session, you MUST include the following sections in your summary: diff --git a/src/hooks/context-window-monitor.ts b/src/hooks/context-window-monitor.ts index d2a7af24c..3b9219114 100644 --- a/src/hooks/context-window-monitor.ts +++ b/src/hooks/context-window-monitor.ts @@ -1,4 +1,5 @@ import type { PluginInput } from "@opencode-ai/plugin" +import { createSystemDirective, SystemDirectiveTypes } from "../shared/system-directive" const ANTHROPIC_DISPLAY_LIMIT = 1_000_000 const ANTHROPIC_ACTUAL_LIMIT = @@ -8,7 +9,7 @@ const ANTHROPIC_ACTUAL_LIMIT = : 200_000 const CONTEXT_WARNING_THRESHOLD = 0.70 -const CONTEXT_REMINDER = `[SYSTEM REMINDER - 1M Context Window] +const CONTEXT_REMINDER = `${createSystemDirective(SystemDirectiveTypes.CONTEXT_WINDOW_MONITOR)} You are using Anthropic Claude with 1M context window. You have plenty of context remaining - do NOT rush or skip tasks. diff --git a/src/hooks/keyword-detector/index.ts b/src/hooks/keyword-detector/index.ts index 48145ceda..428474d54 100644 --- a/src/hooks/keyword-detector/index.ts +++ b/src/hooks/keyword-detector/index.ts @@ -1,6 +1,7 @@ import type { PluginInput } from "@opencode-ai/plugin" import { detectKeywordsWithType, extractPromptText, removeCodeBlocks } from "./detector" import { log } from "../../shared" +import { isSystemDirective } from "../../shared/system-directive" import { getMainSessionID } from "../../features/claude-code-session-state" import type { ContextCollector } from "../../features/context-injector" @@ -23,6 +24,12 @@ export function createKeywordDetectorHook(ctx: PluginInput, collector?: ContextC } ): Promise => { const promptText = extractPromptText(output.parts) + + if (isSystemDirective(promptText)) { + log(`[keyword-detector] Skipping system directive message`, { sessionID: input.sessionID }) + return + } + let detectedKeywords = detectKeywordsWithType(removeCodeBlocks(promptText), input.agent) if (detectedKeywords.length === 0) { diff --git a/src/hooks/prometheus-md-only/constants.ts b/src/hooks/prometheus-md-only/constants.ts index 0c24b0498..eef0c3f88 100644 --- a/src/hooks/prometheus-md-only/constants.ts +++ b/src/hooks/prometheus-md-only/constants.ts @@ -1,3 +1,5 @@ +import { createSystemDirective, SystemDirectiveTypes } from "../../shared/system-directive" + export const HOOK_NAME = "prometheus-md-only" export const PROMETHEUS_AGENTS = ["Prometheus (Planner)"] @@ -12,7 +14,7 @@ export const PLANNING_CONSULT_WARNING = ` --- -[SYSTEM DIRECTIVE - READ-ONLY PLANNING CONSULTATION] +${createSystemDirective(SystemDirectiveTypes.PROMETHEUS_READ_ONLY)} You are being invoked by Prometheus (Planner), a READ-ONLY planning agent. diff --git a/src/hooks/prometheus-md-only/index.test.ts b/src/hooks/prometheus-md-only/index.test.ts index 71e31aa0b..c703c1dd6 100644 --- a/src/hooks/prometheus-md-only/index.test.ts +++ b/src/hooks/prometheus-md-only/index.test.ts @@ -3,6 +3,7 @@ import { mkdirSync, rmSync, writeFileSync } from "node:fs" import { join } from "node:path" import { createPrometheusMdOnlyHook } from "./index" import { MESSAGE_STORAGE } from "../../features/hook-message-injector" +import { SYSTEM_DIRECTIVE_PREFIX, createSystemDirective, SystemDirectiveTypes } from "../../shared/system-directive" describe("prometheus-md-only", () => { const TEST_SESSION_ID = "test-session-prometheus" @@ -167,7 +168,7 @@ describe("prometheus-md-only", () => { await hook["tool.execute.before"](input, output) // #then - expect(output.args.prompt).toContain("[SYSTEM DIRECTIVE - READ-ONLY PLANNING CONSULTATION]") + expect(output.args.prompt).toContain(SYSTEM_DIRECTIVE_PREFIX) expect(output.args.prompt).toContain("DO NOT modify any files") }) @@ -187,7 +188,7 @@ describe("prometheus-md-only", () => { await hook["tool.execute.before"](input, output) // #then - expect(output.args.prompt).toContain("[SYSTEM DIRECTIVE - READ-ONLY PLANNING CONSULTATION]") + expect(output.args.prompt).toContain(SYSTEM_DIRECTIVE_PREFIX) }) test("should inject read-only warning when Prometheus calls call_omo_agent", async () => { @@ -206,7 +207,7 @@ describe("prometheus-md-only", () => { await hook["tool.execute.before"](input, output) // #then - expect(output.args.prompt).toContain("[SYSTEM DIRECTIVE - READ-ONLY PLANNING CONSULTATION]") + expect(output.args.prompt).toContain(SYSTEM_DIRECTIVE_PREFIX) }) test("should not double-inject warning if already present", async () => { @@ -217,7 +218,7 @@ describe("prometheus-md-only", () => { sessionID: TEST_SESSION_ID, callID: "call-1", } - const promptWithWarning = "Some prompt [SYSTEM DIRECTIVE - READ-ONLY PLANNING CONSULTATION] already here" + const promptWithWarning = `Some prompt ${SYSTEM_DIRECTIVE_PREFIX} already here` const output = { args: { prompt: promptWithWarning }, } @@ -226,7 +227,7 @@ describe("prometheus-md-only", () => { await hook["tool.execute.before"](input, output) // #then - const occurrences = (output.args.prompt as string).split("[SYSTEM DIRECTIVE - READ-ONLY PLANNING CONSULTATION]").length - 1 + const occurrences = (output.args.prompt as string).split(SYSTEM_DIRECTIVE_PREFIX).length - 1 expect(occurrences).toBe(1) }) }) @@ -272,7 +273,7 @@ describe("prometheus-md-only", () => { // #then expect(output.args.prompt).toBe(originalPrompt) - expect(output.args.prompt).not.toContain("[SYSTEM DIRECTIVE - READ-ONLY PLANNING CONSULTATION]") + expect(output.args.prompt).not.toContain(SYSTEM_DIRECTIVE_PREFIX) }) }) diff --git a/src/hooks/prometheus-md-only/index.ts b/src/hooks/prometheus-md-only/index.ts index c562e39e6..470e870a3 100644 --- a/src/hooks/prometheus-md-only/index.ts +++ b/src/hooks/prometheus-md-only/index.ts @@ -5,6 +5,7 @@ import { HOOK_NAME, PROMETHEUS_AGENTS, ALLOWED_EXTENSIONS, ALLOWED_PATH_PREFIX, import { findNearestMessageWithFields, findFirstMessageWithAgent, MESSAGE_STORAGE } from "../../features/hook-message-injector" import { getSessionAgent } from "../../features/claude-code-session-state" import { log } from "../../shared/logger" +import { SYSTEM_DIRECTIVE_PREFIX } from "../../shared/system-directive" export * from "./constants" @@ -89,7 +90,7 @@ export function createPrometheusMdOnlyHook(ctx: PluginInput) { // 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 - READ-ONLY PLANNING CONSULTATION]")) { + if (prompt && !prompt.includes(SYSTEM_DIRECTIVE_PREFIX)) { output.args.prompt = prompt + PLANNING_CONSULT_WARNING log(`[${HOOK_NAME}] Injected read-only planning warning to ${toolName}`, { sessionID: input.sessionID, diff --git a/src/hooks/ralph-loop/index.ts b/src/hooks/ralph-loop/index.ts index aef0cb3d0..c2b4de32a 100644 --- a/src/hooks/ralph-loop/index.ts +++ b/src/hooks/ralph-loop/index.ts @@ -2,6 +2,7 @@ import type { PluginInput } from "@opencode-ai/plugin" import { existsSync, readFileSync, readdirSync } from "node:fs" import { join } from "node:path" import { log } from "../../shared/logger" +import { SYSTEM_DIRECTIVE_PREFIX } from "../../shared/system-directive" import { readState, writeState, clearState, incrementIteration } from "./storage" import { HOOK_NAME, @@ -42,7 +43,7 @@ interface OpenCodeSessionMessage { }> } -const CONTINUATION_PROMPT = `[RALPH LOOP - ITERATION {{ITERATION}}/{{MAX}}] +const CONTINUATION_PROMPT = `${SYSTEM_DIRECTIVE_PREFIX} - RALPH LOOP {{ITERATION}}/{{MAX}}] Your previous attempt did not output the completion promise. Continue working on the task. diff --git a/src/hooks/sisyphus-orchestrator/index.ts b/src/hooks/sisyphus-orchestrator/index.ts index b032cfe8c..2b836c71d 100644 --- a/src/hooks/sisyphus-orchestrator/index.ts +++ b/src/hooks/sisyphus-orchestrator/index.ts @@ -10,6 +10,7 @@ import { import { getMainSessionID, subagentSessions } from "../../features/claude-code-session-state" 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 type { BackgroundManager } from "../../features/background-agent" export const HOOK_NAME = "sisyphus-orchestrator" @@ -28,7 +29,7 @@ const DIRECT_WORK_REMINDER = ` --- -[SYSTEM REMINDER - DELEGATION REQUIRED] +${createSystemDirective(SystemDirectiveTypes.DELEGATION_REQUIRED)} You just performed direct file modifications outside \`.sisyphus/\`. @@ -52,7 +53,7 @@ You should NOT: --- ` -const BOULDER_CONTINUATION_PROMPT = `[SYSTEM REMINDER - BOULDER CONTINUATION] +const BOULDER_CONTINUATION_PROMPT = `${createSystemDirective(SystemDirectiveTypes.BOULDER_CONTINUATION)} You have an active work plan with incomplete tasks. Continue working. @@ -107,7 +108,7 @@ const ORCHESTRATOR_DELEGATION_REQUIRED = ` --- -⚠️⚠️⚠️ [CRITICAL SYSTEM DIRECTIVE - DELEGATION REQUIRED] ⚠️⚠️⚠️ +⚠️⚠️⚠️ ${createSystemDirective(SystemDirectiveTypes.DELEGATION_REQUIRED)} ⚠️⚠️⚠️ **STOP. YOU ARE VIOLATING ORCHESTRATOR PROTOCOL.** @@ -155,7 +156,7 @@ sisyphus_task( const SINGLE_TASK_DIRECTIVE = ` -[SYSTEM DIRECTIVE - SINGLE TASK ONLY] +${createSystemDirective(SystemDirectiveTypes.SINGLE_TASK_ONLY)} **STOP. READ THIS BEFORE PROCEEDING.** @@ -626,7 +627,7 @@ export function createSisyphusOrchestratorHook( // Check sisyphus_task - inject single-task directive if (input.tool === "sisyphus_task") { const prompt = output.args.prompt as string | undefined - if (prompt && !prompt.includes("[SYSTEM DIRECTIVE - SINGLE TASK ONLY]")) { + if (prompt && !prompt.includes(SYSTEM_DIRECTIVE_PREFIX)) { output.args.prompt = prompt + `\n${SINGLE_TASK_DIRECTIVE}` log(`[${HOOK_NAME}] Injected single-task directive to sisyphus_task`, { sessionID: input.sessionID, diff --git a/src/hooks/todo-continuation-enforcer.ts b/src/hooks/todo-continuation-enforcer.ts index e88103a0b..161b88ff5 100644 --- a/src/hooks/todo-continuation-enforcer.ts +++ b/src/hooks/todo-continuation-enforcer.ts @@ -9,6 +9,7 @@ import { type ToolPermission, } from "../features/hook-message-injector" import { log } from "../shared/logger" +import { createSystemDirective, SystemDirectiveTypes } from "../shared/system-directive" const HOOK_NAME = "todo-continuation-enforcer" @@ -40,7 +41,7 @@ interface SessionState { abortDetectedAt?: number } -const CONTINUATION_PROMPT = `[SYSTEM REMINDER - TODO CONTINUATION] +const CONTINUATION_PROMPT = `${createSystemDirective(SystemDirectiveTypes.TODO_CONTINUATION)} Incomplete tasks remain in your todo list. Continue working on the next pending task. diff --git a/src/shared/index.ts b/src/shared/index.ts index c0e6d0bbb..41dd97890 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -24,3 +24,4 @@ export * from "./zip-extractor" export * from "./agent-variant" export * from "./session-cursor" export * from "./shell-env" +export * from "./system-directive" diff --git a/src/shared/system-directive.ts b/src/shared/system-directive.ts new file mode 100644 index 000000000..2252dddf2 --- /dev/null +++ b/src/shared/system-directive.ts @@ -0,0 +1,40 @@ +/** + * Unified system directive prefix for oh-my-opencode internal messages. + * All system-generated messages should use this prefix for consistent filtering. + * + * Format: [SYSTEM DIRECTIVE: OH-MY-OPENCODE - {TYPE}] + */ + +export const SYSTEM_DIRECTIVE_PREFIX = "[SYSTEM DIRECTIVE: OH-MY-OPENCODE" + +/** + * Creates a system directive header with the given type. + * @param type - The directive type (e.g., "TODO CONTINUATION", "RALPH LOOP") + * @returns Formatted directive string like "[SYSTEM DIRECTIVE: OH-MY-OPENCODE - TODO CONTINUATION]" + */ +export function createSystemDirective(type: string): string { + return `${SYSTEM_DIRECTIVE_PREFIX} - ${type}]` +} + +/** + * Checks if a message starts with the oh-my-opencode system directive prefix. + * Used by keyword-detector and other hooks to skip system-generated messages. + * @param text - The message text to check + * @returns true if the message is a system directive + */ +export function isSystemDirective(text: string): boolean { + return text.trimStart().startsWith(SYSTEM_DIRECTIVE_PREFIX) +} + +export const SystemDirectiveTypes = { + TODO_CONTINUATION: "TODO CONTINUATION", + RALPH_LOOP: "RALPH LOOP", + BOULDER_CONTINUATION: "BOULDER CONTINUATION", + DELEGATION_REQUIRED: "DELEGATION REQUIRED", + SINGLE_TASK_ONLY: "SINGLE TASK ONLY", + COMPACTION_CONTEXT: "COMPACTION CONTEXT", + CONTEXT_WINDOW_MONITOR: "CONTEXT WINDOW MONITOR", + PROMETHEUS_READ_ONLY: "PROMETHEUS READ-ONLY", +} as const + +export type SystemDirectiveType = (typeof SystemDirectiveTypes)[keyof typeof SystemDirectiveTypes]