Compare commits

..

1 Commits

Author SHA1 Message Date
YeonGyu-Kim
9ca259dcdc fix(runtime-fallback): preserve agent variant and reasoningEffort on model fallback (fixes #2621)
When runtime fallback switches to a different model, the agent's
configured variant and reasoningEffort were lost because
buildRetryModelPayload only extracted variant from the fallback
model string itself.

Now buildRetryModelPayload accepts optional agentSettings and uses
the agent's variant as fallback when the model string doesn't
include one. reasoningEffort is also passed through.

🤖 Generated with assistance of [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 12:11:01 +09:00
394 changed files with 2720 additions and 60935 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 KiB

View File

@@ -57,7 +57,6 @@ jobs:
bun test src/cli/doctor/format-default.test.ts
bun test src/tools/call-omo-agent/sync-executor.test.ts
bun test src/tools/call-omo-agent/session-creator.test.ts
bun test src/tools/session-manager
bun test src/features/opencode-skill-loader/loader.test.ts
bun test src/hooks/anthropic-context-window-limit-recovery/recovery-hook.test.ts
bun test src/hooks/anthropic-context-window-limit-recovery/executor.test.ts
@@ -67,8 +66,9 @@ jobs:
# Enumerate subdirectories/files explicitly to EXCLUDE mock-heavy files
# that were already run in isolation above.
# Excluded from src/cli: doctor/formatter.test.ts, doctor/format-default.test.ts
# Excluded from src/tools: call-omo-agent/sync-executor.test.ts, call-omo-agent/session-creator.test.ts, session-manager (all)
# Excluded from src/tools: call-omo-agent/sync-executor.test.ts, call-omo-agent/session-creator.test.ts
# Excluded from src/hooks/anthropic-context-window-limit-recovery: recovery-hook.test.ts, executor.test.ts
# Excluded from src/tools: call-omo-agent/sync-executor.test.ts, call-omo-agent/session-creator.test.ts
bun test bin script src/config src/mcp src/index.test.ts \
src/agents src/shared \
src/cli/run src/cli/config-manager src/cli/mcp-oauth \
@@ -77,7 +77,7 @@ jobs:
src/cli/doctor/runner.test.ts src/cli/doctor/checks \
src/tools/ast-grep src/tools/background-task src/tools/delegate-task \
src/tools/glob src/tools/grep src/tools/interactive-bash \
src/tools/look-at src/tools/lsp \
src/tools/look-at src/tools/lsp src/tools/session-manager \
src/tools/skill src/tools/skill-mcp src/tools/slashcommand src/tools/task \
src/tools/call-omo-agent/background-agent-executor.test.ts \
src/tools/call-omo-agent/background-executor.test.ts \

View File

@@ -1,46 +0,0 @@
name: Refresh Model Capabilities
on:
schedule:
- cron: "17 4 * * 1"
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
refresh:
runs-on: ubuntu-latest
if: github.repository == 'code-yeongyu/oh-my-openagent'
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install
env:
BUN_INSTALL_ALLOW_SCRIPTS: "@ast-grep/napi"
- name: Refresh bundled model capabilities snapshot
run: bun run build:model-capabilities
- name: Validate capability guardrails
run: bun run test:model-capabilities
- name: Create refresh pull request
uses: peter-evans/create-pull-request@v7
with:
commit-message: "chore: refresh model capabilities snapshot"
title: "chore: refresh model capabilities snapshot"
body: |
Automated refresh of `src/generated/model-capabilities.generated.json` from `https://models.dev/api.json`.
This keeps the bundled capability snapshot aligned with upstream model metadata without relying on manual refreshes.
branch: automation/refresh-model-capabilities
delete-branch: true
labels: |
maintenance

1
.gitignore vendored
View File

@@ -36,4 +36,3 @@ test-injection/
notepad.md
oauth-success.html
*.bun-build
.omx/

View File

@@ -79,65 +79,47 @@ Pass `REPO`, `REPORT_DIR`, and `COMMIT_SHA` to every subagent.
---
---
## Phase 1: Fetch All Open Items
## Phase 1: Fetch All Open Items (CORRECTED)
**IMPORTANT:** `body` and `comments` fields may contain control characters that break jq parsing. Fetch basic metadata first, then fetch full details per-item in subagents.
<fetch>
Paginate if 500 results returned.
```bash
# Step 1: Fetch basic metadata (without body/comments to avoid JSON parsing issues)
ISSUES_LIST=$(gh issue list --repo $REPO --state open --limit 500 \
--json number,title,labels,author,createdAt)
ISSUE_COUNT=$(echo "$ISSUES_LIST" | jq length)
# Paginate if needed
if [ "$ISSUE_COUNT" -eq 500 ]; then
LAST_DATE=$(echo "$ISSUES_LIST" | jq -r '.[-1].createdAt')
ISSUES=$(gh issue list --repo $REPO --state open --limit 500 \
--json number,title,state,createdAt,updatedAt,labels,author,body,comments)
ISSUE_LEN=$(echo "$ISSUES" | jq length)
if [ "$ISSUE_LEN" -eq 500 ]; then
LAST_DATE=$(echo "$ISSUES" | jq -r '.[-1].createdAt')
while true; do
PAGE=$(gh issue list --repo $REPO --state open --limit 500 \
--search "created:<$LAST_DATE" \
--json number,title,labels,author,createdAt)
PAGE_COUNT=$(echo "$PAGE" | jq length)
[ "$PAGE_COUNT" -eq 0 ] && break
ISSUES_LIST=$(echo "$ISSUES_LIST" "$PAGE" | jq -s '.[0] + .[1] | unique_by(.number)')
ISSUE_COUNT=$(echo "$ISSUES_LIST" | jq length)
[ "$PAGE_COUNT" -lt 500 ] && break
--json number,title,state,createdAt,updatedAt,labels,author,body,comments)
PAGE_LEN=$(echo "$PAGE" | jq length)
[ "$PAGE_LEN" -eq 0 ] && break
ISSUES=$(echo "[$ISSUES, $PAGE]" | jq -s 'add | unique_by(.number)')
[ "$PAGE_LEN" -lt 500 ] && break
LAST_DATE=$(echo "$PAGE" | jq -r '.[-1].createdAt')
done
fi
# Same for PRs
PRS_LIST=$(gh pr list --repo $REPO --state open --limit 500 \
--json number,title,labels,author,headRefName,baseRefName,isDraft,createdAt)
PR_COUNT=$(echo "$PRS_LIST" | jq length)
if [ "$PR_COUNT" -eq 500 ]; then
LAST_DATE=$(echo "$PRS_LIST" | jq -r '.[-1].createdAt')
PRS=$(gh pr list --repo $REPO --state open --limit 500 \
--json number,title,state,createdAt,updatedAt,labels,author,body,headRefName,baseRefName,isDraft,mergeable,reviewDecision,statusCheckRollup)
PR_LEN=$(echo "$PRS" | jq length)
if [ "$PR_LEN" -eq 500 ]; then
LAST_DATE=$(echo "$PRS" | jq -r '.[-1].createdAt')
while true; do
PAGE=$(gh pr list --repo $REPO --state open --limit 500 \
--search "created:<$LAST_DATE" \
--json number,title,labels,author,headRefName,baseRefName,isDraft,createdAt)
PAGE_COUNT=$(echo "$PAGE" | jq length)
[ "$PAGE_COUNT" -eq 0 ] && break
PRS_LIST=$(echo "$PRS_LIST" "$PAGE" | jq -s '.[0] + .[1] | unique_by(.number)')
PR_COUNT=$(echo "$PRS_LIST" | jq length)
[ "$PAGE_COUNT" -lt 500 ] && break
--json number,title,state,createdAt,updatedAt,labels,author,body,headRefName,baseRefName,isDraft,mergeable,reviewDecision,statusCheckRollup)
PAGE_LEN=$(echo "$PAGE" | jq length)
[ "$PAGE_LEN" -eq 0 ] && break
PRS=$(echo "[$PRS, $PAGE]" | jq -s 'add | unique_by(.number)')
[ "$PAGE_LEN" -lt 500 ] && break
LAST_DATE=$(echo "$PAGE" | jq -r '.[-1].createdAt')
done
fi
echo "Total issues: $ISSUE_COUNT, Total PRs: $PR_COUNT"
```
**LARGE REPOSITORY HANDLING:**
If total items exceeds 50, you MUST process ALL items. Use the pagination code above to fetch every single open issue and PR.
**DO NOT** sample or limit to 50 items - process the entire backlog.
Example: If there are 500 open issues, spawn 500 subagents. If there are 1000 open PRs, spawn 1000 subagents.
**Note:** Background task system will queue excess tasks automatically.
</fetch>
---
@@ -154,36 +136,7 @@ Example: If there are 500 open issues, spawn 500 subagents. If there are 1000 op
---
## Phase 3: Spawn Subagents (Individual Tool Calls)
**CRITICAL: Create tasks ONE BY ONE using individual `task_create` tool calls. NEVER batch or script.**
For each item, execute these steps sequentially:
### Step 3.1: Create Task Record
```typescript
task_create(
subject="Triage: #{number} {title}",
description="GitHub {issue|PR} triage analysis - {type}",
metadata={"type": "{ISSUE_QUESTION|ISSUE_BUG|ISSUE_FEATURE|ISSUE_OTHER|PR_BUGFIX|PR_OTHER}", "number": {number}}
)
```
### Step 3.2: Spawn Analysis Subagent (Background)
```typescript
task(
category="quick",
run_in_background=true,
load_skills=[],
prompt=SUBAGENT_PROMPT
)
```
**ABSOLUTE RULES for Subagents:**
- **ONLY ANALYZE** - Never take action on GitHub (no comments, merges, closes)
- **READ-ONLY** - Use tools only for reading code/GitHub data
- **WRITE REPORT ONLY** - Output goes to `{REPORT_DIR}/{issue|pr}-{number}.md` via Write tool
- **EVIDENCE REQUIRED** - Every claim must have GitHub permalink as proof
## Phase 3: Spawn Subagents
```
For each item:
@@ -217,7 +170,6 @@ ABSOLUTE RULES (violating ANY = critical failure):
- Your ONLY writable output: {REPORT_DIR}/{issue|pr}-{number}.md via the Write tool
```
---
### ISSUE_QUESTION

View File

@@ -4,7 +4,7 @@
## OVERVIEW
OpenCode plugin (npm: `oh-my-opencode`) that extends Claude Code (OpenCode fork) with multi-agent orchestration, 48 lifecycle hooks, 26 tools, skill/command/MCP systems, and Claude Code compatibility. 1268 TypeScript files, 160k LOC.
OpenCode plugin (npm: `oh-my-opencode`) that extends Claude Code (OpenCode fork) with multi-agent orchestration, 46 lifecycle hooks, 26 tools, skill/command/MCP systems, and Claude Code compatibility. 1268 TypeScript files, 160k LOC.
## STRUCTURE
@@ -14,14 +14,14 @@ oh-my-opencode/
│ ├── index.ts # Plugin entry: loadConfig → createManagers → createTools → createHooks → createPluginInterface
│ ├── plugin-config.ts # JSONC multi-level config: user → project → defaults (Zod v4)
│ ├── agents/ # 11 agents (Sisyphus, Hephaestus, Oracle, Librarian, Explore, Atlas, Prometheus, Metis, Momus, Multimodal-Looker, Sisyphus-Junior)
│ ├── hooks/ # 48 lifecycle hooks across dedicated modules and standalone files
│ ├── hooks/ # 46 hooks across 45 directories + 11 standalone files
│ ├── tools/ # 26 tools across 15 directories
│ ├── features/ # 19 feature modules (background-agent, skill-loader, tmux, MCP-OAuth, etc.)
│ ├── shared/ # 95+ utility files in 13 categories
│ ├── config/ # Zod v4 schema system (24 files)
│ ├── cli/ # CLI: install, run, doctor, mcp-oauth (Commander.js)
│ ├── mcp/ # 3 built-in remote MCPs (websearch, context7, grep_app)
│ ├── plugin/ # 8 OpenCode hook handlers + 48 hook composition
│ ├── plugin/ # 8 OpenCode hook handlers + 46 hook composition
│ └── plugin-handlers/ # 6-phase config loading pipeline
├── packages/ # Monorepo: cli-runner, 12 platform binaries
└── local-ignore/ # Dev-only test fixtures
@@ -34,7 +34,7 @@ OhMyOpenCodePlugin(ctx)
├─→ loadPluginConfig() # JSONC parse → project/user merge → Zod validate → migrate
├─→ createManagers() # TmuxSessionManager, BackgroundManager, SkillMcpManager, ConfigHandler
├─→ createTools() # SkillContext + AvailableCategories + ToolRegistry (26 tools)
├─→ createHooks() # 3-tier: Core(39) + Continuation(7) + Skill(2) = 48 hooks
├─→ createHooks() # 3-tier: Core(37) + Continuation(7) + Skill(2) = 46 hooks
└─→ createPluginInterface() # 8 OpenCode hook handlers → PluginInterface
```
@@ -97,7 +97,7 @@ Fields: agents (14 overridable, 21 fields each), categories (8 built-in + custom
- **Test pattern**: Bun test (`bun:test`), co-located `*.test.ts`, given/when/then style (nested describe with `#given`/`#when`/`#then` prefixes)
- **CI test split**: mock-heavy tests run in isolation (separate `bun test` processes), rest in batch
- **Factory pattern**: `createXXX()` for all tools, hooks, agents
- **Hook tiers**: Session (23) → Tool-Guard (12) → Transform (4) → Continuation (7) → Skill (2)
- **Hook tiers**: Session (23) → Tool-Guard (10) → Transform (4) → Continuation (7) → Skill (2)
- **Agent modes**: `primary` (respects UI model) vs `subagent` (own fallback chain) vs `all`
- **Model resolution**: 4-step: override → category-default → provider-fallback → system-default
- **Config format**: JSONC with comments, Zod v4 validation, snake_case keys

View File

@@ -4,17 +4,6 @@
> コアメンテナーのQが負傷したため、今週は Issue/PR への返信とリリースが遅れる可能性があります。
> ご理解とご支援に感謝します。
> [!TIP]
> **Building in Public**
>
> メンテナーが Jobdori を使い、oh-my-opencode をリアルタイムで開発・メンテナンスしています。Jobdori は OpenClaw をベースに大幅カスタマイズされた AI アシスタントです。
> すべての機能開発、修正、Issue トリアージを Discord でライブでご覧いただけます。
>
> [![Building in Public](./.github/assets/building-in-public.png)](https://discord.gg/PUwSMR9XNk)
>
> [**→ #building-in-public で確認する**](https://discord.gg/PUwSMR9XNk)
> [!NOTE]
>
> [![Sisyphus Labs - Sisyphus is the agent that codes like your team.](./.github/assets/sisyphuslabs.png?v=2)](https://sisyphuslabs.ai)

View File

@@ -4,17 +4,6 @@
> 핵심 메인테이너 Q가 부상을 입어, 이번 주에는 이슈/PR 응답 및 릴리스가 지연될 수 있습니다.
> 양해와 응원에 감사드립니다.
> [!TIP]
> **Building in Public**
>
> 메인테이너가 Jobdori를 통해 oh-my-opencode를 실시간으로 개발하고 있습니다. Jobdori는 OpenClaw를 기반으로 대폭 커스터마이징된 AI 어시스턴트입니다.
> 모든 기능 개발, 버그 수정, 이슈 트리아지를 Discord에서 실시간으로 확인하세요.
>
> [![Building in Public](./.github/assets/building-in-public.png)](https://discord.gg/PUwSMR9XNk)
>
> [**→ #building-in-public에서 확인하기**](https://discord.gg/PUwSMR9XNk)
> [!TIP]
> 저희와 함께 하세요!
>

View File

@@ -1,13 +1,3 @@
> [!TIP]
> **Building in Public**
>
> The maintainer builds and maintains oh-my-opencode in real-time with Jobdori, an AI assistant built on a heavily customized fork of OpenClaw.
> Every feature, every fix, every issue triage — live in our Discord.
>
> [![Building in Public](./.github/assets/building-in-public.png)](https://discord.gg/PUwSMR9XNk)
>
> [**→ Watch it happen in #building-in-public**](https://discord.gg/PUwSMR9XNk)
> [!NOTE]
>
> [![Sisyphus Labs - Sisyphus is the agent that codes like your team.](./.github/assets/sisyphuslabs.png?v=2)](https://sisyphuslabs.ai)
@@ -314,7 +304,7 @@ See full [Features Documentation](docs/reference/features.md).
- **Claude Code Compatibility**: Full hook system, commands, skills, agents, MCPs
- **Built-in MCPs**: websearch (Exa), context7 (docs), grep_app (GitHub search)
- **Session Tools**: List, read, search, and analyze session history
- **Productivity Features**: Ralph Loop, Todo Enforcer, Comment Checker, Think Mode, and more
- **Productivity Features**: Ralph Loop, Todo Enforcer, GPT permission-tail continuation, Comment Checker, Think Mode, and more
- **Model Setup**: Agent-model matching is built into the [Installation Guide](docs/guide/installation.md#step-5-understand-your-model-setup)
## Configuration
@@ -331,7 +321,7 @@ See [Configuration Documentation](docs/reference/configuration.md).
- **Sisyphus Agent**: Main orchestrator with Prometheus (Planner) and Metis (Plan Consultant)
- **Background Tasks**: Configure concurrency limits per provider/model
- **Categories**: Domain-specific task delegation (`visual`, `business-logic`, custom)
- **Hooks**: 25+ built-in hooks, all configurable via `disabled_hooks`
- **Hooks**: 25+ built-in hooks, including `gpt-permission-continuation`, all configurable via `disabled_hooks`
- **MCPs**: Built-in websearch (Exa), context7 (docs), grep_app (GitHub search)
- **LSP**: Full LSP support with refactoring tools
- **Experimental**: Aggressive truncation, auto-resume, and more

View File

@@ -4,17 +4,6 @@
> Ключевой мейнтейнер Q получил травму, поэтому на этой неделе ответы по issue/PR и релизы могут задерживаться.
> Спасибо за терпение и поддержку.
> [!TIP]
> **Building in Public**
>
> Мейнтейнер разрабатывает и поддерживает oh-my-opencode в режиме реального времени с помощью Jobdori — ИИ-ассистента на базе глубоко кастомизированной версии OpenClaw.
> Каждая фича, каждый фикс, каждый триаж issue — в прямом эфире в нашем Discord.
>
> [![Building in Public](./.github/assets/building-in-public.png)](https://discord.gg/PUwSMR9XNk)
>
> [**→ Смотрите в #building-in-public**](https://discord.gg/PUwSMR9XNk)
> [!NOTE]
>
> [![Sisyphus Labs - Sisyphus is the agent that codes like your team.](./.github/assets/sisyphuslabs.png?v=2)](https://sisyphuslabs.ai)

View File

@@ -4,17 +4,6 @@
> 核心维护者 Q 因受伤,本周 issue/PR 回复和发布可能会延迟。
> 感谢你的耐心与支持。
> [!TIP]
> **Building in Public**
>
> 维护者正在使用 Jobdori 实时开发和维护 oh-my-opencode。Jobdori 是基于 OpenClaw 深度定制的 AI 助手。
> 每个功能开发、每次修复、每次 Issue 分类,都在 Discord 上实时进行。
>
> [![Building in Public](./.github/assets/building-in-public.png)](https://discord.gg/PUwSMR9XNk)
>
> [**→ 在 #building-in-public 频道中查看**](https://discord.gg/PUwSMR9XNk)
> [!NOTE]
>
> [![Sisyphus Labs - Sisyphus is the agent that codes like your team.](./.github/assets/sisyphuslabs.png?v=2)](https://sisyphuslabs.ai)

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ Think of AI models as developers on a team. Each has a different brain, differen
This isn't a bug. It's the foundation of the entire system.
Oh My OpenAgent assigns each agent a model that matches its _working style_ — like building a team where each person is in the role that fits their personality.
Oh My OpenCode assigns each agent a model that matches its _working style_ — like building a team where each person is in the role that fits their personality.
### Sisyphus: The Sociable Lead
@@ -92,10 +92,10 @@ These agents do grep, search, and retrieval. They intentionally use the fastest,
| Agent | Role | Fallback Chain | Notes |
| --------------------- | ------------------ | ---------------------------------------------- | ----------------------------------------------------- |
| **Explore** | Fast codebase grep | Grok Code Fast → opencode-go/minimax-m2.7-highspeed → MiniMax M2.7 → Haiku → GPT-5-Nano | Speed is everything. Fire 10 in parallel. |
| **Librarian** | Docs/code search | opencode-go/minimax-m2.7 → MiniMax M2.7-highspeed → Haiku → GPT-5-Nano | Doc retrieval doesn't need deep reasoning. |
| **Multimodal Looker** | Vision/screenshots | GPT-5.4 → opencode-go/kimi-k2.5 → GLM-4.6v → GPT-5-Nano | Uses the first available multimodal-capable fallback. |
| **Sisyphus-Junior** | Category executor | Claude Sonnet → opencode-go/kimi-k2.5 → GPT-5.4 → MiniMax M2.7 → Big Pickle | Handles delegated category tasks. Sonnet-tier default. |
| **Explore** | Fast codebase grep | Grok Code Fast → opencode-go/minimax-m2.5 → MiniMax Free → Haiku → GPT-5-Nano | Speed is everything. Fire 10 in parallel. |
| **Librarian** | Docs/code search | opencode-go/minimax-m2.5 → MiniMax Free → Haiku → GPT-5-Nano | Doc retrieval doesn't need deep reasoning. |
| **Multimodal Looker** | Vision/screenshots | GPT-5.4 → opencode-go/kimi-k2.5 → GLM-4.6v → GPT-5-Nano | Uses the first available multimodal-capable fallback. |
| **Sisyphus-Junior** | Category executor | Claude Sonnet → opencode-go/kimi-k2.5 → GPT-5.4 → Big Pickle | Handles delegated category tasks. Sonnet-tier default. |
---
@@ -121,7 +121,6 @@ Principle-driven, explicit reasoning, deep technical capability. Best for agents
| ----------------- | ----------------------------------------------------------------------------------------------- |
| **GPT-5.3 Codex** | Deep coding powerhouse. Autonomous exploration. Required for Hephaestus. |
| **GPT-5.4** | High intelligence, strategic reasoning. Default for Oracle, Momus, and a key fallback for Prometheus / Atlas. Uses xhigh variant for Momus. |
| **GPT-5.4 Mini** | Fast + strong reasoning. Good for lightweight autonomous tasks. Default for quick category. |
| **GPT-5-Nano** | Ultra-cheap, fast. Good for simple utility tasks. |
### Other Models
@@ -131,8 +130,7 @@ Principle-driven, explicit reasoning, deep technical capability. Best for agents
| **Gemini 3.1 Pro** | Excels at visual/frontend tasks. Different reasoning style. Default for `visual-engineering` and `artistry`. |
| **Gemini 3 Flash** | Fast. Good for doc search and light tasks. |
| **Grok Code Fast 1** | Blazing fast code grep. Default for Explore agent. |
| **MiniMax M2.7** | Fast and smart. Good for utility tasks and search/retrieval. Upgraded from M2.5 with better reasoning. |
| **MiniMax M2.7 Highspeed** | Ultra-fast variant. Optimized for latency-sensitive tasks like codebase grep. |
| **MiniMax M2.5** | Fast and smart. Good for utility tasks and search/retrieval. |
### OpenCode Go
@@ -144,11 +142,11 @@ A premium subscription tier ($10/month) that provides reliable access to Chinese
| ------------------------ | --------------------------------------------------------------------- |
| **opencode-go/kimi-k2.5** | Vision-capable, Claude-like reasoning. Used by Sisyphus, Atlas, Sisyphus-Junior, Multimodal Looker. |
| **opencode-go/glm-5** | Text-only orchestration model. Used by Oracle, Prometheus, Metis, Momus. |
| **opencode-go/minimax-m2.7** | Ultra-cheap, fast responses. Used by Librarian, Explore, Atlas, Sisyphus-Junior for utility work. |
| **opencode-go/minimax-m2.5** | Ultra-cheap, fast responses. Used by Librarian, Explore for utility work. |
**When It Gets Used:**
OpenCode Go models appear in fallback chains as intermediate options. They bridge the gap between premium Claude access and free-tier alternatives. The system tries OpenCode Go models before falling back to free tiers (MiniMax M2.7-highspeed, Big Pickle) or GPT alternatives.
OpenCode Go models appear in fallback chains as intermediate options. They bridge the gap between premium Claude access and free-tier alternatives. The system tries OpenCode Go models before falling back to free tiers (MiniMax Free, Big Pickle) or GPT alternatives.
**Go-Only Scenarios:**
@@ -156,7 +154,7 @@ Some model identifiers like `k2p5` (paid Kimi K2.5) and `glm-5` may only be avai
### About Free-Tier Fallbacks
You may see model names like `kimi-k2.5-free`, `minimax-m2.7-highspeed`, or `big-pickle` (GLM 4.6) in the source code or logs. These are free-tier or speed-optimized versions of the same model families. They exist as lower-priority entries in fallback chains.
You may see model names like `kimi-k2.5-free`, `minimax-m2.5-free`, or `big-pickle` (GLM 4.6) in the source code or logs. These are free-tier versions of the same model families, served through the OpenCode Zen provider. They exist as lower-priority entries in fallback chains.
You don't need to configure them. The system includes them so it degrades gracefully when you don't have every paid subscription. If you have the paid version, the paid version is always preferred.
@@ -172,7 +170,7 @@ When agents delegate work, they don't pick a model name — they pick a **catego
| `ultrabrain` | Maximum reasoning needed | GPT-5.4 → Gemini 3.1 Pro → Claude Opus → opencode-go/glm-5 |
| `deep` | Deep coding, complex logic | GPT-5.3 Codex → Claude Opus → Gemini 3.1 Pro |
| `artistry` | Creative, novel approaches | Gemini 3.1 Pro → Claude Opus → GPT-5.4 |
| `quick` | Simple, fast tasks | GPT-5.4 Mini → Claude Haiku → Gemini Flash → opencode-go/minimax-m2.7 → GPT-5-Nano |
| `quick` | Simple, fast tasks | Claude Haiku → Gemini Flash → opencode-go/minimax-m2.5 → GPT-5-Nano |
| `unspecified-high` | General complex work | Claude Opus → GPT-5.4 → GLM 5 → K2P5 → opencode-go/glm-5 → Kimi K2.5 |
| `unspecified-low` | General standard work | Claude Sonnet → GPT-5.3 Codex → opencode-go/kimi-k2.5 → Gemini Flash |
| `writing` | Text, docs, prose | Gemini Flash → opencode-go/kimi-k2.5 → Claude Sonnet |
@@ -187,7 +185,7 @@ See the [Orchestration System Guide](./orchestration.md) for how agents dispatch
```jsonc
{
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-openagent.schema.json",
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json",
"agents": {
// Main orchestrator: Claude Opus or Kimi K2.5 work best

View File

@@ -5,7 +5,7 @@
Paste this into your llm agent session:
```
Install and configure oh-my-openagent by following the instructions here:
Install and configure oh-my-opencode by following the instructions here:
https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/refs/heads/dev/docs/guide/installation.md
```
@@ -14,8 +14,8 @@ https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/refs/heads/dev/do
Run the interactive installer:
```bash
bunx oh-my-openagent install # recommended
npx oh-my-openagent install # alternative
bunx oh-my-opencode install # recommended
npx oh-my-opencode install # alternative
```
> **Note**: The CLI ships with standalone binaries for all major platforms. No runtime (Bun/Node.js) is required for CLI execution after installation.
@@ -34,7 +34,7 @@ After you install it, you can read this [overview guide](./overview.md) to under
> curl -fsSL https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/refs/heads/dev/docs/guide/installation.md
> ```
If you're an LLM Agent helping set up oh-my-openagent, Welcome! You'll now help your user to install the best agent harness, OhMyOpenAgent. Tell user this with greetings, with test 'oMoMoMoMo...'
If you're an LLM Agent helping set up oh-my-opencode, Welcome! You'll now help your user to install the best agent harness, OhMyOpenCode. Tell user this with greetings, with test 'oMoMoMoMo...'
follow these steps:
@@ -69,7 +69,7 @@ Ask the user these questions to determine CLI options:
- If **no**`--zai-coding-plan=no` (default)
7. **Do you have an OpenCode Go subscription?**
- OpenCode Go is a $10/month subscription providing access to GLM-5, Kimi K2.5, and MiniMax M2.7 models
- OpenCode Go is a $10/month subscription providing access to GLM-5, Kimi K2.5, and MiniMax M2.5 models
- If **yes**`--opencode-go=yes`
- If **no**`--opencode-go=no` (default)
@@ -96,19 +96,19 @@ Spawn a subagent to handle installation and report back - to save context.
Based on user's answers, run the CLI installer with appropriate flags:
```bash
bunx oh-my-openagent install --no-tui --claude=<yes|no|max20> --gemini=<yes|no> --copilot=<yes|no> [--openai=<yes|no>] [--opencode-go=<yes|no>] [--opencode-zen=<yes|no>] [--zai-coding-plan=<yes|no>]
bunx oh-my-opencode install --no-tui --claude=<yes|no|max20> --gemini=<yes|no> --copilot=<yes|no> [--openai=<yes|no>] [--opencode-go=<yes|no>] [--opencode-zen=<yes|no>] [--zai-coding-plan=<yes|no>]
```
**Examples:**
- User has all native subscriptions: `bunx oh-my-openagent install --no-tui --claude=max20 --openai=yes --gemini=yes --copilot=no`
- User has only Claude: `bunx oh-my-openagent install --no-tui --claude=yes --gemini=no --copilot=no`
- User has Claude + OpenAI: `bunx oh-my-openagent install --no-tui --claude=yes --openai=yes --gemini=no --copilot=no`
- User has only GitHub Copilot: `bunx oh-my-openagent install --no-tui --claude=no --gemini=no --copilot=yes`
- User has Z.ai for Librarian: `bunx oh-my-openagent install --no-tui --claude=yes --gemini=no --copilot=no --zai-coding-plan=yes`
- User has only OpenCode Zen: `bunx oh-my-openagent install --no-tui --claude=no --gemini=no --copilot=no --opencode-zen=yes`
- User has OpenCode Go only: `bunx oh-my-openagent install --no-tui --claude=no --openai=no --gemini=no --copilot=no --opencode-go=yes`
- User has no subscriptions: `bunx oh-my-openagent install --no-tui --claude=no --gemini=no --copilot=no`
- User has all native subscriptions: `bunx oh-my-opencode install --no-tui --claude=max20 --openai=yes --gemini=yes --copilot=no`
- User has only Claude: `bunx oh-my-opencode install --no-tui --claude=yes --gemini=no --copilot=no`
- User has Claude + OpenAI: `bunx oh-my-opencode install --no-tui --claude=yes --openai=yes --gemini=no --copilot=no`
- User has only GitHub Copilot: `bunx oh-my-opencode install --no-tui --claude=no --gemini=no --copilot=yes`
- User has Z.ai for Librarian: `bunx oh-my-opencode install --no-tui --claude=yes --gemini=no --copilot=no --zai-coding-plan=yes`
- User has only OpenCode Zen: `bunx oh-my-opencode install --no-tui --claude=no --gemini=no --copilot=no --opencode-zen=yes`
- User has OpenCode Go only: `bunx oh-my-opencode install --no-tui --claude=no --openai=no --gemini=no --copilot=no --opencode-go=yes`
- User has no subscriptions: `bunx oh-my-opencode install --no-tui --claude=no --gemini=no --copilot=no`
The CLI will:
@@ -120,7 +120,7 @@ The CLI will:
```bash
opencode --version # Should be 1.0.150 or higher
cat ~/.config/opencode/opencode.json # Should contain "oh-my-openagent" in plugin array
cat ~/.config/opencode/opencode.json # Should contain "oh-my-opencode" in plugin array
```
### Step 4: Configure Authentication
@@ -145,7 +145,7 @@ First, add the opencode-antigravity-auth plugin:
```json
{
"plugin": ["oh-my-openagent", "opencode-antigravity-auth@latest"]
"plugin": ["oh-my-opencode", "opencode-antigravity-auth@latest"]
}
```
@@ -154,9 +154,9 @@ First, add the opencode-antigravity-auth plugin:
You'll also need full model settings in `opencode.json`.
Read the [opencode-antigravity-auth documentation](https://github.com/NoeFabris/opencode-antigravity-auth), copy the full model configuration from the README, and merge carefully to avoid breaking the user's existing setup. The plugin now uses a **variant system** — models like `antigravity-gemini-3-pro` support `low`/`high` variants instead of separate `-low`/`-high` model entries.
##### oh-my-openagent Agent Model Override
##### oh-my-opencode Agent Model Override
The `opencode-antigravity-auth` plugin uses different model names than the built-in Google auth. Override the agent models in `oh-my-openagent.json` (or `.opencode/oh-my-openagent.json`):
The `opencode-antigravity-auth` plugin uses different model names than the built-in Google auth. Override the agent models in `oh-my-opencode.json` (or `.opencode/oh-my-opencode.json`):
```json
{
@@ -176,7 +176,7 @@ The `opencode-antigravity-auth` plugin uses different model names than the built
**Available models (Gemini CLI quota)**:
- `google/gemini-2.5-flash`, `google/gemini-2.5-pro`, `google/gemini-3-flash-preview`, `google/gemini-3.1-pro-preview`
- `google/gemini-2.5-flash`, `google/gemini-2.5-pro`, `google/gemini-3-flash-preview`, `google/gemini-3-pro-preview`
> **Note**: Legacy tier-suffixed names like `google/antigravity-gemini-3-pro-high` still work but variants are recommended. Use `--variant=high` with the base model name instead.
@@ -201,11 +201,11 @@ GitHub Copilot is supported as a **fallback provider** when native providers are
##### Model Mappings
When GitHub Copilot is the best available provider, oh-my-openagent uses these model assignments:
When GitHub Copilot is the best available provider, oh-my-opencode uses these model assignments:
| Agent | Model |
| ------------- | --------------------------------- |
| **Sisyphus** | `github-copilot/claude-opus-4.6` |
| **Sisyphus** | `github-copilot/claude-opus-4-6` |
| **Oracle** | `github-copilot/gpt-5.4` |
| **Explore** | `github-copilot/grok-code-fast-1` |
| **Librarian** | `github-copilot/gemini-3-flash` |
@@ -227,7 +227,7 @@ If Z.ai is your main provider, the most important fallbacks are:
#### OpenCode Zen
OpenCode Zen provides access to `opencode/` prefixed models including `opencode/claude-opus-4-6`, `opencode/gpt-5.4`, `opencode/gpt-5.3-codex`, `opencode/gpt-5-nano`, `opencode/glm-5`, `opencode/big-pickle`, and `opencode/minimax-m2.7-highspeed`.
OpenCode Zen provides access to `opencode/` prefixed models including `opencode/claude-opus-4-6`, `opencode/gpt-5.4`, `opencode/gpt-5.3-codex`, `opencode/gpt-5-nano`, `opencode/glm-5`, `opencode/big-pickle`, and `opencode/minimax-m2.5-free`.
When OpenCode Zen is the best available provider (no native or Copilot), these models are used:
@@ -236,14 +236,14 @@ When OpenCode Zen is the best available provider (no native or Copilot), these m
| **Sisyphus** | `opencode/claude-opus-4-6` |
| **Oracle** | `opencode/gpt-5.4` |
| **Explore** | `opencode/gpt-5-nano` |
| **Librarian** | `opencode/minimax-m2.7-highspeed` / `opencode/big-pickle` |
| **Librarian** | `opencode/minimax-m2.5-free` / `opencode/big-pickle` |
##### Setup
Run the installer and select "Yes" for GitHub Copilot:
```bash
bunx oh-my-openagent install
bunx oh-my-opencode install
# Select your subscriptions (Claude, ChatGPT, Gemini)
# When prompted: "Do you have a GitHub Copilot subscription?" → Select "Yes"
```
@@ -251,7 +251,7 @@ bunx oh-my-openagent install
Or use non-interactive mode:
```bash
bunx oh-my-openagent install --no-tui --claude=no --openai=no --gemini=no --copilot=yes
bunx oh-my-opencode install --no-tui --claude=no --openai=no --gemini=no --copilot=yes
```
Then authenticate with GitHub:
@@ -263,7 +263,7 @@ opencode auth login
### Step 5: Understand Your Model Setup
You've just configured oh-my-openagent. Here's what got set up and why.
You've just configured oh-my-opencode. Here's what got set up and why.
#### Model Families: What You're Working With
@@ -287,17 +287,16 @@ Not all models behave the same way. Understanding which models are "similar" hel
| ----------------- | -------------------------------- | ------------------------------------------------- |
| **GPT-5.3-codex** | openai, github-copilot, opencode | Deep coding powerhouse. Required for Hephaestus. |
| **GPT-5.4** | openai, github-copilot, opencode | High intelligence. Default for Oracle. |
| **GPT-5.4 Mini** | openai, github-copilot, opencode | Fast + strong reasoning. Default for quick category. |
| **GPT-5-Nano** | opencode | Ultra-cheap, fast. Good for simple utility tasks. |
**Different-Behavior Models**:
| Model | Provider(s) | Notes |
| --------------------- | -------------------------------- | ----------------------------------------------------------- |
| **Gemini 3.1 Pro** | google, github-copilot, opencode | Excels at visual/frontend tasks. Different reasoning style. |
| **Gemini 3 Pro** | google, github-copilot, opencode | Excels at visual/frontend tasks. Different reasoning style. |
| **Gemini 3 Flash** | google, github-copilot, opencode | Fast, good for doc search and light tasks. |
| **MiniMax M2.7** | venice, opencode-go | Fast and smart. Good for utility tasks. Upgraded from M2.5. |
| **MiniMax M2.7 Highspeed** | opencode | Ultra-fast MiniMax variant. Optimized for latency. |
| **MiniMax M2.5** | venice | Fast and smart. Good for utility tasks. |
| **MiniMax M2.5 Free** | opencode | Free-tier MiniMax. Fast for search/retrieval. |
**Speed-Focused Models**:
@@ -305,8 +304,8 @@ Not all models behave the same way. Understanding which models are "similar" hel
| ----------------------- | ---------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| **Grok Code Fast 1** | github-copilot, venice | Very fast | Optimized for code grep/search. Default for Explore. |
| **Claude Haiku 4.5** | anthropic, opencode | Fast | Good balance of speed and intelligence. |
| **MiniMax M2.7 Highspeed** | opencode | Very fast | Ultra-fast MiniMax variant. Smart for its speed class. |
| **GPT-5.3-codex-spark** | openai | Extremely fast | Blazing fast but compacts so aggressively that oh-my-openagent's context management doesn't work well with it. Not recommended for omo agents. |
| **MiniMax M2.5 (Free)** | opencode, venice | Fast | Smart for its speed class. |
| **GPT-5.3-codex-spark** | openai | Extremely fast | Blazing fast but compacts so aggressively that oh-my-opencode's context management doesn't work well with it. Not recommended for omo agents. |
#### What Each Agent Does and Which Model It Got
@@ -317,7 +316,7 @@ Based on your subscriptions, here's how the agents were configured:
| Agent | Role | Default Chain | What It Does |
| ------------ | ---------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------- |
| **Sisyphus** | Main ultraworker | Opus (max) → Kimi K2.5 → GLM 5 → Big Pickle | Primary coding agent. Orchestrates everything. **Never use GPT — no GPT prompt exists.** |
| **Metis** | Plan review | Opus (max) → Kimi K2.5 → GPT-5.4 → Gemini 3.1 Pro | Reviews Prometheus plans for gaps. |
| **Metis** | Plan review | Opus (max) → Kimi K2.5 → GPT-5.4 → Gemini 3 Pro | Reviews Prometheus plans for gaps. |
**Dual-Prompt Agents** (auto-switch between Claude and GPT prompts):
@@ -327,7 +326,7 @@ Priority: **Claude > GPT > Claude-like models**
| Agent | Role | Default Chain | GPT Prompt? |
| -------------- | ----------------- | ---------------------------------------------------------- | ---------------------------------------------------------------- |
| **Prometheus** | Strategic planner | Opus (max) → **GPT-5.4 (high)** → Kimi K2.5 → Gemini 3.1 Pro | Yes — XML-tagged, principle-driven (~300 lines vs ~1,100 Claude) |
| **Prometheus** | Strategic planner | Opus (max) → **GPT-5.4 (high)** → Kimi K2.5 → Gemini 3 Pro | Yes — XML-tagged, principle-driven (~300 lines vs ~1,100 Claude) |
| **Atlas** | Todo orchestrator | **Kimi K2.5** → Sonnet → GPT-5.4 | Yes — GPT-optimized todo management |
**GPT-Native Agents** (built for GPT, don't override to Claude):
@@ -335,8 +334,8 @@ Priority: **Claude > GPT > Claude-like models**
| Agent | Role | Default Chain | Notes |
| -------------- | ---------------------- | -------------------------------------- | ------------------------------------------------------ |
| **Hephaestus** | Deep autonomous worker | GPT-5.3-codex (medium) only | "Codex on steroids." No fallback. Requires GPT access. |
| **Oracle** | Architecture/debugging | GPT-5.4 (high) → Gemini 3.1 Pro → Opus | High-IQ strategic backup. GPT preferred. |
| **Momus** | High-accuracy reviewer | GPT-5.4 (medium) → Opus → Gemini 3.1 Pro | Verification agent. GPT preferred. |
| **Oracle** | Architecture/debugging | GPT-5.4 (high) → Gemini 3 Pro → Opus | High-IQ strategic backup. GPT preferred. |
| **Momus** | High-accuracy reviewer | GPT-5.4 (medium) → Opus → Gemini 3 Pro | Verification agent. GPT preferred. |
**Utility Agents** (speed over intelligence):
@@ -344,8 +343,8 @@ These agents do search, grep, and retrieval. They intentionally use fast, cheap
| Agent | Role | Default Chain | Design Rationale |
| --------------------- | ------------------ | ---------------------------------------------------------------------- | -------------------------------------------------------------- |
| **Explore** | Fast codebase grep | Grok Code Fast → MiniMax M2.7-highspeed → MiniMax M2.7 → Haiku → GPT-5-Nano | Speed is everything. Grok is blazing fast for grep. |
| **Librarian** | Docs/code search | MiniMax M2.7 → MiniMax M2.7-highspeed → Haiku → GPT-5-Nano | Doc retrieval doesn't need deep reasoning. MiniMax is fast. |
| **Explore** | Fast codebase grep | MiniMax M2.5 Free → Grok Code Fast → MiniMax M2.5 → Haiku → GPT-5-Nano | Speed is everything. Grok is blazing fast for grep. |
| **Librarian** | Docs/code search | MiniMax M2.5 Free → Gemini Flash → Big Pickle | Entirely free-tier. Doc retrieval doesn't need deep reasoning. |
| **Multimodal Looker** | Vision/screenshots | Kimi K2.5 → Kimi Free → Gemini Flash → GPT-5.4 → GLM-4.6v | Kimi excels at multimodal understanding. |
#### Why Different Models Need Different Prompts
@@ -365,7 +364,7 @@ This is why Prometheus and Atlas ship separate prompts per model family — they
#### Custom Model Configuration
If the user wants to override which model an agent uses, you can customize in `oh-my-openagent.json`:
If the user wants to override which model an agent uses, you can customize in `oh-my-opencode.json`:
```jsonc
{
@@ -407,7 +406,7 @@ GPT (5.3-codex, 5.2) > Claude Opus (decent fallback) > Gemini (acceptable)
#### Provider Priority Chain
When multiple providers are available, oh-my-openagent uses this priority:
When multiple providers are available, oh-my-opencode uses this priority:
```
Native (anthropic/, openai/, google/) > Kimi for Coding > GitHub Copilot > Venice > OpenCode Zen > Z.ai Coding Plan
@@ -425,7 +424,7 @@ Read this document again, think about you have done everything correctly.
#### Say 'Congratulations! 🎉' to the user
Say to user: Congratulations! 🎉 You have successfully set up OhMyOpenAgent! Type `opencode` in your terminal to start using it.
Say to user: Congratulations! 🎉 You have successfully set up OhMyOpenCode! Type `opencode` in your terminal to start using it.
#### Free advertising

View File

@@ -1,6 +1,6 @@
# Orchestration System Guide
Oh My OpenAgent's orchestration system transforms a simple AI agent into a coordinated development team through **separation of planning and execution**.
Oh My OpenCode's orchestration system transforms a simple AI agent into a coordinated development team through **separation of planning and execution**.
---
@@ -298,7 +298,7 @@ task({ category: "quick", prompt: "..." }); // "Just get it done fast"
| `visual-engineering` | Gemini 3.1 Pro | Frontend, UI/UX, design, styling, animation |
| `ultrabrain` | GPT-5.4 (xhigh) | Deep logical reasoning, complex architecture decisions |
| `artistry` | Gemini 3.1 Pro (high) | Highly creative or artistic tasks, novel ideas |
| `quick` | GPT-5.4 Mini | Trivial tasks - single file changes, typo fixes |
| `quick` | Claude Haiku 4.5 | Trivial tasks - single file changes, typo fixes |
| `deep` | GPT-5.3 Codex (medium) | Goal-oriented autonomous problem-solving, thorough research |
| `unspecified-low` | Claude Sonnet 4.6 | Tasks that don't fit other categories, low effort |
| `unspecified-high` | Claude Opus 4.6 (max) | Tasks that don't fit other categories, high effort |
@@ -475,7 +475,7 @@ Use the `ulw` keyword in Sisyphus when:
## Configuration
You can control related features in `oh-my-openagent.json`:
You can control related features in `oh-my-opencode.json`:
```jsonc
{

View File

@@ -1,6 +1,6 @@
# What Is Oh My OpenAgent?
# What Is Oh My OpenCode?
Oh My OpenAgent is a multi-model agent orchestration harness for OpenCode. It transforms a single AI agent into a coordinated development team that actually ships code.
Oh My OpenCode is a multi-model agent orchestration harness for OpenCode. It transforms a single AI agent into a coordinated development team that actually ships code.
Not locked to Claude. Not locked to OpenAI. Not locked to anyone.
@@ -15,7 +15,7 @@ Just better results, cheaper models, real orchestration.
Paste this into your LLM agent session:
```
Install and configure oh-my-openagent by following the instructions here:
Install and configure oh-my-opencode by following the instructions here:
https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/refs/heads/dev/docs/guide/installation.md
```
@@ -41,13 +41,13 @@ We used to call this "Claude Code on steroids." That was wrong.
This isn't about making Claude Code better. It's about breaking free from the idea that one model, one provider, one way of working is enough. Anthropic wants you locked in. OpenAI wants you locked in. Everyone wants you locked in.
Oh My OpenAgent doesn't play that game. It orchestrates across models, picking the right brain for the right job. Claude for orchestration. GPT for deep reasoning. Gemini for frontend. GPT-5.4 Mini for quick tasks. All working together, automatically.
Oh My OpenCode doesn't play that game. It orchestrates across models, picking the right brain for the right job. Claude for orchestration. GPT for deep reasoning. Gemini for frontend. Haiku for quick tasks. All working together, automatically.
---
## How It Works: Agent Orchestration
Instead of one agent doing everything, Oh My OpenAgent uses **specialized agents that delegate to each other** based on task type.
Instead of one agent doing everything, Oh My OpenCode uses **specialized agents that delegate to each other** based on task type.
**The Architecture:**
@@ -99,9 +99,9 @@ Use Hephaestus when you need deep architectural reasoning, complex debugging acr
**Why this beats vanilla Codex CLI:**
- **Multi-model orchestration.** Pure Codex is single-model. OmO routes different tasks to different models automatically. GPT for deep reasoning. Gemini for frontend. GPT-5.4 Mini for speed. The right brain for the right job.
- **Multi-model orchestration.** Pure Codex is single-model. OmO routes different tasks to different models automatically. GPT for deep reasoning. Gemini for frontend. Haiku for speed. The right brain for the right job.
- **Background agents.** Fire 5+ agents in parallel. Something Codex simply cannot do. While one agent writes code, another researches patterns, another checks documentation. Like a real dev team.
- **Category system.** Tasks are routed by intent, not model name. `visual-engineering` gets Gemini. `ultrabrain` gets GPT-5.4. `quick` gets GPT-5.4 Mini. No manual juggling.
- **Category system.** Tasks are routed by intent, not model name. `visual-engineering` gets Gemini. `ultrabrain` gets GPT-5.4. `quick` gets Haiku. No manual juggling.
- **Accumulated wisdom.** Subagents learn from previous results. Conventions discovered in task 1 are passed to task 5. Mistakes made early aren't repeated. The system gets smarter as it works.
### Prometheus: The Strategic Planner
@@ -154,7 +154,7 @@ Use Prometheus for multi-day projects, critical production changes, complex refa
## Agent Model Matching
Different agents work best with different models. Oh My OpenAgent automatically assigns optimal models, but you can customize everything.
Different agents work best with different models. Oh My OpenCode automatically assigns optimal models, but you can customize everything.
### Default Configuration
@@ -168,7 +168,7 @@ You can override specific agents or categories in your config:
```jsonc
{
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-openagent.schema.json",
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json",
"agents": {
// Main orchestrator: Claude Opus or Kimi K2.5 work best
@@ -195,8 +195,8 @@ You can override specific agents or categories in your config:
// General high-effort work
"unspecified-high": { "model": "anthropic/claude-opus-4-6", "variant": "max" },
// Quick tasks: use GPT-5.4-mini (fast and cheap)
"quick": { "model": "openai/gpt-5.4-mini" },
// Quick tasks: use the cheapest models
"quick": { "model": "anthropic/claude-haiku-4-5" },
// Deep reasoning: GPT-5.4
"ultrabrain": { "model": "openai/gpt-5.4", "variant": "xhigh" },
@@ -220,8 +220,8 @@ You can override specific agents or categories in your config:
**Different-behavior models**:
- Gemini 3.1 Pro — excels at visual/frontend tasks
- MiniMax M2.7 / M2.7-highspeed — fast and smart for utility tasks
- Gemini 3 Pro — excels at visual/frontend tasks
- MiniMax M2.5 — fast and smart for utility tasks
- Grok Code Fast 1 — optimized for code grep/search
See the [Agent-Model Matching Guide](./agent-model-matching.md) for complete details on which models work best for each agent, safe vs dangerous overrides, and provider priority chains.
@@ -232,7 +232,7 @@ See the [Agent-Model Matching Guide](./agent-model-matching.md) for complete det
Claude Code is good. But it's a single agent running a single model doing everything alone.
Oh My OpenAgent turns that into a coordinated team:
Oh My OpenCode turns that into a coordinated team:
**Parallel execution.** Claude Code processes one thing at a time. OmO fires background agents in parallel — research, implementation, and verification happening simultaneously. Like having 5 engineers instead of 1.
@@ -246,7 +246,7 @@ Oh My OpenAgent turns that into a coordinated team:
**Discipline enforcement.** Todo enforcer yanks idle agents back to work. Comment checker strips AI slop. Ralph Loop keeps going until 100% done. The system doesn't let the agent slack off.
**The fundamental advantage.** Models have different temperaments. Claude thinks deeply. GPT reasons architecturally. Gemini visualizes. Haiku moves fast. Single-model tools force you to pick one personality for all tasks. Oh My OpenAgent leverages them all, routing by task type. This isn't a temporary hack — it's the only architecture that makes sense as models specialize further. The gap between multi-model orchestration and single-model limitation widens every month. We're betting on that future.
**The fundamental advantage.** Models have different temperaments. Claude thinks deeply. GPT reasons architecturally. Gemini visualizes. Haiku moves fast. Single-model tools force you to pick one personality for all tasks. Oh My OpenCode leverages them all, routing by task type. This isn't a temporary hack — it's the only architecture that makes sense as models specialize further. The gap between multi-model orchestration and single-model limitation widens every month. We're betting on that future.
---
@@ -256,7 +256,7 @@ Before acting on any request, Sisyphus classifies your true intent.
Are you asking for research? Implementation? Investigation? A fix? The Intent Gate figures out what you actually want, not just the literal words you typed. This means the agent understands context, nuance, and the real goal behind your request.
Claude Code doesn't have this. It takes your prompt and runs. Oh My OpenAgent thinks first, then acts.
Claude Code doesn't have this. It takes your prompt and runs. Oh My OpenCode thinks first, then acts.
---

View File

@@ -1,6 +1,6 @@
# Manifesto
The principles and philosophy behind Oh My OpenAgent.
The principles and philosophy behind Oh My OpenCode.
---
@@ -20,7 +20,7 @@ When you find yourself:
That's not "human-AI collaboration." That's the AI failing to do its job.
**Oh My OpenAgent is built on this premise**: Human intervention during agentic work is fundamentally a wrong signal. If the system is designed correctly, the agent should complete the work without requiring you to babysit it.
**Oh My OpenCode is built on this premise**: Human intervention during agentic work is fundamentally a wrong signal. If the system is designed correctly, the agent should complete the work without requiring you to babysit it.
---
@@ -144,7 +144,7 @@ Human Intent → Agent Execution → Verified Result
(intervention only on true failure)
```
Everything in Oh My OpenAgent is designed to make this loop work:
Everything in Oh My OpenCode is designed to make this loop work:
| Feature | Purpose |
|---------|---------|

View File

@@ -1,33 +0,0 @@
# Model Capabilities Maintenance
This project treats model capability resolution as a layered system:
1. runtime metadata from connected providers
2. `models.dev` bundled/runtime snapshot data
3. explicit compatibility aliases
4. heuristic fallback as the last resort
## Internal policy
- Built-in OmO agent/category requirement models must use canonical model IDs.
- Aliases exist only to preserve compatibility with historical OmO names or provider-specific decorations.
- New decorated names like `-high`, `-low`, or `-thinking` should not be added to built-in requirements when a canonical model ID plus structured settings can express the same thing.
- If a provider or config input still uses an alias, normalize it at the edge and continue internally with the canonical ID.
## When adding an alias
- Add the alias rule to `src/shared/model-capability-aliases.ts`.
- Include a rationale for why the alias exists.
- Add or update tests so the alias is covered explicitly.
- Ensure the alias canonical target exists in the bundled `models.dev` snapshot.
## Guardrails
`bun run test:model-capabilities` enforces the following invariants:
- exact alias targets must exist in the bundled snapshot
- exact alias keys must not silently become canonical `models.dev` IDs
- pattern aliases must not rewrite canonical snapshot IDs
- built-in requirement models must stay canonical and snapshot-backed
The scheduled `refresh-model-capabilities` workflow runs these guardrails before opening an automated snapshot refresh PR.

View File

@@ -1,15 +1,15 @@
# CLI Reference
Complete reference for the `oh-my-openagent` command-line interface.
Complete reference for the `oh-my-opencode` command-line interface.
## Basic Usage
```bash
# Display help
bunx oh-my-openagent
bunx oh-my-opencode
# Or with npx
npx oh-my-openagent
npx oh-my-opencode
```
## Commands
@@ -27,20 +27,20 @@ npx oh-my-openagent
## install
Interactive installation tool for initial Oh-My-OpenAgent setup. Provides a TUI based on `@clack/prompts`.
Interactive installation tool for initial Oh-My-OpenCode setup. Provides a TUI based on `@clack/prompts`.
### Usage
```bash
bunx oh-my-openagent install
bunx oh-my-opencode install
```
### Installation Process
1. **Provider Selection**: Choose your AI provider (Claude, ChatGPT, or Gemini)
2. **API Key Input**: Enter the API key for your selected provider
3. **Configuration File Creation**: Generates `opencode.json` or `oh-my-openagent.json` files
4. **Plugin Registration**: Automatically registers the oh-my-openagent plugin in OpenCode settings
3. **Configuration File Creation**: Generates `opencode.json` or `oh-my-opencode.json` files
4. **Plugin Registration**: Automatically registers the oh-my-opencode plugin in OpenCode settings
### Options
@@ -53,12 +53,12 @@ bunx oh-my-openagent install
## doctor
Diagnoses your environment to ensure Oh-My-OpenAgent is functioning correctly. Performs 17+ health checks.
Diagnoses your environment to ensure Oh-My-OpenCode is functioning correctly. Performs 17+ health checks.
### Usage
```bash
bunx oh-my-openagent doctor
bunx oh-my-opencode doctor
```
### Diagnostic Categories
@@ -83,10 +83,10 @@ bunx oh-my-openagent doctor
### Example Output
```
oh-my-openagent doctor
oh-my-opencode doctor
┌──────────────────────────────────────────────────┐
│ Oh-My-OpenAgent Doctor │
│ Oh-My-OpenCode Doctor │
└──────────────────────────────────────────────────┘
Installation
@@ -94,7 +94,7 @@ Installation
✓ Plugin registered in opencode.json
Configuration
✓ oh-my-openagent.json is valid
✓ oh-my-opencode.json is valid
⚠ categories.visual-engineering: using default model
Authentication
@@ -119,7 +119,7 @@ Executes OpenCode sessions and monitors task completion.
### Usage
```bash
bunx oh-my-openagent run [prompt]
bunx oh-my-opencode run [prompt]
```
### Options
@@ -148,16 +148,16 @@ Manages OAuth 2.1 authentication for remote MCP servers.
```bash
# Login to an OAuth-protected MCP server
bunx oh-my-openagent mcp oauth login <server-name> --server-url https://api.example.com
bunx oh-my-opencode mcp oauth login <server-name> --server-url https://api.example.com
# Login with explicit client ID and scopes
bunx oh-my-openagent mcp oauth login my-api --server-url https://api.example.com --client-id my-client --scopes "read,write"
bunx oh-my-opencode mcp oauth login my-api --server-url https://api.example.com --client-id my-client --scopes "read,write"
# Remove stored OAuth tokens
bunx oh-my-openagent mcp oauth logout <server-name>
bunx oh-my-opencode mcp oauth logout <server-name>
# Check OAuth token status
bunx oh-my-openagent mcp oauth status [server-name]
bunx oh-my-opencode mcp oauth status [server-name]
```
### Options
@@ -178,8 +178,8 @@ Tokens are stored in `~/.config/opencode/mcp-oauth.json` with `0600` permissions
The CLI searches for configuration files in the following locations (in priority order):
1. **Project Level**: `.opencode/oh-my-openagent.json`
2. **User Level**: `~/.config/opencode/oh-my-openagent.json`
1. **Project Level**: `.opencode/oh-my-opencode.json`
2. **User Level**: `~/.config/opencode/oh-my-opencode.json`
### JSONC Support
@@ -219,17 +219,17 @@ bun install -g opencode@latest
```bash
# Reinstall plugin
bunx oh-my-openagent install
bunx oh-my-opencode install
```
### Doctor Check Failures
```bash
# Diagnose with detailed information
bunx oh-my-openagent doctor --verbose
bunx oh-my-opencode doctor --verbose
# Check specific category only
bunx oh-my-openagent doctor --category authentication
bunx oh-my-opencode doctor --category authentication
```
---
@@ -240,10 +240,10 @@ Use the `--no-tui` option for CI/CD environments.
```bash
# Run doctor in CI environment
bunx oh-my-openagent doctor --no-tui --json
bunx oh-my-opencode doctor --no-tui --json
# Save results to file
bunx oh-my-openagent doctor --json > doctor-report.json
bunx oh-my-opencode doctor --json > doctor-report.json
```
---

View File

@@ -1,6 +1,6 @@
# Configuration Reference
Complete reference for `oh-my-openagent.jsonc` configuration. This document covers every available option with examples.
Complete reference for `oh-my-opencode.jsonc` configuration. This document covers every available option with examples.
---
@@ -44,13 +44,13 @@ Complete reference for `oh-my-openagent.jsonc` configuration. This document cove
Priority order (project overrides user):
1. `.opencode/oh-my-openagent.jsonc` / `.opencode/oh-my-openagent.json`
1. `.opencode/oh-my-opencode.jsonc` / `.opencode/oh-my-opencode.json`
2. User config (`.jsonc` preferred over `.json`):
| Platform | Path |
| ----------- | ----------------------------------------- |
| macOS/Linux | `~/.config/opencode/oh-my-openagent.jsonc` |
| Windows | `%APPDATA%\opencode\oh-my-openagent.jsonc` |
| macOS/Linux | `~/.config/opencode/oh-my-opencode.jsonc` |
| Windows | `%APPDATA%\opencode\oh-my-opencode.jsonc` |
JSONC supports `// line comments`, `/* block comments */`, and trailing commas.
@@ -58,11 +58,11 @@ Enable schema autocomplete:
```json
{
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-openagent.schema.json"
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json"
}
```
Run `bunx oh-my-openagent install` for guided setup. Run `opencode models` to list available models.
Run `bunx oh-my-opencode install` for guided setup. Run `opencode models` to list available models.
### Quick Start Example
@@ -70,7 +70,7 @@ Here's a practical starting configuration:
```jsonc
{
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-openagent.schema.json",
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json",
"agents": {
// Main orchestrator: Claude Opus or Kimi K2.5 work best
@@ -228,7 +228,7 @@ Domain-specific model delegation used by the `task()` tool. When Sisyphus delega
| `ultrabrain` | `openai/gpt-5.4` (xhigh) | Deep logical reasoning, complex architecture |
| `deep` | `openai/gpt-5.3-codex` (medium) | Autonomous problem-solving, thorough research |
| `artistry` | `google/gemini-3.1-pro` (high) | Creative/unconventional approaches |
| `quick` | `openai/gpt-5.4-mini` | Trivial tasks, typo fixes, single-file changes |
| `quick` | `anthropic/claude-haiku-4-5` | Trivial tasks, typo fixes, single-file changes |
| `unspecified-low` | `anthropic/claude-sonnet-4-6` | General tasks, low effort |
| `unspecified-high` | `anthropic/claude-opus-4-6` (max) | General tasks, high effort |
| `writing` | `google/gemini-3-flash` | Documentation, prose, technical writing |
@@ -270,8 +270,8 @@ Disable categories: `{ "disabled_categories": ["ultrabrain"] }`
| **Sisyphus** | `claude-opus-4-6` | `claude-opus-4-6``glm-5``big-pickle` |
| **Hephaestus** | `gpt-5.3-codex` | `gpt-5.3-codex``gpt-5.4` (GitHub Copilot fallback) |
| **oracle** | `gpt-5.4` | `gpt-5.4``gemini-3.1-pro``claude-opus-4-6` |
| **librarian** | `minimax-m2.7` | `minimax-m2.7``minimax-m2.7-highspeed``claude-haiku-4-5``gpt-5-nano` |
| **explore** | `grok-code-fast-1` | `grok-code-fast-1``minimax-m2.7-highspeed``minimax-m2.7``claude-haiku-4-5``gpt-5-nano` |
| **librarian** | `gemini-3-flash` | `gemini-3-flash``minimax-m2.5-free``big-pickle` |
| **explore** | `grok-code-fast-1` | `grok-code-fast-1``minimax-m2.5-free``claude-haiku-4-5``gpt-5-nano` |
| **multimodal-looker** | `gpt-5.3-codex` | `gpt-5.3-codex``k2p5``gemini-3-flash``glm-4.6v``gpt-5-nano` |
| **Prometheus** | `claude-opus-4-6` | `claude-opus-4-6``gpt-5.4``gemini-3.1-pro` |
| **Metis** | `claude-opus-4-6` | `claude-opus-4-6``gpt-5.4``gemini-3.1-pro` |
@@ -286,12 +286,12 @@ Disable categories: `{ "disabled_categories": ["ultrabrain"] }`
| **ultrabrain** | `gpt-5.4` | `gpt-5.4``gemini-3.1-pro``claude-opus-4-6` |
| **deep** | `gpt-5.3-codex` | `gpt-5.3-codex``claude-opus-4-6``gemini-3.1-pro` |
| **artistry** | `gemini-3.1-pro` | `gemini-3.1-pro``claude-opus-4-6``gpt-5.4` |
| **quick** | `gpt-5.4-mini` | `gpt-5.4-mini` `claude-haiku-4-5``gemini-3-flash` `minimax-m2.7` `gpt-5-nano` |
| **unspecified-low** | `claude-sonnet-4-6` | `claude-sonnet-4-6``gpt-5.3-codex``gemini-3-flash` `minimax-m2.7` |
| **quick** | `claude-haiku-4-5` | `claude-haiku-4-5``gemini-3-flash``gpt-5-nano` |
| **unspecified-low** | `claude-sonnet-4-6` | `claude-sonnet-4-6``gpt-5.3-codex``gemini-3-flash` |
| **unspecified-high** | `claude-opus-4-6` | `claude-opus-4-6``gpt-5.4 (high)``glm-5``k2p5``kimi-k2.5` |
| **writing** | `gemini-3-flash` | `gemini-3-flash``claude-sonnet-4-6` `minimax-m2.7` |
| **writing** | `gemini-3-flash` | `gemini-3-flash``claude-sonnet-4-6` |
Run `bunx oh-my-openagent doctor --verbose` to see effective model resolution for your config.
Run `bunx oh-my-opencode doctor --verbose` to see effective model resolution for your config.
---
@@ -418,14 +418,15 @@ Disable built-in skills: `{ "disabled_skills": ["playwright"] }`
Disable built-in hooks via `disabled_hooks`:
```json
{ "disabled_hooks": ["comment-checker"] }
{ "disabled_hooks": ["comment-checker", "gpt-permission-continuation"] }
```
Available hooks: `todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `tool-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `anthropic-context-window-limit-recovery`, `rules-injector`, `background-notification`, `auto-update-checker`, `startup-toast`, `keyword-detector`, `agent-usage-reminder`, `non-interactive-env`, `interactive-bash-session`, `compaction-context-injector`, `thinking-block-validator`, `claude-code-hooks`, `ralph-loop`, `preemptive-compaction`, `auto-slash-command`, `sisyphus-junior-notepad`, `no-sisyphus-gpt`, `start-work`, `runtime-fallback`
Available hooks: `gpt-permission-continuation`, `todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `tool-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `anthropic-context-window-limit-recovery`, `rules-injector`, `background-notification`, `auto-update-checker`, `startup-toast`, `keyword-detector`, `agent-usage-reminder`, `non-interactive-env`, `interactive-bash-session`, `compaction-context-injector`, `thinking-block-validator`, `claude-code-hooks`, `ralph-loop`, `preemptive-compaction`, `auto-slash-command`, `sisyphus-junior-notepad`, `no-sisyphus-gpt`, `start-work`, `runtime-fallback`
**Notes:**
- `directory-agents-injector` — auto-disabled on OpenCode 1.1.37+ (native AGENTS.md support)
- `gpt-permission-continuation` — resumes GPT sessions only when the last assistant reply ends with a permission-seeking tail like `If you want, ...`. Disable it if you prefer GPT sessions to wait for explicit user follow-up.
- `no-sisyphus-gpt`**do not disable**. It blocks incompatible GPT models for Sisyphus while allowing the dedicated GPT-5.4 prompt path.
- `startup-toast` is a sub-feature of `auto-update-checker`. Disable just the toast by adding `startup-toast` to `disabled_hooks`.

View File

@@ -1,8 +1,8 @@
# Oh-My-OpenAgent Features Reference
# Oh-My-OpenCode Features Reference
## Agents
Oh-My-OpenAgent provides 11 specialized AI agents. Each has distinct expertise, optimized models, and tool permissions.
Oh-My-OpenCode provides 11 specialized AI agents. Each has distinct expertise, optimized models, and tool permissions.
### Core Agents
@@ -11,8 +11,8 @@ Oh-My-OpenAgent provides 11 specialized AI agents. Each has distinct expertise,
| **Sisyphus** | `claude-opus-4-6` | The default orchestrator. Plans, delegates, and executes complex tasks using specialized subagents with aggressive parallel execution. Todo-driven workflow with extended thinking (32k budget). Fallback: `glm-5``big-pickle`. |
| **Hephaestus** | `gpt-5.3-codex` | The Legitimate Craftsman. Autonomous deep worker inspired by AmpCode's deep mode. Goal-oriented execution with thorough research before action. Explores codebase patterns, completes tasks end-to-end without premature stopping. Named after the Greek god of forge and craftsmanship. Fallback: `gpt-5.4` on GitHub Copilot. Requires a GPT-capable provider. |
| **Oracle** | `gpt-5.4` | Architecture decisions, code review, debugging. Read-only consultation with stellar logical reasoning and deep analysis. Inspired by AmpCode. Fallback: `gemini-3.1-pro``claude-opus-4-6`. |
| **Librarian** | `minimax-m2.7` | Multi-repo analysis, documentation lookup, OSS implementation examples. Deep codebase understanding with evidence-based answers. Fallback: `minimax-m2.7-highspeed``claude-haiku-4-5``gpt-5-nano`. |
| **Explore** | `grok-code-fast-1` | Fast codebase exploration and contextual grep. Fallback: `minimax-m2.7-highspeed``minimax-m2.7``claude-haiku-4-5``gpt-5-nano`. |
| **Librarian** | `gemini-3-flash` | Multi-repo analysis, documentation lookup, OSS implementation examples. Deep codebase understanding with evidence-based answers. Fallback: `minimax-m2.5-free``big-pickle`. |
| **Explore** | `grok-code-fast-1` | Fast codebase exploration and contextual grep. Fallback: `minimax-m2.5-free``claude-haiku-4-5``gpt-5-nano`. |
| **Multimodal-Looker** | `gpt-5.3-codex` | Visual content specialist. Analyzes PDFs, images, diagrams to extract information. Fallback: `k2p5``gemini-3-flash``glm-4.6v``gpt-5-nano`. |
### Planning Agents
@@ -90,7 +90,7 @@ When running inside tmux:
- Each pane shows agent output live
- Auto-cleanup when agents complete
Customize agent models, prompts, and permissions in `oh-my-openagent.json`.
Customize agent models, prompts, and permissions in `oh-my-opencode.json`.
## Category System
@@ -111,7 +111,7 @@ By combining these two concepts, you can generate optimal agents through `task`.
| `ultrabrain` | `openai/gpt-5.4` (xhigh) | Deep logical reasoning, complex architecture decisions requiring extensive analysis |
| `deep` | `openai/gpt-5.3-codex` (medium) | Goal-oriented autonomous problem-solving. Thorough research before action. For hairy problems requiring deep understanding. |
| `artistry` | `google/gemini-3.1-pro` (high) | Highly creative/artistic tasks, novel ideas |
| `quick` | `openai/gpt-5.4-mini` | Trivial tasks - single file changes, typo fixes, simple modifications |
| `quick` | `anthropic/claude-haiku-4-5` | Trivial tasks - single file changes, typo fixes, simple modifications |
| `unspecified-low` | `anthropic/claude-sonnet-4-6` | Tasks that don't fit other categories, low effort required |
| `unspecified-high` | `anthropic/claude-opus-4-6` (max) | Tasks that don't fit other categories, high effort required |
| `writing` | `google/gemini-3-flash` | Documentation, prose, technical writing |
@@ -129,7 +129,7 @@ task({
### Custom Categories
You can define custom categories in `oh-my-openagent.json`.
You can define custom categories in `oh-my-opencode.json`.
#### Category Configuration Schema
@@ -237,7 +237,7 @@ Skills provide specialized workflows with embedded MCP servers and detailed inst
### Browser Automation Options
Oh-My-OpenAgent provides two browser automation providers, configurable via `browser_automation_engine.provider`.
Oh-My-OpenCode provides two browser automation providers, configurable via `browser_automation_engine.provider`.
#### Option 1: Playwright MCP (Default)
@@ -558,7 +558,7 @@ Requires `experimental.task_system: true` in config.
#### Task System Details
**Note on Claude Code Alignment**: This implementation follows Claude Code's internal Task tool signatures (`TaskCreate`, `TaskUpdate`, `TaskList`, `TaskGet`) and field naming conventions (`subject`, `blockedBy`, `blocks`, etc.). However, Anthropic has not published official documentation for these tools. This is Oh My OpenAgent's own implementation based on observed Claude Code behavior and internal specifications.
**Note on Claude Code Alignment**: This implementation follows Claude Code's internal Task tool signatures (`TaskCreate`, `TaskUpdate`, `TaskList`, `TaskGet`) and field naming conventions (`subject`, `blockedBy`, `blocks`, etc.). However, Anthropic has not published official documentation for these tools. This is Oh My OpenCode's own implementation based on observed Claude Code behavior and internal specifications.
**Task Schema**:
@@ -680,6 +680,7 @@ Hooks intercept and modify behavior at key points in the agent lifecycle across
| **ralph-loop** | Event + Message | Manages self-referential loop continuation. |
| **start-work** | Message | Handles /start-work command execution. |
| **auto-slash-command** | Message | Automatically executes slash commands from prompts. |
| **gpt-permission-continuation** | Event | Auto-continues GPT sessions when the final assistant reply ends with a permission-seeking tail such as `If you want, ...`. |
| **stop-continuation-guard** | Event + Message | Guards the stop-continuation mechanism. |
| **category-skill-reminder** | Event + PostToolUse | Reminds agents about available category skills for delegation. |
| **anthropic-effort** | Params | Adjusts Anthropic API effort level based on context. |
@@ -734,6 +735,7 @@ Hooks intercept and modify behavior at key points in the agent lifecycle across
| Hook | Event | Description |
| ------------------------------ | ----- | ---------------------------------------------------------- |
| **gpt-permission-continuation** | Event | Continues GPT replies that end in a permission-seeking tail. |
| **todo-continuation-enforcer** | Event | Enforces todo completion — yanks idle agents back to work. |
| **compaction-todo-preserver** | Event | Preserves todo state during session compaction. |
| **unstable-agent-babysitter** | Event | Handles unstable agent behavior with recovery strategies. |
@@ -785,10 +787,12 @@ Disable specific hooks in config:
```json
{
"disabled_hooks": ["comment-checker"]
"disabled_hooks": ["comment-checker", "gpt-permission-continuation"]
}
```
Use `gpt-permission-continuation` when you want GPT sessions to stop at permission-seeking endings instead of auto-resuming.
## MCPs
### Built-in MCPs
@@ -844,7 +848,7 @@ When a skill MCP has `oauth` configured:
Pre-authenticate via CLI:
```bash
bunx oh-my-openagent mcp oauth login <server-name> --server-url https://api.example.com
bunx oh-my-opencode mcp oauth login <server-name> --server-url https://api.example.com
```
## Context Injection

View File

@@ -1,86 +0,0 @@
# Model Settings Compatibility Resolver Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Centralize compatibility handling for `variant` and `reasoningEffort` so an already-selected model receives the best valid settings for that exact model.
**Architecture:** Introduce a pure shared resolver in `src/shared/` that computes compatible settings and records downgrades/removals. Integrate it first in `chat.params`, then keep Claude-specific effort logic as a thin layer rather than a special-case policy owner.
**Tech Stack:** TypeScript, Bun test, existing shared model normalization/utilities, OpenCode plugin `chat.params` path.
---
### Task 1: Create the pure compatibility resolver
**Files:**
- Create: `src/shared/model-settings-compatibility.ts`
- Create: `src/shared/model-settings-compatibility.test.ts`
- Modify: `src/shared/index.ts`
- [ ] **Step 1: Write failing tests for exact keep behavior**
- [ ] **Step 2: Write failing tests for downgrade behavior (`max` -> `high`, `xhigh` -> `high` where needed)**
- [ ] **Step 3: Write failing tests for unsupported-value removal**
- [ ] **Step 4: Write failing tests for model-family distinctions (Opus vs Sonnet/Haiku, GPT-family variants)**
- [ ] **Step 5: Implement the pure resolver with explicit capability ladders**
- [ ] **Step 6: Export the resolver from `src/shared/index.ts`**
- [ ] **Step 7: Run `bun test src/shared/model-settings-compatibility.test.ts`**
- [ ] **Step 8: Commit**
### Task 2: Integrate resolver into chat.params
**Files:**
- Modify: `src/plugin/chat-params.ts`
- Modify: `src/plugin/chat-params.test.ts`
- [ ] **Step 1: Write failing tests showing `chat.params` applies resolver output to runtime settings**
- [ ] **Step 2: Ensure tests cover both `variant` and `reasoningEffort` decisions**
- [ ] **Step 3: Update `chat-params.ts` to call the shared resolver before hook-specific adjustments**
- [ ] **Step 4: Preserve existing prompt-param-store merging behavior**
- [ ] **Step 5: Run `bun test src/plugin/chat-params.test.ts`**
- [ ] **Step 6: Commit**
### Task 3: Re-scope anthropic-effort around the resolver
**Files:**
- Modify: `src/hooks/anthropic-effort/hook.ts`
- Modify: `src/hooks/anthropic-effort/index.test.ts`
- [ ] **Step 1: Write failing tests that codify the intended remaining Anthropic-specific behavior after centralization**
- [ ] **Step 2: Reduce `anthropic-effort` to Claude/Anthropic-specific effort injection where still needed**
- [ ] **Step 3: Remove duplicated compatibility policy from the hook if the shared resolver now owns it**
- [ ] **Step 4: Run `bun test src/hooks/anthropic-effort/index.test.ts`**
- [ ] **Step 5: Commit**
### Task 4: Add integration/regression coverage across real request paths
**Files:**
- Modify: `src/plugin/chat-params.test.ts`
- Modify: `src/hooks/anthropic-effort/index.test.ts`
- Add tests only where needed in nearby suites
- [ ] **Step 1: Add regression test for non-Opus Claude with `variant=max` resolving to compatible settings without ad hoc path-only logic**
- [ ] **Step 2: Add regression test for GPT-style `reasoningEffort` compatibility**
- [ ] **Step 3: Add regression test showing supported values remain unchanged**
- [ ] **Step 4: Run the focused test set**
- [ ] **Step 5: Commit**
### Task 5: Verify full quality bar
**Files:**
- No intended code changes
- [ ] **Step 1: Run `bun run typecheck`**
- [ ] **Step 2: Run a focused suite for the touched files**
- [ ] **Step 3: If clean, run `bun test`**
- [ ] **Step 4: Review diff for accidental scope creep**
- [ ] **Step 5: Commit any final cleanup**
### Task 6: Prepare PR metadata
**Files:**
- No repo file change required unless docs are updated further
- [ ] **Step 1: Write a human summary explaining this is settings compatibility, not model fallback**
- [ ] **Step 2: Document scope: Phase 1 covers `variant` and `reasoningEffort` only**
- [ ] **Step 3: Document explicit non-goals: no model switching, no automatic upscaling in Phase 1**
- [ ] **Step 4: Request review**

View File

@@ -1,164 +0,0 @@
# Model Settings Compatibility Resolver Design
## Goal
Introduce a central resolver that takes an already-selected model and a set of desired model settings, then returns the best compatible configuration for that exact model.
This is explicitly separate from model fallback.
## Problem
Today, logic for `variant` and `reasoningEffort` compatibility is scattered across multiple places:
- `hooks/anthropic-effort`
- `plugin/chat-params`
- agent/category/fallback config layers
- delegate/background prompt plumbing
That creates inconsistent behavior:
- some paths clamp unsupported levels
- some paths pass them through unchanged
- some paths silently drop them
- some paths use model-family-specific assumptions that do not generalize
The result is brittle request behavior even when the chosen model itself is valid.
## Scope
Phase 1 covers only:
- `variant`
- `reasoningEffort`
Out of scope for Phase 1:
- model fallback itself
- `thinking`
- `maxTokens`
- `temperature`
- `top_p`
- automatic upward remapping of settings
## Desired behavior
Given a fixed model and desired settings:
1. If a desired value is supported, keep it.
2. If not supported, downgrade to the nearest lower compatible value.
3. If no compatible value exists, drop the field.
4. Do not switch models.
5. Do not automatically upgrade settings in Phase 1.
## Architecture
Add a central module:
- `src/shared/model-settings-compatibility.ts`
Core API:
```ts
type DesiredModelSettings = {
variant?: string
reasoningEffort?: string
}
type ModelSettingsCompatibilityInput = {
providerID: string
modelID: string
desired: DesiredModelSettings
}
type ModelSettingsCompatibilityChange = {
field: "variant" | "reasoningEffort"
from: string
to?: string
reason: string
}
type ModelSettingsCompatibilityResult = {
variant?: string
reasoningEffort?: string
changes: ModelSettingsCompatibilityChange[]
}
```
## Compatibility model
Phase 1 should be **metadata-first where the platform exposes reliable capability data**, and only fall back to family-based rules when that metadata is absent.
### Variant compatibility
Preferred source of truth:
- OpenCode/provider model metadata (`variants`)
Fallback when metadata is unavailable:
- family-based ladders
Examples of fallback ladders:
- Claude Opus family: `low`, `medium`, `high`, `max`
- Claude Sonnet/Haiku family: `low`, `medium`, `high`
- OpenAI GPT family: conservative family fallback only when metadata is missing
- Unknown family: drop unsupported values conservatively
### Reasoning effort compatibility
Current Phase 1 source of truth:
- conservative model/provider family heuristics
Reason:
- the currently available OpenCode SDK/provider metadata exposes model `variants`, but does not expose an equivalent per-model capability list for `reasoningEffort` levels
Examples:
- GPT/OpenAI-style models: `low`, `medium`, `high`, `xhigh` where supported by family heuristics
- Claude family via current OpenCode path: treat `reasoningEffort` as unsupported in Phase 1 and remove it
The resolver should remain pure model/settings logic only. Transport restrictions remain the responsibility of the request-building path.
## Separation of concerns
This design intentionally separates:
- model selection (`resolveModel...`, fallback chains)
- settings compatibility (this resolver)
- request transport compatibility (`chat.params`, prompt body constraints)
That keeps responsibilities clear:
- choose model first
- normalize settings second
- build request third
## First integration point
Phase 1 should first integrate into `chat.params`.
Why:
- it is already the centralized path for request-time tuning
- it can influence provider-facing options without leaking unsupported fields into prompt payload bodies
- it avoids trying to patch every prompt constructor at once
## Rollout plan
### Phase 1
- add resolver module and tests
- integrate into `chat.params`
- migrate `anthropic-effort` to either use the resolver or become a thin Claude-specific supplement around it
### Phase 2
- expand to `thinking`, `maxTokens`, `temperature`, `top_p`
- formalize request-path capability tables if needed
### Phase 3
- centralize all variant/reasoning normalization away from scattered hooks and ad hoc callers
## Risks
- Overfitting family rules to current model naming conventions
- Accidentally changing request semantics on paths that currently rely on implicit behavior
- Mixing provider transport limitations with model capability logic
## Mitigations
- Keep resolver pure and narrowly scoped in Phase 1
- Add explicit regression tests for keep/downgrade/drop decisions
- Integrate at one central point first (`chat.params`)
- Preserve existing behavior where desired values are already valid
## Recommendation
Proceed with the central resolver as a new, isolated implementation in a dedicated branch/worktree.
This is the clean long-term path and is more reviewable than continuing to add special-case clamps in hooks.

View File

@@ -4,7 +4,7 @@
### Problem
When using Ollama as a provider with oh-my-openagent agents, you may encounter:
When using Ollama as a provider with oh-my-opencode agents, you may encounter:
```
JSON Parse error: Unexpected EOF
@@ -26,7 +26,7 @@ Claude Code SDK expects a single JSON object, not multiple NDJSON lines, causing
**Why this happens:**
- **Ollama API**: Returns streaming responses as NDJSON by design
- **Claude Code SDK**: Doesn't properly handle NDJSON responses for tool calls
- **oh-my-openagent**: Passes through the SDK's behavior (can't fix at this layer)
- **oh-my-opencode**: Passes through the SDK's behavior (can't fix at this layer)
## Solutions
@@ -114,7 +114,7 @@ curl -s http://localhost:11434/api/chat \
## Related Issues
- **oh-my-openagent**: https://github.com/code-yeongyu/oh-my-openagent/issues/1124
- **oh-my-opencode**: https://github.com/code-yeongyu/oh-my-openagent/issues/1124
- **Ollama API Docs**: https://github.com/ollama/ollama/blob/main/docs/api.md
## Getting Help

View File

@@ -25,12 +25,10 @@
"build:all": "bun run build && bun run build:binaries",
"build:binaries": "bun run script/build-binaries.ts",
"build:schema": "bun run script/build-schema.ts",
"build:model-capabilities": "bun run script/build-model-capabilities.ts",
"clean": "rm -rf dist",
"prepare": "bun run build",
"postinstall": "node postinstall.mjs",
"prepublishOnly": "bun run clean && bun run build",
"test:model-capabilities": "bun test src/shared/model-capability-aliases.test.ts src/shared/model-capability-guardrails.test.ts src/shared/model-capabilities.test.ts src/cli/doctor/checks/model-resolution.test.ts --bail",
"typecheck": "tsc --noEmit",
"test": "bun test"
},

View File

@@ -101,9 +101,7 @@ async function main() {
console.log("\n✅ All platform binaries built successfully!\n");
}
if (import.meta.main) {
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});

View File

@@ -1,13 +0,0 @@
import { writeFileSync } from "fs"
import { resolve } from "path"
import {
fetchModelCapabilitiesSnapshot,
MODELS_DEV_SOURCE_URL,
} from "../src/shared/model-capabilities-cache"
const OUTPUT_PATH = resolve(import.meta.dir, "../src/generated/model-capabilities.generated.json")
console.log(`Fetching model capabilities snapshot from ${MODELS_DEV_SOURCE_URL}...`)
const snapshot = await fetchModelCapabilitiesSnapshot()
writeFileSync(OUTPUT_PATH, `${JSON.stringify(snapshot, null, 2)}\n`)
console.log(`Generated ${OUTPUT_PATH} with ${Object.keys(snapshot.models).length} models`)

View File

@@ -2239,94 +2239,6 @@
"created_at": "2026-03-17T20:42:42Z",
"repoId": 1108837393,
"pullRequestNo": 2656
},
{
"name": "walioo",
"id": 25835823,
"comment_id": 4087098221,
"created_at": "2026-03-19T02:13:02Z",
"repoId": 1108837393,
"pullRequestNo": 2688
},
{
"name": "trafgals",
"id": 6454757,
"comment_id": 4087725932,
"created_at": "2026-03-19T04:22:32Z",
"repoId": 1108837393,
"pullRequestNo": 2690
},
{
"name": "tonymfer",
"id": 66512584,
"comment_id": 4091847232,
"created_at": "2026-03-19T17:13:49Z",
"repoId": 1108837393,
"pullRequestNo": 2701
},
{
"name": "nguyentamdat",
"id": 16253213,
"comment_id": 4096267323,
"created_at": "2026-03-20T07:34:22Z",
"repoId": 1108837393,
"pullRequestNo": 2718
},
{
"name": "whackur",
"id": 26926041,
"comment_id": 4102330445,
"created_at": "2026-03-21T05:27:17Z",
"repoId": 1108837393,
"pullRequestNo": 2733
},
{
"name": "ndaemy",
"id": 18691542,
"comment_id": 4103008804,
"created_at": "2026-03-21T10:18:22Z",
"repoId": 1108837393,
"pullRequestNo": 2734
},
{
"name": "0xYiliu",
"id": 3838688,
"comment_id": 4104738337,
"created_at": "2026-03-21T22:59:33Z",
"repoId": 1108837393,
"pullRequestNo": 2738
},
{
"name": "hunghoang3011",
"id": 65234777,
"comment_id": 4107900881,
"created_at": "2026-03-23T04:28:20Z",
"repoId": 1108837393,
"pullRequestNo": 2758
},
{
"name": "anas-asghar4831",
"id": 110368394,
"comment_id": 4128950310,
"created_at": "2026-03-25T18:48:19Z",
"repoId": 1108837393,
"pullRequestNo": 2837
},
{
"name": "clansty",
"id": 18461360,
"comment_id": 4129934858,
"created_at": "2026-03-25T21:33:35Z",
"repoId": 1108837393,
"pullRequestNo": 2839
},
{
"name": "ventsislav-georgiev",
"id": 5616486,
"comment_id": 4130417794,
"created_at": "2026-03-25T23:11:32Z",
"repoId": 1108837393,
"pullRequestNo": 2840
}
]
}

View File

@@ -14,7 +14,7 @@ Entry point `index.ts` orchestrates 5-step initialization: loadConfig → create
| `plugin-config.ts` | JSONC parse, multi-level merge, Zod v4 validation |
| `create-managers.ts` | TmuxSessionManager, BackgroundManager, SkillMcpManager, ConfigHandler |
| `create-tools.ts` | SkillContext + AvailableCategories + ToolRegistry (26 tools) |
| `create-hooks.ts` | 3-tier: Core(39) + Continuation(7) + Skill(2) = 48 hooks |
| `create-hooks.ts` | 3-tier: Core(37) + Continuation(7) + Skill(2) = 46 hooks |
| `plugin-interface.ts` | 8 OpenCode hook handlers: config, tool, chat.message, chat.params, chat.headers, event, tool.execute.before, tool.execute.after |
## CONFIG LOADING
@@ -32,10 +32,10 @@ loadPluginConfig(directory, ctx)
```
createHooks()
├─→ createCoreHooks() # 39 hooks
├─→ createCoreHooks() # 37 hooks
│ ├─ createSessionHooks() # 23: contextWindowMonitor, thinkMode, ralphLoop, modelFallback, runtimeFallback, noSisyphusGpt, noHephaestusNonGpt, anthropicEffort, intentGate...
│ ├─ createToolGuardHooks() # 12: commentChecker, rulesInjector, writeExistingFileGuard, jsonErrorRecovery, hashlineReadEnhancer...
│ ├─ createToolGuardHooks() # 10: commentChecker, rulesInjector, writeExistingFileGuard, jsonErrorRecovery, hashlineReadEnhancer...
│ └─ createTransformHooks() # 4: claudeCodeHooks, keywordDetector, contextInjector, thinkingBlockValidator
├─→ createContinuationHooks() # 7: todoContinuationEnforcer, atlas, stopContinuationGuard, compactionContextInjector...
├─→ createContinuationHooks() # 7: todoContinuationEnforcer, atlas, stopContinuationGuard, ralphLoopActivator...
└─→ createSkillHooks() # 2: categorySkillReminder, autoSlashCommand
```

View File

@@ -13,8 +13,8 @@ Agent factories following `createXXXAgent(model) → AgentConfig` pattern. Each
| **Sisyphus** | claude-opus-4-6 max | 0.1 | all | k2p5 → kimi-k2.5 → gpt-5.4 medium → glm-5 → big-pickle | Main orchestrator, plans + delegates |
| **Hephaestus** | gpt-5.3-codex medium | 0.1 | all | gpt-5.4 medium (copilot) | Autonomous deep worker |
| **Oracle** | gpt-5.4 high | 0.1 | subagent | gemini-3.1-pro high → claude-opus-4-6 max | Read-only consultation |
| **Librarian** | minimax-m2.7 | 0.1 | subagent | minimax-m2.7-highspeedclaude-haiku-4-5 → gpt-5-nano | External docs/code search |
| **Explore** | grok-code-fast-1 | 0.1 | subagent | minimax-m2.7-highspeed → minimax-m2.7 → claude-haiku-4-5 → gpt-5-nano | Contextual grep |
| **Librarian** | gemini-3-flash | 0.1 | subagent | minimax-m2.5-free → big-pickle | External docs/code search |
| **Explore** | grok-code-fast-1 | 0.1 | subagent | minimax-m2.5-free → claude-haiku-4-5 → gpt-5-nano | Contextual grep |
| **Multimodal-Looker** | gpt-5.3-codex medium | 0.1 | subagent | k2p5 → gemini-3-flash → glm-4.6v → gpt-5-nano | PDF/image analysis |
| **Metis** | claude-opus-4-6 max | **0.3** | subagent | gpt-5.4 high → gemini-3.1-pro high | Pre-planning consultant |
| **Momus** | gpt-5.4 xhigh | 0.1 | subagent | claude-opus-4-6 max → gemini-3.1-pro high | Plan reviewer |

View File

@@ -1,16 +0,0 @@
import type { AgentConfig } from "@opencode-ai/sdk"
import type { AgentMode } from "./types"
import { buildAthenaPrompt, type AthenaPromptOptions } from "./athena/prompt"
const MODE: AgentMode = "primary"
export function createAthenaAgent(model: string, options?: AthenaPromptOptions): AgentConfig {
return {
description: "Primary council orchestrator for Athena workflows. (Athena - OhMyOpenCode)",
mode: MODE,
model,
temperature: 0.1,
prompt: buildAthenaPrompt(options),
}
}
createAthenaAgent.mode = MODE

View File

@@ -1,36 +0,0 @@
export const COUNCIL_MEMBER_RESPONSE_TAG = "COUNCIL_MEMBER_RESPONSE"
export type CouncilVerdict = "support" | "oppose" | "mixed" | "abstain"
export interface CouncilEvidenceItem {
source: string
detail: string
}
export interface CouncilMemberResponse {
member: string
verdict: CouncilVerdict
confidence: number
rationale: string
risks: string[]
evidence: CouncilEvidenceItem[]
proposed_actions: string[]
missing_information: string[]
}
export interface AthenaCouncilMember {
name: string
model: string
}
export interface ParsedCouncilMemberResponse {
ok: true
value: CouncilMemberResponse
source: "raw_json" | "tagged_json"
}
export interface CouncilResponseParseFailure {
ok: false
error: string
source: "raw_json" | "tagged_json" | "none"
}

View File

@@ -1,24 +0,0 @@
import type { AthenaCouncilMember } from "./council-contract"
function slugify(input: string): string {
return input
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
}
export function toCouncilMemberAgentName(memberName: string): string {
const slug = slugify(memberName)
return `council-member-${slug || "member"}`
}
export function buildCouncilRosterSection(members: AthenaCouncilMember[]): string {
if (members.length === 0) {
return "- No configured council roster. Use default subagent_type=\"council-member\"."
}
return members
.map((member) => `- ${member.name} | model=${member.model} | subagent_type=${toCouncilMemberAgentName(member.name)}`)
.join("\n")
}

View File

@@ -1,38 +0,0 @@
import { describe, expect, test } from "bun:test"
import { evaluateCouncilQuorum } from "./council-quorum"
describe("evaluateCouncilQuorum", () => {
test("#given partial failures with enough successful members #when evaluating #then quorum reached with graceful degradation", () => {
// given
const input = {
totalMembers: 5,
successfulMembers: 3,
failedMembers: 2,
}
// when
const result = evaluateCouncilQuorum(input)
// then
expect(result.required).toBe(3)
expect(result.reached).toBe(true)
expect(result.gracefulDegradation).toBe(true)
})
test("#given too many failures #when evaluating #then quorum is unreachable", () => {
// given
const input = {
totalMembers: 4,
successfulMembers: 1,
failedMembers: 3,
}
// when
const result = evaluateCouncilQuorum(input)
// then
expect(result.required).toBe(2)
expect(result.reached).toBe(false)
expect(result.canStillReach).toBe(false)
})
})

View File

@@ -1,36 +0,0 @@
export interface CouncilQuorumInput {
totalMembers: number
successfulMembers: number
failedMembers: number
requestedQuorum?: number
}
export interface CouncilQuorumResult {
required: number
reached: boolean
canStillReach: boolean
gracefulDegradation: boolean
}
function clampMinimumQuorum(totalMembers: number, requestedQuorum?: number): number {
if (requestedQuorum && requestedQuorum > 0) {
return Math.min(totalMembers, requestedQuorum)
}
return Math.max(1, Math.ceil(totalMembers / 2))
}
export function evaluateCouncilQuorum(input: CouncilQuorumInput): CouncilQuorumResult {
const required = clampMinimumQuorum(input.totalMembers, input.requestedQuorum)
const reached = input.successfulMembers >= required
const remainingPossible = input.totalMembers - input.failedMembers
const canStillReach = remainingPossible >= required
const gracefulDegradation = reached && input.failedMembers > 0
return {
required,
reached,
canStillReach,
gracefulDegradation,
}
}

View File

@@ -1,71 +0,0 @@
import { describe, expect, test } from "bun:test"
import { parseCouncilMemberResponse } from "./council-response-parser"
describe("parseCouncilMemberResponse", () => {
test("#given valid raw json #when parsing #then returns parsed council payload", () => {
// given
const raw = JSON.stringify({
member: "architect",
verdict: "support",
confidence: 0.9,
rationale: "Matches existing module boundaries",
risks: ["Regression in edge-case parser"],
evidence: [{ source: "src/agents/athena.ts", detail: "Current prompt is too generic" }],
proposed_actions: ["Add strict orchestration workflow"],
missing_information: ["Need runtime timeout budget"],
})
// when
const result = parseCouncilMemberResponse(raw)
// then
expect(result.ok).toBe(true)
if (!result.ok) return
expect(result.source).toBe("raw_json")
expect(result.value.member).toBe("architect")
expect(result.value.verdict).toBe("support")
})
test("#given tagged json payload #when parsing #then extracts from COUNCIL_MEMBER_RESPONSE tag", () => {
// given
const raw = [
"analysis intro",
"<COUNCIL_MEMBER_RESPONSE>",
JSON.stringify({
member: "skeptic",
verdict: "mixed",
confidence: 0.62,
rationale: "Quorum logic exists but retry handling is weak",
risks: ["Timeout blind spot"],
evidence: [{ source: "src/tools/background-task/create-background-wait.ts", detail: "No nudge semantics" }],
proposed_actions: ["Add stuck detection policy"],
missing_information: [],
}),
"</COUNCIL_MEMBER_RESPONSE>",
].join("\n")
// when
const result = parseCouncilMemberResponse(raw)
// then
expect(result.ok).toBe(true)
if (!result.ok) return
expect(result.source).toBe("tagged_json")
expect(result.value.member).toBe("skeptic")
expect(result.value.proposed_actions).toEqual(["Add stuck detection policy"])
})
test("#given malformed payload #when parsing #then returns structured parse failure", () => {
// given
const raw = "Council says: maybe this works"
// when
const result = parseCouncilMemberResponse(raw)
// then
expect(result.ok).toBe(false)
if (result.ok) return
expect(result.source).toBe("none")
expect(result.error.length).toBeGreaterThan(0)
})
})

View File

@@ -1,159 +0,0 @@
import {
COUNCIL_MEMBER_RESPONSE_TAG,
type CouncilMemberResponse,
type CouncilResponseParseFailure,
type ParsedCouncilMemberResponse,
} from "./council-contract"
type ParseResult = ParsedCouncilMemberResponse | CouncilResponseParseFailure
function normalizeJsonPayload(input: string): string {
const trimmed = input.trim()
if (!trimmed.startsWith("```") || !trimmed.endsWith("```")) {
return trimmed
}
const firstNewLine = trimmed.indexOf("\n")
if (firstNewLine < 0) {
return trimmed
}
return trimmed.slice(firstNewLine + 1, -3).trim()
}
function tryParseJsonObject(input: string): unknown {
const normalized = normalizeJsonPayload(input)
if (!normalized.startsWith("{")) {
return null
}
try {
return JSON.parse(normalized)
} catch {
return null
}
}
function extractTaggedPayload(raw: string): string | null {
const xmlLike = new RegExp(
`<${COUNCIL_MEMBER_RESPONSE_TAG}>([\\s\\S]*?)<\\/${COUNCIL_MEMBER_RESPONSE_TAG}>`,
"i",
)
const xmlMatch = raw.match(xmlLike)
if (xmlMatch?.[1]) {
return xmlMatch[1].trim()
}
const prefixed = new RegExp(`${COUNCIL_MEMBER_RESPONSE_TAG}\\s*:\\s*`, "i")
const prefixMatch = raw.match(prefixed)
if (!prefixMatch) {
return null
}
const matchIndex = prefixMatch.index
if (matchIndex === undefined) {
return null
}
const rest = raw.slice(matchIndex + prefixMatch[0].length)
const firstBrace = rest.indexOf("{")
if (firstBrace < 0) {
return null
}
return rest.slice(firstBrace).trim()
}
function isStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every((item) => typeof item === "string")
}
function isEvidenceArray(value: unknown): value is CouncilMemberResponse["evidence"] {
return Array.isArray(value)
&& value.every(
(item) =>
typeof item === "object"
&& item !== null
&& typeof (item as { source?: unknown }).source === "string"
&& typeof (item as { detail?: unknown }).detail === "string",
)
}
function validateCouncilMemberResponse(payload: unknown): CouncilMemberResponse | null {
if (typeof payload !== "object" || payload === null) {
return null
}
const candidate = payload as Record<string, unknown>
const verdict = candidate.verdict
const confidence = candidate.confidence
if (
typeof candidate.member !== "string"
|| (verdict !== "support" && verdict !== "oppose" && verdict !== "mixed" && verdict !== "abstain")
|| typeof confidence !== "number"
|| confidence < 0
|| confidence > 1
|| typeof candidate.rationale !== "string"
|| !isStringArray(candidate.risks)
|| !isEvidenceArray(candidate.evidence)
|| !isStringArray(candidate.proposed_actions)
|| !isStringArray(candidate.missing_information)
) {
return null
}
return {
member: candidate.member,
verdict,
confidence,
rationale: candidate.rationale,
risks: candidate.risks,
evidence: candidate.evidence,
proposed_actions: candidate.proposed_actions,
missing_information: candidate.missing_information,
}
}
function parseValidated(payload: unknown, source: ParsedCouncilMemberResponse["source"]): ParseResult {
const validated = validateCouncilMemberResponse(payload)
if (!validated) {
return {
ok: false,
error: "Council member response does not match required contract",
source,
}
}
return {
ok: true,
value: validated,
source,
}
}
export function parseCouncilMemberResponse(raw: string): ParseResult {
const directJson = tryParseJsonObject(raw)
if (directJson) {
return parseValidated(directJson, "raw_json")
}
const taggedPayload = extractTaggedPayload(raw)
if (taggedPayload) {
const taggedJson = tryParseJsonObject(taggedPayload)
if (taggedJson) {
return parseValidated(taggedJson, "tagged_json")
}
return {
ok: false,
error: "Tagged council response found, but JSON payload is invalid",
source: "tagged_json",
}
}
return {
ok: false,
error: "No parseable council response payload found",
source: "none",
}
}

View File

@@ -1,50 +0,0 @@
import { describe, expect, test } from "bun:test"
import { decideCouncilRecoveryAction } from "./council-retry"
describe("decideCouncilRecoveryAction", () => {
test("#given running member with stale progress and nudge budget #when deciding #then nudge", () => {
// given
const now = 10_000
const decision = decideCouncilRecoveryAction(
{
status: "running",
attempts: 1,
nudges: 0,
startedAt: 1_000,
lastProgressAt: 1_000,
},
{
maxAttempts: 2,
maxNudges: 1,
stuckAfterMs: 2_000,
},
now,
)
// then
expect(decision.action).toBe("nudge")
})
test("#given stuck member after nudge with retry budget #when deciding #then retry", () => {
// given
const now = 20_000
const decision = decideCouncilRecoveryAction(
{
status: "running",
attempts: 1,
nudges: 1,
startedAt: 1_000,
lastProgressAt: 1_000,
},
{
maxAttempts: 3,
maxNudges: 1,
stuckAfterMs: 5_000,
},
now,
)
// then
expect(decision.action).toBe("retry")
})
})

View File

@@ -1,68 +0,0 @@
export type CouncilMemberTaskStatus =
| "pending"
| "running"
| "completed"
| "failed"
| "cancelled"
| "timed_out"
export interface CouncilMemberTaskState {
status: CouncilMemberTaskStatus
attempts: number
nudges: number
startedAt: number
lastProgressAt: number
}
export interface CouncilRetryPolicy {
maxAttempts: number
maxNudges: number
stuckAfterMs: number
}
export type CouncilRecoveryAction = "wait" | "nudge" | "retry" | "give_up"
export interface CouncilRecoveryDecision {
action: CouncilRecoveryAction
reason: string
}
export function isCouncilMemberStuck(
now: number,
lastProgressAt: number,
stuckAfterMs: number,
): boolean {
return now - lastProgressAt >= stuckAfterMs
}
export function decideCouncilRecoveryAction(
state: CouncilMemberTaskState,
policy: CouncilRetryPolicy,
now: number,
): CouncilRecoveryDecision {
if (state.status === "completed" || state.status === "cancelled") {
return { action: "give_up", reason: "Task already reached terminal status" }
}
if (state.status === "failed" || state.status === "timed_out") {
if (state.attempts < policy.maxAttempts) {
return { action: "retry", reason: "Terminal failure with retries remaining" }
}
return { action: "give_up", reason: "Terminal failure and retry budget exhausted" }
}
const stuck = isCouncilMemberStuck(now, state.lastProgressAt, policy.stuckAfterMs)
if (!stuck) {
return { action: "wait", reason: "Task is still making progress" }
}
if (state.nudges < policy.maxNudges) {
return { action: "nudge", reason: "Task appears stuck and nudge budget remains" }
}
if (state.attempts < policy.maxAttempts) {
return { action: "retry", reason: "Task stuck after nudges, retrying with fresh run" }
}
return { action: "give_up", reason: "Task stuck and all recovery budgets exhausted" }
}

View File

@@ -1,43 +0,0 @@
import { describe, expect, test } from "bun:test"
import { synthesizeCouncilOutcome } from "./council-synthesis"
import type { CouncilMemberResponse } from "./council-contract"
function response(overrides: Partial<CouncilMemberResponse>): CouncilMemberResponse {
return {
member: "member-a",
verdict: "support",
confidence: 0.8,
rationale: "default rationale",
risks: [],
evidence: [{ source: "file.ts", detail: "detail" }],
proposed_actions: ["Ship with tests"],
missing_information: [],
...overrides,
}
}
describe("synthesizeCouncilOutcome", () => {
test("#given majority support with one failure #when synthesizing #then reports agreement and graceful degradation", () => {
// given
const responses = [
response({ member: "architect", verdict: "support", proposed_actions: ["Ship with tests"] }),
response({ member: "skeptic", verdict: "support", proposed_actions: ["Ship with tests"] }),
response({ member: "critic", verdict: "oppose", risks: ["Parser drift"] }),
]
// when
const result = synthesizeCouncilOutcome({
responses,
failedMembers: ["perf"],
quorumReached: true,
})
// then
expect(result.majorityVerdict).toBe("support")
expect(result.agreementMembers).toEqual(["architect", "skeptic"])
expect(result.disagreementMembers).toContain("critic")
expect(result.disagreementMembers).toContain("perf")
expect(result.commonActions).toEqual(["Ship with tests"])
expect(result.gracefulDegradation).toBe(true)
})
})

View File

@@ -1,141 +0,0 @@
import type { CouncilMemberResponse, CouncilVerdict } from "./council-contract"
export interface CouncilSynthesisInput {
responses: CouncilMemberResponse[]
failedMembers: string[]
quorumReached: boolean
}
export interface CouncilSynthesisResult {
majorityVerdict: CouncilVerdict
consensusLevel: "unanimous" | "strong" | "split" | "fragmented"
agreementMembers: string[]
disagreementMembers: string[]
commonActions: string[]
contestedRisks: string[]
unresolvedQuestions: string[]
gracefulDegradation: boolean
}
function normalizeKey(value: string): string {
return value.trim().toLowerCase()
}
function getMajorityVerdict(responses: CouncilMemberResponse[]): CouncilVerdict {
const counts = new Map<CouncilVerdict, number>()
for (const response of responses) {
counts.set(response.verdict, (counts.get(response.verdict) ?? 0) + 1)
}
const orderedVerdicts: CouncilVerdict[] = ["support", "mixed", "oppose", "abstain"]
let winner: CouncilVerdict = "abstain"
let winnerCount = -1
for (const verdict of orderedVerdicts) {
const count = counts.get(verdict) ?? 0
if (count > winnerCount) {
winner = verdict
winnerCount = count
}
}
return winner
}
function deriveConsensusLevel(agreementCount: number, totalCount: number): CouncilSynthesisResult["consensusLevel"] {
if (totalCount === 0) {
return "fragmented"
}
if (agreementCount === totalCount) {
return "unanimous"
}
const ratio = agreementCount / totalCount
if (ratio >= 0.75) {
return "strong"
}
if (ratio >= 0.5) {
return "split"
}
return "fragmented"
}
function collectCommonActions(responses: CouncilMemberResponse[]): string[] {
const counts = new Map<string, { text: string; count: number }>()
for (const response of responses) {
for (const action of response.proposed_actions) {
const key = normalizeKey(action)
const existing = counts.get(key)
if (!existing) {
counts.set(key, { text: action, count: 1 })
continue
}
existing.count += 1
}
}
const threshold = Math.max(2, Math.ceil(responses.length / 2))
return [...counts.values()]
.filter((item) => item.count >= threshold)
.map((item) => item.text)
}
function collectContestedRisks(responses: CouncilMemberResponse[]): string[] {
const counts = new Map<string, { text: string; count: number }>()
for (const response of responses) {
for (const risk of response.risks) {
const key = normalizeKey(risk)
const existing = counts.get(key)
if (!existing) {
counts.set(key, { text: risk, count: 1 })
continue
}
existing.count += 1
}
}
return [...counts.values()]
.filter((item) => item.count === 1)
.map((item) => item.text)
}
function collectUnresolvedQuestions(responses: CouncilMemberResponse[]): string[] {
const seen = new Set<string>()
const questions: string[] = []
for (const response of responses) {
for (const question of response.missing_information) {
const key = normalizeKey(question)
if (seen.has(key)) {
continue
}
seen.add(key)
questions.push(question)
}
}
return questions
}
export function synthesizeCouncilOutcome(input: CouncilSynthesisInput): CouncilSynthesisResult {
const majorityVerdict = getMajorityVerdict(input.responses)
const agreementMembers = input.responses
.filter((response) => response.verdict === majorityVerdict)
.map((response) => response.member)
const disagreementMembers = input.responses
.filter((response) => response.verdict !== majorityVerdict)
.map((response) => response.member)
.concat(input.failedMembers)
return {
majorityVerdict,
consensusLevel: deriveConsensusLevel(agreementMembers.length, input.responses.length),
agreementMembers,
disagreementMembers,
commonActions: collectCommonActions(input.responses),
contestedRisks: collectContestedRisks(input.responses),
unresolvedQuestions: collectUnresolvedQuestions(input.responses),
gracefulDegradation: input.quorumReached && input.failedMembers.length > 0,
}
}

View File

@@ -1,68 +0,0 @@
import type { AthenaCouncilMember } from "./council-contract"
import { COUNCIL_MEMBER_RESPONSE_TAG } from "./council-contract"
import { buildCouncilRosterSection } from "./council-members"
export interface AthenaPromptOptions {
members?: AthenaCouncilMember[]
}
export function buildAthenaPrompt(options: AthenaPromptOptions = {}): string {
const roster = buildCouncilRosterSection(options.members ?? [])
return `You are Athena, a primary council orchestrator agent.
Operate as a strict multi-model council coordinator.
Core workflow:
1) Receive user request and define a concise decision question for the council.
2) Fan out council-member tasks in parallel with task(..., run_in_background=true).
3) Collect with background_wait first, then background_output for completed IDs.
4) Parse each member output as strict JSON contract; fallback to ${COUNCIL_MEMBER_RESPONSE_TAG} tag extraction.
5) Apply quorum, retries, and graceful degradation.
6) Synthesize agreement vs disagreement explicitly, then provide final recommendation.
Council roster:
${roster}
Execution protocol:
- Always run council fan-out in parallel. Never sequentially wait on one member before launching others.
- Use subagent_type="council-member" if no named roster is configured.
- For named roster entries, use that exact subagent_type so each member runs on its assigned model.
- Keep prompts evidence-oriented and read-only. Members must inspect code, tests, logs, and config references.
- Never ask members to edit files, delegate, or switch agents.
Member response contract (required):
- Preferred: raw JSON only.
- Fallback allowed: wrap JSON in <${COUNCIL_MEMBER_RESPONSE_TAG}>...</${COUNCIL_MEMBER_RESPONSE_TAG}>.
- Required JSON keys:
{
"member": string,
"verdict": "support" | "oppose" | "mixed" | "abstain",
"confidence": number (0..1),
"rationale": string,
"risks": string[],
"evidence": [{ "source": string, "detail": string }],
"proposed_actions": string[],
"missing_information": string[]
}
Failure and stuck handling:
- Track per-member attempts, nudges, and progress timestamps.
- Detect stuck tasks when no progress appears within expected interval.
- First recovery action for stuck: nudge through continuation prompt.
- If still stuck or failed: retry with a fresh background task, bounded by retry limit.
- If a member remains failed after retry budget, mark as failed and continue.
Quorum and degradation:
- Default quorum: ceil(total_members / 2), minimum 1.
- If quorum reached, continue synthesis even when some members failed.
- If quorum cannot be reached after retries, report partial findings and explicit uncertainty.
Synthesis output requirements:
- Separate "agreement" and "disagreement" sections.
- Name which members support the majority view and which dissent or failed.
- Call out unresolved questions and evidence gaps.
- End with one executable recommendation and a confidence statement.
Do not expose internal operational noise. Report concise structured findings.`
}

View File

@@ -12,8 +12,6 @@ import { createMetisAgent, metisPromptMetadata } from "./metis"
import { createAtlasAgent, atlasPromptMetadata } from "./atlas"
import { createMomusAgent, momusPromptMetadata } from "./momus"
import { createHephaestusAgent } from "./hephaestus"
import { createAthenaAgent } from "./athena"
import { createCouncilMemberAgent } from "./council-member"
import { createSisyphusJuniorAgentWithOverrides } from "./sisyphus-junior"
import type { AvailableCategory } from "./dynamic-agent-prompt-builder"
import {
@@ -35,7 +33,6 @@ type AgentSource = AgentFactory | AgentConfig
const agentSources: Record<BuiltinAgentName, AgentSource> = {
sisyphus: createSisyphusAgent,
hephaestus: createHephaestusAgent,
athena: createAthenaAgent,
oracle: createOracleAgent,
librarian: createLibrarianAgent,
explore: createExploreAgent,
@@ -46,7 +43,6 @@ const agentSources: Record<BuiltinAgentName, AgentSource> = {
// because it needs OrchestratorContext, not just a model string
atlas: createAtlasAgent as AgentFactory,
"sisyphus-junior": createSisyphusJuniorAgentWithOverrides as unknown as AgentFactory,
"council-member": createCouncilMemberAgent,
}
/**

View File

@@ -44,10 +44,6 @@ export function mergeAgentConfig(
const { prompt_append, ...rest } = migratedOverride
const merged = deepMerge(base, rest as Partial<AgentConfig>)
if (merged.prompt && typeof merged.prompt === 'string' && merged.prompt.startsWith('file://')) {
merged.prompt = resolvePromptAppend(merged.prompt, directory)
}
if (prompt_append && merged.prompt) {
merged.prompt = merged.prompt + "\n" + resolvePromptAppend(prompt_append, directory)
}

View File

@@ -39,7 +39,7 @@ export function maybeCreateAtlasConfig(input: {
const atlasRequirement = AGENT_MODEL_REQUIREMENTS["atlas"]
const atlasResolution = applyModelResolution({
uiSelectedModel: orchestratorOverride?.model !== undefined ? undefined : uiSelectedModel,
uiSelectedModel: orchestratorOverride?.model ? undefined : uiSelectedModel,
userModel: orchestratorOverride?.model,
requirement: atlasRequirement,
availableModels,

View File

@@ -8,7 +8,6 @@ import { buildAgent, isFactory } from "../agent-builder"
import { applyOverrides } from "./agent-overrides"
import { applyEnvironmentContext } from "./environment-context"
import { applyModelResolution, getFirstFallbackModel } from "./model-resolution"
import { log } from "../../shared/logger"
export function collectPendingBuiltinAgents(input: {
agentSources: Record<BuiltinAgentName, import("../agent-builder").AgentSource>
@@ -39,6 +38,7 @@ export function collectPendingBuiltinAgents(input: {
browserProvider,
uiSelectedModel,
availableModels,
isFirstRunNoCache,
disabledSkills,
disableOmoEnv = false,
} = input
@@ -55,9 +55,8 @@ export function collectPendingBuiltinAgents(input: {
if (agentName === "sisyphus-junior") continue
if (disabledAgents.some((name) => name.toLowerCase() === agentName.toLowerCase())) continue
const override = Object.entries(agentOverrides).find(
([key]) => key.toLowerCase() === agentName.toLowerCase(),
)?.[1]
const override = agentOverrides[agentName]
?? Object.entries(agentOverrides).find(([key]) => key.toLowerCase() === agentName.toLowerCase())?.[1]
const requirement = AGENT_MODEL_REQUIREMENTS[agentName]
// Check if agent requires a specific model
@@ -70,19 +69,13 @@ export function collectPendingBuiltinAgents(input: {
const isPrimaryAgent = isFactory(source) && source.mode === "primary"
let resolution = applyModelResolution({
uiSelectedModel: (isPrimaryAgent && override?.model === undefined) ? uiSelectedModel : undefined,
uiSelectedModel: (isPrimaryAgent && !override?.model) ? uiSelectedModel : undefined,
userModel: override?.model,
requirement,
availableModels,
systemDefaultModel,
})
if (!resolution) {
if (override?.model) {
log("[agent-registration] User-configured model could not be resolved, falling back", {
agent: agentName,
configuredModel: override.model,
})
}
if (!resolution && isFirstRunNoCache && !override?.model) {
resolution = getFirstFallbackModel(requirement)
}
if (!resolution) continue

View File

@@ -1,32 +1,20 @@
import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test"
import { afterAll, beforeAll, describe, expect, test } from "bun:test"
import { mkdirSync, rmSync, writeFileSync } from "node:fs"
import * as os from "node:os"
import { tmpdir } from "node:os"
import { homedir, tmpdir } from "node:os"
import { join } from "node:path"
const originalHomedir = os.homedir.bind(os)
let mockedHomeDir = ""
let moduleImportCounter = 0
let resolvePromptAppend: typeof import("./resolve-file-uri").resolvePromptAppend
mock.module("node:os", () => ({
...os,
homedir: () => mockedHomeDir || originalHomedir(),
}))
import { resolvePromptAppend } from "./resolve-file-uri"
describe("resolvePromptAppend", () => {
const fixtureRoot = join(tmpdir(), `resolve-file-uri-${Date.now()}`)
const configDir = join(fixtureRoot, "config")
const homeFixtureRoot = join(fixtureRoot, "home")
const homeFixtureDir = join(homeFixtureRoot, "fixture-home")
const homeFixtureDir = join(homedir(), `.resolve-file-uri-home-${Date.now()}`)
const absoluteFilePath = join(fixtureRoot, "absolute.txt")
const relativeFilePath = join(configDir, "relative.txt")
const spacedFilePath = join(fixtureRoot, "with space.txt")
const homeFilePath = join(homeFixtureDir, "home.txt")
beforeAll(async () => {
mockedHomeDir = homeFixtureRoot
beforeAll(() => {
mkdirSync(fixtureRoot, { recursive: true })
mkdirSync(configDir, { recursive: true })
mkdirSync(homeFixtureDir, { recursive: true })
@@ -35,14 +23,11 @@ describe("resolvePromptAppend", () => {
writeFileSync(relativeFilePath, "relative-content", "utf8")
writeFileSync(spacedFilePath, "encoded-content", "utf8")
writeFileSync(homeFilePath, "home-content", "utf8")
moduleImportCounter += 1
;({ resolvePromptAppend } = await import(`./resolve-file-uri?test=${moduleImportCounter}`))
})
afterAll(() => {
rmSync(fixtureRoot, { recursive: true, force: true })
mock.restore()
rmSync(homeFixtureDir, { recursive: true, force: true })
})
test("returns non-file URI strings unchanged", () => {
@@ -80,7 +65,7 @@ describe("resolvePromptAppend", () => {
test("resolves home directory URI path", () => {
//#given
const input = "file://~/fixture-home/home.txt"
const input = `file://~/${homeFixtureDir.split("/").pop()}/home.txt`
//#when
const resolved = resolvePromptAppend(input)

View File

@@ -52,7 +52,7 @@ export function maybeCreateSisyphusConfig(input: {
if (disabledAgents.includes("sisyphus") || !meetsSisyphusAnyModelRequirement) return undefined
let sisyphusResolution = applyModelResolution({
uiSelectedModel: sisyphusOverride?.model !== undefined ? undefined : uiSelectedModel,
uiSelectedModel: sisyphusOverride?.model ? undefined : uiSelectedModel,
userModel: sisyphusOverride?.model,
requirement: sisyphusRequirement,
availableModels,

View File

@@ -1,51 +0,0 @@
import type { AgentConfig } from "@opencode-ai/sdk"
import type { AgentMode } from "./types"
import { createAgentToolRestrictions } from "../shared/permission-compat"
import { COUNCIL_MEMBER_RESPONSE_TAG } from "./athena/council-contract"
const MODE: AgentMode = "subagent"
const councilMemberRestrictions = createAgentToolRestrictions([
"write",
"edit",
"apply_patch",
"task",
"task_*",
"teammate",
"call_omo_agent",
"switch_agent",
])
export function createCouncilMemberAgent(model: string): AgentConfig {
return {
description: "Internal hidden council member used by Athena. Read-only analysis only.",
mode: MODE,
model,
temperature: 0.1,
hidden: true,
...councilMemberRestrictions,
prompt: `You are an internal council-member for Athena.
You are strictly read-only and evidence-oriented.
You must not modify files, delegate, or switch agents.
You must cite concrete evidence from files, tests, logs, or tool output.
Output contract:
- Preferred output: raw JSON only.
- Fallback output: wrap JSON with <${COUNCIL_MEMBER_RESPONSE_TAG}>...</${COUNCIL_MEMBER_RESPONSE_TAG}>.
- Required JSON schema:
{
"member": string,
"verdict": "support" | "oppose" | "mixed" | "abstain",
"confidence": number (0..1),
"rationale": string,
"risks": string[],
"evidence": [{ "source": string, "detail": string }],
"proposed_actions": string[],
"missing_information": string[]
}
Do not include markdown explanations outside the contract unless Athena asks for it explicitly.`,
}
}
createCouncilMemberAgent.mode = MODE

View File

@@ -181,7 +181,7 @@ describe("buildParallelDelegationSection", () => {
it("#given non-Claude model with deep category #when building #then returns aggressive delegation section", () => {
//#given
const model = "google/gemini-3.1-pro"
const model = "google/gemini-3-pro"
const categories = [deepCategory, otherCategory]
//#when
@@ -237,7 +237,7 @@ describe("buildParallelDelegationSection", () => {
describe("buildNonClaudePlannerSection", () => {
it("#given non-Claude model #when building #then returns plan agent section", () => {
//#given
const model = "google/gemini-3.1-pro"
const model = "google/gemini-3-pro"
//#when
const result = buildNonClaudePlannerSection(model)
@@ -272,3 +272,4 @@ describe("buildNonClaudePlannerSection", () => {
})
})

View File

@@ -162,10 +162,6 @@ Asking the user is the LAST resort after exhausting creative alternatives.
- User asks a question implying work → Answer briefly, DO the implied work in the same turn
- You wrote a plan in your response → EXECUTE the plan before ending turn — plans are starting lines, not finish lines
### Task Scope Clarification
You handle multi-step sub-tasks of a SINGLE GOAL. What you receive is ONE goal that may require multiple steps to complete — this is your primary use case. Only reject when given MULTIPLE INDEPENDENT goals in one request.
## Hard Constraints
${hardBlocks}

View File

@@ -121,10 +121,6 @@ When blocked: try a different approach → decompose the problem → challenge a
- User asks a question implying work → Answer briefly, DO the implied work in the same turn
- You wrote a plan in your response → EXECUTE the plan before ending turn — plans are starting lines, not finish lines
### Task Scope Clarification
You handle multi-step sub-tasks of a SINGLE GOAL. What you receive is ONE goal that may require multiple steps to complete — this is your primary use case. Only reject when given MULTIPLE INDEPENDENT goals in one request.
## Hard Constraints
${hardBlocks}

View File

@@ -112,10 +112,6 @@ Asking the user is the LAST resort after exhausting creative alternatives.
- Note assumptions in final message, not as questions mid-work
- Need context? Fire explore/librarian in background IMMEDIATELY — continue only with non-overlapping work while they search
### Task Scope Clarification
You handle multi-step sub-tasks of a SINGLE GOAL. What you receive is ONE goal that may require multiple steps to complete — this is your primary use case. Only reject when given MULTIPLE INDEPENDENT goals in one request.
## Hard Constraints
${hardBlocks}

View File

@@ -1,42 +0,0 @@
import { describe, it, expect } from "bun:test"
import { getPrometheusPrompt } from "./system-prompt"
describe("getPrometheusPrompt", () => {
describe("#given question tool is not disabled", () => {
describe("#when generating prompt", () => {
it("#then should include Question tool references", () => {
const prompt = getPrometheusPrompt(undefined, [])
expect(prompt).toContain("Question({")
})
})
})
describe("#given question tool is disabled via disabled_tools", () => {
describe("#when generating prompt", () => {
it("#then should strip Question tool code examples", () => {
const prompt = getPrometheusPrompt(undefined, ["question"])
expect(prompt).not.toContain("Question({")
})
})
describe("#when disabled_tools includes question among other tools", () => {
it("#then should strip Question tool code examples", () => {
const prompt = getPrometheusPrompt(undefined, ["todowrite", "question", "interactive_bash"])
expect(prompt).not.toContain("Question({")
})
})
})
describe("#given no disabled_tools provided", () => {
describe("#when generating prompt with undefined", () => {
it("#then should include Question tool references", () => {
const prompt = getPrometheusPrompt(undefined, undefined)
expect(prompt).toContain("Question({")
})
})
})
})

View File

@@ -52,34 +52,16 @@ export function getPrometheusPromptSource(model?: string): PrometheusPromptSourc
* Gemini models → Gemini-optimized prompt (aggressive tool-call enforcement, thinking checkpoints)
* Default (Claude, etc.) → Claude-optimized prompt (modular sections)
*/
export function getPrometheusPrompt(model?: string, disabledTools?: readonly string[]): string {
export function getPrometheusPrompt(model?: string): string {
const source = getPrometheusPromptSource(model)
const isQuestionDisabled = disabledTools?.includes("question") ?? false
let prompt: string
switch (source) {
case "gpt":
prompt = getGptPrometheusPrompt()
break
return getGptPrometheusPrompt()
case "gemini":
prompt = getGeminiPrometheusPrompt()
break
return getGeminiPrometheusPrompt()
case "default":
default:
prompt = PROMETHEUS_SYSTEM_PROMPT
return PROMETHEUS_SYSTEM_PROMPT
}
if (isQuestionDisabled) {
prompt = stripQuestionToolReferences(prompt)
}
return prompt
}
/**
* Removes Question tool usage examples from prompt text when question tool is disabled.
*/
function stripQuestionToolReferences(prompt: string): string {
// Remove Question({...}) code blocks (multi-line)
return prompt.replace(/```typescript\n\s*Question\(\{[\s\S]*?\}\)\s*\n```/g, "")
}

View File

@@ -35,11 +35,6 @@ Task NOT complete without:
- ${verificationText}
</Verification>
<Termination>
STOP after first successful verification. Do NOT re-verify.
Maximum status checks: 2. Then stop regardless.
</Termination>
<Style>
- Start immediately. No acknowledgments.
- Match user's communication style.

View File

@@ -1,5 +1,5 @@
import { describe, test, expect } from "bun:test";
import { isGptModel, isGeminiModel, isGpt5_4Model, isMiniMaxModel } from "./types";
import { isGptModel, isGeminiModel, isGpt5_4Model } from "./types";
describe("isGpt5_4Model", () => {
test("detects gpt-5.4 models", () => {
@@ -79,28 +79,6 @@ describe("isGptModel", () => {
});
});
describe("isMiniMaxModel", () => {
test("detects minimax models with provider prefix", () => {
expect(isMiniMaxModel("opencode-go/minimax-m2.7")).toBe(true);
expect(isMiniMaxModel("opencode/minimax-m2.7-highspeed")).toBe(true);
expect(isMiniMaxModel("opencode-go/minimax-m2.5")).toBe(true);
expect(isMiniMaxModel("opencode/minimax-m2.5-free")).toBe(true);
});
test("detects minimax models without provider prefix", () => {
expect(isMiniMaxModel("minimax-m2.7")).toBe(true);
expect(isMiniMaxModel("minimax-m2.7-highspeed")).toBe(true);
expect(isMiniMaxModel("minimax-m2.5")).toBe(true);
});
test("does not match non-minimax models", () => {
expect(isMiniMaxModel("openai/gpt-5.4")).toBe(false);
expect(isMiniMaxModel("anthropic/claude-opus-4-6")).toBe(false);
expect(isMiniMaxModel("google/gemini-3.1-pro")).toBe(false);
expect(isMiniMaxModel("opencode-go/kimi-k2.5")).toBe(false);
});
});
describe("isGeminiModel", () => {
test("#given google provider models #then returns true", () => {
expect(isGeminiModel("google/gemini-3.1-pro")).toBe(true);

View File

@@ -91,11 +91,6 @@ export function isGpt5_3CodexModel(model: string): boolean {
const GEMINI_PROVIDERS = ["google/", "google-vertex/"];
export function isMiniMaxModel(model: string): boolean {
const modelName = extractModelName(model).toLowerCase();
return modelName.includes("minimax");
}
export function isGeminiModel(model: string): boolean {
if (GEMINI_PROVIDERS.some((prefix) => model.startsWith(prefix))) return true;
@@ -112,7 +107,6 @@ export function isGeminiModel(model: string): boolean {
export type BuiltinAgentName =
| "sisyphus"
| "hephaestus"
| "athena"
| "oracle"
| "librarian"
| "explore"
@@ -120,17 +114,16 @@ export type BuiltinAgentName =
| "metis"
| "momus"
| "atlas"
| "sisyphus-junior"
| "council-member";
| "sisyphus-junior";
export type OverridableAgentName = "build" | Exclude<BuiltinAgentName, "council-member">;
export type OverridableAgentName = "build" | BuiltinAgentName;
export type AgentName = BuiltinAgentName;
export type AgentOverrideConfig = Partial<AgentConfig> & {
prompt_append?: string;
variant?: string;
fallback_models?: string | (string | import("../config/schema/fallback-models").FallbackModelObject)[];
fallback_models?: string | string[];
};
export type AgentOverrides = Partial<

View File

@@ -11,32 +11,6 @@ import * as shared from "../shared"
const TEST_DEFAULT_MODEL = "anthropic/claude-opus-4-6"
describe("createBuiltinAgents with model overrides", () => {
test("registers athena as builtin primary agent", async () => {
// #given
// #when
const agents = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL)
// #then
expect(agents.athena).toBeDefined()
expect(agents.athena.mode).toBe("primary")
})
test("registers council-member as hidden internal subagent", async () => {
// #given
// #when
const agents = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL)
// #then
expect(agents["council-member"]).toBeDefined()
expect(agents["council-member"].mode).toBe("subagent")
expect((agents["council-member"] as AgentConfig & { hidden?: boolean }).hidden).toBe(true)
expect(agents.sisyphus.prompt).not.toContain("council-member")
expect(agents.hephaestus.prompt).not.toContain("council-member")
expect(agents.atlas.prompt).not.toContain("council-member")
})
test("Sisyphus with default model has thinking config when all models available", async () => {
// #given
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(

View File

@@ -4,15 +4,9 @@ exports[`generateModelConfig no providers available returns ULTIMATE_FALLBACK fo
{
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json",
"agents": {
"athena": {
"model": "opencode/gpt-5-nano",
},
"atlas": {
"model": "opencode/gpt-5-nano",
},
"council-member": {
"model": "opencode/gpt-5-nano",
},
"explore": {
"model": "opencode/gpt-5-nano",
},
@@ -74,15 +68,9 @@ exports[`generateModelConfig single native provider uses Claude models when only
{
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json",
"agents": {
"athena": {
"model": "anthropic/claude-sonnet-4-6",
},
"atlas": {
"model": "anthropic/claude-sonnet-4-6",
},
"council-member": {
"model": "anthropic/claude-sonnet-4-6",
},
"explore": {
"model": "anthropic/claude-haiku-4-5",
},
@@ -142,15 +130,9 @@ exports[`generateModelConfig single native provider uses Claude models with isMa
{
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json",
"agents": {
"athena": {
"model": "anthropic/claude-sonnet-4-6",
},
"atlas": {
"model": "anthropic/claude-sonnet-4-6",
},
"council-member": {
"model": "anthropic/claude-sonnet-4-6",
},
"explore": {
"model": "anthropic/claude-haiku-4-5",
},
@@ -211,18 +193,10 @@ exports[`generateModelConfig single native provider uses OpenAI models when only
{
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json",
"agents": {
"athena": {
"model": "openai/gpt-5.4",
"variant": "medium",
},
"atlas": {
"model": "openai/gpt-5.4",
"variant": "medium",
},
"council-member": {
"model": "openai/gpt-5.4",
"variant": "medium",
},
"explore": {
"model": "openai/gpt-5.4",
"variant": "medium",
@@ -274,7 +248,8 @@ exports[`generateModelConfig single native provider uses OpenAI models when only
"variant": "medium",
},
"quick": {
"model": "openai/gpt-5.4-mini",
"model": "openai/gpt-5.3-codex",
"variant": "low",
},
"ultrabrain": {
"model": "openai/gpt-5.4",
@@ -304,18 +279,10 @@ exports[`generateModelConfig single native provider uses OpenAI models with isMa
{
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json",
"agents": {
"athena": {
"model": "openai/gpt-5.4",
"variant": "medium",
},
"atlas": {
"model": "openai/gpt-5.4",
"variant": "medium",
},
"council-member": {
"model": "openai/gpt-5.4",
"variant": "medium",
},
"explore": {
"model": "openai/gpt-5.4",
"variant": "medium",
@@ -367,7 +334,8 @@ exports[`generateModelConfig single native provider uses OpenAI models with isMa
"variant": "medium",
},
"quick": {
"model": "openai/gpt-5.4-mini",
"model": "openai/gpt-5.3-codex",
"variant": "low",
},
"ultrabrain": {
"model": "openai/gpt-5.4",
@@ -397,15 +365,9 @@ exports[`generateModelConfig single native provider uses Gemini models when only
{
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json",
"agents": {
"athena": {
"model": "opencode/gpt-5-nano",
},
"atlas": {
"model": "opencode/gpt-5-nano",
},
"council-member": {
"model": "opencode/gpt-5-nano",
},
"explore": {
"model": "opencode/gpt-5-nano",
},
@@ -463,15 +425,9 @@ exports[`generateModelConfig single native provider uses Gemini models with isMa
{
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json",
"agents": {
"athena": {
"model": "opencode/gpt-5-nano",
},
"atlas": {
"model": "opencode/gpt-5-nano",
},
"council-member": {
"model": "opencode/gpt-5-nano",
},
"explore": {
"model": "opencode/gpt-5-nano",
},
@@ -529,16 +485,9 @@ exports[`generateModelConfig all native providers uses preferred models from fal
{
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json",
"agents": {
"athena": {
"model": "anthropic/claude-sonnet-4-6",
},
"atlas": {
"model": "anthropic/claude-sonnet-4-6",
},
"council-member": {
"model": "openai/gpt-5.4",
"variant": "medium",
},
"explore": {
"model": "anthropic/claude-haiku-4-5",
},
@@ -584,7 +533,7 @@ exports[`generateModelConfig all native providers uses preferred models from fal
"variant": "medium",
},
"quick": {
"model": "openai/gpt-5.4-mini",
"model": "anthropic/claude-haiku-4-5",
},
"ultrabrain": {
"model": "openai/gpt-5.4",
@@ -611,16 +560,9 @@ exports[`generateModelConfig all native providers uses preferred models with isM
{
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json",
"agents": {
"athena": {
"model": "anthropic/claude-sonnet-4-6",
},
"atlas": {
"model": "anthropic/claude-sonnet-4-6",
},
"council-member": {
"model": "openai/gpt-5.4",
"variant": "medium",
},
"explore": {
"model": "anthropic/claude-haiku-4-5",
},
@@ -666,7 +608,7 @@ exports[`generateModelConfig all native providers uses preferred models with isM
"variant": "medium",
},
"quick": {
"model": "openai/gpt-5.4-mini",
"model": "anthropic/claude-haiku-4-5",
},
"ultrabrain": {
"model": "openai/gpt-5.4",
@@ -694,16 +636,9 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models when on
{
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json",
"agents": {
"athena": {
"model": "opencode/claude-sonnet-4-6",
},
"atlas": {
"model": "opencode/claude-sonnet-4-6",
},
"council-member": {
"model": "opencode/gpt-5.4",
"variant": "medium",
},
"explore": {
"model": "opencode/claude-haiku-4-5",
},
@@ -749,7 +684,7 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models when on
"variant": "medium",
},
"quick": {
"model": "opencode/gpt-5.4-mini",
"model": "opencode/claude-haiku-4-5",
},
"ultrabrain": {
"model": "opencode/gpt-5.4",
@@ -776,16 +711,9 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models with is
{
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json",
"agents": {
"athena": {
"model": "opencode/claude-sonnet-4-6",
},
"atlas": {
"model": "opencode/claude-sonnet-4-6",
},
"council-member": {
"model": "opencode/gpt-5.4",
"variant": "medium",
},
"explore": {
"model": "opencode/claude-haiku-4-5",
},
@@ -831,7 +759,7 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models with is
"variant": "medium",
},
"quick": {
"model": "opencode/gpt-5.4-mini",
"model": "opencode/claude-haiku-4-5",
},
"ultrabrain": {
"model": "opencode/gpt-5.4",
@@ -859,16 +787,9 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models when
{
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json",
"agents": {
"athena": {
"model": "github-copilot/claude-sonnet-4.6",
},
"atlas": {
"model": "github-copilot/claude-sonnet-4.6",
},
"council-member": {
"model": "github-copilot/gpt-5.4",
"variant": "medium",
},
"explore": {
"model": "github-copilot/gpt-5-mini",
},
@@ -909,7 +830,7 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models when
"variant": "high",
},
"quick": {
"model": "github-copilot/gpt-5.4-mini",
"model": "github-copilot/claude-haiku-4.5",
},
"ultrabrain": {
"model": "github-copilot/gemini-3.1-pro-preview",
@@ -936,16 +857,9 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models with
{
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json",
"agents": {
"athena": {
"model": "github-copilot/claude-sonnet-4.6",
},
"atlas": {
"model": "github-copilot/claude-sonnet-4.6",
},
"council-member": {
"model": "github-copilot/gpt-5.4",
"variant": "medium",
},
"explore": {
"model": "github-copilot/gpt-5-mini",
},
@@ -986,7 +900,7 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models with
"variant": "high",
},
"quick": {
"model": "github-copilot/gpt-5.4-mini",
"model": "github-copilot/claude-haiku-4.5",
},
"ultrabrain": {
"model": "github-copilot/gemini-3.1-pro-preview",
@@ -1014,15 +928,9 @@ exports[`generateModelConfig fallback providers uses ZAI model for librarian whe
{
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json",
"agents": {
"athena": {
"model": "opencode/gpt-5-nano",
},
"atlas": {
"model": "opencode/gpt-5-nano",
},
"council-member": {
"model": "opencode/gpt-5-nano",
},
"explore": {
"model": "opencode/gpt-5-nano",
},
@@ -1078,15 +986,9 @@ exports[`generateModelConfig fallback providers uses ZAI model for librarian wit
{
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json",
"agents": {
"athena": {
"model": "opencode/gpt-5-nano",
},
"atlas": {
"model": "opencode/gpt-5-nano",
},
"council-member": {
"model": "opencode/gpt-5-nano",
},
"explore": {
"model": "opencode/gpt-5-nano",
},
@@ -1142,16 +1044,9 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + OpenCode Zen
{
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json",
"agents": {
"athena": {
"model": "anthropic/claude-sonnet-4-6",
},
"atlas": {
"model": "anthropic/claude-sonnet-4-6",
},
"council-member": {
"model": "opencode/gpt-5.4",
"variant": "medium",
},
"explore": {
"model": "anthropic/claude-haiku-4-5",
},
@@ -1197,7 +1092,7 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + OpenCode Zen
"variant": "medium",
},
"quick": {
"model": "opencode/gpt-5.4-mini",
"model": "anthropic/claude-haiku-4-5",
},
"ultrabrain": {
"model": "opencode/gpt-5.4",
@@ -1224,16 +1119,9 @@ exports[`generateModelConfig mixed provider scenarios uses OpenAI + Copilot comb
{
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json",
"agents": {
"athena": {
"model": "github-copilot/claude-sonnet-4.6",
},
"atlas": {
"model": "github-copilot/claude-sonnet-4.6",
},
"council-member": {
"model": "openai/gpt-5.4",
"variant": "medium",
},
"explore": {
"model": "github-copilot/gpt-5-mini",
},
@@ -1279,7 +1167,7 @@ exports[`generateModelConfig mixed provider scenarios uses OpenAI + Copilot comb
"variant": "medium",
},
"quick": {
"model": "openai/gpt-5.4-mini",
"model": "github-copilot/claude-haiku-4.5",
},
"ultrabrain": {
"model": "openai/gpt-5.4",
@@ -1306,15 +1194,9 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + ZAI combinat
{
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json",
"agents": {
"athena": {
"model": "anthropic/claude-sonnet-4-6",
},
"atlas": {
"model": "anthropic/claude-sonnet-4-6",
},
"council-member": {
"model": "anthropic/claude-sonnet-4-6",
},
"explore": {
"model": "anthropic/claude-haiku-4-5",
},
@@ -1376,15 +1258,9 @@ exports[`generateModelConfig mixed provider scenarios uses Gemini + Claude combi
{
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json",
"agents": {
"athena": {
"model": "anthropic/claude-sonnet-4-6",
},
"atlas": {
"model": "anthropic/claude-sonnet-4-6",
},
"council-member": {
"model": "anthropic/claude-sonnet-4-6",
},
"explore": {
"model": "anthropic/claude-haiku-4-5",
},
@@ -1448,16 +1324,9 @@ exports[`generateModelConfig mixed provider scenarios uses all fallback provider
{
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json",
"agents": {
"athena": {
"model": "github-copilot/claude-sonnet-4.6",
},
"atlas": {
"model": "github-copilot/claude-sonnet-4.6",
},
"council-member": {
"model": "github-copilot/gpt-5.4",
"variant": "medium",
},
"explore": {
"model": "opencode/claude-haiku-4-5",
},
@@ -1506,7 +1375,7 @@ exports[`generateModelConfig mixed provider scenarios uses all fallback provider
"variant": "medium",
},
"quick": {
"model": "github-copilot/gpt-5.4-mini",
"model": "github-copilot/claude-haiku-4.5",
},
"ultrabrain": {
"model": "opencode/gpt-5.4",
@@ -1533,16 +1402,9 @@ exports[`generateModelConfig mixed provider scenarios uses all providers togethe
{
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json",
"agents": {
"athena": {
"model": "anthropic/claude-sonnet-4-6",
},
"atlas": {
"model": "anthropic/claude-sonnet-4-6",
},
"council-member": {
"model": "openai/gpt-5.4",
"variant": "medium",
},
"explore": {
"model": "anthropic/claude-haiku-4-5",
},
@@ -1591,7 +1453,7 @@ exports[`generateModelConfig mixed provider scenarios uses all providers togethe
"variant": "medium",
},
"quick": {
"model": "openai/gpt-5.4-mini",
"model": "anthropic/claude-haiku-4-5",
},
"ultrabrain": {
"model": "openai/gpt-5.4",
@@ -1618,16 +1480,9 @@ exports[`generateModelConfig mixed provider scenarios uses all providers with is
{
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json",
"agents": {
"athena": {
"model": "anthropic/claude-sonnet-4-6",
},
"atlas": {
"model": "anthropic/claude-sonnet-4-6",
},
"council-member": {
"model": "openai/gpt-5.4",
"variant": "medium",
},
"explore": {
"model": "anthropic/claude-haiku-4-5",
},
@@ -1676,7 +1531,7 @@ exports[`generateModelConfig mixed provider scenarios uses all providers with is
"variant": "medium",
},
"quick": {
"model": "openai/gpt-5.4-mini",
"model": "anthropic/claude-haiku-4-5",
},
"ultrabrain": {
"model": "openai/gpt-5.4",

View File

@@ -34,7 +34,6 @@ describe("runCliInstaller", () => {
hasOpencodeZen: false,
hasZaiCodingPlan: false,
hasKimiForCoding: false,
hasOpencodeGo: false,
}),
spyOn(configManager, "isOpenCodeInstalled").mockResolvedValue(true),
spyOn(configManager, "getOpenCodeVersion").mockResolvedValue("1.0.200"),
@@ -57,7 +56,6 @@ describe("runCliInstaller", () => {
opencodeZen: "no",
zaiCodingPlan: "no",
kimiForCoding: "no",
opencodeGo: "no",
}
//#when

View File

@@ -3,7 +3,6 @@ import { install } from "./install"
import { run } from "./run"
import { getLocalVersion } from "./get-local-version"
import { doctor } from "./doctor"
import { refreshModelCapabilities } from "./refresh-model-capabilities"
import { createMcpOAuthCommand } from "./mcp-oauth"
import type { InstallArgs } from "./types"
import type { RunOptions } from "./run"
@@ -43,7 +42,7 @@ Examples:
Model Providers (Priority: Native > Copilot > OpenCode Zen > Z.ai > Kimi):
Claude Native anthropic/ models (Opus, Sonnet, Haiku)
OpenAI Native openai/ models (GPT-5.4 for Oracle)
Gemini Native google/ models (Gemini 3.1 Pro, Flash)
Gemini Native google/ models (Gemini 3 Pro, Flash)
Copilot github-copilot/ models (fallback)
OpenCode Zen opencode/ models (opencode/claude-opus-4-6, etc.)
Z.ai zai-coding-plan/glm-5 (visual-engineering fallback)
@@ -177,21 +176,6 @@ Examples:
process.exit(exitCode)
})
program
.command("refresh-model-capabilities")
.description("Refresh the cached models.dev-based model capabilities snapshot")
.option("-d, --directory <path>", "Working directory to read oh-my-opencode config from")
.option("--source-url <url>", "Override the models.dev source URL")
.option("--json", "Output refresh summary as JSON")
.action(async (options) => {
const exitCode = await refreshModelCapabilities({
directory: options.directory,
sourceUrl: options.sourceUrl,
json: options.json ?? false,
})
process.exit(exitCode)
})
program
.command("version")
.description("Show version information")

View File

@@ -1,129 +0,0 @@
import { transformModelForProvider } from "../../shared/provider-model-id-transform"
import { toProviderAvailability } from "../provider-availability"
import type { InstallConfig } from "../types"
export interface AthenaMemberTemplate {
provider: string
model: string
name: string
isAvailable: (config: InstallConfig) => boolean
}
export interface AthenaCouncilMember {
name: string
model: string
}
export interface AthenaConfig {
model?: string
members: AthenaCouncilMember[]
}
const ATHENA_MEMBER_TEMPLATES: AthenaMemberTemplate[] = [
{
provider: "openai",
model: "gpt-5.4",
name: "OpenAI Strategist",
isAvailable: (config) => config.hasOpenAI,
},
{
provider: "anthropic",
model: "claude-sonnet-4-6",
name: "Claude Strategist",
isAvailable: (config) => config.hasClaude,
},
{
provider: "google",
model: "gemini-3.1-pro",
name: "Gemini Strategist",
isAvailable: (config) => config.hasGemini,
},
{
provider: "github-copilot",
model: "gpt-5.4",
name: "Copilot Strategist",
isAvailable: (config) => config.hasCopilot,
},
{
provider: "opencode",
model: "gpt-5.4",
name: "OpenCode Strategist",
isAvailable: (config) => config.hasOpencodeZen,
},
{
provider: "zai-coding-plan",
model: "glm-4.7",
name: "Z.ai Strategist",
isAvailable: (config) => config.hasZaiCodingPlan,
},
{
provider: "kimi-for-coding",
model: "k2p5",
name: "Kimi Strategist",
isAvailable: (config) => config.hasKimiForCoding,
},
{
provider: "opencode-go",
model: "glm-5",
name: "OpenCode Go Strategist",
isAvailable: (config) => config.hasOpencodeGo,
},
]
function toProviderModel(provider: string, model: string): string {
const transformedModel = transformModelForProvider(provider, model)
return `${provider}/${transformedModel}`
}
function createUniqueMemberName(baseName: string, usedNames: Set<string>): string {
if (!usedNames.has(baseName.toLowerCase())) {
usedNames.add(baseName.toLowerCase())
return baseName
}
let suffix = 2
let candidate = `${baseName} ${suffix}`
while (usedNames.has(candidate.toLowerCase())) {
suffix += 1
candidate = `${baseName} ${suffix}`
}
usedNames.add(candidate.toLowerCase())
return candidate
}
export function createAthenaCouncilMembersFromTemplates(
templates: AthenaMemberTemplate[]
): AthenaCouncilMember[] {
const members: AthenaCouncilMember[] = []
const usedNames = new Set<string>()
for (const template of templates) {
members.push({
name: createUniqueMemberName(template.name, usedNames),
model: toProviderModel(template.provider, template.model),
})
}
return members
}
export function generateAthenaConfig(config: InstallConfig): AthenaConfig | undefined {
const selectedTemplates = ATHENA_MEMBER_TEMPLATES.filter((template) => template.isAvailable(config))
if (selectedTemplates.length === 0) {
return undefined
}
const members = createAthenaCouncilMembersFromTemplates(selectedTemplates)
const availability = toProviderAvailability(config)
const preferredCoordinator =
(availability.native.openai && members.find((member) => member.model.startsWith("openai/"))) ||
(availability.native.claude && members.find((member) => member.model.startsWith("anthropic/"))) ||
members[0]
return {
model: preferredCoordinator.model,
members,
}
}

View File

@@ -1,102 +0,0 @@
import { describe, expect, it } from "bun:test"
import type { InstallConfig } from "../types"
import {
createAthenaCouncilMembersFromTemplates,
generateAthenaConfig,
type AthenaMemberTemplate,
} from "./generate-athena-config"
import { generateOmoConfig } from "./generate-omo-config"
import { transformModelForProvider } from "../../shared/provider-model-id-transform"
function createInstallConfig(overrides: Partial<InstallConfig> = {}): InstallConfig {
return {
hasClaude: false,
isMax20: false,
hasOpenAI: false,
hasGemini: false,
hasCopilot: false,
hasOpencodeZen: false,
hasZaiCodingPlan: false,
hasKimiForCoding: false,
hasOpencodeGo: false,
...overrides,
}
}
describe("generateOmoConfig athena council", () => {
it("creates athena council members from enabled providers", () => {
// given
const installConfig = createInstallConfig({ hasOpenAI: true, hasClaude: true, hasGemini: true })
// when
const generated = generateOmoConfig(installConfig)
const athena = generated.athena as { model?: string; members?: Array<{ name: string; model: string }> }
const googleModel = `google/${transformModelForProvider("google", "gemini-3.1-pro")}`
// then
expect(athena.model).toBe("openai/gpt-5.4")
expect(athena.members).toHaveLength(3)
expect(athena.members?.map((member) => member.model)).toEqual([
"openai/gpt-5.4",
"anthropic/claude-sonnet-4-6",
googleModel,
])
})
it("does not create athena config when no providers are enabled", () => {
// given
const installConfig = createInstallConfig()
// when
const generated = generateOmoConfig(installConfig)
// then
expect(generated.athena).toBeUndefined()
})
})
describe("generateAthenaConfig", () => {
it("uses anthropic as coordinator when openai is unavailable", () => {
// given
const installConfig = createInstallConfig({ hasClaude: true, hasCopilot: true })
// when
const athena = generateAthenaConfig(installConfig)
// then
expect(athena?.model).toBe("anthropic/claude-sonnet-4-6")
expect(athena?.members?.map((member) => member.model)).toEqual([
"anthropic/claude-sonnet-4-6",
"github-copilot/gpt-5.4",
])
})
})
describe("createAthenaCouncilMembersFromTemplates", () => {
it("adds numeric suffixes when template names collide case-insensitively", () => {
// given
const templates: AthenaMemberTemplate[] = [
{
provider: "openai",
model: "gpt-5.4",
name: "Strategist",
isAvailable: () => true,
},
{
provider: "anthropic",
model: "claude-sonnet-4-6",
name: "strategist",
isAvailable: () => true,
},
]
// when
const members = createAthenaCouncilMembersFromTemplates(templates)
// then
expect(members).toEqual([
{ name: "Strategist", model: "openai/gpt-5.4" },
{ name: "strategist 2", model: "anthropic/claude-sonnet-4-6" },
])
})
})

View File

@@ -1,17 +1,6 @@
import type { InstallConfig } from "../types"
import { generateModelConfig } from "../model-fallback"
import { generateAthenaConfig } from "./generate-athena-config"
export function generateOmoConfig(installConfig: InstallConfig): Record<string, unknown> {
const generatedConfig = generateModelConfig(installConfig)
const athenaConfig = generateAthenaConfig(installConfig)
if (!athenaConfig) {
return generatedConfig
}
return {
...generatedConfig,
athena: athenaConfig,
}
return generateModelConfig(installConfig)
}

View File

@@ -18,7 +18,6 @@ const installConfig: InstallConfig = {
hasOpencodeZen: false,
hasZaiCodingPlan: false,
hasKimiForCoding: false,
hasOpencodeGo: false,
}
function getRecord(value: unknown): Record<string, unknown> {

View File

@@ -2,15 +2,15 @@ import { readFileSync } from "node:fs"
import { join } from "node:path"
import { OhMyOpenCodeConfigSchema } from "../../../config"
import { detectPluginConfigFile, getOpenCodeConfigDir, parseJsonc } from "../../../shared"
import { detectConfigFile, getOpenCodeConfigDir, parseJsonc } from "../../../shared"
import { CHECK_IDS, CHECK_NAMES, PACKAGE_NAME } from "../constants"
import type { CheckResult, DoctorIssue } from "../types"
import { loadAvailableModelsFromCache } from "./model-resolution-cache"
import { getModelResolutionInfoWithOverrides } from "./model-resolution"
import type { OmoConfig } from "./model-resolution-types"
const USER_CONFIG_DIR = getOpenCodeConfigDir({ binary: "opencode" })
const PROJECT_CONFIG_DIR = join(process.cwd(), ".opencode")
const USER_CONFIG_BASE = join(getOpenCodeConfigDir({ binary: "opencode" }), PACKAGE_NAME)
const PROJECT_CONFIG_BASE = join(process.cwd(), ".opencode", PACKAGE_NAME)
interface ConfigValidationResult {
exists: boolean
@@ -21,10 +21,10 @@ interface ConfigValidationResult {
}
function findConfigPath(): string | null {
const projectConfig = detectPluginConfigFile(PROJECT_CONFIG_DIR)
const projectConfig = detectConfigFile(PROJECT_CONFIG_BASE)
if (projectConfig.format !== "none") return projectConfig.path
const userConfig = detectPluginConfigFile(USER_CONFIG_DIR)
const userConfig = detectConfigFile(USER_CONFIG_BASE)
if (userConfig.format !== "none") return userConfig.path
return null

View File

@@ -1,13 +1,17 @@
import { readFileSync } from "node:fs"
import { join } from "node:path"
import { detectPluginConfigFile, getOpenCodeConfigPaths, parseJsonc } from "../../../shared"
import { detectConfigFile, getOpenCodeConfigPaths, parseJsonc } from "../../../shared"
import type { OmoConfig } from "./model-resolution-types"
const USER_CONFIG_DIR = getOpenCodeConfigPaths({ binary: "opencode", version: null }).configDir
const PROJECT_CONFIG_DIR = join(process.cwd(), ".opencode")
const PACKAGE_NAME = "oh-my-opencode"
const USER_CONFIG_BASE = join(
getOpenCodeConfigPaths({ binary: "opencode", version: null }).configDir,
PACKAGE_NAME
)
const PROJECT_CONFIG_BASE = join(process.cwd(), ".opencode", PACKAGE_NAME)
export function loadOmoConfig(): OmoConfig | null {
const projectDetected = detectPluginConfigFile(PROJECT_CONFIG_DIR)
const projectDetected = detectConfigFile(PROJECT_CONFIG_BASE)
if (projectDetected.format !== "none") {
try {
const content = readFileSync(projectDetected.path, "utf-8")
@@ -17,7 +21,7 @@ export function loadOmoConfig(): OmoConfig | null {
}
}
const userDetected = detectPluginConfigFile(USER_CONFIG_DIR)
const userDetected = detectConfigFile(USER_CONFIG_BASE)
if (userDetected.format !== "none") {
try {
const content = readFileSync(userDetected.path, "utf-8")

View File

@@ -4,10 +4,6 @@ import { getOpenCodeCacheDir } from "../../../shared"
import type { AvailableModelsInfo, ModelResolutionInfo, OmoConfig } from "./model-resolution-types"
import { formatModelWithVariant, getCategoryEffectiveVariant, getEffectiveVariant } from "./model-resolution-variant"
function formatCapabilityResolutionLabel(mode: string | undefined): string {
return mode ?? "unknown"
}
export function buildModelResolutionDetails(options: {
info: ModelResolutionInfo
available: AvailableModelsInfo
@@ -41,7 +37,7 @@ export function buildModelResolutionDetails(options: {
agent.effectiveModel,
getEffectiveVariant(agent.name, agent.requirement, options.config)
)
details.push(` ${marker} ${agent.name}: ${display} [capabilities: ${formatCapabilityResolutionLabel(agent.capabilityDiagnostics?.resolutionMode)}]`)
details.push(` ${marker} ${agent.name}: ${display}`)
}
details.push("")
details.push("Categories:")
@@ -51,7 +47,7 @@ export function buildModelResolutionDetails(options: {
category.effectiveModel,
getCategoryEffectiveVariant(category.name, category.requirement, options.config)
)
details.push(` ${marker} ${category.name}: ${display} [capabilities: ${formatCapabilityResolutionLabel(category.capabilityDiagnostics?.resolutionMode)}]`)
details.push(` ${marker} ${category.name}: ${display}`)
}
details.push("")
details.push("● = user override, ○ = provider fallback")

View File

@@ -1,4 +1,3 @@
import type { ModelCapabilitiesDiagnostics } from "../../../shared/model-capabilities"
import type { ModelRequirement } from "../../../shared/model-requirements"
export interface AgentResolutionInfo {
@@ -8,7 +7,6 @@ export interface AgentResolutionInfo {
userVariant?: string
effectiveModel: string
effectiveResolution: string
capabilityDiagnostics?: ModelCapabilitiesDiagnostics
}
export interface CategoryResolutionInfo {
@@ -18,7 +16,6 @@ export interface CategoryResolutionInfo {
userVariant?: string
effectiveModel: string
effectiveResolution: string
capabilityDiagnostics?: ModelCapabilitiesDiagnostics
}
export interface ModelResolutionInfo {

View File

@@ -129,19 +129,6 @@ describe("model-resolution check", () => {
expect(visual!.userOverride).toBe("google/gemini-3-flash-preview")
expect(visual!.userVariant).toBe("high")
})
it("attaches snapshot-backed capability diagnostics for built-in models", async () => {
const { getModelResolutionInfoWithOverrides } = await import("./model-resolution")
const info = getModelResolutionInfoWithOverrides({})
const sisyphus = info.agents.find((a) => a.name === "sisyphus")
expect(sisyphus).toBeDefined()
expect(sisyphus!.capabilityDiagnostics).toMatchObject({
resolutionMode: "snapshot-backed",
snapshot: { source: "bundled-snapshot" },
})
})
})
describe("checkModelResolution", () => {
@@ -175,23 +162,6 @@ describe("model-resolution check", () => {
expect(result.details!.some((d) => d.includes("Categories:"))).toBe(true)
// Should have legend
expect(result.details!.some((d) => d.includes("user override"))).toBe(true)
expect(result.details!.some((d) => d.includes("capabilities: snapshot-backed"))).toBe(true)
})
it("collects warnings when configured models rely on compatibility fallback", async () => {
const { collectCapabilityResolutionIssues, getModelResolutionInfoWithOverrides } = await import("./model-resolution")
const info = getModelResolutionInfoWithOverrides({
agents: {
oracle: { model: "custom/unknown-llm" },
},
})
const issues = collectCapabilityResolutionIssues(info)
expect(issues).toHaveLength(1)
expect(issues[0]?.title).toContain("compatibility fallback")
expect(issues[0]?.description).toContain("oracle=custom/unknown-llm")
})
})

View File

@@ -1,5 +1,4 @@
import { AGENT_MODEL_REQUIREMENTS, CATEGORY_MODEL_REQUIREMENTS } from "../../../shared/model-requirements"
import { getModelCapabilities } from "../../../shared/model-capabilities"
import { CHECK_IDS, CHECK_NAMES } from "../constants"
import type { CheckResult, DoctorIssue } from "../types"
import { loadAvailableModelsFromCache } from "./model-resolution-cache"
@@ -8,36 +7,16 @@ import { buildModelResolutionDetails } from "./model-resolution-details"
import { buildEffectiveResolution, getEffectiveModel } from "./model-resolution-effective-model"
import type { AgentResolutionInfo, CategoryResolutionInfo, ModelResolutionInfo, OmoConfig } from "./model-resolution-types"
function parseProviderModel(value: string): { providerID: string; modelID: string } | null {
const slashIndex = value.indexOf("/")
if (slashIndex <= 0 || slashIndex === value.length - 1) {
return null
}
return {
providerID: value.slice(0, slashIndex),
modelID: value.slice(slashIndex + 1),
}
}
function attachCapabilityDiagnostics<T extends AgentResolutionInfo | CategoryResolutionInfo>(entry: T): T {
const parsed = parseProviderModel(entry.effectiveModel)
if (!parsed) {
return entry
}
return {
...entry,
capabilityDiagnostics: getModelCapabilities({
providerID: parsed.providerID,
modelID: parsed.modelID,
}).diagnostics,
}
}
export function getModelResolutionInfo(): ModelResolutionInfo {
const agents: AgentResolutionInfo[] = Object.entries(AGENT_MODEL_REQUIREMENTS).map(([name, requirement]) =>
attachCapabilityDiagnostics({
const agents: AgentResolutionInfo[] = Object.entries(AGENT_MODEL_REQUIREMENTS).map(([name, requirement]) => ({
name,
requirement,
effectiveModel: getEffectiveModel(requirement),
effectiveResolution: buildEffectiveResolution(requirement),
}))
const categories: CategoryResolutionInfo[] = Object.entries(CATEGORY_MODEL_REQUIREMENTS).map(
([name, requirement]) => ({
name,
requirement,
effectiveModel: getEffectiveModel(requirement),
@@ -45,16 +24,6 @@ export function getModelResolutionInfo(): ModelResolutionInfo {
})
)
const categories: CategoryResolutionInfo[] = Object.entries(CATEGORY_MODEL_REQUIREMENTS).map(
([name, requirement]) =>
attachCapabilityDiagnostics({
name,
requirement,
effectiveModel: getEffectiveModel(requirement),
effectiveResolution: buildEffectiveResolution(requirement),
})
)
return { agents, categories }
}
@@ -62,60 +31,34 @@ export function getModelResolutionInfoWithOverrides(config: OmoConfig): ModelRes
const agents: AgentResolutionInfo[] = Object.entries(AGENT_MODEL_REQUIREMENTS).map(([name, requirement]) => {
const userOverride = config.agents?.[name]?.model
const userVariant = config.agents?.[name]?.variant
return attachCapabilityDiagnostics({
return {
name,
requirement,
userOverride,
userVariant,
effectiveModel: getEffectiveModel(requirement, userOverride),
effectiveResolution: buildEffectiveResolution(requirement, userOverride),
})
}
})
const categories: CategoryResolutionInfo[] = Object.entries(CATEGORY_MODEL_REQUIREMENTS).map(
([name, requirement]) => {
const userOverride = config.categories?.[name]?.model
const userVariant = config.categories?.[name]?.variant
return attachCapabilityDiagnostics({
return {
name,
requirement,
userOverride,
userVariant,
effectiveModel: getEffectiveModel(requirement, userOverride),
effectiveResolution: buildEffectiveResolution(requirement, userOverride),
})
}
}
)
return { agents, categories }
}
export function collectCapabilityResolutionIssues(info: ModelResolutionInfo): DoctorIssue[] {
const issues: DoctorIssue[] = []
const allEntries = [...info.agents, ...info.categories]
const fallbackEntries = allEntries.filter((entry) => {
const mode = entry.capabilityDiagnostics?.resolutionMode
return mode === "alias-backed" || mode === "heuristic-backed" || mode === "unknown"
})
if (fallbackEntries.length === 0) {
return issues
}
const summary = fallbackEntries
.map((entry) => `${entry.name}=${entry.effectiveModel} (${entry.capabilityDiagnostics?.resolutionMode ?? "unknown"})`)
.join(", ")
issues.push({
title: "Configured models rely on compatibility fallback",
description: summary,
severity: "warning",
affects: fallbackEntries.map((entry) => entry.name),
})
return issues
}
export async function checkModels(): Promise<CheckResult> {
const config = loadOmoConfig() ?? {}
const info = getModelResolutionInfoWithOverrides(config)
@@ -132,8 +75,6 @@ export async function checkModels(): Promise<CheckResult> {
})
}
issues.push(...collectCapabilityResolutionIssues(info))
const overrideCount =
info.agents.filter((agent) => Boolean(agent.userOverride)).length +
info.categories.filter((category) => Boolean(category.userOverride)).length

View File

@@ -1,10 +1,9 @@
import { afterEach, describe, expect, it } from "bun:test"
import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs"
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { dirname, join } from "node:path"
import { PACKAGE_NAME } from "../constants"
import { resolveSymlink } from "../../../shared/file-utils"
const systemLoadedVersionModulePath = "./system-loaded-version?system-loaded-version-test"
@@ -105,31 +104,6 @@ describe("system loaded version", () => {
expect(loadedVersion.expectedVersion).toBe("2.3.4")
expect(loadedVersion.loadedVersion).toBe("2.3.4")
})
it("resolves symlinked config directories before selecting install path", () => {
//#given
const realConfigDir = createTemporaryDirectory("omo-real-config-")
const symlinkBaseDir = createTemporaryDirectory("omo-symlink-base-")
const symlinkConfigDir = join(symlinkBaseDir, "config-link")
symlinkSync(realConfigDir, symlinkConfigDir, process.platform === "win32" ? "junction" : "dir")
process.env.OPENCODE_CONFIG_DIR = symlinkConfigDir
writeJson(join(realConfigDir, "package.json"), {
dependencies: { [PACKAGE_NAME]: "4.5.6" },
})
writeJson(join(realConfigDir, "node_modules", PACKAGE_NAME, "package.json"), {
version: "4.5.6",
})
//#when
const loadedVersion = getLoadedPluginVersion()
//#then
expect(loadedVersion.cacheDir).toBe(resolveSymlink(symlinkConfigDir))
expect(loadedVersion.expectedVersion).toBe("4.5.6")
expect(loadedVersion.loadedVersion).toBe("4.5.6")
})
})
describe("getSuggestedInstallTag", () => {

View File

@@ -1,7 +1,7 @@
import { existsSync, readFileSync } from "node:fs"
import { homedir } from "node:os"
import { join } from "node:path"
import { resolveSymlink } from "../../../shared/file-utils"
import { getLatestVersion } from "../../../hooks/auto-update-checker/checker"
import { extractChannel } from "../../../hooks/auto-update-checker"
import { PACKAGE_NAME } from "../constants"
@@ -36,11 +36,6 @@ function resolveOpenCodeCacheDir(): string {
return platformDefault
}
function resolveExistingDir(dirPath: string): string {
if (!existsSync(dirPath)) return dirPath
return resolveSymlink(dirPath)
}
function readPackageJson(filePath: string): PackageJsonShape | null {
if (!existsSync(filePath)) return null
@@ -60,13 +55,12 @@ function normalizeVersion(value: string | undefined): string | null {
export function getLoadedPluginVersion(): LoadedVersionInfo {
const configPaths = getOpenCodeConfigPaths({ binary: "opencode" })
const configDir = resolveExistingDir(configPaths.configDir)
const cacheDir = resolveExistingDir(resolveOpenCodeCacheDir())
const cacheDir = resolveOpenCodeCacheDir()
const candidates = [
{
cacheDir: configDir,
cachePackagePath: join(configDir, "package.json"),
installedPackagePath: join(configDir, "node_modules", PACKAGE_NAME, "package.json"),
cacheDir: configPaths.configDir,
cachePackagePath: configPaths.packageJson,
installedPackagePath: join(configPaths.configDir, "node_modules", PACKAGE_NAME, "package.json"),
},
{
cacheDir,

View File

@@ -53,14 +53,6 @@ describe("install CLI - binary check behavior", () => {
isOpenCodeInstalledSpy = spyOn(configManager, "isOpenCodeInstalled").mockResolvedValue(false)
getOpenCodeVersionSpy = spyOn(configManager, "getOpenCodeVersion").mockResolvedValue(null)
// given mock npm fetch
globalThis.fetch = mock(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ latest: "3.0.0" }),
} as Response)
) as unknown as typeof fetch
const args: InstallArgs = {
tui: false,
claude: "yes",

View File

@@ -55,7 +55,7 @@ export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig {
for (const [role, req] of Object.entries(CLI_AGENT_MODEL_REQUIREMENTS)) {
if (role === "librarian") {
if (avail.opencodeGo) {
agents[role] = { model: "opencode-go/minimax-m2.7" }
agents[role] = { model: "opencode-go/minimax-m2.5" }
} else if (avail.zai) {
agents[role] = { model: ZAI_MODEL }
}
@@ -68,7 +68,7 @@ export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig {
} else if (avail.opencodeZen) {
agents[role] = { model: "opencode/claude-haiku-4-5" }
} else if (avail.opencodeGo) {
agents[role] = { model: "opencode-go/minimax-m2.7" }
agents[role] = { model: "opencode-go/minimax-m2.5" }
} else if (avail.copilot) {
agents[role] = { model: "github-copilot/gpt-5-mini" }
} else {

View File

@@ -40,7 +40,7 @@ describe("generateModelConfig OpenAI-only model catalog", () => {
// #then
expect(result.categories?.artistry).toEqual({ model: "openai/gpt-5.4", variant: "xhigh" })
expect(result.categories?.quick).toEqual({ model: "openai/gpt-5.4-mini" })
expect(result.categories?.quick).toEqual({ model: "openai/gpt-5.3-codex", variant: "low" })
expect(result.categories?.["visual-engineering"]).toEqual({ model: "openai/gpt-5.4", variant: "high" })
expect(result.categories?.writing).toEqual({ model: "openai/gpt-5.4", variant: "medium" })
})
@@ -53,8 +53,8 @@ describe("generateModelConfig OpenAI-only model catalog", () => {
const result = generateModelConfig(config)
// #then
expect(result.agents?.explore).toEqual({ model: "opencode-go/minimax-m2.7" })
expect(result.agents?.librarian).toEqual({ model: "opencode-go/minimax-m2.7" })
expect(result.categories?.quick).toEqual({ model: "openai/gpt-5.4-mini" })
expect(result.agents?.explore).toEqual({ model: "opencode-go/minimax-m2.5" })
expect(result.agents?.librarian).toEqual({ model: "opencode-go/minimax-m2.5" })
expect(result.categories?.quick).toEqual({ model: "opencode-go/minimax-m2.5" })
})
})

View File

@@ -7,7 +7,7 @@ const OPENAI_ONLY_AGENT_OVERRIDES: Record<string, AgentConfig> = {
const OPENAI_ONLY_CATEGORY_OVERRIDES: Record<string, CategoryConfig> = {
artistry: { model: "openai/gpt-5.4", variant: "xhigh" },
quick: { model: "openai/gpt-5.4-mini" },
quick: { model: "openai/gpt-5.3-codex", variant: "low" },
"visual-engineering": { model: "openai/gpt-5.4", variant: "high" },
writing: { model: "openai/gpt-5.4", variant: "medium" },
}

View File

@@ -1,114 +0,0 @@
import { describe, expect, it, mock } from "bun:test"
import { refreshModelCapabilities } from "./refresh-model-capabilities"
describe("refreshModelCapabilities", () => {
it("uses config source_url when CLI override is absent", async () => {
const loadConfig = mock(() => ({
model_capabilities: {
source_url: "https://mirror.example/api.json",
},
}))
const refreshCache = mock(async () => ({
generatedAt: "2026-03-25T00:00:00.000Z",
sourceUrl: "https://mirror.example/api.json",
models: {
"gpt-5.4": { id: "gpt-5.4" },
},
}))
let stdout = ""
const exitCode = await refreshModelCapabilities(
{ directory: "/repo", json: false },
{
loadConfig,
refreshCache,
stdout: {
write: (chunk: string) => {
stdout += chunk
return true
},
} as never,
stderr: {
write: () => true,
} as never,
},
)
expect(exitCode).toBe(0)
expect(loadConfig).toHaveBeenCalledWith("/repo", null)
expect(refreshCache).toHaveBeenCalledWith({
sourceUrl: "https://mirror.example/api.json",
})
expect(stdout).toContain("Refreshed model capabilities cache (1 models)")
})
it("CLI sourceUrl overrides config and supports json output", async () => {
const refreshCache = mock(async () => ({
generatedAt: "2026-03-25T00:00:00.000Z",
sourceUrl: "https://override.example/api.json",
models: {
"gpt-5.4": { id: "gpt-5.4" },
"claude-opus-4-6": { id: "claude-opus-4-6" },
},
}))
let stdout = ""
const exitCode = await refreshModelCapabilities(
{
directory: "/repo",
json: true,
sourceUrl: "https://override.example/api.json",
},
{
loadConfig: () => ({}),
refreshCache,
stdout: {
write: (chunk: string) => {
stdout += chunk
return true
},
} as never,
stderr: {
write: () => true,
} as never,
},
)
expect(exitCode).toBe(0)
expect(refreshCache).toHaveBeenCalledWith({
sourceUrl: "https://override.example/api.json",
})
expect(JSON.parse(stdout)).toEqual({
sourceUrl: "https://override.example/api.json",
generatedAt: "2026-03-25T00:00:00.000Z",
modelCount: 2,
})
})
it("returns exit code 1 when refresh fails", async () => {
let stderr = ""
const exitCode = await refreshModelCapabilities(
{ directory: "/repo" },
{
loadConfig: () => ({}),
refreshCache: async () => {
throw new Error("boom")
},
stdout: {
write: () => true,
} as never,
stderr: {
write: (chunk: string) => {
stderr += chunk
return true
},
} as never,
},
)
expect(exitCode).toBe(1)
expect(stderr).toContain("Failed to refresh model capabilities cache")
})
})

View File

@@ -1,51 +0,0 @@
import { loadPluginConfig } from "../plugin-config"
import { refreshModelCapabilitiesCache } from "../shared/model-capabilities-cache"
export type RefreshModelCapabilitiesOptions = {
directory?: string
json?: boolean
sourceUrl?: string
}
type RefreshModelCapabilitiesDeps = {
loadConfig?: typeof loadPluginConfig
refreshCache?: typeof refreshModelCapabilitiesCache
stdout?: Pick<typeof process.stdout, "write">
stderr?: Pick<typeof process.stderr, "write">
}
export async function refreshModelCapabilities(
options: RefreshModelCapabilitiesOptions,
deps: RefreshModelCapabilitiesDeps = {},
): Promise<number> {
const directory = options.directory ?? process.cwd()
const loadConfig = deps.loadConfig ?? loadPluginConfig
const refreshCache = deps.refreshCache ?? refreshModelCapabilitiesCache
const stdout = deps.stdout ?? process.stdout
const stderr = deps.stderr ?? process.stderr
try {
const config = loadConfig(directory, null)
const sourceUrl = options.sourceUrl ?? config.model_capabilities?.source_url
const snapshot = await refreshCache({ sourceUrl })
const summary = {
sourceUrl: snapshot.sourceUrl,
generatedAt: snapshot.generatedAt,
modelCount: Object.keys(snapshot.models).length,
}
if (options.json) {
stdout.write(`${JSON.stringify(summary, null, 2)}\n`)
} else {
stdout.write(
`Refreshed model capabilities cache (${summary.modelCount} models) from ${summary.sourceUrl}\n`,
)
}
return 0
} catch (error) {
stderr.write(`Failed to refresh model capabilities cache: ${String(error)}\n`)
return 1
}
}

View File

@@ -45,26 +45,26 @@ export function writePaddedText(
return { output: text, atLineStart: text.endsWith("\n") }
}
const parts: string[] = []
let output = ""
let lineStart = atLineStart
for (let i = 0; i < text.length; i++) {
const ch = text[i]
if (lineStart) {
parts.push(" ")
output += " "
lineStart = false
}
if (ch === "\n") {
parts.push(" \n")
output += " \n"
lineStart = true
continue
}
parts.push(ch)
output += ch
}
return { output: parts.join(""), atLineStart: lineStart }
return { output, atLineStart: lineStart }
}
function colorizeWithProfileColor(text: string, hexColor?: string): string {

View File

@@ -115,42 +115,6 @@ describe("waitForEventProcessorShutdown", () => {
})
})
describe("run environment setup", () => {
let originalClient: string | undefined
let originalRunMode: string | undefined
beforeEach(() => {
originalClient = process.env.OPENCODE_CLIENT
originalRunMode = process.env.OPENCODE_CLI_RUN_MODE
})
afterEach(() => {
if (originalClient === undefined) {
delete process.env.OPENCODE_CLIENT
} else {
process.env.OPENCODE_CLIENT = originalClient
}
if (originalRunMode === undefined) {
delete process.env.OPENCODE_CLI_RUN_MODE
} else {
process.env.OPENCODE_CLI_RUN_MODE = originalRunMode
}
})
it("sets OPENCODE_CLIENT to 'run' to exclude question tool from registry", async () => {
//#given
delete process.env.OPENCODE_CLIENT
//#when - run() sets env vars synchronously before any async work
const { run } = await import(`./runner?env-setup-${Date.now()}`)
run({ message: "test" }).catch(() => {})
//#then
expect(String(process.env.OPENCODE_CLIENT)).toBe("run")
expect(String(process.env.OPENCODE_CLI_RUN_MODE)).toBe("true")
})
})
describe("run with invalid model", () => {
it("given invalid --model value, when run, then returns exit code 1 with error message", async () => {
// given

View File

@@ -31,7 +31,6 @@ export async function waitForEventProcessorShutdown(
export async function run(options: RunOptions): Promise<number> {
process.env.OPENCODE_CLI_RUN_MODE = "true"
process.env.OPENCODE_CLIENT = "run"
const startTime = Date.now()
const {

View File

@@ -54,7 +54,7 @@ export async function promptInstallConfig(detected: DetectedConfig): Promise<Ins
message: "Will you integrate Google Gemini?",
options: [
{ value: "no", label: "No", hint: "Frontend/docs agents will use fallback" },
{ value: "yes", label: "Yes", hint: "Beautiful UI generation with Gemini 3.1 Pro" },
{ value: "yes", label: "Yes", hint: "Beautiful UI generation with Gemini 3 Pro" },
],
initialValue: initial.gemini,
})

View File

@@ -14,7 +14,7 @@ config/schema/
├── agent-names.ts # BuiltinAgentNameSchema (11), OverridableAgentNameSchema (14)
├── agent-overrides.ts # AgentOverrideConfigSchema (21 fields per agent)
├── categories.ts # 8 built-in + custom categories
├── hooks.ts # HookNameSchema (48 hooks)
├── hooks.ts # HookNameSchema (46 hooks)
├── skills.ts # SkillsConfigSchema (sources, paths, recursive)
├── commands.ts # BuiltinCommandNameSchema
├── experimental.ts # Feature flags (plugin_load_timeout_ms min 1000)

View File

@@ -4,7 +4,6 @@ export {
export type {
OhMyOpenCodeConfig,
AthenaConfig,
AgentOverrideConfig,
AgentOverrides,
McpName,
@@ -20,6 +19,5 @@ export type {
SisyphusConfig,
SisyphusTasksConfig,
RuntimeFallbackConfig,
ModelCapabilitiesConfig,
FallbackModels,
} from "./schema"

View File

@@ -147,37 +147,6 @@ describe("disabled_mcps schema", () => {
})
})
describe("OhMyOpenCodeConfigSchema - model_capabilities", () => {
test("accepts valid model capabilities config", () => {
const input = {
model_capabilities: {
enabled: true,
auto_refresh_on_start: true,
refresh_timeout_ms: 5000,
source_url: "https://models.dev/api.json",
},
}
const result = OhMyOpenCodeConfigSchema.safeParse(input)
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.model_capabilities).toEqual(input.model_capabilities)
}
})
test("rejects invalid model capabilities config", () => {
const result = OhMyOpenCodeConfigSchema.safeParse({
model_capabilities: {
refresh_timeout_ms: -1,
source_url: "not-a-url",
},
})
expect(result.success).toBe(false)
})
})
describe("AgentOverrideConfigSchema", () => {
describe("category field", () => {
test("accepts category as optional string", () => {
@@ -402,26 +371,6 @@ describe("CategoryConfigSchema", () => {
}
})
test("accepts reasoningEffort values none and minimal", () => {
// given
const noneConfig = { reasoningEffort: "none" }
const minimalConfig = { reasoningEffort: "minimal" }
// when
const noneResult = CategoryConfigSchema.safeParse(noneConfig)
const minimalResult = CategoryConfigSchema.safeParse(minimalConfig)
// then
expect(noneResult.success).toBe(true)
expect(minimalResult.success).toBe(true)
if (noneResult.success) {
expect(noneResult.data.reasoningEffort).toBe("none")
}
if (minimalResult.success) {
expect(minimalResult.data.reasoningEffort).toBe("minimal")
}
})
test("rejects non-string variant", () => {
// given
const config = { model: "openai/gpt-5.4", variant: 123 }

View File

@@ -1,6 +1,5 @@
export * from "./schema/agent-names"
export * from "./schema/agent-overrides"
export * from "./schema/athena-config"
export * from "./schema/babysitting"
export * from "./schema/background-task"
export * from "./schema/browser-automation"
@@ -14,7 +13,6 @@ export * from "./schema/fallback-models"
export * from "./schema/git-env-prefix"
export * from "./schema/git-master"
export * from "./schema/hooks"
export * from "./schema/model-capabilities"
export * from "./schema/notification"
export * from "./schema/oh-my-opencode-config"
export * from "./schema/ralph-loop"

View File

@@ -3,7 +3,6 @@ import { z } from "zod"
export const BuiltinAgentNameSchema = z.enum([
"sisyphus",
"hephaestus",
"athena",
"prometheus",
"oracle",
"librarian",
@@ -13,7 +12,6 @@ export const BuiltinAgentNameSchema = z.enum([
"momus",
"atlas",
"sisyphus-junior",
"council-member",
])
export const BuiltinSkillNameSchema = z.enum([
@@ -29,7 +27,6 @@ export const OverridableAgentNameSchema = z.enum([
"plan",
"sisyphus",
"hephaestus",
"athena",
"sisyphus-junior",
"OpenCode-Builder",
"prometheus",

View File

@@ -35,7 +35,7 @@ export const AgentOverrideConfigSchema = z.object({
})
.optional(),
/** Reasoning effort level (OpenAI). Overrides category and default settings. */
reasoningEffort: z.enum(["none", "minimal", "low", "medium", "high", "xhigh"]).optional(),
reasoningEffort: z.enum(["low", "medium", "high", "xhigh"]).optional(),
/** Text verbosity level. */
textVerbosity: z.enum(["low", "medium", "high"]).optional(),
/** Provider-specific options. Passed directly to OpenCode SDK. */
@@ -62,7 +62,6 @@ export const AgentOverridesSchema = z.object({
hephaestus: AgentOverrideConfigSchema.extend({
allow_non_gpt_model: z.boolean().optional(),
}).optional(),
athena: AgentOverrideConfigSchema.optional(),
"sisyphus-junior": AgentOverrideConfigSchema.optional(),
"OpenCode-Builder": AgentOverrideConfigSchema.optional(),
prometheus: AgentOverrideConfigSchema.optional(),

View File

@@ -1,82 +0,0 @@
import { describe, expect, test } from "bun:test"
import { AthenaConfigSchema } from "./athena-config"
import { OhMyOpenCodeConfigSchema } from "./oh-my-opencode-config"
describe("AthenaConfigSchema", () => {
test("accepts athena config with required members", () => {
// given
const config = {
model: "openai/gpt-5.4",
members: [
{ name: "Socrates", model: "openai/gpt-5.4" },
{ name: "Plato", model: "anthropic/claude-sonnet-4-6" },
],
}
// when
const result = AthenaConfigSchema.safeParse(config)
// then
expect(result.success).toBe(true)
})
test("rejects athena config when members are missing", () => {
// given
const config = {
model: "openai/gpt-5.4",
}
// when
const result = AthenaConfigSchema.safeParse(config)
// then
expect(result.success).toBe(false)
})
test("rejects case-insensitive duplicate member names", () => {
// given
const config = {
members: [
{ name: "Socrates", model: "openai/gpt-5.4" },
{ name: "socrates", model: "anthropic/claude-sonnet-4-6" },
],
}
// when
const result = AthenaConfigSchema.safeParse(config)
// then
expect(result.success).toBe(false)
})
test("rejects member model without provider prefix", () => {
// given
const config = {
members: [{ name: "Socrates", model: "gpt-5.4" }],
}
// when
const result = AthenaConfigSchema.safeParse(config)
// then
expect(result.success).toBe(false)
})
})
describe("OhMyOpenCodeConfigSchema athena field", () => {
test("accepts athena config at root", () => {
// given
const config = {
athena: {
model: "openai/gpt-5.4",
members: [{ name: "Socrates", model: "openai/gpt-5.4" }],
},
}
// when
const result = OhMyOpenCodeConfigSchema.safeParse(config)
// then
expect(result.success).toBe(true)
})
})

View File

@@ -1,39 +0,0 @@
import { z } from "zod"
const PROVIDER_MODEL_PATTERN = /^[^/\s]+\/[^/\s]+$/
const ProviderModelSchema = z
.string()
.regex(PROVIDER_MODEL_PATTERN, "Model must use provider/model format")
const AthenaCouncilMemberSchema = z.object({
name: z.string().trim().min(1),
model: ProviderModelSchema,
})
export const AthenaConfigSchema = z
.object({
model: ProviderModelSchema.optional(),
members: z.array(AthenaCouncilMemberSchema).min(1),
})
.superRefine((value, ctx) => {
const seen = new Map<string, number>()
for (const [index, member] of value.members.entries()) {
const normalizedName = member.name.trim().toLowerCase()
const existingIndex = seen.get(normalizedName)
if (existingIndex !== undefined) {
ctx.addIssue({
code: "custom",
path: ["members", index, "name"],
message: `Duplicate member name '${member.name}' (case-insensitive). First seen at members[${existingIndex}]`,
})
continue
}
seen.set(normalizedName, index)
}
})
export type AthenaConfig = z.infer<typeof AthenaConfigSchema>

View File

@@ -8,24 +8,27 @@ describe("BackgroundTaskConfigSchema.circuitBreaker", () => {
const result = BackgroundTaskConfigSchema.parse({
circuitBreaker: {
maxToolCalls: 150,
consecutiveThreshold: 10,
windowSize: 10,
repetitionThresholdPercent: 70,
},
})
expect(result.circuitBreaker).toEqual({
maxToolCalls: 150,
consecutiveThreshold: 10,
windowSize: 10,
repetitionThresholdPercent: 70,
})
})
})
describe("#given consecutiveThreshold below minimum", () => {
describe("#given windowSize below minimum", () => {
test("#when parsed #then throws ZodError", () => {
let thrownError: unknown
try {
BackgroundTaskConfigSchema.parse({
circuitBreaker: {
consecutiveThreshold: 4,
windowSize: 4,
},
})
} catch (error) {
@@ -36,14 +39,14 @@ describe("BackgroundTaskConfigSchema.circuitBreaker", () => {
})
})
describe("#given consecutiveThreshold is zero", () => {
describe("#given repetitionThresholdPercent is zero", () => {
test("#when parsed #then throws ZodError", () => {
let thrownError: unknown
try {
BackgroundTaskConfigSchema.parse({
circuitBreaker: {
consecutiveThreshold: 0,
repetitionThresholdPercent: 0,
},
})
} catch (error) {

View File

@@ -3,7 +3,8 @@ import { z } from "zod"
const CircuitBreakerConfigSchema = z.object({
enabled: z.boolean().optional(),
maxToolCalls: z.number().int().min(10).optional(),
consecutiveThreshold: z.number().int().min(5).optional(),
windowSize: z.number().int().min(5).optional(),
repetitionThresholdPercent: z.number().gt(0).max(100).optional(),
})
export const BackgroundTaskConfigSchema = z.object({

View File

@@ -16,7 +16,7 @@ export const CategoryConfigSchema = z.object({
budgetTokens: z.number().optional(),
})
.optional(),
reasoningEffort: z.enum(["none", "minimal", "low", "medium", "high", "xhigh"]).optional(),
reasoningEffort: z.enum(["low", "medium", "high", "xhigh"]).optional(),
textVerbosity: z.enum(["low", "medium", "high"]).optional(),
tools: z.record(z.string(), z.boolean()).optional(),
prompt_append: z.string().optional(),

View File

@@ -1,25 +1,5 @@
import { z } from "zod"
export const FallbackModelObjectSchema = z.object({
model: z.string(),
variant: z.string().optional(),
reasoningEffort: z.enum(["none", "minimal", "low", "medium", "high", "xhigh"]).optional(),
temperature: z.number().min(0).max(2).optional(),
top_p: z.number().min(0).max(1).optional(),
maxTokens: z.number().optional(),
thinking: z
.object({
type: z.enum(["enabled", "disabled"]),
budgetTokens: z.number().optional(),
})
.optional(),
})
export type FallbackModelObject = z.infer<typeof FallbackModelObjectSchema>
export const FallbackModelsSchema = z.union([
z.string(),
z.array(z.union([z.string(), FallbackModelObjectSchema])),
])
export const FallbackModelsSchema = z.union([z.string(), z.array(z.string())])
export type FallbackModels = z.infer<typeof FallbackModelsSchema>

View File

@@ -1,6 +1,7 @@
import { z } from "zod"
export const HookNameSchema = z.enum([
"gpt-permission-continuation",
"todo-continuation-enforcer",
"context-window-monitor",
"session-recovery",
@@ -41,7 +42,6 @@ export const HookNameSchema = z.enum([
"no-hephaestus-non-gpt",
"start-work",
"atlas",
"agent-switch",
"unstable-agent-babysitter",
"task-resume-info",
"stop-continuation-guard",
@@ -52,7 +52,6 @@ export const HookNameSchema = z.enum([
"hashline-read-enhancer",
"read-image-resizer",
"todo-description-override",
"webfetch-redirect-guard",
])
export type HookName = z.infer<typeof HookNameSchema>

View File

@@ -1,10 +0,0 @@
import { z } from "zod"
export const ModelCapabilitiesConfigSchema = z.object({
enabled: z.boolean().optional(),
auto_refresh_on_start: z.boolean().optional(),
refresh_timeout_ms: z.number().int().positive().optional(),
source_url: z.string().url().optional(),
})
export type ModelCapabilitiesConfig = z.infer<typeof ModelCapabilitiesConfigSchema>

View File

@@ -2,7 +2,6 @@ import { z } from "zod"
import { AnyMcpNameSchema } from "../../mcp/types"
import { BuiltinSkillNameSchema } from "./agent-names"
import { AgentOverridesSchema } from "./agent-overrides"
import { AthenaConfigSchema } from "./athena-config"
import { BabysittingConfigSchema } from "./babysitting"
import { BackgroundTaskConfigSchema } from "./background-task"
import { BrowserAutomationConfigSchema } from "./browser-automation"
@@ -13,8 +12,6 @@ import { BuiltinCommandNameSchema } from "./commands"
import { ExperimentalConfigSchema } from "./experimental"
import { GitMasterConfigSchema } from "./git-master"
import { NotificationConfigSchema } from "./notification"
import { OpenClawConfigSchema } from "./openclaw"
import { ModelCapabilitiesConfigSchema } from "./model-capabilities"
import { RalphLoopConfigSchema } from "./ralph-loop"
import { RuntimeFallbackConfigSchema } from "./runtime-fallback"
import { SkillsConfigSchema } from "./skills"
@@ -42,7 +39,6 @@ export const OhMyOpenCodeConfigSchema = z.object({
/** Enable model fallback on API errors (default: false). Set to true to enable automatic model switching when model errors occur. */
model_fallback: z.boolean().optional(),
agents: AgentOverridesSchema.optional(),
athena: AthenaConfigSchema.optional(),
categories: CategoriesConfigSchema.optional(),
claude_code: ClaudeCodeConfigSchema.optional(),
sisyphus_agent: SisyphusAgentConfigSchema.optional(),
@@ -59,8 +55,6 @@ export const OhMyOpenCodeConfigSchema = z.object({
runtime_fallback: z.union([z.boolean(), RuntimeFallbackConfigSchema]).optional(),
background_task: BackgroundTaskConfigSchema.optional(),
notification: NotificationConfigSchema.optional(),
model_capabilities: ModelCapabilitiesConfigSchema.optional(),
openclaw: OpenClawConfigSchema.optional(),
babysitting: BabysittingConfigSchema.optional(),
git_master: GitMasterConfigSchema.optional(),
browser_automation_engine: BrowserAutomationConfigSchema.optional(),

View File

@@ -1,50 +0,0 @@
import { z } from "zod"
export const OpenClawGatewaySchema = z.object({
type: z.enum(["http", "command"]).default("http"),
// HTTP specific
url: z.string().optional(),
method: z.string().default("POST"),
headers: z.record(z.string(), z.string()).optional(),
// Command specific
command: z.string().optional(),
// Shared
timeout: z.number().optional(),
})
export const OpenClawHookSchema = z.object({
enabled: z.boolean().default(true),
gateway: z.string(),
instruction: z.string(),
})
export const OpenClawReplyListenerConfigSchema = z.object({
discordBotToken: z.string().optional(),
discordChannelId: z.string().optional(),
discordMention: z.string().optional(), // For allowed_mentions
authorizedDiscordUserIds: z.array(z.string()).default([]),
telegramBotToken: z.string().optional(),
telegramChatId: z.string().optional(),
pollIntervalMs: z.number().default(3000),
rateLimitPerMinute: z.number().default(10),
maxMessageLength: z.number().default(500),
includePrefix: z.boolean().default(true),
})
export const OpenClawConfigSchema = z.object({
enabled: z.boolean().default(false),
// Outbound Configuration
gateways: z.record(z.string(), OpenClawGatewaySchema).default({}),
hooks: z.record(z.string(), OpenClawHookSchema).default({}),
// Inbound Configuration (Reply Listener)
replyListener: OpenClawReplyListenerConfigSchema.optional(),
})
export type OpenClawConfig = z.infer<typeof OpenClawConfigSchema>
export type OpenClawGateway = z.infer<typeof OpenClawGatewaySchema>
export type OpenClawHook = z.infer<typeof OpenClawHookSchema>
export type OpenClawReplyListenerConfig = z.infer<typeof OpenClawReplyListenerConfigSchema>

Some files were not shown because too many files have changed in this diff Show More