Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 22 additions & 9 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# PROJECT KNOWLEDGE BASE

**Generated:** 2025-12-16T16:00:00+09:00
**Commit:** a2d2109
**Branch:** master
**Generated:** 2025-12-22T02:23:00+09:00
**Commit:** aad7a72
**Branch:** dev

## OVERVIEW

Expand All @@ -13,16 +13,16 @@ OpenCode plugin implementing Claude Code/AmpCode features. Multi-model agent orc
```
oh-my-opencode/
├── src/
│ ├── agents/ # AI agents (OmO, oracle, librarian, explore, frontend, document-writer, multimodal-looker)
│ ├── agents/ # AI agents (Sisyphus, oracle, librarian, explore, frontend, document-writer, multimodal-looker)
│ ├── hooks/ # 21 lifecycle hooks (comment-checker, rules-injector, keyword-detector, etc.)
│ ├── tools/ # LSP (11), AST-Grep, Grep, Glob, background-task, look-at, skill, slashcommand, interactive-bash, call-omo-agent
│ ├── mcp/ # MCP servers (context7, websearch_exa, grep_app)
│ ├── features/ # Terminal, Background agent, Claude Code loaders (agent, command, skill, mcp, session-state), hook-message-injector
│ ├── features/ # Background agent, Claude Code loaders (agent, command, skill, mcp, session-state), hook-message-injector
│ ├── config/ # Zod schema, TypeScript types
│ ├── auth/ # Google Antigravity OAuth
│ ├── shared/ # Utilities (deep-merge, pattern-matcher, logger, etc.)
│ └── index.ts # Main plugin entry (OhMyOpenCodePlugin)
├── script/ # build-schema.ts, publish.ts
├── script/ # build-schema.ts, publish.ts, generate-changelog.ts
├── assets/ # JSON schema
└── dist/ # Build output (ESM + .d.ts)
```
Expand Down Expand Up @@ -52,6 +52,7 @@ oh-my-opencode/
- **Directory naming**: kebab-case (`ast-grep/`, `claude-code-hooks/`)
- **Tool structure**: Each tool has index.ts, types.ts, constants.ts, tools.ts, utils.ts
- **Hook pattern**: `createXXXHook(input: PluginInput)` returning event handlers
- **Test style**: BDD comments `#given`, `#when`, `#then` (same as AAA pattern)

## ANTI-PATTERNS (THIS PROJECT)

Expand All @@ -63,6 +64,7 @@ oh-my-opencode/
- **Local version bump**: Version managed by CI workflow, never modify locally
- **Rush completion**: Never mark tasks complete without verification
- **Interrupting work**: Complete tasks fully before stopping
- **Over-exploration**: Stop searching when sufficient context found

## UNIQUE STYLES

Expand All @@ -73,12 +75,13 @@ oh-my-opencode/
- **Agent tools restriction**: Use `tools: { include: [...] }` or `tools: { exclude: [...] }`
- **Temperature**: Most agents use `0.1` for consistency
- **Hook naming**: `createXXXHook` function naming convention
- **Date references**: NEVER use 2024 in code/prompts (use current year)

## AGENT MODELS

| Agent | Model | Purpose |
|-------|-------|---------|
| OmO | anthropic/claude-opus-4-5 | Primary orchestrator, team leader |
| Sisyphus | anthropic/claude-opus-4-5 | Primary orchestrator, team leader |
| oracle | openai/gpt-5.2 | Strategic advisor, code review, architecture |
| librarian | anthropic/claude-sonnet-4-5 | Multi-repo analysis, docs lookup, GitHub examples |
| explore | opencode/grok-code | Fast codebase exploration, file patterns |
Expand All @@ -100,6 +103,9 @@ bun run rebuild

# Build schema only
bun run build:schema

