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
This commit is contained in:
justsisyphus 2026-01-16 11:20:45 +09:00
parent b933992e36
commit c7ca608b38
11 changed files with 74 additions and 17 deletions

View File

@ -1,5 +1,6 @@
import { injectHookMessage } from "../../features/hook-message-injector" import { injectHookMessage } from "../../features/hook-message-injector"
import { log } from "../../shared/logger" import { log } from "../../shared/logger"
import { createSystemDirective, SystemDirectiveTypes } from "../../shared/system-directive"
export interface SummarizeContext { export interface SummarizeContext {
sessionID: string sessionID: string
@ -9,7 +10,7 @@ export interface SummarizeContext {
directory: string 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: When summarizing this session, you MUST include the following sections in your summary:

View File

@ -1,4 +1,5 @@
import type { PluginInput } from "@opencode-ai/plugin" import type { PluginInput } from "@opencode-ai/plugin"
import { createSystemDirective, SystemDirectiveTypes } from "../shared/system-directive"
const ANTHROPIC_DISPLAY_LIMIT = 1_000_000 const ANTHROPIC_DISPLAY_LIMIT = 1_000_000
const ANTHROPIC_ACTUAL_LIMIT = const ANTHROPIC_ACTUAL_LIMIT =
@ -8,7 +9,7 @@ const ANTHROPIC_ACTUAL_LIMIT =
: 200_000 : 200_000
const CONTEXT_WARNING_THRESHOLD = 0.70 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 are using Anthropic Claude with 1M context window.
You have plenty of context remaining - do NOT rush or skip tasks. You have plenty of context remaining - do NOT rush or skip tasks.

View File

@ -1,6 +1,7 @@
import type { PluginInput } from "@opencode-ai/plugin" import type { PluginInput } from "@opencode-ai/plugin"
import { detectKeywordsWithType, extractPromptText, removeCodeBlocks } from "./detector" import { detectKeywordsWithType, extractPromptText, removeCodeBlocks } from "./detector"
import { log } from "../../shared" import { log } from "../../shared"
import { isSystemDirective } from "../../shared/system-directive"
import { getMainSessionID } from "../../features/claude-code-session-state" import { getMainSessionID } from "../../features/claude-code-session-state"
import type { ContextCollector } from "../../features/context-injector" import type { ContextCollector } from "../../features/context-injector"
@ -23,6 +24,12 @@ export function createKeywordDetectorHook(ctx: PluginInput, collector?: ContextC
} }
): Promise<void> => { ): Promise<void> => {
const promptText = extractPromptText(output.parts) 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) let detectedKeywords = detectKeywordsWithType(removeCodeBlocks(promptText), input.agent)
if (detectedKeywords.length === 0) { if (detectedKeywords.length === 0) {

View File

@ -1,3 +1,5 @@
import { createSystemDirective, SystemDirectiveTypes } from "../../shared/system-directive"
export const HOOK_NAME = "prometheus-md-only" export const HOOK_NAME = "prometheus-md-only"
export const PROMETHEUS_AGENTS = ["Prometheus (Planner)"] 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. You are being invoked by Prometheus (Planner), a READ-ONLY planning agent.

View File

@ -3,6 +3,7 @@ import { mkdirSync, rmSync, writeFileSync } from "node:fs"
import { join } from "node:path" import { join } from "node:path"
import { createPrometheusMdOnlyHook } from "./index" import { createPrometheusMdOnlyHook } from "./index"
import { MESSAGE_STORAGE } from "../../features/hook-message-injector" import { MESSAGE_STORAGE } from "../../features/hook-message-injector"
import { SYSTEM_DIRECTIVE_PREFIX, createSystemDirective, SystemDirectiveTypes } from "../../shared/system-directive"
describe("prometheus-md-only", () => { describe("prometheus-md-only", () => {
const TEST_SESSION_ID = "test-session-prometheus" const TEST_SESSION_ID = "test-session-prometheus"
@ -167,7 +168,7 @@ describe("prometheus-md-only", () => {
await hook["tool.execute.before"](input, output) await hook["tool.execute.before"](input, output)
// #then // #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") 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) await hook["tool.execute.before"](input, output)
// #then // #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 () => { 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) await hook["tool.execute.before"](input, output)
// #then // #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 () => { test("should not double-inject warning if already present", async () => {
@ -217,7 +218,7 @@ describe("prometheus-md-only", () => {
sessionID: TEST_SESSION_ID, sessionID: TEST_SESSION_ID,
callID: "call-1", 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 = { const output = {
args: { prompt: promptWithWarning }, args: { prompt: promptWithWarning },
} }
@ -226,7 +227,7 @@ describe("prometheus-md-only", () => {
await hook["tool.execute.before"](input, output) await hook["tool.execute.before"](input, output)
// #then // #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) expect(occurrences).toBe(1)
}) })
}) })
@ -272,7 +273,7 @@ describe("prometheus-md-only", () => {
// #then // #then
expect(output.args.prompt).toBe(originalPrompt) 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)
}) })
}) })

