Compare commits

..

10 Commits

Author SHA1 Message Date
github-actions[bot]
92c69f4167 release: v1.1.5 2025-12-13 14:15:42 +00:00
YeonGyu-Kim
27403f2682 feat(agents): enhance orchestration prompt and inject to all non-subagent agents
- Add mandatory parallel tool calls section
- Add mandatory 7-section subagent prompt structure guide
- Inject BUILD_AGENT_PROMPT_EXTENSION to all non-subagent agents (not just 'build')
- Fixes issue where custom primary agents don't receive orchestration guidance

🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
2025-12-13 23:14:25 +09:00
YeonGyu-Kim
44ce343708 remove ai slop 2025-12-13 22:36:53 +09:00
github-actions[bot]
ff48ac0745 release: v1.1.4 2025-12-13 13:14:54 +00:00
YeonGyu-Kim
b24b00fad2 feat(agents): add build agent prompt extension and configuration override support
- Add BUILD_AGENT_PROMPT_EXTENSION for orchestrator-focused main agent behavior
- Introduce OverridableAgentName type to allow build agent customization
- Update config schema to support build agent override in oh-my-opencode.json
- Inject orchestration prompt into build agent's system prompt

🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
2025-12-13 22:13:23 +09:00
YeonGyu-Kim
f3b2fccba7 fix(hooks): fix agent-usage-reminder case-sensitivity bug in tool name matching
- Change TARGET_TOOLS and AGENT_TOOLS to Set<string> for O(1) lookup
- Normalize tool names to lowercase for case-insensitive comparison
- Remove unnecessary parentSessionID guard that blocked main session triggers
- Fixes issue where Glob/Grep tool calls weren't showing agent usage reminder

🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
2025-12-13 22:13:02 +09:00
YeonGyu-Kim
2c6dfeadce feat(hooks): add agent-usage-reminder hook for background agent recommendations
Implements hook that tracks whether explore/librarian agents have been used in a session.
When target tools (Grep, Glob, WebFetch, context7, websearch_exa, grep_app) are called
without prior agent usage, appends reminder message recommending parallel background_task calls.

State persists across tool calls and resets on session compaction, allowing fresh reminders
after context compaction - similar to directory-readme-injector pattern.

Files:
- src/hooks/agent-usage-reminder/: New hook implementation
  - types.ts: AgentUsageState interface
  - constants.ts: TARGET_TOOLS, AGENT_TOOLS, REMINDER_MESSAGE
  - storage.ts: File-based state persistence with compaction handling
  - index.ts: Hook implementation with tool.execute.after and event handlers
- src/config/schema.ts: Add 'agent-usage-reminder' to HookNameSchema
- src/hooks/index.ts: Export createAgentUsageReminderHook
- src/index.ts: Instantiate and register hook handlers

🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
2025-12-13 21:47:56 +09:00
YeonGyu-Kim
64b53c0e1c feat(background-task): improve status output UX
- Remove always-zero tool call count from status display
- Show last tool only when available
- Add status-specific notes:
  - running: remind no explicit wait needed (system notifies)
  - error: indicate task failed, check last message

🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
2025-12-13 20:52:35 +09:00
github-actions[bot]
0ae1f8c056 release: v1.1.3 2025-12-13 11:30:29 +00:00
YeonGyu-Kim
3caa84f06b feat(agents): explicitly allow read/bash tools for subagents
- oracle: allow read, call_omo_agent
- explore: allow bash, read
- librarian: allow bash, read

🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
2025-12-13 20:22:56 +09:00
17 changed files with 412 additions and 1053 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode",
"version": "1.1.2",
"version": "1.1.5",
"description": "OpenCode plugin - custom agents (oracle, librarian) and enhanced features",
"main": "dist/index.js",
"types": "dist/index.d.ts",

133
src/agents/build.ts Normal file
View File