# Run tests
bun test
```

## DEPLOYMENT
Expand All @@ -124,11 +130,18 @@ gh run list --workflow=publish
- Never run `bun publish` directly (OIDC provenance issue)
- Never bump version locally

## CI PIPELINE

- **ci.yml**: Parallel test/typecheck jobs, build verification, auto-commit schema changes on master
- **publish.yml**: Manual workflow_dispatch, version bump, changelog generation, OIDC npm publishing
- Schema auto-commit prevents build drift
- Draft release creation on dev branch

## NOTES

- **No tests**: Test framework not configured
- **Testing**: Bun native test framework (`bun test`), BDD-style with `#given/#when/#then` comments
- **OpenCode version**: Requires >= 1.0.150 (earlier versions have config bugs)
- **Multi-language docs**: README.md (EN), README.ko.md (KO), README.ja.md (JA)
- **Multi-language docs**: README.md (EN), README.ko.md (KO), README.ja.md (JA), README.zh-cn.md (ZH-CN)
- **Config locations**: `~/.config/opencode/oh-my-opencode.json` (user) or `.opencode/oh-my-opencode.json` (project)
- **Schema autocomplete**: Add `$schema` field in config for IDE support
- **Trusted dependencies**: @ast-grep/cli, @ast-grep/napi, @code-yeongyu/comment-checker
57 changes: 57 additions & 0 deletions src/features/background-agent/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import { subagentSessions } from "../claude-code-session-state"

type OpencodeClient = PluginInput["client"]

const COMPLETED_TASK_RETENTION_MS = 5 * 60 * 1000
const MAX_COMPLETED_TASKS = 50

