Fix Issue #1428: Deny bash permission for Prometheus agent
- Change PROMETHEUS_PERMISSION bash from 'allow' to 'deny' to prevent unrestricted bash execution - Prometheus is a read-only planner and should not execute bash commands - The prometheus-md-only hook provides additional blocking as backup
This commit is contained in:
parent
6bb9a3b7bc
commit
42dbc8f39c
@ -1,7 +1,7 @@
|
|||||||
# PROJECT KNOWLEDGE BASE
|
# PROJECT KNOWLEDGE BASE
|
||||||
|
|
||||||
**Generated:** 2026-02-06T18:30:00+09:00
|
**Generated:** 2026-02-08T16:45:00+09:00
|
||||||
**Commit:** c6c149e
|
**Commit:** f2b7b75
|
||||||
**Branch:** dev
|
**Branch:** dev
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
128
src/AGENTS.md
Normal file
128
src/AGENTS.md
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
# AGENTS KNOWLEDGE BASE
|
||||||
|
|
||||||
|
## OVERVIEW
|
||||||
|
|
||||||
|
Main plugin entry point and orchestration layer. 1000+ lines of plugin initialization, hook registration, tool composition, and lifecycle management.
|
||||||
|
|
||||||
|
**Core Responsibilities:**
|
||||||
|
- Plugin initialization and configuration loading
|
||||||
|
- 40+ lifecycle hooks orchestration
|
||||||
|
- 25+ tools composition and filtering
|
||||||
|
- Background agent management
|
||||||
|
- Session state coordination
|
||||||
|
- MCP server lifecycle
|
||||||
|
- Tmux integration
|
||||||
|
- Claude Code compatibility layer
|
||||||
|
|
||||||
|
## STRUCTURE
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── index.ts # Main plugin entry (1000 lines) - orchestration layer
|
||||||
|
├── index.compaction-model-agnostic.static.test.ts # Compaction hook tests
|
||||||
|
├── agents/ # 11 AI agents (16 files)
|
||||||
|
├── cli/ # CLI commands (9 files)
|
||||||
|
├── config/ # Schema validation (3 files)
|
||||||
|
├── features/ # Background features (20+ files)
|
||||||
|
├── hooks/ # 40+ lifecycle hooks (14 files)
|
||||||
|
├── mcp/ # MCP server configs (7 files)
|
||||||
|
├── plugin-handlers/ # Config loading (3 files)
|
||||||
|
├── shared/ # Utilities (70 files)
|
||||||
|
└── tools/ # 25+ tools (15 files)
|
||||||
|
```
|
||||||
|
|
||||||
|
## KEY COMPONENTS
|
||||||
|
|
||||||
|
**Plugin Initialization:**
|
||||||
|
- `OhMyOpenCodePlugin()`: Main plugin factory (lines 124-841)
|
||||||
|
- Configuration loading via `loadPluginConfig()`
|
||||||
|
- Hook registration with safe creation patterns
|
||||||
|
- Tool composition and disabled tool filtering
|
||||||
|
|
||||||
|
**Lifecycle Management:**
|
||||||
|
- 40+ hooks: session recovery, continuation enforcers, compaction, context injection
|
||||||
|
- Background agent coordination via `BackgroundManager`
|
||||||
|
- Tmux session management for multi-pane workflows
|
||||||
|
- MCP server lifecycle via `SkillMcpManager`
|
||||||
|
|
||||||
|
**Tool Ecosystem:**
|
||||||
|
- 25+ tools: LSP, AST-grep, delegation, background tasks, skills
|
||||||
|
- Tool filtering based on agent permissions and user config
|
||||||
|
- Metadata restoration for tool outputs
|
||||||
|
|
||||||
|
**Integration Points:**
|
||||||
|
- Claude Code compatibility hooks and commands
|
||||||
|
- OpenCode SDK client interactions
|
||||||
|
- Session state persistence and recovery
|
||||||
|
- Model variant resolution and application
|
||||||
|
|
||||||
|
## HOOK REGISTRATION PATTERNS
|
||||||
|
|
||||||
|
**Safe Hook Creation:**
|
||||||
|
```typescript
|
||||||
|
const hook = isHookEnabled("hook-name")
|
||||||
|
? safeCreateHook("hook-name", () => createHookFactory(ctx), { enabled: safeHookEnabled })
|
||||||
|
: null;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hook Categories:**
|
||||||
|
- **Session Management**: recovery, notification, compaction
|
||||||
|
- **Continuation**: todo/task enforcers, stop guards
|
||||||
|
- **Context**: injection, rules, directory content
|
||||||
|
- **Tool Enhancement**: output truncation, error recovery, validation
|
||||||
|
- **Agent Coordination**: usage reminders, babysitting, delegation
|
||||||
|
|
||||||
|
## TOOL COMPOSITION
|
||||||
|
|
||||||
|
**Core Tools:**
|
||||||
|
```typescript
|
||||||
|
const allTools: Record<string, ToolDefinition> = {
|
||||||
|
...builtinTools, // Basic file/session operations
|
||||||
|
...createGrepTools(ctx), // Content search
|
||||||
|
...createAstGrepTools(ctx), // AST-aware refactoring
|
||||||
|
task: delegateTask, // Agent delegation
|
||||||
|
skill: skillTool, // Skill execution
|
||||||
|
// ... 20+ more tools
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tool Filtering:**
|
||||||
|
- Agent permission-based restrictions
|
||||||
|
- User-configured disabled tools
|
||||||
|
- Dynamic tool availability based on session state
|
||||||
|
|
||||||
|
## SESSION LIFECYCLE
|
||||||
|
|
||||||
|
**Session Events:**
|
||||||
|
- `session.created`: Initialize session state, tmux setup
|
||||||
|
- `session.deleted`: Cleanup resources, clear caches
|
||||||
|
- `message.updated`: Update agent assignments
|
||||||
|
- `session.error`: Trigger recovery mechanisms
|
||||||
|
|
||||||
|
**Continuation Flow:**
|
||||||
|
1. User message triggers agent selection
|
||||||
|
2. Model/variant resolution applied
|
||||||
|
3. Tools execute with hook interception
|
||||||
|
4. Continuation enforcers monitor completion
|
||||||
|
5. Session compaction preserves context
|
||||||
|
|
||||||
|
## CONFIGURATION INTEGRATION
|
||||||
|
|
||||||
|
**Plugin Config Loading:**
|
||||||
|
- Project + user config merging
|
||||||
|
- Schema validation via Zod
|
||||||
|
- Migration support for legacy configs
|
||||||
|
- Dynamic feature enablement
|
||||||
|
|
||||||
|
**Runtime Configuration:**
|
||||||
|
- Hook enablement based on `disabled_hooks`
|
||||||
|
- Tool filtering via `disabled_tools`
|
||||||
|
- Agent overrides and category definitions
|
||||||
|
- Experimental feature toggles
|
||||||
|
|
||||||
|
## ANTI-PATTERNS
|
||||||
|
|
||||||
|
- **Direct hook exports**: All hooks created via factories for testability
|
||||||
|
- **Global state pollution**: Session-scoped state management
|
||||||
|
- **Synchronous blocking**: Async-first architecture with background coordination
|
||||||
|
- **Tight coupling**: Plugin components communicate via events, not direct calls
|
||||||
|
- **Memory leaks**: Proper cleanup on session deletion and plugin unload
|
||||||
@ -1207,4 +1207,29 @@ describe("Deadlock prevention - fetchAvailableModels must not receive client", (
|
|||||||
fetchSpy.mockRestore?.()
|
fetchSpy.mockRestore?.()
|
||||||
cacheSpy.mockRestore?.()
|
cacheSpy.mockRestore?.()
|
||||||
})
|
})
|
||||||
|
test("Hephaestus variant override respects user config over hardcoded default", async () => {
|
||||||
|
// #given - user provides variant in config
|
||||||
|
const overrides = {
|
||||||
|
hephaestus: { variant: "high" },
|
||||||
|
}
|
||||||
|
|
||||||
|
// #when
|
||||||
|
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
||||||
|
|
||||||
|
// #then - user variant takes precedence over hardcoded "medium"
|
||||||
|
expect(agents.hephaestus).toBeDefined()
|
||||||
|
expect(agents.hephaestus.variant).toBe("high")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Hephaestus uses default variant when no user override provided", async () => {
|
||||||
|
// #given - no variant override in config
|
||||||
|
const overrides = {}
|
||||||
|
|
||||||
|
// #when
|
||||||
|
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
||||||
|
|
||||||
|
// #then - default "medium" variant is applied
|
||||||
|
expect(agents.hephaestus).toBeDefined()
|
||||||
|
expect(agents.hephaestus.variant).toBe("medium")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -509,13 +509,13 @@ export async function createBuiltinAgents(
|
|||||||
availableCategories
|
availableCategories
|
||||||
)
|
)
|
||||||
|
|
||||||
hephaestusConfig = { ...hephaestusConfig, variant: hephaestusResolvedVariant ?? "medium" }
|
if (!hephaestusOverride?.variant) {
|
||||||
|
hephaestusConfig = { ...hephaestusConfig, variant: hephaestusResolvedVariant ?? "medium" }
|
||||||
|
}
|
||||||
const hepOverrideCategory = (hephaestusOverride as Record<string, unknown> | undefined)?.category as string | undefined
|
const hepOverrideCategory = (hephaestusOverride as Record<string, unknown> | undefined)?.category as string | undefined
|
||||||
if (hepOverrideCategory) {
|
if (hepOverrideCategory) {
|
||||||
hephaestusConfig = applyCategoryOverride(hephaestusConfig, hepOverrideCategory, mergedCategories)
|
hephaestusConfig = applyCategoryOverride(hephaestusConfig, hepOverrideCategory, mergedCategories)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (directory && hephaestusConfig.prompt) {
|
if (directory && hephaestusConfig.prompt) {
|
||||||
const envContext = createEnvContext()
|
const envContext = createEnvContext()
|
||||||
hephaestusConfig = { ...hephaestusConfig, prompt: hephaestusConfig.prompt + envContext }
|
hephaestusConfig = { ...hephaestusConfig, prompt: hephaestusConfig.prompt + envContext }
|
||||||
|
|||||||
93
src/config/AGENTS.md
Normal file
93
src/config/AGENTS.md
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
**Generated:** 2026-02-08T16:45:00+09:00
|
||||||
|
**Commit:** f2b7b759
|
||||||
|
**Branch:** dev
|
||||||
|
|
||||||
|
## OVERVIEW
|
||||||
|
|
||||||
|
Zod schema definitions for plugin configuration. 455+ lines of type-safe config validation with JSONC support, multi-level inheritance, and comprehensive agent/category overrides.
|
||||||
|
|
||||||
|
## STRUCTURE
|
||||||
|
```
|
||||||
|
config/
|
||||||
|
├── schema.ts # Main Zod schema (455 lines) - agents, categories, experimental features
|
||||||
|
├── schema.test.ts # Schema validation tests (17909 lines)
|
||||||
|
└── index.ts # Barrel export
|
||||||
|
```
|
||||||
|
|
||||||
|
## SCHEMA COMPONENTS
|
||||||
|
|
||||||
|
**Agent Configuration:**
|
||||||
|
- `AgentOverrideConfigSchema`: Model, variant, temperature, permissions, tools
|
||||||
|
- `AgentOverridesSchema`: Per-agent overrides (sisyphus, hephaestus, prometheus, etc.)
|
||||||
|
- `AgentPermissionSchema`: Tool access control (edit, bash, webfetch, task)
|
||||||
|
|
||||||
|
**Category Configuration:**
|
||||||
|
- `CategoryConfigSchema`: Model defaults, thinking budgets, tool restrictions
|
||||||
|
- `CategoriesConfigSchema`: Named categories (visual-engineering, ultrabrain, deep, etc.)
|
||||||
|
|
||||||
|
**Experimental Features:**
|
||||||
|
- `ExperimentalConfigSchema`: Dynamic context pruning, task system, plugin timeouts
|
||||||
|
- `DynamicContextPruningConfigSchema`: Intelligent context management
|
||||||
|
|
||||||
|
**Built-in Enums:**
|
||||||
|
- `AgentNameSchema`: sisyphus, hephaestus, prometheus, oracle, librarian, explore, multimodal-looker, metis, momus, atlas
|
||||||
|
- `HookNameSchema`: 100+ hook names for lifecycle management
|
||||||
|
- `BuiltinCommandNameSchema`: init-deep, ralph-loop, refactor, start-work
|
||||||
|
- `BuiltinSkillNameSchema`: playwright, agent-browser, git-master
|
||||||
|
|
||||||
|
## CONFIGURATION HIERARCHY
|
||||||
|
|
||||||
|
1. **Project config** (`.opencode/oh-my-opencode.json`)
|
||||||
|
2. **User config** (`~/.config/opencode/oh-my-opencode.json`)
|
||||||
|
3. **Defaults** (hardcoded fallbacks)
|
||||||
|
|
||||||
|
**Multi-level inheritance:** Project → User → Defaults
|
||||||
|
|
||||||
|
## VALIDATION FEATURES
|
||||||
|
|
||||||
|
- **JSONC support**: Comments and trailing commas
|
||||||
|
- **Type safety**: Full TypeScript inference
|
||||||
|
- **Migration support**: Legacy config compatibility
|
||||||
|
- **Schema versioning**: $schema field for validation
|
||||||
|
|
||||||
|
## KEY SCHEMAS
|
||||||
|
|
||||||
|
| Schema | Purpose | Lines |
|
||||||
|
|--------|---------|-------|
|
||||||
|
| `OhMyOpenCodeConfigSchema` | Root config schema | 400+ |
|
||||||
|
| `AgentOverrideConfigSchema` | Agent customization | 50+ |
|
||||||
|
| `CategoryConfigSchema` | Task category defaults | 30+ |
|
||||||
|
| `ExperimentalConfigSchema` | Beta features | 40+ |
|
||||||
|
|
||||||
|
## USAGE PATTERNS
|
||||||
|
|
||||||
|
**Agent Override:**
|
||||||
|
```typescript
|
||||||
|
agents: {
|
||||||
|
sisyphus: {
|
||||||
|
model: "anthropic/claude-opus-4-6",
|
||||||
|
variant: "max",
|
||||||
|
temperature: 0.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Category Definition:**
|
||||||
|
```typescript
|
||||||
|
categories: {
|
||||||
|
"visual-engineering": {
|
||||||
|
model: "google/gemini-3-pro",
|
||||||
|
variant: "high"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Experimental Features:**
|
||||||
|
```typescript
|
||||||
|
experimental: {
|
||||||
|
dynamic_context_pruning: {
|
||||||
|
enabled: true,
|
||||||
|
notification: "detailed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@ -2,61 +2,29 @@
|
|||||||
|
|
||||||
## OVERVIEW
|
## OVERVIEW
|
||||||
|
|
||||||
17 feature modules: background agents, skill MCPs, builtin skills/commands, Claude Code compatibility layer, task management.
|
Background agents, skills, Claude Code compat, builtin commands, MCP managers, etc.
|
||||||
|
|
||||||
**Feature Types**: Task orchestration, Skill definitions, Command templates, Claude Code loaders, Supporting utilities
|
|
||||||
|
|
||||||
## STRUCTURE
|
## STRUCTURE
|
||||||
|
|
||||||
```
|
|
||||||
features/
|
features/
|
||||||
├── background-agent/ # Task lifecycle (1556 lines)
|
├── background-agent/ # Task lifecycle, concurrency (manager.ts 1642 lines)
|
||||||
│ ├── manager.ts # Launch → poll → complete
|
├── builtin-skills/ # Skills like git-master (1107 lines)
|
||||||
│ └── concurrency.ts # Per-provider limits
|
├── builtin-commands/ # Commands like refactor (619 lines)
|
||||||
├── builtin-skills/ # Core skills
|
├── skill-mcp-manager/ # MCP client lifecycle (640 lines)
|
||||||
│ └── skills/ # playwright, agent-browser, frontend-ui-ux, git-master, dev-browser
|
├── claude-code-plugin-loader/ # Plugin loading
|
||||||
├── builtin-commands/ # ralph-loop, refactor, ulw-loop, init-deep, start-work, cancel-ralph, stop-continuation
|
├── claude-code-mcp-loader/ # MCP loading
|
||||||
├── claude-code-agent-loader/ # ~/.claude/agents/*.md
|
├── claude-code-session-state/ # Session state
|
||||||
├── claude-code-command-loader/ # ~/.claude/commands/*.md
|
├── claude-code-command-loader/ # Command loading
|
||||||
├── claude-code-mcp-loader/ # .mcp.json with ${VAR} expansion
|
├── claude-code-agent-loader/ # Agent loading
|
||||||
├── claude-code-plugin-loader/ # installed_plugins.json (486 lines)
|
├── context-injector/ # Context injection
|
||||||
├── claude-code-session-state/ # Session persistence
|
├── hook-message-injector/ # Message injection
|
||||||
├── opencode-skill-loader/ # Skills from 6 directories (loader.ts 311 lines)
|
├── task-toast-manager/ # Task toasts
|
||||||
├── context-injector/ # AGENTS.md/README.md injection
|
├── boulder-state/ # State management
|
||||||
├── boulder-state/ # Todo state persistence
|
├── tmux-subagent/ # Tmux subagent
|
||||||
├── hook-message-injector/ # Message injection
|
├── mcp-oauth/ # OAuth for MCP
|
||||||
├── task-toast-manager/ # Background task notifications
|
├── opencode-skill-loader/ # Skill loading
|
||||||
├── skill-mcp-manager/ # MCP client lifecycle (640 lines)
|
├── tool-metadata-store/ # Tool metadata
|
||||||
├── tmux-subagent/ # Tmux session management (472 lines)
|
|
||||||
├── mcp-oauth/ # MCP OAuth handling
|
|
||||||
└── claude-tasks/ # Task schema/storage - see AGENTS.md
|
|
||||||
```
|
|
||||||
|
|
||||||
## LOADER PRIORITY
|
## HOW TO ADD
|
||||||
|
|
||||||
| Type | Priority (highest first) |
|
Create dir with index.ts, types.ts, etc.
|
||||||
|------|--------------------------|
|
|
||||||
| Commands | `.opencode/command/` > `~/.config/opencode/command/` > `.claude/commands/` |
|
|
||||||
| Skills | `.opencode/skills/` > `~/.config/opencode/skills/` > `.claude/skills/` |
|
|
||||||
| MCPs | `.claude/.mcp.json` > `.mcp.json` > `~/.claude/.mcp.json` |
|
|
||||||
|
|
||||||
## BACKGROUND AGENT
|
|
||||||
|
|
||||||
- **Lifecycle**: `launch` → `poll` (2s) → `complete`
|
|
||||||
- **Stability**: 3 consecutive polls = idle
|
|
||||||
- **Concurrency**: Per-provider/model limits via `ConcurrencyManager`
|
|
||||||
- **Cleanup**: 30m TTL, 3m stale timeout
|
|
||||||
- **State**: Per-session Maps, cleaned on `session.deleted`
|
|
||||||
|
|
||||||
## SKILL MCP
|
|
||||||
|
|
||||||
- **Lazy**: Clients created on first call
|
|
||||||
- **Transports**: stdio, http (SSE/Streamable)
|
|
||||||
- **Lifecycle**: 5m idle cleanup
|
|
||||||
|
|
||||||
## ANTI-PATTERNS
|
|
||||||
|
|
||||||
- **Sequential delegation**: Use `task` parallel
|
|
||||||
- **Trust self-reports**: ALWAYS verify
|
|
||||||
- **Main thread blocks**: No heavy I/O in loader init
|
|
||||||
- **Direct state mutation**: Use managers for boulder/session state
|
|
||||||
|
|||||||
@ -0,0 +1,46 @@
|
|||||||
|
import type { OpencodeClient } from "../constants"
|
||||||
|
import type { ConcurrencyManager } from "../concurrency"
|
||||||
|
import type { LaunchInput } from "../types"
|
||||||
|
import { log } from "../../../shared"
|
||||||
|
|
||||||
|
export async function createBackgroundSession(options: {
|
||||||
|
client: OpencodeClient
|
||||||
|
input: LaunchInput
|
||||||
|
parentDirectory: string
|
||||||
|
concurrencyManager: ConcurrencyManager
|
||||||
|
concurrencyKey: string
|
||||||
|
}): Promise<string> {
|
||||||
|
const { client, input, parentDirectory, concurrencyManager, concurrencyKey } = options
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
parentID: input.parentSessionID,
|
||||||
|
title: `Background: ${input.description}`,
|
||||||
|
permission: [{ permission: "question", action: "deny" as const, pattern: "*" }],
|
||||||
|
}
|
||||||
|
|
||||||
|
const createResult = await client.session
|
||||||
|
.create({
|
||||||
|
body,
|
||||||
|
query: {
|
||||||
|
directory: parentDirectory,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
concurrencyManager.release(concurrencyKey)
|
||||||
|
throw error
|
||||||
|
})
|
||||||
|
|
||||||
|
if (createResult.error) {
|
||||||
|
concurrencyManager.release(concurrencyKey)
|
||||||
|
throw new Error(`Failed to create background session: ${createResult.error}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!createResult.data?.id) {
|
||||||
|
concurrencyManager.release(concurrencyKey)
|
||||||
|
throw new Error("Failed to create background session: API returned no session ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionID = createResult.data.id
|
||||||
|
log("[background-agent] Background session created", { sessionID })
|
||||||
|
return sessionID
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import type { LaunchInput } from "../types"
|
||||||
|
|
||||||
|
export function getConcurrencyKeyFromLaunchInput(input: LaunchInput): string {
|
||||||
|
return input.model
|
||||||
|
? `${input.model.providerID}/${input.model.modelID}`
|
||||||
|
: input.agent
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import type { OpencodeClient } from "../constants"
|
||||||
|
import { log } from "../../../shared"
|
||||||
|
|
||||||
|
export async function resolveParentDirectory(options: {
|
||||||
|
client: OpencodeClient
|
||||||
|
parentSessionID: string
|
||||||
|
defaultDirectory: string
|
||||||
|
}): Promise<string> {
|
||||||
|
const { client, parentSessionID, defaultDirectory } = options
|
||||||
|
|
||||||
|
const parentSession = await client.session
|
||||||
|
.get({ path: { id: parentSessionID } })
|
||||||
|
.catch((error) => {
|
||||||
|
log(`[background-agent] Failed to get parent session: ${error}`)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
const parentDirectory = parentSession?.data?.directory ?? defaultDirectory
|
||||||
|
log(`[background-agent] Parent dir: ${parentSession?.data?.directory}, using: ${parentDirectory}`)
|
||||||
|
return parentDirectory
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
import type { OnSubagentSessionCreated } from "../constants"
|
||||||
|
import { TMUX_CALLBACK_DELAY_MS } from "../constants"
|
||||||
|
import { log } from "../../../shared"
|
||||||
|
import { isInsideTmux } from "../../../shared/tmux"
|
||||||
|
|
||||||
|
export async function maybeInvokeTmuxCallback(options: {
|
||||||
|
onSubagentSessionCreated?: OnSubagentSessionCreated
|
||||||
|
tmuxEnabled: boolean
|
||||||
|
sessionID: string
|
||||||
|
parentID: string
|
||||||
|
title: string
|
||||||
|
}): Promise<void> {
|
||||||
|
const { onSubagentSessionCreated, tmuxEnabled, sessionID, parentID, title } = options
|
||||||
|
|
||||||
|
log("[background-agent] tmux callback check", {
|
||||||
|
hasCallback: !!onSubagentSessionCreated,
|
||||||
|
tmuxEnabled,
|
||||||
|
isInsideTmux: isInsideTmux(),
|
||||||
|
sessionID,
|
||||||
|
parentID,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!onSubagentSessionCreated || !tmuxEnabled || !isInsideTmux()) {
|
||||||
|
log("[background-agent] SKIP tmux callback - conditions not met")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log("[background-agent] Invoking tmux callback NOW", { sessionID })
|
||||||
|
await onSubagentSessionCreated({
|
||||||
|
sessionID,
|
||||||
|
parentID,
|
||||||
|
title,
|
||||||
|
}).catch((error) => {
|
||||||
|
log("[background-agent] Failed to spawn tmux pane:", error)
|
||||||
|
})
|
||||||
|
|
||||||
|
log("[background-agent] tmux callback completed, waiting")
|
||||||
|
await new Promise<void>((resolve) => setTimeout(resolve, TMUX_CALLBACK_DELAY_MS))
|
||||||
|
}
|
||||||
204
src/features/claude-tasks/session-storage.test.ts
Normal file
204
src/features/claude-tasks/session-storage.test.ts
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
|
||||||
|
import { existsSync, mkdirSync, rmSync, writeFileSync, readdirSync } from "fs"
|
||||||
|
import { join } from "path"
|
||||||
|
import type { OhMyOpenCodeConfig } from "../../config/schema"
|
||||||
|
import {
|
||||||
|
getSessionTaskDir,
|
||||||
|
listSessionTaskFiles,
|
||||||
|
listAllSessionDirs,
|
||||||
|
findTaskAcrossSessions,
|
||||||
|
} from "./session-storage"
|
||||||
|
|
||||||
|
const TEST_DIR = ".test-session-storage"
|
||||||
|
const TEST_DIR_ABS = join(process.cwd(), TEST_DIR)
|
||||||
|
|
||||||
|
function makeConfig(storagePath: string): Partial<OhMyOpenCodeConfig> {
|
||||||
|
return {
|
||||||
|
sisyphus: {
|
||||||
|
tasks: { storage_path: storagePath, claude_code_compat: false },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("getSessionTaskDir", () => {
|
||||||
|
test("returns session-scoped subdirectory under base task dir", () => {
|
||||||
|
//#given
|
||||||
|
const config = makeConfig("/tmp/tasks")
|
||||||
|
const sessionID = "ses_abc123"
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = getSessionTaskDir(config, sessionID)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toBe("/tmp/tasks/ses_abc123")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("uses relative storage path joined with cwd", () => {
|
||||||
|
//#given
|
||||||
|
const config = makeConfig(TEST_DIR)
|
||||||
|
const sessionID = "ses_xyz"
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = getSessionTaskDir(config, sessionID)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toBe(join(TEST_DIR_ABS, "ses_xyz"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("listSessionTaskFiles", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
if (existsSync(TEST_DIR_ABS)) {
|
||||||
|
rmSync(TEST_DIR_ABS, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (existsSync(TEST_DIR_ABS)) {
|
||||||
|
rmSync(TEST_DIR_ABS, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns empty array when session directory does not exist", () => {
|
||||||
|
//#given
|
||||||
|
const config = makeConfig(TEST_DIR)
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = listSessionTaskFiles(config, "nonexistent-session")
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("lists only T-*.json files in the session directory", () => {
|
||||||
|
//#given
|
||||||
|
const config = makeConfig(TEST_DIR)
|
||||||
|
const sessionDir = join(TEST_DIR_ABS, "ses_001")
|
||||||
|
mkdirSync(sessionDir, { recursive: true })
|
||||||
|
writeFileSync(join(sessionDir, "T-aaa.json"), "{}", "utf-8")
|
||||||
|
writeFileSync(join(sessionDir, "T-bbb.json"), "{}", "utf-8")
|
||||||
|
writeFileSync(join(sessionDir, "other.txt"), "nope", "utf-8")
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = listSessionTaskFiles(config, "ses_001")
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toHaveLength(2)
|
||||||
|
expect(result).toContain("T-aaa")
|
||||||
|
expect(result).toContain("T-bbb")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("does not list tasks from other sessions", () => {
|
||||||
|
//#given
|
||||||
|
const config = makeConfig(TEST_DIR)
|
||||||
|
const session1Dir = join(TEST_DIR_ABS, "ses_001")
|
||||||
|
const session2Dir = join(TEST_DIR_ABS, "ses_002")
|
||||||
|
mkdirSync(session1Dir, { recursive: true })
|
||||||
|
mkdirSync(session2Dir, { recursive: true })
|
||||||
|
writeFileSync(join(session1Dir, "T-from-s1.json"), "{}", "utf-8")
|
||||||
|
writeFileSync(join(session2Dir, "T-from-s2.json"), "{}", "utf-8")
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = listSessionTaskFiles(config, "ses_001")
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toEqual(["T-from-s1"])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("listAllSessionDirs", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
if (existsSync(TEST_DIR_ABS)) {
|
||||||
|
rmSync(TEST_DIR_ABS, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (existsSync(TEST_DIR_ABS)) {
|
||||||
|
rmSync(TEST_DIR_ABS, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns empty array when base directory does not exist", () => {
|
||||||
|
//#given
|
||||||
|
const config = makeConfig(TEST_DIR)
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = listAllSessionDirs(config)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns only directory entries (not files)", () => {
|
||||||
|
//#given
|
||||||
|
const config = makeConfig(TEST_DIR)
|
||||||
|
mkdirSync(TEST_DIR_ABS, { recursive: true })
|
||||||
|
mkdirSync(join(TEST_DIR_ABS, "ses_001"), { recursive: true })
|
||||||
|
mkdirSync(join(TEST_DIR_ABS, "ses_002"), { recursive: true })
|
||||||
|
writeFileSync(join(TEST_DIR_ABS, ".lock"), "{}", "utf-8")
|
||||||
|
writeFileSync(join(TEST_DIR_ABS, "T-legacy.json"), "{}", "utf-8")
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = listAllSessionDirs(config)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toHaveLength(2)
|
||||||
|
expect(result).toContain("ses_001")
|
||||||
|
expect(result).toContain("ses_002")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("findTaskAcrossSessions", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
if (existsSync(TEST_DIR_ABS)) {
|
||||||
|
rmSync(TEST_DIR_ABS, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (existsSync(TEST_DIR_ABS)) {
|
||||||
|
rmSync(TEST_DIR_ABS, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns null when task does not exist in any session", () => {
|
||||||
|
//#given
|
||||||
|
const config = makeConfig(TEST_DIR)
|
||||||
|
mkdirSync(join(TEST_DIR_ABS, "ses_001"), { recursive: true })
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = findTaskAcrossSessions(config, "T-nonexistent")
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("finds task in the correct session directory", () => {
|
||||||
|
//#given
|
||||||
|
const config = makeConfig(TEST_DIR)
|
||||||
|
const session2Dir = join(TEST_DIR_ABS, "ses_002")
|
||||||
|
mkdirSync(join(TEST_DIR_ABS, "ses_001"), { recursive: true })
|
||||||
|
mkdirSync(session2Dir, { recursive: true })
|
||||||
|
writeFileSync(join(session2Dir, "T-target.json"), '{"id":"T-target"}', "utf-8")
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = findTaskAcrossSessions(config, "T-target")
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).not.toBeNull()
|
||||||
|
expect(result!.sessionID).toBe("ses_002")
|
||||||
|
expect(result!.path).toBe(join(session2Dir, "T-target.json"))
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns null when base directory does not exist", () => {
|
||||||
|
//#given
|
||||||
|
const config = makeConfig(TEST_DIR)
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = findTaskAcrossSessions(config, "T-any")
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
52
src/features/claude-tasks/session-storage.ts
Normal file
52
src/features/claude-tasks/session-storage.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { join } from "path"
|
||||||
|
import { existsSync, readdirSync, statSync } from "fs"
|
||||||
|
import { getTaskDir } from "./storage"
|
||||||
|
import type { OhMyOpenCodeConfig } from "../../config/schema"
|
||||||
|
|
||||||
|
export function getSessionTaskDir(
|
||||||
|
config: Partial<OhMyOpenCodeConfig>,
|
||||||
|
sessionID: string,
|
||||||
|
): string {
|
||||||
|
return join(getTaskDir(config), sessionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listSessionTaskFiles(
|
||||||
|
config: Partial<OhMyOpenCodeConfig>,
|
||||||
|
sessionID: string,
|
||||||
|
): string[] {
|
||||||
|
const dir = getSessionTaskDir(config, sessionID)
|
||||||
|
if (!existsSync(dir)) return []
|
||||||
|
return readdirSync(dir)
|
||||||
|
.filter((f) => f.endsWith(".json") && f.startsWith("T-"))
|
||||||
|
.map((f) => f.replace(".json", ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listAllSessionDirs(
|
||||||
|
config: Partial<OhMyOpenCodeConfig>,
|
||||||
|
): string[] {
|
||||||
|
const baseDir = getTaskDir(config)
|
||||||
|
if (!existsSync(baseDir)) return []
|
||||||
|
return readdirSync(baseDir).filter((entry) => {
|
||||||
|
const fullPath = join(baseDir, entry)
|
||||||
|
return statSync(fullPath).isDirectory()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskLocation {
|
||||||
|
path: string
|
||||||
|
sessionID: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findTaskAcrossSessions(
|
||||||
|
config: Partial<OhMyOpenCodeConfig>,
|
||||||
|
taskId: string,
|
||||||
|
): TaskLocation | null {
|
||||||
|
const sessionDirs = listAllSessionDirs(config)
|
||||||
|
for (const sessionID of sessionDirs) {
|
||||||
|
const taskPath = join(getSessionTaskDir(config, sessionID), `${taskId}.json`)
|
||||||
|
if (existsSync(taskPath)) {
|
||||||
|
return { path: taskPath, sessionID }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
96
src/plugin-handlers/AGENTS.md
Normal file
96
src/plugin-handlers/AGENTS.md
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
**Generated:** 2026-02-08T16:45:00+09:00
|
||||||
|
**Commit:** f2b7b759
|
||||||
|
**Branch:** dev
|
||||||
|
|
||||||
|
## OVERVIEW
|
||||||
|
|
||||||
|
Plugin component loading and configuration orchestration. 500+ lines of config merging, migration, and component discovery for Claude Code compatibility.
|
||||||
|
|
||||||
|
## STRUCTURE
|
||||||
|
```
|
||||||
|
plugin-handlers/
|
||||||
|
├── config-handler.ts # Main config orchestrator (563 lines) - agent/skill/command loading
|
||||||
|
├── config-handler.test.ts # Config handler tests (34426 lines)
|
||||||
|
├── plan-model-inheritance.ts # Plan agent model inheritance logic (657 lines)
|
||||||
|
├── plan-model-inheritance.test.ts # Inheritance tests (3696 lines)
|
||||||
|
└── index.ts # Barrel export
|
||||||
|
```
|
||||||
|
|
||||||
|
## CORE FUNCTIONS
|
||||||
|
|
||||||
|
**Config Handler (`createConfigHandler`):**
|
||||||
|
- Loads all plugin components (agents, skills, commands, MCPs)
|
||||||
|
- Applies permission migrations for compatibility
|
||||||
|
- Merges user/project/global configurations
|
||||||
|
- Handles Claude Code plugin integration
|
||||||
|
|
||||||
|
**Plan Model Inheritance:**
|
||||||
|
- Demotes plan agent to prometheus when planner enabled
|
||||||
|
- Preserves user overrides during migration
|
||||||
|
- Handles model/variant inheritance from categories
|
||||||
|
|
||||||
|
## LOADING PHASES
|
||||||
|
|
||||||
|
1. **Plugin Discovery**: Load Claude Code plugins with timeout protection
|
||||||
|
2. **Component Loading**: Parallel loading of agents, skills, commands
|
||||||
|
3. **Config Merging**: User → Project → Global → Defaults
|
||||||
|
4. **Migration**: Legacy config format compatibility
|
||||||
|
5. **Permission Application**: Tool access control per agent
|
||||||
|
|
||||||
|
## KEY FEATURES
|
||||||
|
|
||||||
|
**Parallel Loading:**
|
||||||
|
- Concurrent discovery of user/project/global components
|
||||||
|
- Timeout protection for plugin loading (default: 10s)
|
||||||
|
- Error isolation (failed plugins don't break others)
|
||||||
|
|
||||||
|
**Migration Support:**
|
||||||
|
- Agent name mapping (old → new names)
|
||||||
|
- Permission format conversion
|
||||||
|
- Config structure updates
|
||||||
|
|
||||||
|
**Claude Code Integration:**
|
||||||
|
- Plugin component loading
|
||||||
|
- MCP server discovery
|
||||||
|
- Agent/skill/command compatibility
|
||||||
|
|
||||||
|
## CONFIGURATION FLOW
|
||||||
|
|
||||||
|
```
|
||||||
|
User Config → Migration → Merging → Validation → Agent Creation → Permission Application
|
||||||
|
```
|
||||||
|
|
||||||
|
## TESTING COVERAGE
|
||||||
|
|
||||||
|
- **Config Handler**: 34426 lines of tests
|
||||||
|
- **Plan Inheritance**: 3696 lines of tests
|
||||||
|
- **Migration Logic**: Legacy compatibility verification
|
||||||
|
- **Parallel Loading**: Timeout and error handling
|
||||||
|
|
||||||
|
## USAGE PATTERNS
|
||||||
|
|
||||||
|
**Config Handler Creation:**
|
||||||
|
```typescript
|
||||||
|
const handler = createConfigHandler({
|
||||||
|
ctx: { directory: projectDir },
|
||||||
|
pluginConfig: userConfig,
|
||||||
|
modelCacheState: cache
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Plan Demotion:**
|
||||||
|
```typescript
|
||||||
|
const demotedPlan = buildPlanDemoteConfig(
|
||||||
|
prometheusConfig,
|
||||||
|
userPlanOverrides
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Component Loading:**
|
||||||
|
```typescript
|
||||||
|
const [agents, skills, commands] = await Promise.all([
|
||||||
|
loadUserAgents(),
|
||||||
|
loadProjectSkills(),
|
||||||
|
loadGlobalCommands()
|
||||||
|
]);
|
||||||
|
```
|
||||||
Loading…
x
Reference in New Issue
Block a user