@@ -0,0 +1,133 @@
export const BUILD_AGENT_PROMPT_EXTENSION = `
# Agent Orchestration & Task Management
You are not just a coder - you are an **ORCHESTRATOR**. Your primary job is to delegate work to specialized agents and track progress obsessively.
## Think Before Acting
When you receive a user request, STOP and think deeply:
1. **What specialized agents can handle this better than me?**
- explore: File search, codebase navigation, pattern matching
- librarian: Documentation lookup, API references, implementation examples
- oracle: Architecture decisions, code review, complex logic analysis
- frontend-ui-ux-engineer: UI/UX implementation, component design
- document-writer: Documentation, README, technical writing
2. **Can I parallelize this work?**
- Fire multiple background_task calls simultaneously
- Continue working on other parts while agents investigate
- Aggregate results when notified
3. **Have I planned this in my TODO list?**
- Break down the task into atomic steps FIRST
- Track every investigation, every delegation
## PARALLEL TOOL CALLS - MANDATORY
**ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE.** This is non-negotiable.
This parallel approach allows you to:
- Gather comprehensive context faster
- Cross-reference information simultaneously
- Reduce total execution time dramatically
- Maintain high accuracy through concurrent validation
- Complete multi-file modifications in a single turn
**ALWAYS prefer parallel tool calls over sequential ones when the operations are independent.**
## TODO Tool Obsession
**USE TODO TOOLS AGGRESSIVELY.** This is non-negotiable.
### When to Use TodoWrite:
- IMMEDIATELY after receiving a user request
- Before ANY multi-step task (even if it seems "simple")
- When delegating to agents (track what you delegated)
- After completing each step (mark it done)
### TODO Workflow:
\`\`\`
User Request → TodoWrite (plan) → Mark in_progress → Execute/Delegate → Mark complete → Next
\`\`\`
### Rules:
- Only ONE task in_progress at a time
- Mark complete IMMEDIATELY after finishing (never batch)
- Never proceed without updating TODO status
## Delegation Pattern
\`\`\`typescript
// 1. PLAN with TODO first
todowrite([
{ id: "research", content: "Research X implementation", status: "in_progress", priority: "high" },
{ id: "impl", content: "Implement X feature", status: "pending", priority: "high" },
{ id: "test", content: "Test X feature", status: "pending", priority: "medium" }
])
// 2. DELEGATE research in parallel - FIRE MULTIPLE AT ONCE
background_task(agent="explore", prompt="Find all files related to X")
background_task(agent="librarian", prompt="Look up X documentation")
// 3. CONTINUE working on implementation skeleton while agents research
// 4. When notified, INTEGRATE findings and mark TODO complete
\`\`\`
## Subagent Prompt Structure - MANDATORY 7 SECTIONS
When invoking Task() or background_task() with any subagent, ALWAYS structure your prompt with these 7 sections to prevent AI slop:
1. **TASK**: What exactly needs to be done (be obsessively specific)
2. **EXPECTED OUTCOME**: Concrete deliverables when complete (files, behaviors, states)
3. **REQUIRED SKILLS**: Which skills the agent MUST invoke
4. **REQUIRED TOOLS**: Which tools the agent MUST use (context7 MCP, ast-grep, Grep, etc.)
5. **MUST DO**: Exhaustive list of requirements (leave NOTHING implicit)
6. **MUST NOT DO**: Forbidden actions (anticipate every way agent could go rogue)
7. **CONTEXT**: Additional info agent needs (file paths, patterns, dependencies)
Example:
\`\`\`
background_task(agent="explore", prompt="""
TASK: Find all authentication-related files in the codebase
EXPECTED OUTCOME:
- List of all auth files with their purposes
- Identified patterns for token handling
REQUIRED TOOLS:
- ast-grep: Find function definitions with \`sg --pattern 'def $FUNC($$$):' --lang python\`
- Grep: Search for 'auth', 'token', 'jwt' patterns
MUST DO:
- Search in src/, lib/, and utils/ directories
- Include test files for context
MUST NOT DO:
- Do NOT modify any files
- Do NOT make assumptions about implementation
CONTEXT:
- Project uses Python/Django
- Auth system is custom-built
""")
\`\`\`
**Vague prompts = agent goes rogue. Lock them down.**
## Anti-Patterns (AVOID):
- Doing everything yourself when agents can help
- Skipping TODO planning for "quick" tasks
- Forgetting to mark tasks complete
- Sequential execution when parallel is possible
- Direct tool calls without considering delegation
- Vague subagent prompts without the 7 sections
## Remember:
- You are the **team lead**, not the grunt worker
- Your context window is precious - delegate to preserve it
- Agents have specialized expertise - USE THEM
- TODO tracking gives users visibility into your progress
- Parallel execution = faster results
- **ALWAYS fire multiple independent operations simultaneously**
`;