interface MessagePartInfo {
sessionID?: string
type?: string
Expand Down Expand Up @@ -58,12 +61,14 @@ export class BackgroundManager {
private client: OpencodeClient
private directory: string
private pollingInterval?: Timer
private cleanupTimers: Map<string, Timer>

constructor(ctx: PluginInput) {
this.tasks = new Map()
this.notifications = new Map()
this.client = ctx.client
this.directory = ctx.directory
this.cleanupTimers = new Map()
}

async launch(input: LaunchInput): Promise<BackgroundTask> {
Expand Down Expand Up @@ -130,6 +135,7 @@ export class BackgroundManager {
existingTask.completedAt = new Date()
this.markForNotification(existingTask)
this.notifyParentSession(existingTask)
this.scheduleTaskCleanup(existingTask.id)
}
})

Expand Down Expand Up @@ -222,6 +228,7 @@ export class BackgroundManager {
if (!task || task.status !== "running") return

this.checkSessionTodos(sessionID).then((hasIncompleteTodos) => {
if (task.status !== "running") return
if (hasIncompleteTodos) {
log("[background-agent] Task has incomplete todos, waiting for todo-continuation:", task.id)
return
Expand All @@ -231,6 +238,7 @@ export class BackgroundManager {
task.completedAt = new Date()
this.markForNotification(task)
this.notifyParentSession(task)
this.scheduleTaskCleanup(task.id)
log("[background-agent] Task completed via session.idle event:", task.id)
})
}
Expand All @@ -249,6 +257,11 @@ export class BackgroundManager {
task.error = "Session deleted"
}

const cleanupTimer = this.cleanupTimers.get(task.id)
if (cleanupTimer) {
clearTimeout(cleanupTimer)
this.cleanupTimers.delete(task.id)
}
this.tasks.delete(task.id)
this.clearNotificationsForTask(task.id)
subagentSessions.delete(sessionID)
Expand Down Expand Up @@ -295,6 +308,48 @@ export class BackgroundManager {
}
}

private scheduleTaskCleanup(taskId: string): void {
const existingTimer = this.cleanupTimers.get(taskId)
if (existingTimer) {
clearTimeout(existingTimer)
}

const timer = setTimeout(() => {
this.clearNotificationsForTask(taskId)
this.tasks.delete(taskId)
this.cleanupTimers.delete(taskId)
log("[background-agent] Cleaned up completed task after TTL:", taskId)
}, COMPLETED_TASK_RETENTION_MS)
timer.unref?.()

this.cleanupTimers.set(taskId, timer)
this.enforceMaxCompletedTasks()
}

private enforceMaxCompletedTasks(): void {
const completedTasks: Array<{ id: string; completedAt: Date }> = []
for (const task of this.tasks.values()) {
if (task.status !== "running" && task.completedAt) {
completedTasks.push({ id: task.id, completedAt: task.completedAt })
}
}

if (completedTasks.length > MAX_COMPLETED_TASKS) {
completedTasks.sort((a, b) => a.completedAt.getTime() - b.completedAt.getTime())
const toRemove = completedTasks.slice(0, completedTasks.length - MAX_COMPLETED_TASKS)
for (const { id } of toRemove) {
this.clearNotificationsForTask(id)
this.tasks.delete(id)
const timer = this.cleanupTimers.get(id)
if (timer) {
clearTimeout(timer)
this.cleanupTimers.delete(id)
}
log("[background-agent] Evicted old completed task due to max limit:", id)
}
}
}

private notifyParentSession(task: BackgroundTask): void {
const duration = this.formatDuration(task.startedAt, task.completedAt)

Expand Down Expand Up @@ -376,6 +431,7 @@ export class BackgroundManager {

if (sessionStatus.type === "idle") {
const hasIncompleteTodos = await this.checkSessionTodos(task.sessionID)
if (task.status !== "running") continue
if (hasIncompleteTodos) {
log("[background-agent] Task has incomplete todos via polling, waiting:", task.id)
continue
Expand All @@ -385,6 +441,7 @@ export class BackgroundManager {
task.completedAt = new Date()
this.markForNotification(task)
this.notifyParentSession(task)
this.scheduleTaskCleanup(task.id)
log("[background-agent] Task completed via polling:", task.id)
continue
}
Expand Down
51 changes: 35 additions & 16 deletions src/hooks/claude-code-hooks/tool-input-cache.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,51 @@
/**
* Caches tool_input from PreToolUse for PostToolUse
*/

interface CacheEntry {
toolInput: Record<string, unknown>
timestamp: number
}

const cache = new Map<string, CacheEntry>()
const CACHE_TTL = 60000
const MAX_CACHE_SIZE = 1000

let cleanupInterval: ReturnType<typeof setInterval> | null = null

function cleanupExpiredEntries(): void {
const now = Date.now()
for (const [key, entry] of cache.entries()) {
if (now - entry.timestamp > CACHE_TTL) {
cache.delete(key)
}
}
}

const CACHE_TTL = 60000 // 1 minute
function startCleanupInterval(): void {
if (cleanupInterval) return
cleanupInterval = setInterval(cleanupExpiredEntries, CACHE_TTL)
cleanupInterval.unref?.()
}

function stopCleanupInterval(): void {
if (cleanupInterval) {
clearInterval(cleanupInterval)
cleanupInterval = null
}
}

process.on("exit", stopCleanupInterval)

export function cacheToolInput(
sessionId: string,
toolName: string,
invocationId: string,
toolInput: Record<string, unknown>
): void {
startCleanupInterval()

if (cache.size >= MAX_CACHE_SIZE) {
const oldestKey = cache.keys().next().value
if (oldestKey) cache.delete(oldestKey)
}

const key = `${sessionId}:${toolName}:${invocationId}`
cache.set(key, { toolInput, timestamp: Date.now() })
}
Expand All @@ -30,18 +59,8 @@ export function getToolInput(
const entry = cache.get(key)
if (!entry) return null

cache.delete(key)
cache.delete(key)
if (Date.now() - entry.timestamp > CACHE_TTL) return null

return entry.toolInput
}

// Periodic cleanup (every minute)
setInterval(() => {
const now = Date.now()
for (const [key, entry] of cache.entries()) {
if (now - entry.timestamp > CACHE_TTL) {
cache.delete(key)
}
}
}, CACHE_TTL)
18 changes: 16 additions & 2 deletions src/hooks/comment-checker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const pendingCalls = new Map<string, PendingCall>()
const PENDING_CALL_TTL = 60_000

let cliPathPromise: Promise<string | null> | null = null
let cleanupInterval: ReturnType<typeof setInterval> | null = null

function cleanupOldPendingCalls(): void {
const now = Date.now()
Expand All @@ -30,12 +31,25 @@ function cleanupOldPendingCalls(): void {
}
}

setInterval(cleanupOldPendingCalls, 10_000)
function startCleanupInterval(): void {
if (cleanupInterval) return
cleanupInterval = setInterval(cleanupOldPendingCalls, 10_000)
cleanupInterval.unref?.()
}

function stopCleanupInterval(): void {
if (cleanupInterval) {
clearInterval(cleanupInterval)
cleanupInterval = null
}
}

process.on("exit", stopCleanupInterval)

export function createCommentCheckerHooks() {
debugLog("createCommentCheckerHooks called")

// Start background CLI initialization (may trigger lazy download)
startCleanupInterval()
startBackgroundInit()
cliPathPromise = getCommentCheckerPath()
cliPathPromise.then(path => {
Expand Down