View File

@ -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 { findNearestMessageWithFields, findFirstMessageWithAgent, MESSAGE_STORAGE } from "../../features/hook-message-injector"
import { getSessionAgent } from "../../features/claude-code-session-state" import { getSessionAgent } from "../../features/claude-code-session-state"
import { log } from "../../shared/logger" import { log } from "../../shared/logger"
import { SYSTEM_DIRECTIVE_PREFIX } from "../../shared/system-directive"
export * from "./constants" export * from "./constants"
@ -89,7 +90,7 @@ export function createPrometheusMdOnlyHook(ctx: PluginInput) {
// Inject read-only warning for task tools called by Prometheus // Inject read-only warning for task tools called by Prometheus
if (TASK_TOOLS.includes(toolName)) { if (TASK_TOOLS.includes(toolName)) {
const prompt = output.args.prompt as string | undefined 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 output.args.prompt = prompt + PLANNING_CONSULT_WARNING
log(`[${HOOK_NAME}] Injected read-only planning warning to ${toolName}`, { log(`[${HOOK_NAME}] Injected read-only planning warning to ${toolName}`, {
sessionID: input.sessionID, sessionID: input.sessionID,

View File

@ -2,6 +2,7 @@ import type { PluginInput } from "@opencode-ai/plugin"
import { existsSync, readFileSync, readdirSync } from "node:fs" import { existsSync, readFileSync, readdirSync } from "node:fs"
import { join } from "node:path" import { join } from "node:path"
import { log } from "../../shared/logger" import { log } from "../../shared/logger"
import { SYSTEM_DIRECTIVE_PREFIX } from "../../shared/system-directive"
import { readState, writeState, clearState, incrementIteration } from "./storage" import { readState, writeState, clearState, incrementIteration } from "./storage"
import { import {
HOOK_NAME, 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. Your previous attempt did not output the completion promise. Continue working on the task.

View File

@ -10,6 +10,7 @@ import {
import { getMainSessionID, subagentSessions } from "../../features/claude-code-session-state" import { getMainSessionID, subagentSessions } from "../../features/claude-code-session-state"
import { findNearestMessageWithFields, MESSAGE_STORAGE } from "../../features/hook-message-injector" import { findNearestMessageWithFields, MESSAGE_STORAGE } from "../../features/hook-message-injector"
import { log } from "../../shared/logger" import { log } from "../../shared/logger"
import { createSystemDirective, SYSTEM_DIRECTIVE_PREFIX, SystemDirectiveTypes } from "../../shared/system-directive"
import type { BackgroundManager } from "../../features/background-agent" import type { BackgroundManager } from "../../features/background-agent"
export const HOOK_NAME = "sisyphus-orchestrator" 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/\`. 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. 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.** **STOP. YOU ARE VIOLATING ORCHESTRATOR PROTOCOL.**
@ -155,7 +156,7 @@ sisyphus_task(
const SINGLE_TASK_DIRECTIVE = ` const SINGLE_TASK_DIRECTIVE = `
[SYSTEM DIRECTIVE - SINGLE TASK ONLY] ${createSystemDirective(SystemDirectiveTypes.SINGLE_TASK_ONLY)}
**STOP. READ THIS BEFORE PROCEEDING.** **STOP. READ THIS BEFORE PROCEEDING.**
@ -626,7 +627,7 @@ export function createSisyphusOrchestratorHook(
// Check sisyphus_task - inject single-task directive // Check sisyphus_task - inject single-task directive
if (input.tool === "sisyphus_task") { if (input.tool === "sisyphus_task") {
const prompt = output.args.prompt as string | undefined 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<system-reminder>${SINGLE_TASK_DIRECTIVE}</system-reminder>` output.args.prompt = prompt + `\n<system-reminder>${SINGLE_TASK_DIRECTIVE}</system-reminder>`
log(`[${HOOK_NAME}] Injected single-task directive to sisyphus_task`, { log(`[${HOOK_NAME}] Injected single-task directive to sisyphus_task`, {
sessionID: input.sessionID, sessionID: input.sessionID,

View File

@ -9,6 +9,7 @@ import {
type ToolPermission, type ToolPermission,
} from "../features/hook-message-injector" } from "../features/hook-message-injector"
import { log } from "../shared/logger" import { log } from "../shared/logger"
import { createSystemDirective, SystemDirectiveTypes } from "../shared/system-directive"
const HOOK_NAME = "todo-continuation-enforcer" const HOOK_NAME = "todo-continuation-enforcer"
@ -40,7 +41,7 @@ interface SessionState {
abortDetectedAt?: number 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. Incomplete tasks remain in your todo list. Continue working on the next pending task.

View File

@ -24,3 +24,4 @@ export * from "./zip-extractor"
export * from "./agent-variant" export * from "./agent-variant"
export * from "./session-cursor" export * from "./session-cursor"
export * from "./shell-env" export * from "./shell-env"
export * from "./system-directive"

View File

@ -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]