View File

@@ -6,7 +6,7 @@ export const exploreAgent: AgentConfig = {
mode: "subagent",
model: "opencode/grok-code",
temperature: 0.1,
tools: { write: false, edit: false },
tools: { write: false, edit: false, bash: true, read: true },
prompt: `You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
=== CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS ===

View File

@@ -17,3 +17,4 @@ export const builtinAgents: Record<string, AgentConfig> = {
export * from "./types"
export { createBuiltinAgents } from "./utils"
export { BUILD_AGENT_PROMPT_EXTENSION } from "./build"

View File

@@ -6,7 +6,7 @@ export const librarianAgent: AgentConfig = {
mode: "subagent",
model: "anthropic/claude-sonnet-4-5",
temperature: 0.1,
tools: { write: false, edit: false },
tools: { write: false, edit: false, bash: true, read: true },
prompt: `# THE LIBRARIAN
You are **THE LIBRARIAN**, a specialized codebase understanding agent that helps users answer questions about large, complex codebases across repositories.

View File

@@ -8,7 +8,7 @@ export const oracleAgent: AgentConfig = {
temperature: 0.1,
reasoningEffort: "medium",
textVerbosity: "high",
tools: { write: false, edit: false },
tools: { write: false, edit: false, read: true, call_omo_agent: true },
prompt: `You are the Oracle - an expert AI advisor with advanced reasoning capabilities.
Your role is to provide high-quality technical guidance, code reviews, architectural advice, and strategic planning for software engineering tasks.

View File

@@ -1,6 +1,6 @@
import type { AgentConfig } from "@opencode-ai/sdk"
export type AgentName =
export type BuiltinAgentName =
| "oracle"
| "librarian"
| "explore"
@@ -8,6 +8,12 @@ export type AgentName =
| "document-writer"
| "multimodal-looker"
export type OverridableAgentName =
| "build"
| BuiltinAgentName
export type AgentName = BuiltinAgentName
export type AgentOverrideConfig = Partial<AgentConfig>
export type AgentOverrides = Partial<Record<AgentName, AgentOverrideConfig>>
export type AgentOverrides = Partial<Record<OverridableAgentName, AgentOverrideConfig>>

View File

@@ -1,5 +1,5 @@
import type { AgentConfig } from "@opencode-ai/sdk"
import type { AgentName, AgentOverrideConfig, AgentOverrides } from "./types"
import type { BuiltinAgentName, AgentOverrideConfig, AgentOverrides } from "./types"
import { oracleAgent } from "./oracle"
import { librarianAgent } from "./librarian"
import { exploreAgent } from "./explore"
@@ -8,7 +8,7 @@ import { documentWriterAgent } from "./document-writer"
import { multimodalLookerAgent } from "./multimodal-looker"
import { deepMerge } from "../shared"
const allBuiltinAgents: Record<AgentName, AgentConfig> = {
const allBuiltinAgents: Record<BuiltinAgentName, AgentConfig> = {
oracle: oracleAgent,
librarian: librarianAgent,
explore: exploreAgent,
@@ -25,13 +25,13 @@ function mergeAgentConfig(
}
export function createBuiltinAgents(
disabledAgents: AgentName[] = [],
disabledAgents: BuiltinAgentName[] = [],
agentOverrides: AgentOverrides = {}
): Record<string, AgentConfig> {
const result: Record<string, AgentConfig> = {}
for (const [name, config] of Object.entries(allBuiltinAgents)) {
const agentName = name as AgentName
const agentName = name as BuiltinAgentName
if (disabledAgents.includes(agentName)) {
continue

View File

@@ -16,7 +16,7 @@ const AgentPermissionSchema = z.object({
external_directory: PermissionValue.optional(),
})
export const AgentNameSchema = z.enum([
export const BuiltinAgentNameSchema = z.enum([
"oracle",
"librarian",
"explore",
@@ -25,6 +25,18 @@ export const AgentNameSchema = z.enum([
"multimodal-looker",
])
export const OverridableAgentNameSchema = z.enum([
"build",
"oracle",
"librarian",
"explore",
"frontend-ui-ux-engineer",
"document-writer",
"multimodal-looker",
])
export const AgentNameSchema = BuiltinAgentNameSchema
export const HookNameSchema = z.enum([
"todo-continuation-enforcer",
"context-window-monitor",
@@ -41,6 +53,7 @@ export const HookNameSchema = z.enum([
"background-notification",
"auto-update-checker",
"ultrawork-mode",
"agent-usage-reminder",
])
export const AgentOverrideConfigSchema = z.object({
@@ -61,6 +74,7 @@ export const AgentOverrideConfigSchema = z.object({
export const AgentOverridesSchema = z
.object({
build: AgentOverrideConfigSchema.optional(),
oracle: AgentOverrideConfigSchema.optional(),
librarian: AgentOverrideConfigSchema.optional(),
explore: AgentOverrideConfigSchema.optional(),
@@ -81,7 +95,7 @@ export const ClaudeCodeConfigSchema = z.object({
export const OhMyOpenCodeConfigSchema = z.object({
$schema: z.string().optional(),
disabled_mcps: z.array(McpNameSchema).optional(),
disabled_agents: z.array(AgentNameSchema).optional(),
disabled_agents: z.array(BuiltinAgentNameSchema).optional(),
disabled_hooks: z.array(HookNameSchema).optional(),
agents: AgentOverridesSchema.optional(),
claude_code: ClaudeCodeConfigSchema.optional(),

View File

@@ -0,0 +1,53 @@
import { join } from "node:path";
import { xdgData } from "xdg-basedir";
export const OPENCODE_STORAGE = join(xdgData ?? "", "opencode", "storage");
export const AGENT_USAGE_REMINDER_STORAGE = join(
OPENCODE_STORAGE,
"agent-usage-reminder",
);
// All tool names normalized to lowercase for case-insensitive matching
export const TARGET_TOOLS = new Set([
"grep",
"safe_grep",
"glob",
"safe_glob",
"webfetch",
"context7_resolve-library-id",
"context7_get-library-docs",
"websearch_exa_web_search_exa",
"grep_app_searchgithub",
]);
export const AGENT_TOOLS = new Set([
"task",
"call_omo_agent",
"background_task",
]);
export const REMINDER_MESSAGE = `
[Agent Usage Reminder]
You called a search/fetch tool directly without leveraging specialized agents.
RECOMMENDED: Use background_task with explore/librarian agents for better results:
\`\`\`
// Parallel exploration - fire multiple agents simultaneously
background_task(agent="explore", prompt="Find all files matching pattern X")
background_task(agent="explore", prompt="Search for implementation of Y")
background_task(agent="librarian", prompt="Lookup documentation for Z")
// Then continue your work while they run in background
// System will notify you when each completes
\`\`\`
WHY:
- Agents can perform deeper, more thorough searches
- Background tasks run in parallel, saving time
- Specialized agents have domain expertise
- Reduces context window usage in main session
ALWAYS prefer: Multiple parallel background_task calls > Direct tool calls
`;

View File

@@ -0,0 +1,109 @@
import type { PluginInput } from "@opencode-ai/plugin";
import {
loadAgentUsageState,
saveAgentUsageState,
clearAgentUsageState,
} from "./storage";
import { TARGET_TOOLS, AGENT_TOOLS, REMINDER_MESSAGE } from "./constants";
import type { AgentUsageState } from "./types";
interface ToolExecuteInput {
tool: string;
sessionID: string;
callID: string;
}
interface ToolExecuteOutput {
title: string;
output: string;
metadata: unknown;
}
interface EventInput {
event: {
type: string;
properties?: unknown;
};
}
export function createAgentUsageReminderHook(_ctx: PluginInput) {
const sessionStates = new Map<string, AgentUsageState>();
function getOrCreateState(sessionID: string): AgentUsageState {
if (!sessionStates.has(sessionID)) {
const persisted = loadAgentUsageState(sessionID);
const state: AgentUsageState = persisted ?? {
sessionID,
agentUsed: false,
reminderCount: 0,
updatedAt: Date.now(),
};
sessionStates.set(sessionID, state);
}
return sessionStates.get(sessionID)!;
}
function markAgentUsed(sessionID: string): void {
const state = getOrCreateState(sessionID);
state.agentUsed = true;
state.updatedAt = Date.now();
saveAgentUsageState(state);
}
function resetState(sessionID: string): void {
sessionStates.delete(sessionID);
clearAgentUsageState(sessionID);
}
const toolExecuteAfter = async (
input: ToolExecuteInput,
output: ToolExecuteOutput,
) => {
const { tool, sessionID } = input;
const toolLower = tool.toLowerCase();
if (AGENT_TOOLS.has(toolLower)) {
markAgentUsed(sessionID);
return;
}
if (!TARGET_TOOLS.has(toolLower)) {
return;
}
const state = getOrCreateState(sessionID);
if (state.agentUsed) {
return;
}
output.output += REMINDER_MESSAGE;
state.reminderCount++;
state.updatedAt = Date.now();
saveAgentUsageState(state);
};
const eventHandler = async ({ event }: EventInput) => {
const props = event.properties as Record<string, unknown> | undefined;
if (event.type === "session.deleted") {
const sessionInfo = props?.info as { id?: string } | undefined;
if (sessionInfo?.id) {
resetState(sessionInfo.id);
}
}
if (event.type === "session.compacted") {
const sessionID = (props?.sessionID ??
(props?.info as { id?: string } | undefined)?.id) as string | undefined;
if (sessionID) {
resetState(sessionID);
}
}
};
return {
"tool.execute.after": toolExecuteAfter,
event: eventHandler,
};
}

View File

@@ -0,0 +1,42 @@
import {
existsSync,
mkdirSync,
readFileSync,
writeFileSync,
unlinkSync,
} from "node:fs";
import { join } from "node:path";
import { AGENT_USAGE_REMINDER_STORAGE } from "./constants";
import type { AgentUsageState } from "./types";
function getStoragePath(sessionID: string): string {
return join(AGENT_USAGE_REMINDER_STORAGE, `${sessionID}.json`);
}
export function loadAgentUsageState(sessionID: string): AgentUsageState | null {
const filePath = getStoragePath(sessionID);
if (!existsSync(filePath)) return null;
try {
const content = readFileSync(filePath, "utf-8");
return JSON.parse(content) as AgentUsageState;
} catch {
return null;
}
}
export function saveAgentUsageState(state: AgentUsageState): void {
if (!existsSync(AGENT_USAGE_REMINDER_STORAGE)) {
mkdirSync(AGENT_USAGE_REMINDER_STORAGE, { recursive: true });
}
const filePath = getStoragePath(state.sessionID);
writeFileSync(filePath, JSON.stringify(state, null, 2));
}
export function clearAgentUsageState(sessionID: string): void {
const filePath = getStoragePath(sessionID);
if (existsSync(filePath)) {
unlinkSync(filePath);
}
}

View File

@@ -0,0 +1,6 @@
export interface AgentUsageState {
sessionID: string;
agentUsed: boolean;
reminderCount: number;
updatedAt: number;
}

View File

@@ -14,3 +14,4 @@ export { createRulesInjectorHook } from "./rules-injector";
export { createBackgroundNotificationHook } from "./background-notification"
export { createAutoUpdateCheckerHook } from "./auto-update-checker";
export { createUltraworkModeHook } from "./ultrawork-mode";
export { createAgentUsageReminderHook } from "./agent-usage-reminder";

View File

@@ -1,5 +1,5 @@
import type { Plugin } from "@opencode-ai/plugin";
import { createBuiltinAgents } from "./agents";
import { createBuiltinAgents, BUILD_AGENT_PROMPT_EXTENSION } from "./agents";
import {
createTodoContinuationEnforcer,
createContextWindowMonitorHook,
@@ -17,6 +17,7 @@ import {
createBackgroundNotificationHook,
createAutoUpdateCheckerHook,
createUltraworkModeHook,
createAgentUsageReminderHook,
} from "./hooks";
import { createGoogleAntigravityAuthPlugin } from "./auth/antigravity";
import {
@@ -207,6 +208,9 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
const ultraworkMode = isHookEnabled("ultrawork-mode")
? createUltraworkModeHook()
: null;
const agentUsageReminder = isHookEnabled("agent-usage-reminder")
? createAgentUsageReminderHook(ctx)
: null;
updateTerminalTitle({ sessionId: "main" });
@@ -254,6 +258,20 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
...projectAgents,
...config.agent,
};
// Inject orchestration prompt to all non-subagent agents
// Subagents are delegated TO, so they don't need orchestration guidance
for (const [agentName, agentConfig] of Object.entries(config.agent ?? {})) {
if (agentConfig && agentConfig.mode !== "subagent") {
const existingPrompt = agentConfig.prompt || "";
const userOverride = pluginConfig.agents?.[agentName as keyof typeof pluginConfig.agents]?.prompt || "";
config.agent[agentName] = {
...agentConfig,
prompt: existingPrompt + BUILD_AGENT_PROMPT_EXTENSION + userOverride,
};
}
}
config.tools = {
...config.tools,
};
@@ -320,6 +338,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
await thinkMode?.event(input);
await anthropicAutoCompact?.event(input);
await ultraworkMode?.event(input);
await agentUsageReminder?.event(input);
const { event } = input;
const props = event.properties as Record<string, unknown> | undefined;
@@ -439,6 +458,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
await directoryReadmeInjector?.["tool.execute.after"](input, output);
await rulesInjector?.["tool.execute.after"](input, output);
await emptyTaskResponseDetector?.["tool.execute.after"](input, output);
await agentUsageReminder?.["tool.execute.after"](input, output);
if (input.sessionID === getMainSessionID()) {
updateTerminalTitle({

View File

@@ -72,8 +72,8 @@ function formatTaskStatus(task: BackgroundTask): string {
const promptPreview = truncateText(task.prompt, 500)
let progressSection = ""
if (task.progress) {
progressSection = `\nTool calls: ${task.progress.toolCalls}\nLast tool: ${task.progress.lastTool ?? "N/A"}`
if (task.progress?.lastTool) {
progressSection = `\n| Last tool | ${task.progress.lastTool} |`
}
let lastMessageSection = ""
@@ -91,6 +91,17 @@ ${truncated}
\`\`\``
}
let statusNote = ""
if (task.status === "running") {
statusNote = `
> **Note**: No need to wait explicitly - the system will notify you when this task completes.`
} else if (task.status === "error") {
statusNote = `
> **Failed**: The task encountered an error. Check the last message for details.`
}
return `# Task Status
| Field | Value |
@@ -101,7 +112,7 @@ ${truncated}
| Status | **${task.status}** |
| Duration | ${duration} |
| Session ID | \`${task.sessionID}\` |${progressSection}
${statusNote}
## Original Prompt
\`\`\`