From c7ca608b3848824403889c8f3f4f60a9f910dd1a Mon Sep 17 00:00:00 2001 From: justsisyphus Date: Fri, 16 Jan 2026 11:20:45 +0900 Subject: [PATCH] refactor: unify system directive prefix for keyword-detector filtering - Add shared/system-directive.ts with SYSTEM_DIRECTIVE_PREFIX constant - Unify all system message prefixes to [SYSTEM DIRECTIVE: OH-MY-OPENCODE - ...] - Add isSystemDirective() filter to keyword-detector to skip system messages - Update prometheus-md-only tests to use new prefix constants --- .../compaction-context-injector/index.ts | 3 +- src/hooks/context-window-monitor.ts | 3 +- src/hooks/keyword-detector/index.ts | 7 ++++ src/hooks/prometheus-md-only/constants.ts | 4 +- src/hooks/prometheus-md-only/index.test.ts | 13 +++--- src/hooks/prometheus-md-only/index.ts | 3 +- src/hooks/ralph-loop/index.ts | 3 +- src/hooks/sisyphus-orchestrator/index.ts | 11 ++--- src/hooks/todo-continuation-enforcer.ts | 3 +- src/shared/index.ts | 1 + src/shared/system-directive.ts | 40 +++++++++++++++++++ 11 files changed, 74 insertions(+), 17 deletions(-) create mode 100644 src/shared/system-directive.ts diff --git a/src/hooks/compaction-context-injector/index.ts b/src/hooks/compaction-context-injector/index.ts index 1df79c4a..ee262ab7 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 d2a7af24..3b921911 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 48145ced..428474d5 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 0c24b049..eef0c3f8 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 71e31aa0..c703c1dd 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 c562e39e..470e870a 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 aef0cb3d..c2b4de32 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 b032cfe8..2b836c71 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 e88103a0..161b88ff 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 c0e6d0bb..41dd9789 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 00000000..2252dddf --- /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]